from __future__ import annotations

import copy
import re
from datetime import date, datetime
from typing import Any

from backend.bootstrap.config import normalize_date, validate_text
from backend.data.providers.tushare_client import TushareError
from backend.llm import LLMGatewayError
from backend.features.screener.compiler import (
    LLMCompilerError,
    compile_strategy_with_llm,
)
from backend.features.screener.engine import (
    FACTOR_FIELDS,
    FACTOR_GROUPS,
    REGIMES,
    FactorDataService,
    compile_local_strategy,
)


SCREENER_LIBRARY_VERSION = 8


def automatic_screener_jobs(
    strategies: list[dict[str, Any]], regime_id: str
) -> list[dict[str, Any]]:
    """Build the close-of-day jobs; only stage screening is regime-gated."""
    smart_strategy = next(
        (
            item for item in strategies
            if item.get("formula", {}).get("meta", {}).get("library") != "curated"
            and regime_id in (item.get("regimes") or [])
        ),
        None,
    )
    curated = [
        item for item in strategies
        if item.get("formula", {}).get("meta", {}).get("library") == "curated"
    ]
    jobs = ([{"mode": "smart", "strategy": smart_strategy}] if smart_strategy else [])
    jobs.extend({"mode": "curated", "strategy": item} for item in curated)
    return jobs


