from __future__ import annotations

import json
from datetime import datetime, time
from typing import Any
from zoneinfo import ZoneInfo

from backend.data.contracts import SnapshotState
from backend.data.gateway import DataGateway
from backend.data.repository import MarketRepository
from backend.database.connection import Database
from backend.features.accounts.models import Principal
from backend.features.screener.backtest import attach_historical_estimate, rolling_backtest
from backend.features.screener.catalog import (
    CatalogError,
    factor_catalog,
    strategy_catalog,
    validate_formula,
)
from backend.features.screener.compiler import (
    PROMPT_VERSION,
    compile_messages,
    description_key,
    parse_compiled_formula,
)
from backend.features.screener.engine import execute_formula
from backend.features.screener.factors import build_factor_snapshot
from backend.features.screener.repository import (
    ScreenerRepository,
    decode_custom,
    decode_run,
    decode_track,
)
from backend.llm.gateway import LLMGateway

SHANGHAI = ZoneInfo("Asia/Shanghai")
PHASE_REGIMES = {
    "冰点": "ice",
    "修复": "repair",
    "发酵": "fermentation",
    "高潮": "climax",
    "分化": "divergence",
    "退潮": "retreat",
}
FINISHED = frozenset({"completed", "no_signal", "data_incomplete"})


class ScreenerError(RuntimeError):
    pass


