"""
Backtest Result Schema for BrighterTrading.

Defines the standardized structure for backtest results to ensure
consistency and determinism across runs.
"""

from dataclasses import dataclass, field, asdict
from typing import List, Dict, Any, Optional
from datetime import datetime
import json


@dataclass
class TradeResult:
    """Individual trade result."""
    ref: int  # Trade reference number
    symbol: str
    side: str  # 'buy' or 'sell'
    open_datetime: str  # ISO format
    close_datetime: Optional[str]  # ISO format, None if still open
    size: float
    open_price: float
    close_price: Optional[float]
    pnl: float  # Profit/loss
    pnlcomm: float  # P&L after commission
    commission: float = 0.0

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


@dataclass
class BacktestMetrics:
    """Performance metrics from backtest."""
    total_return: float = 0.0
    sharpe_ratio: float = 0.0
    sortino_ratio: float = 0.0
    calmar_ratio: float = 0.0
    volatility: float = 0.0
    max_drawdown: float = 0.0
    profit_factor: float = 0.0
    average_pnl: float = 0.0
    number_of_trades: int = 0
    win_loss_ratio: float = 0.0
    max_consecutive_wins: int = 0
    max_consecutive_losses: int = 0
    win_rate: float = 0.0
    loss_rate: float = 0.0

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


@dataclass
class BacktestResult:
    """
    Standardized backtest result schema.

    This schema ensures consistent results across runs for the same
    strategy and data, enabling determinism verification.
    """
    # Identification
    strategy_id: str
    strategy_name: str
    user_id: int
    backtest_id: str

    # Timing
    start_date: str  # ISO format
    end_date: str  # ISO format
    run_datetime: str  # When the backtest was run (ISO format)
    run_duration_seconds: float

    # Capital
    initial_capital: float
    final_portfolio_value: float
    commission_rate: float

    # Results
    success: bool
    error_message: Optional[str] = None

    # Data
    equity_curve: List[float] = field(default_factory=list)
    trades: List[Dict[str, Any]] = field(default_factory=list)
    metrics: BacktestMetrics = field(default_factory=BacktestMetrics)

    # Metadata for reproducibility
    data_source: Optional[str] = None
    symbol: Optional[str] = None
    timeframe: Optional[str] = None
    data_points: int = 0

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        result = asdict(self)
        # Convert metrics to dict if it's a dataclass
        if hasattr(self.metrics, 'to_dict'):
            result['metrics'] = self.metrics.to_dict()
        return result

    def to_json(self, indent: int = 2) -> str:
        """Convert to JSON string."""
        return json.dumps(self.to_dict(), indent=indent, default=str)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'BacktestResult':
        """Create from dictionary."""
        # Handle metrics conversion
        if 'metrics' in data and isinstance(data['metrics'], dict):
            data['metrics'] = BacktestMetrics(**data['metrics'])
        return cls(**data)

    def get_determinism_hash(self) -> str:
        """
        Generate a hash of the deterministic parts of the result.

        Excludes non-deterministic fields like run_datetime and run_duration.
        Includes full trade sequence to detect different trade distributions.
        """
        import hashlib

        # Normalize trade data for comparison (include sequence information)
        normalized_trades = []
        for i, t in enumerate(self.trades):
            normalized_trades.append({
                'seq': i,  # Preserve order
                'pnl': round(t.get('pnl', 0), 6),
                'pnlcomm': round(t.get('pnlcomm', t.get('pnl', 0)), 6),
                'size': round(t.get('size', 0), 6),
                'open_price': round(t.get('open_price', 0), 6),
                'close_price': round(t.get('close_price', 0), 6) if t.get('close_price') else None,
            })

        # Include only deterministic fields with full trade sequence
        deterministic_data = {
            'strategy_id': self.strategy_id,
            'initial_capital': self.initial_capital,
            'final_portfolio_value': round(self.final_portfolio_value, 6),
            'equity_curve': [round(e, 6) for e in self.equity_curve],
            'trades': normalized_trades,  # Full trade sequence
            'metrics': {
                'total_return': round(self.metrics.total_return, 6),
                'number_of_trades': self.metrics.number_of_trades,
                'sharpe_ratio': round(self.metrics.sharpe_ratio, 6),
                'max_drawdown': round(self.metrics.max_drawdown, 6),
                'win_rate': round(self.metrics.win_rate, 6),
                'profit_factor': round(self.metrics.profit_factor, 6),
            }
        }

        json_str = json.dumps(deterministic_data, sort_keys=True)
        return hashlib.sha256(json_str.encode()).hexdigest()

    def verify_determinism(self, other: 'BacktestResult') -> bool:
        """
        Verify that another backtest result is deterministically equivalent.

        :param other: Another backtest result to compare.
        :return: True if results are deterministically equivalent.
        """
        return self.get_determinism_hash() == other.get_determinism_hash()


