"""
Backtesting engine module.

This module orchestrates the backtest:
1. Load and validate data
2. Generate signals using the selected strategy
3. Convert signals to positions
4. Apply transaction costs and slippage
5. Compute returns and equity curve
6. Calculate performance metrics
"""

from datetime import datetime
from pathlib import Path
from typing import Any

import pandas as pd

from jbqlab.data import load_csv
from jbqlab.metrics import compute_metrics
from jbqlab.strategies import get_strategy, validate_strategy_params
from jbqlab.types import BacktestConfig, BacktestResult

# =============================================================================
# Public API
# =============================================================================


def run_backtest(
    data: str | Path | pd.DataFrame,
    strategy: str,
    transaction_cost: float = 0.001,
    slippage: float = 0.0005,
    initial_capital: float = 100_000.0,
    risk_free_rate: float = 0.0,
    **strategy_params: Any,
) -> BacktestResult:
    """Run a backtest on price data using a specified strategy.

    This is the main entry point for the SDK. It handles:
    - Loading data from CSV or using provided DataFrame
    - Validating strategy parameters
    - Generating signals
    - Computing positions and returns with transaction costs
    - Calculating performance metrics

    Args:
        data: Path to CSV file or DataFrame with 'date' and 'close' columns.
        strategy: Name of the strategy to use (e.g., 'sma_crossover').
        transaction_cost: Proportional transaction cost (default 0.001 = 0.1%).
        slippage: Proportional slippage cost (default 0.0005 = 0.05%).
        initial_capital: Starting capital for equity curve (default 100,000).
        risk_free_rate: Annual risk-free rate for Sharpe ratio (default 0).
        **strategy_params: Parameters to pass to the strategy function.

    Returns:
        BacktestResult containing equity curve, returns, positions, and metrics.

    Raises:
        FileNotFoundError: If data path does not exist.
        ValueError: If data or parameters are invalid.

    Example:
        >>> result = run_backtest("data.csv", "sma_crossover", fast=10, slow=30)
        >>> print(result.metrics)
        {'total_return': 0.15, 'sharpe': 1.2, ...}
    """
    # Create config
    config = BacktestConfig(
        strategy=strategy,
        strategy_params=strategy_params,
        transaction_cost=transaction_cost,
        slippage=slippage,
        initial_capital=initial_capital,
        risk_free_rate=risk_free_rate,
    )

    return run_backtest_with_config(data, config)


def run_backtest_with_config(
    data: str | Path | pd.DataFrame,
    config: BacktestConfig,
) -> BacktestResult:
    """Run a backtest using a BacktestConfig object.

    Args:
        data: Path to CSV file or DataFrame with 'date' and 'close' columns.
        config: BacktestConfig with all backtest parameters.

    Returns:
        BacktestResult containing equity curve, returns, positions, and metrics.
    """
    # Load data
    if isinstance(data, (str, Path)):
        df = load_csv(data)
        data_source = str(data)
    else:
        df = data.copy()
        # Ensure proper format
        df.columns = df.columns.str.lower().str.strip()
        if "date" in df.columns:
            df["date"] = pd.to_datetime(df["date"])
            df = df.set_index("date")
        data_source = "DataFrame"

    # Validate we have enough data
    if len(df) < 2:
        raise ValueError("Need at least 2 data points for backtesting")

    # Get and validate strategy
    strategy_func = get_strategy(config.strategy)
    validated_params = validate_strategy_params(config.strategy, config.strategy_params)

    # Generate signals
    signals = strategy_func(df, **validated_params)

    # Compute positions and returns
    positions, returns = compute_returns_with_costs(
        df=df,
        signals=signals,
        transaction_cost=config.transaction_cost,
        slippage=config.slippage,
    )

    # Compute equity curve
    equity_curve = compute_equity_curve(returns, config.initial_capital)

    # Compute metrics
    metrics = compute_metrics(
        equity_curve=equity_curve,
        returns=returns,
        risk_free_rate=config.risk_free_rate,
        initial_capital=config.initial_capital,
    )

    # Build metadata
    metadata = {
        "data_source": data_source,
        "strategy": config.strategy,
        "strategy_params": validated_params,
        "transaction_cost": config.transaction_cost,
        "slippage": config.slippage,
        "initial_capital": config.initial_capital,
        "risk_free_rate": config.risk_free_rate,
        "start_date": df.index[0].isoformat(),
        "end_date": df.index[-1].isoformat(),
        "num_days": len(df),
        "run_timestamp": datetime.now().isoformat(),
    }

    return BacktestResult(
        equity_curve=equity_curve,
        returns=returns,
        positions=positions,
        metrics=metrics,
        metadata=metadata,
    )


# =============================================================================
# Internal Functions
# =============================================================================


def compute_returns_with_costs(
    df: pd.DataFrame,
    signals: pd.Series,
    transaction_cost: float,
    slippage: float,
) -> tuple[pd.Series, pd.Series]:
    """Compute position-adjusted returns including transaction costs.

    Transaction costs are applied on position changes (turnover).
    Both transaction_cost and slippage are proportional costs.

    Args:
        df: DataFrame with 'close' column.
        signals: Series of signals (1 = long, 0 = flat).
        transaction_cost: Proportional transaction cost.
        slippage: Proportional slippage.

    Returns:
        Tuple of (positions Series, returns Series).
    """
    close = df["close"]

    # Ensure signals aligned with close
    positions = signals.reindex(close.index).fillna(0).astype(int)

    # Calculate raw asset returns
    asset_returns = close.pct_change().fillna(0)

    # Position-weighted returns (assuming we're long when position=1)
    # On day t, position[t-1] determines exposure to return[t]
    # We shift positions by 1 to avoid lookahead bias
    # (signal generated at close of day t-1, return earned on day t)
    shifted_positions = positions.shift(1).fillna(0)

    strategy_returns = shifted_positions * asset_returns

    # Calculate turnover (position changes), including the initial 0→position transition
    turnover = (positions - shifted_positions).abs()

    # Transaction costs: applied when turnover occurs
    # Cost = turnover * (transaction_cost + slippage) * price
    # But since we're computing returns, we apply as proportional deduction
    total_cost_rate = transaction_cost + slippage
    cost_drag = turnover * total_cost_rate

    # Subtract costs from returns
    net_returns = strategy_returns - cost_drag

    # Name the series
    positions.name = "position"
    net_returns.name = "return"

    return positions, net_returns


def compute_equity_curve(returns: pd.Series, initial_capital: float) -> pd.Series:
    """Compute equity curve from returns series.

    Args:
        returns: Series of returns.
        initial_capital: Starting capital.

    Returns:
        Series of portfolio values.
    """
    # Cumulative returns: (1 + r1) * (1 + r2) * ... * (1 + rn)
    cumulative = (1 + returns).cumprod()

    # Scale by initial capital
    equity = cumulative * initial_capital

    equity.name = "equity"
    return equity
