from __future__ import annotations

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

from .config import Settings
from .exchange import BinanceSpotClient
from .execution import ExecutionEngine, order_intent_from_decision
from .market_data import stream_closed_klines
from .models import AccountSnapshot, Balance, Candle, OrderIntent, OrderResult, TradingMode
from .risk import RiskEngine, RiskSettings
from .storage import Storage
from .strategy import SmaCrossoverStrategy


@dataclass(frozen=True)
class TradeLoopSummary:
    processed_candles: int
    signals: int
    approved: int
    rejected: int
    orders: int


async def run_trade_loop(
    settings: Settings,
    exchange_client=None,
    candle_stream: AsyncIterable[Candle] | None = None,
    max_candles: int | None = None,
) -> TradeLoopSummary:
    if max_candles is not None and max_candles < 0:
        raise ValueError("max_candles must be non-negative")

    storage = Storage(settings.database_path)
    storage.initialize()

    if max_candles == 0:
        return TradeLoopSummary(0, 0, 0, 0, 0)

    exchange = exchange_client or BinanceSpotClient(
        api_key=settings.api_key,
        api_secret=settings.api_secret.get_secret_value(),
        base_url=settings.rest_base_url,
        base_urls=settings.rest_base_urls,
        recv_window=settings.recv_window,
        proxy_url=settings.proxy_url,
    )
    metadata = exchange.exchange_info(settings.symbol)

    candles = candle_stream or stream_closed_klines(settings.symbol, stream_base_url=settings.stream_base_url)
    strategy = SmaCrossoverStrategy(settings.fast_window, settings.slow_window)
    risk = RiskEngine(
        RiskSettings(
            mode=settings.mode,
            max_quote_amount=settings.max_quote_amount,
            cooldown_seconds=settings.cooldown_seconds,
            prevent_repeated_side=settings.prevent_repeated_side,
        )
    )
    execution = ExecutionEngine(settings.mode, exchange)

    processed = signals = approved = rejected = orders = 0
    async for candle in candles:
        processed += 1
        signal = strategy.on_candle(candle)
        storage.record_signal(signal)
        signals += 1

        account = _account_for_mode(
            settings.mode,
            exchange,
            metadata.base_asset,
            metadata.quote_asset,
            settings.max_quote_amount,
        )
        decision = risk.evaluate(
            signal,
            metadata,
            account,
            settings.quote_amount,
            now_ms=candle.close_time,
            last_price=candle.close,
        )
        storage.record_risk_decision(signal, decision.approved, decision.reason, decision.quote_amount)
        if not decision.approved:
            rejected += 1
        else:
            approved += 1
            intent = order_intent_from_decision(decision)
            try:
                result = execution.execute(intent)
            except Exception as exc:
                storage.record_order_result(_failed_order_result(intent, exc))
                raise
            storage.record_order_result(result)
            orders += 1

        if max_candles is not None and processed >= max_candles:
            break

    return TradeLoopSummary(processed, signals, approved, rejected, orders)


def _account_for_mode(
    mode: TradingMode,
    exchange,
    base_asset: str,
    quote_asset: str,
    max_quote_amount: Decimal,
) -> AccountSnapshot:
    if mode in {TradingMode.TESTNET_LIVE, TradingMode.LIVE}:
        return exchange.account()
    return AccountSnapshot(
        {
            quote_asset: Balance(quote_asset, max_quote_amount * Decimal("100")),
            base_asset: Balance(base_asset, Decimal("1")),
        }
    )


def _failed_order_result(intent: OrderIntent, exc: Exception) -> OrderResult:
    return OrderResult(
        symbol=intent.symbol,
        side=intent.side,
        client_order_id=intent.client_order_id,
        status="FAILED",
        raw={
            "error_type": type(exc).__name__,
            "message": "order submission failed",
        },
        dry_run=False,
    )