class ScreenerServiceMixin:
    @staticmethod
    def _strategy_missing_data(
        strategy: dict[str, Any], factor_dates: list[str], factor_health: dict[str, Any]
    ) -> list[str]:
        formula = strategy.get("formula") or {}
        meta = formula.get("meta") or {}
        used_fields = {
            str(item.get("field") or "")
            for item in list(formula.get("filters") or []) + list(formula.get("score") or [])
        }
        valuation_fields = {"pe_ttm", "pb", "ps_ttm", "dividend_yield_ttm", "total_mv_billion"}
        fundamental_fields = {"roe", "roa", "roic", "gross_margin", "netprofit_yoy", "revenue_yoy", "ocf_to_opincome"}
        auction_fields = {"auction_change", "auction_amount_million", "auction_turnover_rate", "auction_volume_ratio"}
        missing = []
        required_history = max(21, min(260, int(meta.get("history_days") or 21)))
        if len(factor_dates) < required_history:
            missing.append(f"历史行情（需{required_history}日）")
        if used_fields & valuation_fields and not factor_health["valuation"]:
            missing.append("估值数据")
        if used_fields & fundamental_fields and not factor_health["fundamental"]:
            missing.append("财务质量")
        if meta.get("requires_valuation") and not factor_health["valuation"]:
            missing.append("估值数据")
        if meta.get("requires_fundamental") and not factor_health["fundamental"]:
            missing.append("财务质量")
        if "dividend_years" in used_fields and not factor_health["dividend_history"]:
            missing.append("历年分红")
        if used_fields & auction_fields and not factor_health["auction"]:
            missing.append("竞价数据")
        if meta.get("requires_benchmark") and not factor_health.get("benchmark"):
            missing.append("沪深300基准")
        if meta.get("requires_moneyflow_history") and not factor_health.get("moneyflow_history"):
            missing.append("近5日资金流")
        if meta.get("requires_earnings_events") and not factor_health.get("earnings_events"):
            missing.append("业绩预告与快报")
        if meta.get("requires_popularity") and not factor_health.get("popularity"):
            missing.append("当日人气榜")
        if meta.get("requires_institutions") and not factor_health.get("institutions"):
            missing.append("龙虎榜机构席位")
        return list(dict.fromkeys(missing))

    def screener_setup(self, trade_date: str) -> dict[str, Any]:
        normalized_date = normalize_date(trade_date)
        regime = self.screener.detect_regime(normalized_date)
        factor_dates = self.database.factor_dates(normalized_date, 300)
        auction_dates = self.database.auction_factor_dates(normalized_date, 100)
        factor_health = self.screener.factor_health(normalized_date)
        strategies = self.database.list_screener_strategies(self.current_user_id)
        for strategy in strategies:
            missing = self._strategy_missing_data(strategy, factor_dates, factor_health)
            strategy["data_ready"] = not missing
            strategy["missing_data"] = missing
        automatic_results = self.database.screener_runs_for_date(0, normalized_date)
        personal_results = self.database.screener_runs_for_date(
            self.current_user_id, normalized_date
        )
        recent_results = [
            *[item for item in automatic_results if item.get("meta", {}).get("mode") in {"smart", "curated"}],
            *[item for item in personal_results if item.get("meta", {}).get("mode") == "quant"],
        ]
        latest_results: dict[str, dict[str, Any]] = {}
        for result in reversed(recent_results):
            mode = str(result.get("meta", {}).get("mode") or "smart")
            latest_results[mode] = result
        automatic_status = self.database.get_data_snapshot(
            "screener_auto_v1", normalized_date
        ) or {}
        return {
            "trade_date": normalized_date,
            "regime": regime,
            "regimes": [{"id": key, "label": value} for key, value in REGIMES.items()],
            "strategies": strategies,
            "factor_fields": [{"id": key, "label": value} for key, value in FACTOR_FIELDS.items()],
            "factor_groups": [
                {
                    "name": name,
                    "fields": [{"id": field, "label": FACTOR_FIELDS[field]} for field in fields],
                }
                for name, fields in FACTOR_GROUPS.items()
            ],
            "operators": [">", ">=", "<", "<=", "==", "between"],
            "factor_data": {
                "date_count": len(factor_dates),
                "start_date": factor_dates[0] if factor_dates else "",
                "end_date": factor_dates[-1] if factor_dates else "",
                "ready": len(factor_dates) >= 21,
                "auction_date_count": len(auction_dates),
                "auction_ready": bool(auction_dates and auction_dates[-1] == factor_dates[-1]) if factor_dates else False,
                "health": factor_health,
            },
            "llm": {
                "configured": self.llm_configured,
                "model": self.llm_primary_model if self.llm_configured else "",
                "fallback_configured": self.llm_fallback_configured,
                "fallback_model": self.llm_fallback_model if self.llm_fallback_configured else "",
            },
            "latest_results": latest_results,
            "recent_results": recent_results,
            "automatic_status": automatic_status,
            # Kept during the client transition for compatibility with older frontends.
            "latest_result": latest_results.get("smart"),
        }

    def screener_tracking(self, limit: int = 12) -> dict[str, Any]:
        return self.strategy_tracking.list_tracking(self.current_user_id, limit)

    def add_screener_tracking(self, payload: dict[str, Any]) -> dict[str, Any]:
        try:
            run_id = int(payload.get("run_id") or 0)
        except (TypeError, ValueError) as exc:
            raise ValueError("选股批次无效。") from exc
        code = str(payload.get("code") or "").strip()
        if run_id <= 0 or not re.fullmatch(r"\d{6}", code):
            raise ValueError("选股批次或股票代码无效。")
        return self.strategy_tracking.add_candidate(self.current_user_id, run_id, code)

    def remove_screener_tracking(self, track_id: int) -> dict[str, Any]:
        return self.strategy_tracking.remove_candidate(self.current_user_id, track_id)

    def refresh_screener_tracking(self, trade_date: str) -> dict[str, Any]:
        normalized_date = normalize_date(trade_date)
        notice = ""
        if self.configured:
            try:
                FactorDataService(self.database, self._tushare_client()).sync(
                    normalized_date, 15
                )
            except TushareError:
                notice = "最新日线暂未补齐，已按现有数据更新跟踪。"
        else:
            notice = "公共行情尚未配置，已按现有数据更新跟踪。"
        return {
            "tracking": self.screener_tracking(),
            "notice": notice,
        }

    def sync_screener_data(self, trade_date: str, lookback: int = 45) -> dict[str, Any]:
        if not self.configured:
            raise ValueError("请先配置 Tushare Token。")
        normalized_date = normalize_date(trade_date)
        lookback = max(25, min(260, int(lookback)))
        with self.sync_lock:
            return FactorDataService(self.database, self._tushare_client()).sync(
                normalized_date, lookback
            )

    def _schedule_automatic_screeners(
        self, trade_date: str, snapshot: dict[str, Any] | None = None
    ) -> bool:
        normalized_date = normalize_date(trade_date)
        now = datetime.now().astimezone()
        if (
            normalized_date != now.strftime("%Y%m%d")
            or now.weekday() >= 5
            or now.time().replace(tzinfo=None) < datetime.strptime("15:10", "%H:%M").time()
            or self.auto_screener_lock.locked()
        ):
            return False
        snapshot = snapshot or self.database.get_snapshot(normalized_date) or {}
        actual_date = str((snapshot.get("meta") or {}).get("trade_date") or "").replace("-", "")
        if actual_date != normalized_date:
            return False
        marker = self.database.get_data_snapshot("screener_auto_v1", normalized_date) or {}
        if (
            marker.get("status") == "complete"
            and int(marker.get("library_version") or 0) == SCREENER_LIBRARY_VERSION
        ):
            return False
        last_attempt = self._auto_screener_last_attempt.get(normalized_date)
        if last_attempt and (now - last_attempt).total_seconds() < 600:
            return False
        self._auto_screener_last_attempt[normalized_date] = now
        return self.jobs.submit(
            "screener.automatic",
            f"{normalized_date}:v{SCREENER_LIBRARY_VERSION}",
            lambda: self.run_automatic_screeners(normalized_date),
            {"trade_date": normalized_date, "trigger": "post-close"},
        )

    def run_automatic_screeners(self, trade_date: str) -> dict[str, Any]:
        normalized_date = normalize_date(trade_date)
        with self.auto_screener_lock:
            started_at = datetime.now().astimezone().isoformat(timespec="seconds")
            status: dict[str, Any] = {
                "trade_date": normalized_date,
                "library_version": SCREENER_LIBRARY_VERSION,
                "status": "running",
                "started_at": started_at,
                "completed": [],
                "skipped": [],
                "failed": [],
            }
            self.database.save_data_snapshot(
                "screener_auto_v1", normalized_date, "system", status
            )
            try:
                factor_sync = FactorDataService(
                    self.database, self._tushare_client()
                ).sync(normalized_date, 260)
                factor_dates = self.database.factor_dates(normalized_date, 300)
                if not factor_dates or factor_dates[-1] != normalized_date:
                    raise ValueError("当日收盘行情尚未入库")
                factor_health = self.screener.factor_health(normalized_date)
                regime = self.screener.detect_regime(normalized_date)
                regime_id = str(regime.get("id") or "repair")
                strategies = self.database.list_screener_strategies(None)
                jobs = automatic_screener_jobs(strategies, regime_id)
                existing = {
                    (
                        str(item.get("meta", {}).get("mode") or "smart"),
                        str(item.get("meta", {}).get("strategy_name") or ""),
                    )
                    for item in self.database.screener_runs_for_date(0, normalized_date)
                    if int(item.get("meta", {}).get("library_version") or 0)
                    == SCREENER_LIBRARY_VERSION
                }
                required_history = max(
                    [
                        int((job["strategy"].get("formula", {}).get("meta", {}) or {}).get("history_days") or 80)
                        for job in jobs if job.get("strategy")
                    ] or [80]
                )
                factors, actual_date = self.screener.build_factors(
                    normalized_date, history_days=required_history
                )
                if actual_date != normalized_date:
                    raise ValueError("当日因子尚未完成收盘定格")
                for job in jobs:
                    strategy = job["strategy"]
                    mode = str(job["mode"])
                    name = str(strategy.get("name") or "未命名策略")
                    if (mode, name) in existing:
                        status["completed"].append({"mode": mode, "name": name, "cached": True})
                        continue
                    missing = self._strategy_missing_data(
                        strategy, factor_dates, factor_health
                    )
                    if missing:
                        status["skipped"].append(
                            {"mode": mode, "name": name, "reason": "、".join(missing)}
                        )
                        continue
                    try:
                        formula = copy.deepcopy(strategy.get("formula") or {})
                        formula.setdefault("meta", {})["library_version"] = (
                            SCREENER_LIBRARY_VERSION
                        )
                        result = self.screener.screen(
                            0,
                            normalized_date,
                            formula,
                            regime_id,
                            name,
                            False,
                            None,
                            mode,
                            factors,
                            actual_date,
                        )
                        status["completed"].append(
                            {
                                "mode": mode,
                                "name": name,
                                "candidate_count": len(result.get("candidates") or []),
                            }
                        )
                    except Exception as exc:
                        status["failed"].append(
                            {"mode": mode, "name": name, "reason": str(exc)}
                        )
                status.update(
                    {
                        "status": "complete" if not status["failed"] else "partial",
                        "finished_at": datetime.now().astimezone().isoformat(timespec="seconds"),
                        "factor_sync": factor_sync,
                        "regime": regime,
                    }
                )
            except Exception as exc:
                status.update(
                    {
                        "status": "failed",
                        "finished_at": datetime.now().astimezone().isoformat(timespec="seconds"),
                        "error": str(exc),
                    }
                )
            self.database.save_data_snapshot(
                "screener_auto_v1", normalized_date, "system", status
            )
            return status

    def compile_screener_strategy(self, prompt: str, regime: str) -> dict[str, Any]:
        prompt = prompt.strip()
        if not prompt or len(prompt) > 3000:
            raise ValueError("策略描述应为 1 至 3000 个字符。")
        if regime not in REGIMES:
            raise ValueError("市场阶段不支持。")
        notice = ""
        source = self.llm_source
        if source == "platform":
            try:
                gateway_result = self.llm_gateway.call(
                    "screener",
                    "strategy-compiler-v1",
                    lambda profile: compile_strategy_with_llm(
                        prompt,
                        regime,
                        profile.api_key,
                        profile.base_url,
                        profile.model,
                    ),
                    (LLMCompilerError,),
                )
                compiled = gateway_result.value
                if gateway_result.role == "fallback":
                    compiled["compiler"] = "llm_fallback"
                    notice = "智能策略生成服务已自动切换。"
            except LLMGatewayError as exc:
                if exc.code != "unavailable":
                    raise
                compiled = compile_local_strategy(prompt, regime)
                notice = "智能策略生成暂不可用，已使用本地模板。"
        else:
            compiled = compile_local_strategy(prompt, regime)
            notice = "智能策略生成暂不可用，已使用本地模板。"
        compiled["formula"] = self.screener.validate_formula(compiled["formula"])
        compiled["notice"] = notice
        return compiled

    def save_screener_strategy(self, payload: dict[str, Any]) -> dict[str, Any]:
        name = validate_text(payload.get("name"), "策略名称", 60, required=True)
        description = validate_text(payload.get("description"), "策略说明", 1000)
        regimes = payload.get("regimes") or []
        if not isinstance(regimes, list) or not regimes or any(item not in REGIMES for item in regimes):
            raise ValueError("策略适用阶段不正确。")
        formula = self.screener.validate_formula(payload.get("formula") or {})
        strategy_id = self.database.save_screener_strategy(
            self.current_user_id, name, description, regimes, formula
        )
        return {
            "id": strategy_id,
            "strategies": self.database.list_screener_strategies(self.current_user_id),
        }

    def delete_screener_strategy(self, strategy_id: int) -> dict[str, Any]:
        deleted = self.database.delete_screener_strategy(self.current_user_id, strategy_id)
        return {
            "deleted": deleted,
            "strategies": self.database.list_screener_strategies(self.current_user_id),
        }

    def run_screener(self, payload: dict[str, Any]) -> dict[str, Any]:
        trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat()))
        regime = str(payload.get("regime") or "")
        if regime not in REGIMES:
            raise ValueError("市场阶段不支持。")
        strategy_name = validate_text(payload.get("strategy_name"), "策略名称", 60, required=True)
        formula = payload.get("formula") or {}
        requested_mode = str(payload.get("mode") or "").strip()
        if requested_mode and requested_mode not in {"smart", "curated", "quant"}:
            raise ValueError("选股模式不受支持。")
        if requested_mode:
            mode = requested_mode
        else:
            meta = formula.get("meta") if isinstance(formula, dict) else {}
            library = str((meta or {}).get("library") or "")
            category = str((meta or {}).get("category") or "")
            if library == "curated":
                mode = "curated"
            elif library == "quant" or (library == "custom" and category == "量化公式"):
                mode = "quant"
            else:
                mode = "smart"
        realtime_snapshot = None
        dashboard = self.get_dashboard(trade_date)
        if self.configured and dashboard.get("meta", {}).get("realtime"):
            try:
                realtime_snapshot = self._tushare_client().realtime_factor_snapshot(trade_date)
            except TushareError as exc:
                raise ValueError(f"实时选股行情不可用，已停止筛选：{exc}") from exc
        result = self.screener.screen(
            self.current_user_id, trade_date, formula, regime, strategy_name,
            bool(payload.get("run_backtest", True)),
            realtime_snapshot,
            mode,
        )
        return result
