from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass
from decimal import Decimal

from .models import Candle, SignalType
from .strategy import SmaCrossoverStrategy


@dataclass(frozen=True)
class BacktestResult:
    total_candles: int
    buy_signals: int
    sell_signals: int
    hold_signals: int


@dataclass(frozen=True)
class FuturesBacktestTrade:
    symbol: str
    side: str
    entry_time: int
    exit_time: int
    entry_price: Decimal
    exit_price: Decimal
    quantity: Decimal
    notional: Decimal
    gross_pnl: Decimal
    fees: Decimal
    slippage: Decimal
    net_pnl: Decimal

    def __post_init__(self) -> None:
        object.__setattr__(self, "symbol", self.symbol.upper())


@dataclass(frozen=True)
class FuturesBacktestResult:
    total_candles: int
    trade_count: int
    win_count: int
    loss_count: int
    gross_pnl: Decimal
    total_fees: Decimal
    total_slippage: Decimal
    net_pnl: Decimal
    initial_equity: Decimal
    final_equity: Decimal
    total_return_percent: Decimal
    max_drawdown_percent: Decimal
    trades: tuple[FuturesBacktestTrade, ...]


def run_backtest(candles: Iterable[Candle], fast_window: int, slow_window: int) -> BacktestResult:
    strategy = SmaCrossoverStrategy(fast_window=fast_window, slow_window=slow_window)
    total = 0
    buys = 0
    sells = 0
    holds = 0
    for candle in candles:
        total += 1
        signal = strategy.on_candle(candle)
        if signal.signal_type is SignalType.BUY:
            buys += 1
        elif signal.signal_type is SignalType.SELL:
            sells += 1
        else:
            holds += 1
    return BacktestResult(total, buys, sells, holds)


def run_futures_backtest(
    candles: Iterable[Candle],
    fast_window: int,
    slow_window: int,
    *,
    initial_equity: Decimal = Decimal("1000"),
    margin_amount: Decimal = Decimal("100"),
    leverage: int = 1,
    fee_rate: Decimal = Decimal("0.0004"),
    slippage_rate: Decimal = Decimal("0.0001"),
) -> FuturesBacktestResult:
    if initial_equity <= 0:
        raise ValueError("initial_equity must be positive")
    if margin_amount <= 0:
        raise ValueError("margin_amount must be positive")
    if leverage <= 0:
        raise ValueError("leverage must be positive")
    if fee_rate < 0:
        raise ValueError("fee_rate must be greater than or equal to 0")
    if slippage_rate < 0:
        raise ValueError("slippage_rate must be greater than or equal to 0")

    rows = list(candles)
    strategy = SmaCrossoverStrategy(fast_window=fast_window, slow_window=slow_window)
    trades: list[FuturesBacktestTrade] = []
    position: _FuturesBacktestPosition | None = None
    equity = initial_equity
    peak_equity = initial_equity
    max_drawdown = Decimal("0")

    for candle in rows:
        signal = strategy.on_candle(candle)
        if signal.signal_type is SignalType.HOLD:
            continue
        desired_side = "LONG" if signal.signal_type is SignalType.BUY else "SHORT"
        if position is not None and position.side == desired_side:
            continue
        if position is not None:
            trade = _close_futures_position(position, candle, fee_rate, slippage_rate)
            trades.append(trade)
            equity += trade.net_pnl
            peak_equity = max(peak_equity, equity)
            max_drawdown = min(max_drawdown, (equity - peak_equity) / peak_equity * Decimal("100"))
        position = _open_futures_position(candle, desired_side, margin_amount, leverage)

    if position is not None and rows:
        trade = _close_futures_position(position, rows[-1], fee_rate, slippage_rate)
        trades.append(trade)
        equity += trade.net_pnl
        peak_equity = max(peak_equity, equity)
        max_drawdown = min(max_drawdown, (equity - peak_equity) / peak_equity * Decimal("100"))

    gross_pnl = sum((trade.gross_pnl for trade in trades), Decimal("0"))
    total_fees = sum((trade.fees for trade in trades), Decimal("0"))
    total_slippage = sum((trade.slippage for trade in trades), Decimal("0"))
    net_pnl = sum((trade.net_pnl for trade in trades), Decimal("0"))
    win_count = sum(1 for trade in trades if trade.net_pnl > 0)
    loss_count = sum(1 for trade in trades if trade.net_pnl < 0)
    return FuturesBacktestResult(
        total_candles=len(rows),
        trade_count=len(trades),
        win_count=win_count,
        loss_count=loss_count,
        gross_pnl=gross_pnl,
        total_fees=total_fees,
        total_slippage=total_slippage,
        net_pnl=net_pnl,
        initial_equity=initial_equity,
        final_equity=equity,
        total_return_percent=(equity - initial_equity) / initial_equity * Decimal("100"),
        max_drawdown_percent=abs(max_drawdown),
        trades=tuple(trades),
    )


@dataclass(frozen=True)
class _FuturesBacktestPosition:
    symbol: str
    side: str
    entry_time: int
    entry_price: Decimal
    quantity: Decimal
    notional: Decimal


def _open_futures_position(
    candle: Candle,
    side: str,
    margin_amount: Decimal,
    leverage: int,
) -> _FuturesBacktestPosition:
    notional = margin_amount * Decimal(leverage)
    return _FuturesBacktestPosition(
        symbol=candle.symbol,
        side=side,
        entry_time=candle.close_time,
        entry_price=candle.close,
        quantity=notional / candle.close,
        notional=notional,
    )


def _close_futures_position(
    position: _FuturesBacktestPosition,
    candle: Candle,
    fee_rate: Decimal,
    slippage_rate: Decimal,
) -> FuturesBacktestTrade:
    price_delta = candle.close - position.entry_price
    gross_pnl = price_delta * position.quantity
    if position.side == "SHORT":
        gross_pnl = -gross_pnl
    exit_notional = position.quantity * candle.close
    fees = (position.notional + exit_notional) * fee_rate
    slippage = (position.notional + exit_notional) * slippage_rate
    return FuturesBacktestTrade(
        symbol=position.symbol,
        side=position.side,
        entry_time=position.entry_time,
        exit_time=candle.close_time,
        entry_price=position.entry_price,
        exit_price=candle.close,
        quantity=position.quantity,
        notional=position.notional,
        gross_pnl=gross_pnl,
        fees=fees,
        slippage=slippage,
        net_pnl=gross_pnl - fees - slippage,
    )
