"""
Backtest runner service that wraps the existing Backtester engine.
"""
import uuid
from datetime import datetime, timezone
from typing import Any

import pandas as pd

from api.models.schemas import (
    BacktestMetrics,
    BacktestRequest,
    BacktestResult,
    EquityPoint,
    TradeRecord,
)
from engine.backtester import Backtester
from engine.data_manager import DataManager
from engine.logging_config import get_logger
from engine.market import MarketType
from strategies.factory import get_strategy

logger = get_logger(__name__)


class BacktestRunner:
    """
    Service for executing backtests via the API.
    
    Wraps the existing Backtester engine and converts results
    to API response format.
    """
    
    def __init__(self):
        self.dm = DataManager()
        self.bt = Backtester(self.dm)
    
    def run(self, request: BacktestRequest) -> BacktestResult:
        """
        Execute a backtest and return structured results.
        
        Args:
            request: BacktestRequest with strategy, symbol, and parameters
            
        Returns:
            BacktestResult with metrics, equity curve, and trades
        """
        # Get strategy instance
        strategy, default_params = get_strategy(request.strategy, is_grid=False)
        
        # Merge default params with request params
        params = {**default_params, **request.params}
        
        # Convert market type string to enum
        market_type = MarketType(request.market_type)
        
        # Override strategy market type if specified
        strategy.default_market_type = market_type
        
        logger.info(
            "Running backtest: %s on %s (%s), params=%s",
            request.strategy, request.symbol, request.timeframe, params
        )
        
        # Execute backtest
        result = self.bt.run_strategy(
            strategy=strategy,
            exchange_id=request.exchange,
            symbol=request.symbol,
            timeframe=request.timeframe,
            start_date=request.start_date,
            end_date=request.end_date,
            init_cash=request.init_cash,
            fees=request.fees,
            slippage=request.slippage,
            sl_stop=request.sl_stop,
            tp_stop=request.tp_stop,
            sl_trail=request.sl_trail,
            leverage=request.leverage,
            **params
        )
        
        # Extract data from portfolio
        portfolio = result.portfolio
        
        # Build trade records
        trades = self._build_trade_records(portfolio)
        
        # Build equity curve (trimmed to trading period)
        equity_curve = self._build_equity_curve(portfolio)
        
        # Build metrics
        metrics = self._build_metrics(result, portfolio)
        
        # Get date range from actual trading period (first trade to end)
        idx = portfolio.wrapper.index
        end_date = idx[-1].strftime("%Y-%m-%d %H:%M")
        
        # Use first trade time as start if trades exist
        trades_df = portfolio.trades.records_readable
        if not trades_df.empty:
            first_entry_col = 'Entry Timestamp' if 'Entry Timestamp' in trades_df.columns else 'Entry Time'
            if first_entry_col in trades_df.columns:
                first_trade_time = pd.to_datetime(trades_df[first_entry_col].iloc[0])
                start_date = first_trade_time.strftime("%Y-%m-%d %H:%M")
            else:
                start_date = idx[0].strftime("%Y-%m-%d %H:%M")
        else:
            start_date = idx[0].strftime("%Y-%m-%d %H:%M")
        
        return BacktestResult(
            run_id=str(uuid.uuid4()),
            strategy=request.strategy,
            symbol=request.symbol,
            market_type=result.market_type.value,
            timeframe=request.timeframe,
            start_date=start_date,
            end_date=end_date,
            leverage=result.leverage,
            params=params,
            metrics=metrics,
            equity_curve=equity_curve,
            trades=trades,
            created_at=datetime.now(timezone.utc).isoformat(),
        )
    
    def _build_equity_curve(self, portfolio) -> list[EquityPoint]:
        """Extract equity curve with drawdown from portfolio, starting from first trade."""
        value_series = portfolio.value()
        drawdown_series = portfolio.drawdown()
        
        # Handle multi-column case (from grid search)
        if hasattr(value_series, 'columns') and len(value_series.columns) > 1:
            value_series = value_series.iloc[:, 0]
            drawdown_series = drawdown_series.iloc[:, 0]
        elif hasattr(value_series, 'columns'):
            value_series = value_series.iloc[:, 0]
            drawdown_series = drawdown_series.iloc[:, 0]
        
        # Find first trade time to trim equity curve
        first_trade_idx = 0
        trades_df = portfolio.trades.records_readable
        if not trades_df.empty:
            first_entry_col = 'Entry Timestamp' if 'Entry Timestamp' in trades_df.columns else 'Entry Time'
            if first_entry_col in trades_df.columns:
                first_trade_time = pd.to_datetime(trades_df[first_entry_col].iloc[0])
                # Find index in value_series closest to first trade
                if hasattr(value_series.index, 'get_indexer'):
                    first_trade_idx = value_series.index.get_indexer([first_trade_time], method='nearest')[0]
                    # Start a few bars before first trade for context
                    first_trade_idx = max(0, first_trade_idx - 5)
        
        # Slice from first trade onwards
        value_series = value_series.iloc[first_trade_idx:]
        drawdown_series = drawdown_series.iloc[first_trade_idx:]
        
        points = []
        for i, (ts, val) in enumerate(value_series.items()):
            dd = drawdown_series.iloc[i] if i < len(drawdown_series) else 0.0
            points.append(EquityPoint(
                timestamp=ts.isoformat(),
                value=float(val),
                drawdown=float(dd) * 100,  # Convert to percentage
            ))
        
        return points
    
    def _build_trade_records(self, portfolio) -> list[TradeRecord]:
        """Extract trade records from portfolio."""
        trades_df = portfolio.trades.records_readable
        
        if trades_df.empty:
            return []
        
        records = []
        for _, row in trades_df.iterrows():
            # Handle different column names in vectorbt
            entry_time = row.get('Entry Timestamp', row.get('Entry Time', ''))
            exit_time = row.get('Exit Timestamp', row.get('Exit Time', ''))
            
            records.append(TradeRecord(
                entry_time=str(entry_time) if pd.notna(entry_time) else "",
                exit_time=str(exit_time) if pd.notna(exit_time) else None,
                entry_price=float(row.get('Avg Entry Price', row.get('Entry Price', 0))),
                exit_price=float(row.get('Avg Exit Price', row.get('Exit Price', 0))) 
                    if pd.notna(row.get('Avg Exit Price', row.get('Exit Price'))) else None,
                size=float(row.get('Size', 0)),
                direction=str(row.get('Direction', 'Long')),
                pnl=float(row.get('PnL', 0)) if pd.notna(row.get('PnL')) else None,
                return_pct=float(row.get('Return', 0)) * 100 
                    if pd.notna(row.get('Return')) else None,
                status="closed" if pd.notna(exit_time) else "open",
            ))
        
        return records
    
    def _build_metrics(self, result, portfolio) -> BacktestMetrics:
        """Build metrics from backtest result."""
        stats = portfolio.stats()
        
        # Extract values, handling potential multi-column results
        def get_stat(key: str, default: float = 0.0) -> float:
            val = stats.get(key, default)
            if hasattr(val, 'mean'):
                return float(val.mean())
            return float(val) if pd.notna(val) else default
        
        total_return = portfolio.total_return()
        if hasattr(total_return, 'mean'):
            total_return = total_return.mean()
        
        # Calculate benchmark return from first trade to end (not full period)
        # This gives accurate comparison when strategy has training period
        close = portfolio.close
        benchmark_return = 0.0
        
        if hasattr(close, 'iloc'):
            # Find first trade entry time
            trades_df = portfolio.trades.records_readable
            if not trades_df.empty:
                # Get the first trade entry timestamp
                first_entry_col = 'Entry Timestamp' if 'Entry Timestamp' in trades_df.columns else 'Entry Time'
                if first_entry_col in trades_df.columns:
                    first_trade_time = pd.to_datetime(trades_df[first_entry_col].iloc[0])
                    
                    # Find the price at first trade
                    if hasattr(close.index, 'get_indexer'):
                        # Find closest index to first trade time
                        idx = close.index.get_indexer([first_trade_time], method='nearest')[0]
                        start_price = close.iloc[idx]
                    else:
                        start_price = close.iloc[0]
                    
                    end_price = close.iloc[-1]
                    
                    if hasattr(start_price, 'mean'):
                        start_price = start_price.mean()
                    if hasattr(end_price, 'mean'):
                        end_price = end_price.mean()
                    
                    benchmark_return = ((end_price - start_price) / start_price)
            else:
                # No trades - use full period
                start_price = close.iloc[0]
                end_price = close.iloc[-1]
                if hasattr(start_price, 'mean'):
                    start_price = start_price.mean()
                if hasattr(end_price, 'mean'):
                    end_price = end_price.mean()
                benchmark_return = ((end_price - start_price) / start_price)
        
        # Alpha = strategy return - benchmark return
        alpha = float(total_return) - float(benchmark_return)
        
        sharpe = portfolio.sharpe_ratio()
        if hasattr(sharpe, 'mean'):
            sharpe = sharpe.mean()
        
        max_dd = portfolio.max_drawdown()
        if hasattr(max_dd, 'mean'):
            max_dd = max_dd.mean()
        
        win_rate = portfolio.trades.win_rate()
        if hasattr(win_rate, 'mean'):
            win_rate = win_rate.mean()
        
        trade_count = portfolio.trades.count()
        if hasattr(trade_count, 'mean'):
            trade_count = int(trade_count.mean())
        else:
            trade_count = int(trade_count)
        
        return BacktestMetrics(
            total_return=float(total_return) * 100,
            benchmark_return=float(benchmark_return) * 100,
            alpha=float(alpha) * 100,
            sharpe_ratio=float(sharpe) if pd.notna(sharpe) else 0.0,
            max_drawdown=float(max_dd) * 100,
            win_rate=float(win_rate) * 100 if pd.notna(win_rate) else 0.0,
            total_trades=trade_count,
            profit_factor=get_stat('Profit Factor'),
            avg_trade_return=get_stat('Avg Winning Trade [%]'),
            total_fees=get_stat('Total Fees Paid'),
            total_funding=result.total_funding_paid,
            liquidation_count=result.liquidation_count,
            liquidation_loss=result.total_liquidation_loss,
            adjusted_return=result.adjusted_return,
        )


# Singleton instance
_runner: BacktestRunner | None = None


def get_runner() -> BacktestRunner:
    """Get or create the backtest runner instance."""
    global _runner
    if _runner is None:
        _runner = BacktestRunner()
    return _runner
