from __future__ import annotations

import statistics
from collections import defaultdict
from datetime import datetime
from typing import Any

from backend.data.numbers import finite_number as _number
from backend.features.screener.factors import FactorBuilder
from backend.features.screener.formula import FormulaEvaluator
from database import ReviewDatabase


class BacktestRunner:
    def __init__(
        self,
        database: ReviewDatabase,
        factor_builder: FactorBuilder,
        formula_evaluator: FormulaEvaluator,
    ) -> None:
        self.database = database
        self.factor_builder = factor_builder
        self.formula_evaluator = formula_evaluator
        self._backtest_factor_cache: dict[tuple[str, int], list[dict[str, Any]]] = {}

    def build_factors(
        self, trade_date: str, history_days: int
    ) -> tuple[list[dict[str, Any]], str]:
        return self.factor_builder.build_factors(
            trade_date, history_days=history_days
        )

    def apply_formula(
        self, rows: list[dict[str, Any]], formula: dict[str, Any], regime: str
    ) -> list[dict[str, Any]]:
        return self.formula_evaluator.apply_formula(rows, formula, regime)

    def backtest(self, trade_date: str, formula: dict[str, Any]) -> dict[str, Any]:
        meta = formula.get("meta") or {}
        history_days = max(21, min(260, int(meta.get("history_days") or 80)))
        holding_days = max(1, min(30, int(meta.get("backtest_days") or 3)))
        take_profit = max(0.5, min(50.0, float(meta.get("take_profit") or 3)))
        stop_loss = min(-0.5, max(-50.0, float(meta.get("stop_loss") or -3)))
        dates = self.database.factor_dates(trade_date, history_days + holding_days + 20)
        eligible_dates = dates[:-holding_days] if len(dates) > holding_days else []
        frequency = str(meta.get("frequency") or "每日")
        if "月" in frequency:
            grouped = {}
            for value in eligible_dates:
                grouped[value[:6]] = value
            evaluation_dates = list(grouped.values())[-8:]
        elif "双周" in frequency:
            weekly_dates = []
            grouped = {}
            for value in eligible_dates:
                parsed = datetime.strptime(value, "%Y%m%d")
                grouped[parsed.strftime("%G-%V")] = value
            weekly_dates = list(grouped.values())
            evaluation_dates = weekly_dates[-16::2][-8:]
        elif "周" in frequency:
            grouped = {}
            for value in eligible_dates:
                parsed = datetime.strptime(value, "%Y%m%d")
                grouped[parsed.strftime("%G-%V")] = value
            evaluation_dates = list(grouped.values())[-8:]
        else:
            evaluation_dates = eligible_dates[-8:]
        wins = 0
        losses = 0
        samples = 0
        returns = []
        drawdowns = []
        all_data = self.database.load_factor_data(
            trade_date, history_days + holding_days + 20
        )
        bars_by_code: dict[str, list[dict[str, Any]]] = defaultdict(list)
        for row in all_data["bars"]:
            bars_by_code[row["ts_code"]].append(row)
        for bars in bars_by_code.values():
            bars.sort(key=lambda item: item["trade_date"])

        for current_date in evaluation_dates:
            try:
                cache_key = (current_date, history_days)
                factors = self._backtest_factor_cache.get(cache_key)
                if factors is None:
                    factors, _ = self.build_factors(
                        current_date, history_days=history_days
                    )
                    if len(self._backtest_factor_cache) >= 64:
                        self._backtest_factor_cache.pop(
                            next(iter(self._backtest_factor_cache))
                        )
                    self._backtest_factor_cache[cache_key] = factors
            except ValueError:
                continue
            selected = self.apply_formula(factors, {**formula, "limit": min(10, formula["limit"])}, "backtest")
            for candidate in selected:
                bars = bars_by_code.get(candidate["ts_code"], [])
                index = next((i for i, row in enumerate(bars) if row["trade_date"] == current_date), -1)
                future = bars[index + 1:index + 1 + holding_days] if index >= 0 else []
                if len(future) < holding_days:
                    continue
                entry = candidate["price"]
                won = False
                lost = False
                for day in future:
                    low_return = (_number(day["low"]) / entry - 1) * 100
                    high_return = (_number(day["high"]) / entry - 1) * 100
                    if low_return <= stop_loss:
                        lost = True
                        break
                    if high_return >= take_profit:
                        won = True
                        break
                if won:
                    wins += 1
                elif lost:
                    losses += 1
                samples += 1
                returns.append((_number(future[-1]["close"]) / entry - 1) * 100)
                drawdowns.append(min((_number(day["low"]) / entry - 1) * 100 for day in future))
        return {
            "samples": samples,
            "wins": wins,
            "losses": losses,
            "win_rate": round(wins / samples * 100, 1) if samples else 0,
            "average_3d_return": round(statistics.fmean(returns), 2) if returns else 0,
            "average_holding_return": round(statistics.fmean(returns), 2) if returns else 0,
            "average_drawdown": round(statistics.fmean(drawdowns), 2) if drawdowns else 0,
            "evaluation_days": len(evaluation_dates),
            "frequency": frequency,
            "holding_days": holding_days,
            "take_profit": take_profit,
            "stop_loss": stop_loss,
            "definition": (
                f"收盘后选股，未来{holding_days}日先触及+{take_profit:g}%且未先触及"
                f"{stop_loss:g}%计为成功；同日双触发按失败处理。"
            ),
            "approximate": True,
        }