def create_backtest_result(
    strategy_id: str,
    strategy_name: str,
    user_id: int,
    backtest_id: str,
    initial_capital: float,
    final_value: float,
    equity_curve: List[float],
    trades: List[Dict[str, Any]],
    stats: Dict[str, Any],
    run_duration: float,
    success: bool = True,
    error_message: Optional[str] = None,
    start_date: str = None,
    end_date: str = None,
    commission: float = 0.001,
    **kwargs
) -> BacktestResult:
    """
    Factory function to create a BacktestResult from raw backtest output.

    :param strategy_id: Strategy identifier.
    :param strategy_name: Strategy name.
    :param user_id: User identifier.
    :param backtest_id: Unique backtest identifier.
    :param initial_capital: Starting capital.
    :param final_value: Final portfolio value.
    :param equity_curve: List of portfolio values over time.
    :param trades: List of trade dictionaries.
    :param stats: Dictionary of performance metrics.
    :param run_duration: Backtest runtime in seconds.
    :param success: Whether backtest succeeded.
    :param error_message: Error message if failed.
    :param start_date: Backtest start date (ISO format).
    :param end_date: Backtest end date (ISO format).
    :param commission: Commission rate.
    :param kwargs: Additional metadata.
    :return: BacktestResult instance.
    """
    # Create metrics from stats dict
    metrics = BacktestMetrics(
        total_return=stats.get('total_return', 0.0),
        sharpe_ratio=stats.get('sharpe_ratio', 0.0),
        sortino_ratio=stats.get('sortino_ratio', 0.0),
        calmar_ratio=stats.get('calmar_ratio', 0.0),
        volatility=stats.get('volatility', 0.0),
        max_drawdown=stats.get('max_drawdown', 0.0),
        profit_factor=stats.get('profit_factor', 0.0),
        average_pnl=stats.get('average_pnl', 0.0),
        number_of_trades=stats.get('number_of_trades', len(trades)),
        win_loss_ratio=stats.get('win_loss_ratio', 0.0),
        max_consecutive_wins=stats.get('max_consecutive_wins', 0),
        max_consecutive_losses=stats.get('max_consecutive_losses', 0),
        win_rate=stats.get('win_rate', 0.0),
        loss_rate=stats.get('loss_rate', 0.0),
    )

    return BacktestResult(
        strategy_id=strategy_id,
        strategy_name=strategy_name,
        user_id=user_id,
        backtest_id=backtest_id,
        start_date=start_date or datetime.now().isoformat(),
        end_date=end_date or datetime.now().isoformat(),
        run_datetime=datetime.now().isoformat(),
        run_duration_seconds=run_duration,
        initial_capital=initial_capital,
        final_portfolio_value=final_value,
        commission_rate=commission,
        success=success,
        error_message=error_message,
        equity_curve=equity_curve,
        trades=trades,
        metrics=metrics,
        data_source=kwargs.get('data_source'),
        symbol=kwargs.get('symbol'),
        timeframe=kwargs.get('timeframe'),
        data_points=kwargs.get('data_points', len(equity_curve)),
    )
