"""
Backtesting Visualization Utilities
====================================
Data preparation utilities for backtesting visualizations.

Prepares data structures suitable for frontend charting libraries
(e.g., recharts, plotly, lightweight-charts).

Note: This module does NOT perform actual plotting. It prepares
data in formats ready for consumption by visualization libraries.

Author: ML-Specialist (NEXUS v4.0)
Version: 1.0.0
Created: 2026-01-25
"""

from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Union, Tuple
from dataclasses import dataclass, field
import numpy as np
import pandas as pd
from loguru import logger


@dataclass
class ChartDataPoint:
    """Single data point for charting."""

    x: Union[float, str, datetime]
    y: float
    label: Optional[str] = None
    metadata: Optional[Dict[str, Any]] = None


@dataclass
class ChartSeries:
    """Series of data points for a chart."""

    name: str
    data: List[ChartDataPoint]
    color: Optional[str] = None
    chart_type: str = "line"


@dataclass
class VisualizationData:
    """
    Container for visualization data.

    Attributes:
        chart_type: Type of chart (line, bar, scatter, heatmap, histogram)
        title: Chart title
        x_label: X-axis label
        y_label: Y-axis label
        series: List of data series
        annotations: Optional annotations/markers
        config: Additional chart configuration
    """

    chart_type: str
    title: str
    x_label: str
    y_label: str
    series: List[ChartSeries] = field(default_factory=list)
    annotations: List[Dict[str, Any]] = field(default_factory=list)
    config: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary suitable for JSON serialization."""
        return {
            'chartType': self.chart_type,
            'title': self.title,
            'xLabel': self.x_label,
            'yLabel': self.y_label,
            'series': [
                {
                    'name': s.name,
                    'data': [
                        {
                            'x': dp.x.isoformat() if isinstance(dp.x, datetime) else dp.x,
                            'y': dp.y,
                            'label': dp.label,
                            'metadata': dp.metadata
                        }
                        for dp in s.data
                    ],
                    'color': s.color,
                    'type': s.chart_type
                }
                for s in self.series
            ],
            'annotations': self.annotations,
            'config': self.config
        }

    def to_recharts_format(self) -> Dict[str, Any]:
        """Convert to format suitable for Recharts library."""
        if not self.series:
            return {'data': [], 'config': self.config}

        data = []
        first_series = self.series[0]

        for i, dp in enumerate(first_series.data):
            point = {
                'x': dp.x.isoformat() if isinstance(dp.x, datetime) else dp.x,
                first_series.name: dp.y
            }

            for other_series in self.series[1:]:
                if i < len(other_series.data):
                    point[other_series.name] = other_series.data[i].y

            data.append(point)

        return {
            'data': data,
            'series': [{'dataKey': s.name, 'stroke': s.color, 'name': s.name} for s in self.series],
            'xAxisKey': 'x',
            'config': self.config
        }

    def to_plotly_format(self) -> Dict[str, Any]:
        """Convert to format suitable for Plotly library."""
        traces = []

        for series in self.series:
            trace = {
                'name': series.name,
                'x': [dp.x.isoformat() if isinstance(dp.x, datetime) else dp.x for dp in series.data],
                'y': [dp.y for dp in series.data],
                'type': self._map_chart_type_to_plotly(series.chart_type)
            }
            if series.color:
                trace['marker'] = {'color': series.color}
            traces.append(trace)

        layout = {
            'title': self.title,
            'xaxis': {'title': self.x_label},
            'yaxis': {'title': self.y_label}
        }

        return {'data': traces, 'layout': layout}

    def _map_chart_type_to_plotly(self, chart_type: str) -> str:
        """Map internal chart type to Plotly type."""
        mapping = {
            'line': 'scatter',
            'scatter': 'scatter',
            'bar': 'bar',
            'area': 'scatter',
            'histogram': 'histogram'
        }
        return mapping.get(chart_type, 'scatter')


def prepare_equity_curve_data(
    backtest_result: Any,
    include_benchmark: bool = False,
    benchmark_returns: Optional[Union[pd.Series, np.ndarray]] = None,
    initial_capital: float = 10000.0
) -> VisualizationData:
    """
    Prepare equity curve data for visualization.

    Args:
        backtest_result: BacktestResult object or dictionary
        include_benchmark: Whether to include benchmark comparison
        benchmark_returns: Optional benchmark returns for comparison
        initial_capital: Initial capital for normalization

    Returns:
        VisualizationData ready for charting
    """
    result_dict = _normalize_result(backtest_result)
    equity_curve = _extract_equity_curve(result_dict)

    if equity_curve is None or len(equity_curve) == 0:
        logger.warning("No equity curve data available")
        return VisualizationData(
            chart_type="line",
            title="Equity Curve",
            x_label="Time",
            y_label="Equity ($)",
            series=[],
            config={'empty': True}
        )

    timestamps = _generate_timestamps(len(equity_curve), result_dict)

    strategy_data = [
        ChartDataPoint(x=t, y=float(v), metadata={'index': i})
        for i, (t, v) in enumerate(zip(timestamps, equity_curve))
    ]

    series = [
        ChartSeries(
            name="Strategy",
            data=strategy_data,
            color="#2196F3",
            chart_type="line"
        )
    ]

    if include_benchmark and benchmark_returns is not None:
        benchmark_equity = _compute_equity_from_returns(benchmark_returns, initial_capital)
        benchmark_data = [
            ChartDataPoint(x=t, y=float(v))
            for t, v in zip(timestamps[:len(benchmark_equity)], benchmark_equity)
        ]
        series.append(
            ChartSeries(
                name="Benchmark",
                data=benchmark_data,
                color="#9E9E9E",
                chart_type="line"
            )
        )

    annotations = []
    if len(equity_curve) > 1:
        max_idx = int(np.argmax(equity_curve))
        min_idx = int(np.argmin(equity_curve))

        annotations.append({
            'type': 'point',
            'x': timestamps[max_idx],
            'y': float(equity_curve[max_idx]),
            'label': f"Peak: ${equity_curve[max_idx]:,.2f}"
        })

        annotations.append({
            'type': 'point',
            'x': timestamps[min_idx],
            'y': float(equity_curve[min_idx]),
            'label': f"Trough: ${equity_curve[min_idx]:,.2f}"
        })

    return VisualizationData(
        chart_type="line",
        title="Equity Curve",
        x_label="Time",
        y_label="Equity ($)",
        series=series,
        annotations=annotations,
        config={
            'initial_capital': initial_capital,
            'final_equity': float(equity_curve[-1]) if len(equity_curve) > 0 else initial_capital,
            'total_return_pct': ((equity_curve[-1] - initial_capital) / initial_capital * 100) if len(equity_curve) > 0 else 0
        }
    )


def prepare_drawdown_chart_data(
    backtest_result: Any,
    as_percentage: bool = True
) -> VisualizationData:
    """
    Prepare drawdown chart data for visualization.

    Args:
        backtest_result: BacktestResult object or dictionary
        as_percentage: Whether to show as percentage (True) or absolute (False)

    Returns:
        VisualizationData for drawdown chart
    """
    result_dict = _normalize_result(backtest_result)
    equity_curve = _extract_equity_curve(result_dict)

    if equity_curve is None or len(equity_curve) < 2:
        logger.warning("Insufficient equity curve data for drawdown")
        return VisualizationData(
            chart_type="area",
            title="Drawdown",
            x_label="Time",
            y_label="Drawdown (%)" if as_percentage else "Drawdown ($)",
            series=[],
            config={'empty': True}
        )

    running_max = np.maximum.accumulate(equity_curve)
    drawdown_abs = running_max - equity_curve
    drawdown_pct = np.where(running_max > 0, drawdown_abs / running_max * 100, 0)

    drawdown = drawdown_pct if as_percentage else drawdown_abs
    drawdown = -np.abs(drawdown)

    timestamps = _generate_timestamps(len(equity_curve), result_dict)

    drawdown_data = [
        ChartDataPoint(
            x=t,
            y=float(v),
            metadata={
                'absolute': float(drawdown_abs[i]),
                'percentage': float(drawdown_pct[i]),
                'peak': float(running_max[i])
            }
        )
        for i, (t, v) in enumerate(zip(timestamps, drawdown))
    ]

    series = [
        ChartSeries(
            name="Drawdown",
            data=drawdown_data,
            color="#F44336",
            chart_type="area"
        )
    ]

    max_dd_idx = int(np.argmax(np.abs(drawdown)))
    annotations = [
        {
            'type': 'point',
            'x': timestamps[max_dd_idx],
            'y': float(drawdown[max_dd_idx]),
            'label': f"Max DD: {drawdown_pct[max_dd_idx]:.1f}%"
        }
    ]

    in_drawdown = drawdown < -0.01
    dd_periods = _count_drawdown_periods(in_drawdown)
    avg_dd_duration = _compute_avg_drawdown_duration(in_drawdown)

    return VisualizationData(
        chart_type="area",
        title="Drawdown",
        x_label="Time",
        y_label="Drawdown (%)" if as_percentage else "Drawdown ($)",
        series=series,
        annotations=annotations,
        config={
            'max_drawdown_pct': float(np.max(drawdown_pct)),
            'max_drawdown_abs': float(np.max(drawdown_abs)),
            'avg_drawdown_pct': float(np.mean(drawdown_pct[drawdown_pct > 0])) if np.any(drawdown_pct > 0) else 0,
            'drawdown_periods': dd_periods,
            'avg_drawdown_duration': avg_dd_duration,
            'fill_color': 'rgba(244, 67, 54, 0.3)'
        }
    )


def prepare_monthly_returns_heatmap_data(
    backtest_result: Any,
    trades: Optional[List[Any]] = None
) -> VisualizationData:
    """
    Prepare monthly returns heatmap data.

    Args:
        backtest_result: BacktestResult object or dictionary
        trades: Optional list of trades for P&L aggregation

    Returns:
        VisualizationData for heatmap
    """
    result_dict = _normalize_result(backtest_result)
    equity_curve = _extract_equity_curve(result_dict)
    trade_list = trades if trades else result_dict.get('trades', [])

    monthly_returns = {}

    if trade_list:
        for t in trade_list:
            exit_time = _get_trade_field(t, 'exit_time')
            pnl = _get_trade_field(t, 'pnl', 0)

            if exit_time and pnl:
                if isinstance(exit_time, str):
                    try:
                        exit_time = datetime.fromisoformat(exit_time.replace('Z', '+00:00'))
                    except Exception:
                        continue

                year = exit_time.year
                month = exit_time.month

                if year not in monthly_returns:
                    monthly_returns[year] = {}
                monthly_returns[year][month] = monthly_returns[year].get(month, 0) + pnl

    elif equity_curve is not None and len(equity_curve) > 30:
        timestamps = _generate_timestamps(len(equity_curve), result_dict)
        for i in range(1, len(equity_curve)):
            ret = (equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1] * 100 if equity_curve[i-1] > 0 else 0
            ts = timestamps[i]
            if isinstance(ts, (int, float)):
                continue
            year = ts.year
            month = ts.month

            if year not in monthly_returns:
                monthly_returns[year] = {}
            monthly_returns[year][month] = monthly_returns[year].get(month, 0) + ret

    if not monthly_returns:
        return VisualizationData(
            chart_type="heatmap",
            title="Monthly Returns",
            x_label="Month",
            y_label="Year",
            series=[],
            config={'empty': True}
        )

    years = sorted(monthly_returns.keys())
    months = list(range(1, 13))
    month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                   'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

    heatmap_data = []
    for year in years:
        for month in months:
            value = monthly_returns.get(year, {}).get(month, None)
            heatmap_data.append(ChartDataPoint(
                x=month_names[month - 1],
                y=float(value) if value is not None else 0,
                label=str(year),
                metadata={
                    'year': year,
                    'month': month,
                    'has_data': value is not None
                }
            ))

    series = [
        ChartSeries(
            name="Monthly Return",
            data=heatmap_data,
            chart_type="heatmap"
        )
    ]

    all_values = [dp.y for dp in heatmap_data if dp.metadata.get('has_data')]
    min_val = min(all_values) if all_values else -10
    max_val = max(all_values) if all_values else 10

    return VisualizationData(
        chart_type="heatmap",
        title="Monthly Returns",
        x_label="Month",
        y_label="Year",
        series=series,
        config={
            'years': years,
            'months': month_names,
            'color_scale': {
                'negative': '#F44336',
                'zero': '#FFFFFF',
                'positive': '#4CAF50'
            },
            'min_value': min_val,
            'max_value': max_val,
            'format': '${:.2f}' if trade_list else '{:.1f}%'
        }
    )


def prepare_trade_distribution_data(
    trades: List[Any],
    bins: int = 30,
    as_percentage: bool = False
) -> VisualizationData:
    """
    Prepare trade profit/loss distribution data.

    Args:
        trades: List of trade objects or dictionaries
        bins: Number of histogram bins
        as_percentage: Whether to show as percentage returns

    Returns:
        VisualizationData for histogram
    """
    if not trades:
        return VisualizationData(
            chart_type="histogram",
            title="Trade P&L Distribution",
            x_label="P&L ($)" if not as_percentage else "Return (%)",
            y_label="Frequency",
            series=[],
            config={'empty': True}
        )

    pnl_values = []
    for t in trades:
        if as_percentage:
            pnl = _get_trade_field(t, 'pnl_pct', _get_trade_field(t, 'profit_loss_pct', 0))
        else:
            pnl = _get_trade_field(t, 'pnl', _get_trade_field(t, 'profit_loss', 0))

        if pnl is not None:
            pnl_values.append(float(pnl))

    if not pnl_values:
        return VisualizationData(
            chart_type="histogram",
            title="Trade P&L Distribution",
            x_label="P&L ($)" if not as_percentage else "Return (%)",
            y_label="Frequency",
            series=[],
            config={'empty': True, 'reason': 'no_valid_pnl'}
        )

    pnl_array = np.array(pnl_values)
    hist, bin_edges = np.histogram(pnl_array, bins=bins)

    histogram_data = []
    for i in range(len(hist)):
        bin_center = (bin_edges[i] + bin_edges[i + 1]) / 2
        is_positive = bin_center >= 0

        histogram_data.append(ChartDataPoint(
            x=float(bin_center),
            y=int(hist[i]),
            metadata={
                'bin_start': float(bin_edges[i]),
                'bin_end': float(bin_edges[i + 1]),
                'is_positive': is_positive
            }
        ))

    series = [
        ChartSeries(
            name="Trade Distribution",
            data=histogram_data,
            chart_type="histogram"
        )
    ]

    winning_trades = [p for p in pnl_values if p > 0]
    losing_trades = [p for p in pnl_values if p < 0]

    annotations = [
        {
            'type': 'line',
            'x': 0,
            'label': 'Break-even',
            'color': '#000000'
        },
        {
            'type': 'line',
            'x': np.mean(pnl_values),
            'label': f'Mean: {"$" if not as_percentage else ""}{np.mean(pnl_values):.2f}{"%" if as_percentage else ""}',
            'color': '#2196F3'
        }
    ]

    return VisualizationData(
        chart_type="histogram",
        title="Trade P&L Distribution",
        x_label="P&L ($)" if not as_percentage else "Return (%)",
        y_label="Frequency",
        series=series,
        annotations=annotations,
        config={
            'total_trades': len(pnl_values),
            'winning_trades': len(winning_trades),
            'losing_trades': len(losing_trades),
            'win_rate': len(winning_trades) / len(pnl_values) if pnl_values else 0,
            'mean': float(np.mean(pnl_values)),
            'median': float(np.median(pnl_values)),
            'std': float(np.std(pnl_values)),
            'skewness': float(_compute_skewness(pnl_array)),
            'kurtosis': float(_compute_kurtosis(pnl_array)),
            'positive_color': '#4CAF50',
            'negative_color': '#F44336'
        }
    )


def prepare_strategy_weights_data(
    ensemble_result: Any,
    strategy_weights: Optional[Dict[str, float]] = None,
    weight_history: Optional[List[Dict[str, float]]] = None
) -> VisualizationData:
    """
    Prepare strategy weights visualization data.

    Args:
        ensemble_result: Ensemble backtest result
        strategy_weights: Static strategy weights
        weight_history: Optional time-varying weights history

    Returns:
        VisualizationData for weight visualization
    """
    result_dict = _normalize_result(ensemble_result)

    if strategy_weights is None:
        strategy_weights = result_dict.get('strategy_weights', {})

    if not strategy_weights:
        return VisualizationData(
            chart_type="pie",
            title="Strategy Weights",
            x_label="Strategy",
            y_label="Weight",
            series=[],
            config={'empty': True}
        )

    colors = ['#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0',
              '#00BCD4', '#FFEB3B', '#795548', '#607D8B', '#3F51B5']

    pie_data = []
    for i, (strategy, weight) in enumerate(strategy_weights.items()):
        pie_data.append(ChartDataPoint(
            x=strategy,
            y=float(weight) * 100,
            metadata={
                'strategy': strategy,
                'weight': float(weight),
                'color': colors[i % len(colors)]
            }
        ))

    pie_series = ChartSeries(
        name="Strategy Weights",
        data=pie_data,
        chart_type="pie"
    )

    series = [pie_series]

    if weight_history:
        strategies = list(strategy_weights.keys())
        for strategy in strategies:
            history_data = []
            for i, weights in enumerate(weight_history):
                weight = weights.get(strategy, 0)
                history_data.append(ChartDataPoint(
                    x=i,
                    y=float(weight) * 100,
                    label=strategy
                ))

            series.append(ChartSeries(
                name=f"{strategy} Weight History",
                data=history_data,
                chart_type="line"
            ))

    return VisualizationData(
        chart_type="pie" if not weight_history else "multi",
        title="Strategy Weights",
        x_label="Strategy",
        y_label="Weight (%)",
        series=series,
        config={
            'static_weights': strategy_weights,
            'has_history': weight_history is not None,
            'total_strategies': len(strategy_weights),
            'colors': colors[:len(strategy_weights)]
        }
    )


def prepare_calibration_plot_data(
    predictions: Union[np.ndarray, List[float]],
    actuals: Union[np.ndarray, List[int]],
    n_bins: int = 10,
    strategy: str = 'uniform'
) -> VisualizationData:
    """
    Prepare calibration plot data (reliability diagram).

    Args:
        predictions: Predicted probabilities (0 to 1)
        actuals: Actual binary outcomes (0 or 1)
        n_bins: Number of bins for calibration
        strategy: Binning strategy ('uniform' or 'quantile')

    Returns:
        VisualizationData for calibration plot
    """
    predictions = np.asarray(predictions).ravel()
    actuals = np.asarray(actuals).ravel()

    if len(predictions) != len(actuals):
        raise ValueError("predictions and actuals must have same length")

    if len(predictions) == 0:
        return VisualizationData(
            chart_type="scatter",
            title="Calibration Plot",
            x_label="Mean Predicted Probability",
            y_label="Fraction of Positives",
            series=[],
            config={'empty': True}
        )

    if strategy == 'quantile':
        percentiles = np.linspace(0, 100, n_bins + 1)
        bin_edges = np.percentile(predictions, percentiles)
    else:
        bin_edges = np.linspace(0, 1, n_bins + 1)

    bin_centers = []
    bin_accuracies = []
    bin_counts = []
    bin_confidence_intervals = []

    for i in range(len(bin_edges) - 1):
        in_bin = (predictions >= bin_edges[i]) & (predictions < bin_edges[i + 1])
        if i == len(bin_edges) - 2:
            in_bin = (predictions >= bin_edges[i]) & (predictions <= bin_edges[i + 1])

        count = np.sum(in_bin)
        if count > 0:
            mean_pred = np.mean(predictions[in_bin])
            mean_actual = np.mean(actuals[in_bin])

            p = mean_actual
            n = count
            ci = 1.96 * np.sqrt(p * (1 - p) / n) if n > 0 and 0 < p < 1 else 0

            bin_centers.append(mean_pred)
            bin_accuracies.append(mean_actual)
            bin_counts.append(count)
            bin_confidence_intervals.append(ci)

    calibration_data = [
        ChartDataPoint(
            x=float(center),
            y=float(acc),
            metadata={
                'count': int(count),
                'ci': float(ci)
            }
        )
        for center, acc, count, ci in zip(bin_centers, bin_accuracies, bin_counts, bin_confidence_intervals)
    ]

    calibration_series = ChartSeries(
        name="Calibration",
        data=calibration_data,
        color="#2196F3",
        chart_type="scatter"
    )

    perfect_line = [
        ChartDataPoint(x=0.0, y=0.0),
        ChartDataPoint(x=1.0, y=1.0)
    ]

    perfect_series = ChartSeries(
        name="Perfect Calibration",
        data=perfect_line,
        color="#9E9E9E",
        chart_type="line"
    )

    ece = _compute_ece(predictions, actuals, bin_edges)

    annotations = [
        {
            'type': 'text',
            'x': 0.05,
            'y': 0.95,
            'label': f'ECE: {ece:.4f}'
        }
    ]

    histogram_data = []
    hist, _ = np.histogram(predictions, bins=bin_edges)
    for i, count in enumerate(hist):
        center = (bin_edges[i] + bin_edges[i + 1]) / 2
        histogram_data.append(ChartDataPoint(
            x=float(center),
            y=int(count),
            metadata={'bin_start': float(bin_edges[i]), 'bin_end': float(bin_edges[i + 1])}
        ))

    histogram_series = ChartSeries(
        name="Prediction Distribution",
        data=histogram_data,
        color="#BBDEFB",
        chart_type="bar"
    )

    return VisualizationData(
        chart_type="multi",
        title="Calibration Plot",
        x_label="Mean Predicted Probability",
        y_label="Fraction of Positives",
        series=[calibration_series, perfect_series, histogram_series],
        annotations=annotations,
        config={
            'ece': float(ece),
            'n_bins': n_bins,
            'total_samples': len(predictions),
            'positive_rate': float(np.mean(actuals)),
            'mean_prediction': float(np.mean(predictions)),
            'show_histogram': True,
            'histogram_opacity': 0.3
        }
    )


def prepare_rolling_metrics_data(
    backtest_result: Any,
    window: int = 30,
    metrics: Optional[List[str]] = None
) -> VisualizationData:
    """
    Prepare rolling performance metrics data.

    Args:
        backtest_result: BacktestResult object or dictionary
        window: Rolling window size
        metrics: List of metrics to compute (default: sharpe, sortino, volatility)

    Returns:
        VisualizationData for rolling metrics
    """
    if metrics is None:
        metrics = ['sharpe_ratio', 'volatility', 'win_rate']

    result_dict = _normalize_result(backtest_result)
    equity_curve = _extract_equity_curve(result_dict)

    if equity_curve is None or len(equity_curve) < window + 1:
        return VisualizationData(
            chart_type="line",
            title="Rolling Metrics",
            x_label="Time",
            y_label="Value",
            series=[],
            config={'empty': True, 'reason': 'insufficient_data'}
        )

    returns = np.diff(equity_curve) / equity_curve[:-1]
    returns = np.nan_to_num(returns, nan=0.0, posinf=0.0, neginf=0.0)

    timestamps = _generate_timestamps(len(returns), result_dict)

    series = []
    colors = {'sharpe_ratio': '#2196F3', 'volatility': '#FF9800', 'win_rate': '#4CAF50',
              'sortino_ratio': '#9C27B0', 'calmar_ratio': '#E91E63'}

    for metric_name in metrics:
        rolling_values = _compute_rolling_metric(returns, window, metric_name)
        valid_start = window - 1

        metric_data = [
            ChartDataPoint(
                x=timestamps[i + valid_start] if i + valid_start < len(timestamps) else i + valid_start,
                y=float(v),
                metadata={'window': window}
            )
            for i, v in enumerate(rolling_values)
        ]

        series.append(ChartSeries(
            name=metric_name.replace('_', ' ').title(),
            data=metric_data,
            color=colors.get(metric_name, '#000000'),
            chart_type="line"
        ))

    return VisualizationData(
        chart_type="line",
        title=f"Rolling Metrics (Window: {window})",
        x_label="Time",
        y_label="Value",
        series=series,
        config={
            'window': window,
            'metrics': metrics,
            'total_periods': len(returns)
        }
    )


def _normalize_result(result: Any) -> Dict[str, Any]:
    """Normalize backtest result to dictionary."""
    if isinstance(result, dict):
        return result
    if hasattr(result, 'to_dict'):
        return result.to_dict()
    if hasattr(result, '__dict__'):
        return vars(result)
    return {'result': result}


def _extract_equity_curve(result_dict: Dict[str, Any]) -> Optional[np.ndarray]:
    """Extract equity curve from result dictionary."""
    if 'equity_curve' in result_dict:
        ec = result_dict['equity_curve']
        if isinstance(ec, pd.Series):
            return ec.values
        if isinstance(ec, np.ndarray):
            return ec
        if isinstance(ec, list):
            return np.array(ec)
    return None


def _generate_timestamps(length: int, result_dict: Dict[str, Any]) -> List[Union[datetime, int]]:
    """Generate timestamps for data points."""
    start_date = result_dict.get('start_date')
    end_date = result_dict.get('end_date')

    if start_date and end_date:
        if isinstance(start_date, str):
            try:
                start_date = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
            except Exception:
                start_date = None
        if isinstance(end_date, str):
            try:
                end_date = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
            except Exception:
                end_date = None

        if start_date and end_date and length > 1:
            delta = (end_date - start_date) / (length - 1)
            return [start_date + delta * i for i in range(length)]

    return list(range(length))


def _compute_equity_from_returns(
    returns: Union[pd.Series, np.ndarray],
    initial_capital: float
) -> np.ndarray:
    """Compute equity curve from returns."""
    returns = np.asarray(returns)
    equity = np.zeros(len(returns) + 1)
    equity[0] = initial_capital
    for i, ret in enumerate(returns):
        equity[i + 1] = equity[i] * (1 + ret)
    return equity


def _get_trade_field(trade: Any, field: str, default: Any = None) -> Any:
    """Get field from trade object or dictionary."""
    if hasattr(trade, field):
        return getattr(trade, field)
    if isinstance(trade, dict):
        return trade.get(field, default)
    return default


def _count_drawdown_periods(in_drawdown: np.ndarray) -> int:
    """Count number of drawdown periods."""
    changes = np.diff(in_drawdown.astype(int))
    return int(np.sum(changes == 1))


def _compute_avg_drawdown_duration(in_drawdown: np.ndarray) -> float:
    """Compute average drawdown duration."""
    durations = []
    current_duration = 0

    for in_dd in in_drawdown:
        if in_dd:
            current_duration += 1
        elif current_duration > 0:
            durations.append(current_duration)
            current_duration = 0

    if current_duration > 0:
        durations.append(current_duration)

    return float(np.mean(durations)) if durations else 0


def _compute_skewness(data: np.ndarray) -> float:
    """Compute skewness of data."""
    n = len(data)
    if n < 3:
        return 0.0
    mean = np.mean(data)
    std = np.std(data)
    if std == 0:
        return 0.0
    return float(np.mean(((data - mean) / std) ** 3))


def _compute_kurtosis(data: np.ndarray) -> float:
    """Compute excess kurtosis of data."""
    n = len(data)
    if n < 4:
        return 0.0
    mean = np.mean(data)
    std = np.std(data)
    if std == 0:
        return 0.0
    return float(np.mean(((data - mean) / std) ** 4) - 3)


def _compute_ece(
    predictions: np.ndarray,
    actuals: np.ndarray,
    bin_edges: np.ndarray
) -> float:
    """Compute Expected Calibration Error."""
    ece = 0.0
    n = len(predictions)

    for i in range(len(bin_edges) - 1):
        in_bin = (predictions >= bin_edges[i]) & (predictions < bin_edges[i + 1])
        if i == len(bin_edges) - 2:
            in_bin = (predictions >= bin_edges[i]) & (predictions <= bin_edges[i + 1])

        prop_in_bin = np.mean(in_bin)
        if prop_in_bin > 0:
            avg_confidence = np.mean(predictions[in_bin])
            avg_accuracy = np.mean(actuals[in_bin])
            ece += np.abs(avg_confidence - avg_accuracy) * prop_in_bin

    return ece


def _compute_rolling_metric(
    returns: np.ndarray,
    window: int,
    metric: str
) -> np.ndarray:
    """Compute rolling metric values."""
    n = len(returns)
    result = []

    for i in range(window - 1, n):
        window_returns = returns[i - window + 1:i + 1]

        if metric == 'sharpe_ratio':
            mean_ret = np.mean(window_returns)
            std_ret = np.std(window_returns)
            value = np.sqrt(252) * mean_ret / std_ret if std_ret > 0 else 0
        elif metric == 'sortino_ratio':
            mean_ret = np.mean(window_returns)
            neg_returns = window_returns[window_returns < 0]
            downside_std = np.std(neg_returns) if len(neg_returns) > 0 else 0.0001
            value = np.sqrt(252) * mean_ret / downside_std if downside_std > 0 else 0
        elif metric == 'volatility':
            value = np.std(window_returns) * np.sqrt(252) * 100
        elif metric == 'win_rate':
            value = np.mean(window_returns > 0) * 100
        elif metric == 'calmar_ratio':
            total_ret = np.sum(window_returns)
            running_max = np.maximum.accumulate(np.cumsum(window_returns))
            max_dd = np.max(running_max - np.cumsum(window_returns))
            value = total_ret / max_dd if max_dd > 0 else 0
        else:
            value = 0

        result.append(value)

    return np.array(result)