class ScreenerService:
    def __init__(
        self,
        database: Database,
        repository: ScreenerRepository,
        market_repository: MarketRepository,
        gateway: DataGateway,
        llm: LLMGateway,
    ) -> None:
        self._database = database
        self._repository = repository
        self._market_repository = market_repository
        self._gateway = gateway
        self._llm = llm

    def catalog(self) -> dict[str, Any]:
        factors = factor_catalog()
        strategies = strategy_catalog()
        return {
            "factor_groups": factors["groups"],
            "factors": factors["factors"],
            "stage": [_public_strategy(item) for item in strategies if item["kind"] == "stage"],
            "curated": [_public_strategy(item) for item in strategies if item["kind"] == "curated"],
        }

    def workspace(self, requested_date: str, user_id: int) -> dict[str, Any]:
        through = self._gateway.trade_context(requested_date).actual_date
        with self._database.read() as connection:
            custom = [
                decode_custom(row)
                for row in self._repository.custom_strategies(connection, user_id)
            ]
        if through is None:
            return {
                "trade_date": None,
                "message": "等待管理员首次同步真实收盘行情",
                "catalog": self.catalog(),
                "stage_runs": [],
                "curated_runs": [],
                "custom_strategies": custom,
                "custom_runs": [],
            }
        with self._database.read() as connection:
            stage = [
                decode_run(row)
                for row in self._repository.latest_runs(connection, "stage", through)
            ]
            curated = [
                decode_run(row)
                for row in self._repository.latest_runs(connection, "curated", through)
            ]
            custom_runs = []
            for row in self._repository.latest_runs(connection, "custom", through, user_id):
                decoded = decode_run(row)
                if decoded is not None:
                    custom_runs.append(
                        attach_historical_estimate(
                            decoded,
                            self._repository.backtest(connection, int(row["id"])),
                        )
                    )
        return {
            "trade_date": through,
            "message": "",
            "catalog": self.catalog(),
            "stage_runs": stage,
            "curated_runs": curated,
            "custom_strategies": custom,
            "custom_runs": custom_runs,
        }

    def sync_and_run(self, trade_date: str) -> dict[str, Any]:
        market = self._market_snapshot(trade_date)
        inputs, coverage, sources = self._gateway.screener_inputs(trade_date)
        snapshot = build_factor_snapshot(trade_date, inputs, coverage, sources)
        state = str(market["state"])
        with self._database.transaction() as connection:
            snapshot_id = self._repository.save_factor_snapshot(
                connection,
                trade_date=trade_date,
                version=snapshot["version"],
                observed_at=snapshot["observed_at"],
                state=state,
                sources=snapshot["sources"],
                coverage=snapshot["coverage"],
                rows=snapshot["rows"],
            )
        phase = str((market["payload"].get("sentiment") or {}).get("phase") or "")
        regime = PHASE_REGIMES.get(phase)
        stage_strategies, curated_strategies = automatic_strategies(regime)
        runs = [
            self._run(snapshot_id, trade_date, strategy, None)
            for strategy in [*stage_strategies, *curated_strategies]
        ]
        self._update_tracks(trade_date, snapshot["rows"])
        return {
            "trade_date": trade_date,
            "factor_version": snapshot["version"],
            "factor_count": len(snapshot["rows"]),
            "phase": phase,
            "stage_runs": len(stage_strategies),
            "curated_runs": len(curated_strategies),
            "completed": sum(run and run["status"] in FINISHED for run in runs),
            "failed": sum(run and run["status"] == "failed" for run in runs),
        }

    def run_after_close(self, now: datetime | None = None) -> dict[str, Any] | None:
        clock = now or datetime.now(SHANGHAI)
        if clock.time() < time(15, 10):
            return None
        trade_date = clock.date().isoformat()
        market = self._market_snapshot(trade_date)
        if market["trade_date"] != trade_date or market["state"] != SnapshotState.FINAL.value:
            return None
        return self.sync_and_run(trade_date)

    def save_custom(self, user_id: int, name: str, formula: dict[str, Any]) -> dict[str, Any]:
        normalized = " ".join(name.split())
        if not normalized or len(normalized) > 30:
            raise ScreenerError("自定义策略名称应为1至30个字符")
        try:
            validate_formula(formula)
        except CatalogError as exc:
            raise ScreenerError(str(exc)) from exc
        with self._database.transaction() as connection:
            return decode_custom(
                self._repository.save_custom_strategy(connection, user_id, normalized, formula)
            )

    def compile_formula(self, principal: Principal, description: str) -> dict[str, Any]:
        normalized = " ".join(description.split())
        if not 5 <= len(normalized) <= 500:
            raise ScreenerError("自然语言条件应为5至500个字符")
        call = self._llm.prepare(
            principal,
            feature="screener_formula",
            prompt_version=PROMPT_VERSION,
            business_id=description_key(normalized),
            input_chars=len(normalized),
        )
        content = "".join(
            event.content
            for event in self._llm.stream(call, compile_messages(normalized))
            if event.type == "delta"
        )
        try:
            formula = parse_compiled_formula(content)
        except CatalogError as exc:
            raise ScreenerError(f"公式转换失败：{exc}") from exc
        return {"formula": formula, "prompt_version": PROMPT_VERSION}

    def delete_custom(self, user_id: int, strategy_id: int) -> None:
        with self._database.transaction() as connection:
            if not self._repository.delete_custom_strategy(connection, user_id, strategy_id):
                raise ScreenerError("未找到该自定义策略")

    def run_custom(self, user_id: int, strategy_id: int, through: str) -> dict[str, Any]:
        with self._database.read() as connection:
            custom = self._repository.custom_strategy(connection, user_id, strategy_id)
            snapshot = self._repository.latest_factor_snapshot(connection, through)
        if custom is None:
            raise ScreenerError("未找到该自定义策略")
        if snapshot is None:
            raise ScreenerError("当前日期尚未生成完整因子快照")
        strategy = {
            "id": f"custom-{custom['id']}",
            "name": str(custom["name"]),
            "version": int(custom["version"]),
            "kind": "custom",
            "formula": json.loads(str(custom["formula_json"])),
        }
        result = self._run(int(snapshot["id"]), str(snapshot["trade_date"]), strategy, user_id)
        if result is None:
            raise ScreenerError("自定义策略执行失败")
        return self._with_backtest(result, strategy["formula"], str(snapshot["trade_date"]))

    def tracks(self, user_id: int) -> list[dict[str, Any]]:
        with self._database.read() as connection:
            return [
                decode_track(row, self._repository.track_bars(connection, int(row["id"])))
                for row in self._repository.tracks(connection, user_id)
            ]

    def add_track(self, user_id: int, run_id: int, identifier: str) -> int:
        with self._database.transaction() as connection:
            run = self._repository.run_for_user(connection, run_id, user_id)
            if run is None:
                raise ScreenerError("未找到可访问的选股结果")
            items = json.loads(str(run["result_json"]))
            candidate = next(
                (item for item in items if str(item.get("identifier")) == identifier), None
            )
            if (
                candidate is None
                or not isinstance(candidate.get("close"), (int, float))
                or float(candidate["close"]) <= 0
            ):
                raise ScreenerError("该候选无法加入持续跟踪")
            return self._repository.add_track(
                connection, user_id=user_id, run=run, candidate=candidate
            )

    def remove_track(self, user_id: int, track_id: int) -> None:
        with self._database.transaction() as connection:
            if not self._repository.remove_track(connection, user_id, track_id):
                raise ScreenerError("未找到该跟踪记录")

    def _run(
        self,
        snapshot_id: int,
        trade_date: str,
        strategy: dict[str, Any],
        owner_user_id: int | None,
    ) -> dict[str, Any] | None:
        mode = str(strategy["kind"])
        with self._database.transaction() as connection:
            row = self._repository.begin_run(
                connection,
                owner_user_id=owner_user_id,
                mode=mode,
                strategy_id=str(strategy["id"]),
                strategy_name=str(strategy["name"]),
                strategy_version=int(strategy["version"]),
                selection_date=trade_date,
                factor_snapshot_id=snapshot_id,
            )
            existing = decode_run(row)
            if existing and existing["status"] in FINISHED:
                return existing
            snapshot = self._repository.factor_snapshot(connection, snapshot_id)
            rows = self._repository.factor_rows(connection, snapshot_id)
        if snapshot is None:
            raise ScreenerError("因子快照不存在")
        coverage = json.loads(str(snapshot["coverage_json"]))
        try:
            outcome = execute_formula(rows, strategy["formula"], coverage)
            status = str(outcome["status"])
            missing = [*outcome["missing_datasets"], *outcome["missing_fields"]]
            score_coverage = min(outcome["field_coverage"].values(), default=0)
            error = ""
        except (CatalogError, KeyError, TypeError, ValueError) as exc:
            status, missing, score_coverage, outcome, error = (
                "failed",
                [],
                0,
                {"items": []},
                str(exc),
            )
        with self._database.transaction() as connection:
            self._repository.finish_run(
                connection,
                int(row["id"]),
                status=status,
                coverage=score_coverage,
                missing_fields=missing,
                result=outcome["items"],
                error_message=error,
            )
            return decode_run(
                connection.execute(
                    "SELECT * FROM screener_runs WHERE id = ?", (row["id"],)
                ).fetchone()
            )

    def _with_backtest(
        self,
        run: dict[str, Any],
        formula: dict[str, Any],
        through: str,
    ) -> dict[str, Any]:
        run_id = int(run["id"])
        with self._database.read() as connection:
            existing = self._repository.backtest(connection, run_id)
        if existing is None:
            with self._database.read() as connection:
                rows = self._repository.factor_snapshots_through(connection, through, 63)
                history = [
                    {
                        "trade_date": str(row["trade_date"]),
                        "coverage": json.loads(str(row["coverage_json"])),
                        "rows": self._repository.factor_rows(connection, int(row["id"])),
                    }
                    for row in reversed(rows)
                ]
            calculated = rolling_backtest(history, formula)
            with self._database.transaction() as connection:
                self._repository.save_backtest(connection, run_id, calculated)
                existing = self._repository.backtest(connection, run_id)
        return attach_historical_estimate(run, existing)

    def _market_snapshot(self, trade_date: str) -> dict[str, Any]:
        with self._database.read() as connection:
            row = self._market_repository.latest_summary(connection, trade_date)
        if row is None or str(row["trade_date"]) != trade_date:
            raise ScreenerError("当日收盘行情尚未完成，选股任务未启动")
        if str(row["state"]) not in {SnapshotState.FINAL.value, SnapshotState.ARCHIVE.value}:
            raise ScreenerError("行情快照尚未收盘定稿")
        return {
            "trade_date": str(row["trade_date"]),
            "state": str(row["state"]),
            "payload": json.loads(str(row["payload_json"])),
        }

    def _update_tracks(self, trade_date: str, rows: list[dict[str, Any]]) -> None:
        current = {str(row["identifier"]): row for row in rows}
        with self._database.transaction() as connection:
            for track in self._repository.tracked_before(connection, trade_date):
                row = current.get(str(track["identifier"]))
                if row is None or any(
                    row.get(field) is None for field in ("open", "high", "low", "close")
                ):
                    continue
                self._repository.save_track_bar(connection, int(track["id"]), trade_date, row)
                days = len(self._repository.track_bars(connection, int(track["id"])))
                if days in {1, 5}:
                    self._repository.record_track_event(connection, int(track["id"]), f"t{days}")


def _public_strategy(strategy: dict[str, Any]) -> dict[str, Any]:
    return {
        "id": strategy["id"],
        "version": strategy["version"],
        "kind": strategy["kind"],
        "name": strategy["name"],
        "display_name": strategy.get("display_name") or strategy["name"],
        "description": strategy["description"],
        "regimes": strategy.get("regimes") or [],
        "formula": strategy["formula"],
    }


def automatic_strategies(
    regime: str | None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    strategies = strategy_catalog()
    stage = [
        item
        for item in strategies
        if item["kind"] == "stage" and regime in (item.get("regimes") or [])
    ]
    curated = [item for item in strategies if item["kind"] == "curated"]
    return stage, curated
