from __future__ import annotations

import copy
import re
from datetime import date, datetime, time as dt_time, timedelta
from typing import Any

from backend.bootstrap.config import (
    normalize_date,
    tushare_code,
    validate_stock_code,
    validate_text,
)
from backend.data.providers.ifind_client import IfindError
from backend.data.providers.tushare_client import TushareClient, TushareError
from backend.features.market.charts import ChartDataError
from backend.features.market.insights import MarketInsightsService
from backend.features.sentiment.engine import SENTIMENT_ENGINE_VERSION


SEARCH_INDEXES = (
    {"id": "000001.SH", "code": "000001.SH", "name": "上证指数", "type": "index", "subtitle": "沪市综合指数"},
    {"id": "399001.SZ", "code": "399001.SZ", "name": "深证成指", "type": "index", "subtitle": "深市成份指数"},
    {"id": "399006.SZ", "code": "399006.SZ", "name": "创业板指", "type": "index", "subtitle": "创业板核心指数"},
)
SEARCH_TYPE_LABELS = {
    "stock": "股票",
    "sector": "板块",
    "theme": "题材",
    "index": "指数",
}
THS_SEARCH_TYPES = {
    "I": ("sector", "行业板块"),
    "R": ("sector", "地域板块"),
    "N": ("theme", "概念题材"),
}


class MarketServiceMixin:
    def _market_insights(self) -> MarketInsightsService:
        if not self.configured:
            raise ValueError("行情数据尚未配置。")
        return MarketInsightsService(
            self.database,
            self._tushare_client(),
            ifind=self.ifind,
        )
    def _tushare_client(self) -> TushareClient:
        gateway = getattr(self, "data_gateway", None)
        if gateway is not None:
            return gateway.tushare()
        # Compatibility for isolated legacy unit-test service stubs.
        return TushareClient(self.token)

    def get_dashboard(self, trade_date: str, force: bool = False) -> dict[str, Any]:
        normalized_date = normalize_date(trade_date)
        now = datetime.now().astimezone()
        if (
            normalized_date == now.strftime("%Y%m%d")
            and now.time().replace(tzinfo=None) < datetime.strptime("09:15", "%H:%M").time()
        ):
            previous = self.database.get_latest_real_snapshot(normalized_date, strictly_before=True)
            if previous:
                carried = self._carry_dashboard(previous, normalized_date, "盘前沿用最近交易日收盘行情")
                return self._apply_reason_overrides(self._with_storage(carried, cached=True))
        if not force:
            snapshot = self.database.get_snapshot(normalized_date)
            if snapshot and str((snapshot.get("meta") or {}).get("source") or "") != "demo":
                snapshot = copy.deepcopy(snapshot)
                if normalized_date != now.strftime("%Y%m%d"):
                    snapshot.setdefault("meta", {}).update(
                        {"realtime": False, "market_status": "closed"}
                    )
                if not self._dashboard_sentiment_ready(snapshot):
                    snapshot = self._enrich_dashboard_sentiment(snapshot, normalized_date)
                    self.database.save_snapshot(
                        normalized_date,
                        str((snapshot.get("meta") or {}).get("source") or "tushare"),
                        snapshot,
                    )
                snapshot.setdefault("meta", {})["requested_date"] = self._display_compact_date(normalized_date)
                return self._apply_reason_overrides(self._with_storage(snapshot, cached=True))
            resolved = self.database.get_data_snapshot(
                "dashboard_request_v1", normalized_date
            )
            if resolved and str((resolved.get("meta") or {}).get("source") or "") != "demo":
                resolved = copy.deepcopy(resolved)
                resolved.setdefault("meta", {})["requested_date"] = self._display_compact_date(
                    normalized_date
                )
                return self._apply_reason_overrides(
                    self._with_storage(resolved, cached=True)
                )
            if datetime.strptime(normalized_date, "%Y%m%d").weekday() >= 5:
                previous = self.database.get_latest_real_snapshot(normalized_date)
                if previous:
                    carried = self._carry_dashboard(
                        previous,
                        normalized_date,
                        "非交易日沿用最近交易日收盘行情",
                    )
                    self.database.save_data_snapshot(
                        "dashboard_request_v1", normalized_date, "sqlite", carried
                    )
                    return self._apply_reason_overrides(
                        self._with_storage(carried, cached=True)
                    )
        return self.sync_dashboard(normalized_date)

    @staticmethod
    def _dashboard_sentiment_ready(dashboard: dict[str, Any]) -> bool:
        overview = dashboard.get("overview") or {}
        return int(overview.get("sentiment_engine_version") or 0) == SENTIMENT_ENGINE_VERSION and all(
            key in overview
            for key in (
                "sentiment_score",
                "sentiment_label",
                "sentiment_phase",
                "sentiment_direction",
                "sentiment_components",
            )
        )

    @staticmethod
    def _display_compact_date(compact: str) -> str:
        return f"{compact[:4]}-{compact[4:6]}-{compact[6:8]}"

    def _carry_dashboard(
        self, snapshot: dict[str, Any], requested_date: str, reason: str
    ) -> dict[str, Any]:
        carried = copy.deepcopy(snapshot)
        meta = carried.setdefault("meta", {})
        meta.update(
            {
                "requested_date": self._display_compact_date(requested_date),
                "carried_forward": True,
                "realtime": False,
                "market_status": "closed",
                "notice": reason,
            }
        )
        return carried

    def _realtime_snapshot_due(
        self,
        normalized_date: str,
        snapshot: dict[str, Any],
    ) -> bool:
        if not self.configured or normalized_date != date.today().strftime("%Y%m%d"):
            return False
        now = datetime.now().astimezone()
        local_time = now.time().replace(tzinfo=None)
        realtime_start = datetime.strptime("09:15", "%H:%M").time()
        morning_end = datetime.strptime("11:35", "%H:%M").time()
        afternoon_start = datetime.strptime("12:55", "%H:%M").time()
        realtime_end = datetime.strptime("15:05", "%H:%M").time()
        in_session = (
            realtime_start <= local_time < morning_end
            or afternoon_start <= local_time < realtime_end
        )
        if not in_session:
            return False
        meta = snapshot.get("meta") or {}
        snapshot_trade_date = str(meta.get("trade_date") or "").replace("-", "")
        if snapshot_trade_date and snapshot_trade_date != normalized_date:
            return False
        if not meta.get("realtime"):
            return True
        try:
            updated_at = datetime.fromisoformat(str(meta.get("updated_at") or ""))
            if updated_at.tzinfo is None:
                updated_at = updated_at.replace(tzinfo=now.tzinfo)
        except ValueError:
            return True
        age_seconds = (now - updated_at.astimezone(now.tzinfo)).total_seconds()
        return age_seconds >= 8

    def sync_dashboard(self, trade_date: str) -> dict[str, Any]:
        normalized_date = normalize_date(trade_date)
        source = "tushare"
        with self.sync_lock:
            sync_id = self.database.start_sync(normalized_date, source)
            try:
                if not self.configured:
                    raise TushareError("公共行情尚未配置")
                dashboard = self._tushare_client().dashboard(normalized_date)

                dashboard["meta"]["source"] = source
                dashboard["meta"]["requested_date"] = self._display_compact_date(normalized_date)
                dashboard = self._enrich_dashboard_sentiment(dashboard, normalized_date)
                record_count = self._record_count(dashboard)
                actual_date = normalize_date(
                    str(dashboard.get("meta", {}).get("trade_date") or normalized_date)
                )
                self.database.save_snapshot(actual_date, source, dashboard)
                if actual_date != normalized_date:
                    dashboard.setdefault("meta", {}).update(
                        {
                            "carried_forward": True,
                            "realtime": False,
                            "market_status": "closed",
                        }
                    )
                    self.database.save_data_snapshot(
                        "dashboard_request_v1", normalized_date, source, dashboard
                    )
                self.database.finish_sync(
                    sync_id,
                    "success",
                    record_count,
                    dashboard.get("meta", {}).get("notice", ""),
                    source,
                )
                return self._apply_reason_overrides(self._with_storage(dashboard, cached=False))
            except TushareError as exc:
                fallback = self.database.get_latest_real_snapshot(normalized_date)
                if fallback:
                    carried = self._carry_dashboard(
                        fallback, normalized_date, f"最新行情暂不可用，沿用最近收盘快照：{exc}"
                    )
                    self.database.finish_sync(
                        sync_id, "fallback", self._record_count(carried), str(exc), "tushare"
                    )
                    return self._apply_reason_overrides(self._with_storage(carried, cached=True))
                self.database.finish_sync(sync_id, "failed", message=str(exc))
                raise ValueError("暂无可用的真实行情快照，请等待后台完成首次同步。") from exc
            except Exception as exc:
                self.database.finish_sync(sync_id, "failed", message=str(exc))
                raise

    def realtime_aggregate_health(self, sector: str = "") -> dict[str, Any]:
        sector = validate_text(sector, "板块名称", 50)
        return self.realtime_aggregator.health_snapshot(sector)

    def _search_market_directory(self) -> list[dict[str, Any]]:
        cached = self.database.get_data_snapshot("search_directory", "ths") or {}
        cached_items = list(cached.get("items") or [])
        if cached_items and int(cached.get("schema_version") or 0) >= 2:
            return cached_items
        if not self.configured:
            return cached_items

        try:
            rows = self._tushare_client().query(
                "ths_index",
                {},
                "ts_code,name,count,exchange,list_date,type",
            )
        except TushareError:
            return cached_items

        items = []
        for row in rows:
            mapping = THS_SEARCH_TYPES.get(str(row.get("type") or "").upper())
            code = str(row.get("ts_code") or "").strip().upper()
            name = str(row.get("name") or "").strip()
            if not mapping or not code or not name or str(row.get("exchange") or "").upper() != "A":
                continue
            entity_type, subtitle = mapping
            items.append(
                {
                    "id": code,
                    "code": code,
                    "name": name,
                    "type": entity_type,
                    "subtitle": subtitle,
                    "member_count": int(float(row.get("count") or 0)),
                }
            )
        if items:
            self.database.save_data_snapshot(
                "search_directory", "ths", "tushare", {"schema_version": 2, "items": items}
            )
        return items

    @staticmethod
    def _search_match_score(item: dict[str, Any], query: str) -> tuple[int, int, str]:
        name = str(item.get("name") or "").casefold()
        code = str(item.get("code") or item.get("id") or "").casefold()
        needle = query.casefold()
        if code == needle:
            rank = 0
        elif name == needle:
            rank = 1
        elif code.startswith(needle):
            rank = 2
        elif name.startswith(needle):
            rank = 3
        else:
            rank = 4
        return rank, len(name), code

    def search_entities(self, query: str, trade_date: str) -> dict[str, Any]:
        needle = str(query or "").strip()
        normalized_date = normalize_date(trade_date)
        groups: dict[str, list[dict[str, Any]]] = {
            "stocks": [],
            "sectors": [],
            "themes": [],
            "indices": [],
        }
        if not needle:
            return {"query": "", "trade_date": normalized_date, "groups": groups}

        stocks = []
        for row in self.database.search_stock_master(needle, 12):
            stocks.append(
                {
                    "id": str(row.get("code") or ""),
                    "code": str(row.get("code") or ""),
                    "name": str(row.get("name") or "--"),
                    "type": "stock",
                    "type_label": SEARCH_TYPE_LABELS["stock"],
                    "industry": str(row.get("industry") or "其他"),
                    "market": str(row.get("market") or ""),
                    "subtitle": " · ".join(
                        part for part in (str(row.get("industry") or ""), str(row.get("market") or "")) if part
                    ) or "A股",
                }
            )
        groups["stocks"] = stocks[:8]

        market_items = list(self._search_market_directory()) + [dict(item) for item in SEARCH_INDEXES]
        matched = [
            item for item in market_items
            if needle.casefold() in str(item.get("name") or "").casefold()
            or needle.casefold() in str(item.get("code") or "").casefold()
        ]
        matched.sort(key=lambda item: self._search_match_score(item, needle))
        group_keys = {"sector": "sectors", "theme": "themes", "index": "indices"}
        for item in matched:
            group_key = group_keys.get(str(item.get("type") or ""))
            if not group_key or len(groups[group_key]) >= 8:
                continue
            groups[group_key].append(
                {
                    **item,
                    "type_label": SEARCH_TYPE_LABELS[str(item["type"])],
                }
            )
        return {"query": needle, "trade_date": normalized_date, "groups": groups}

    def get_search_detail(
        self, entity_type: str, identifier: str, trade_date: str
    ) -> dict[str, Any]:
        entity_type = str(entity_type or "").strip().lower()
        identifier = str(identifier or "").strip().upper()
        normalized_date = normalize_date(trade_date)
        if entity_type not in {"sector", "theme", "index"}:
            raise ValueError("搜索详情类型不支持。")
        if not re.fullmatch(r"[A-Z0-9.]{3,24}", identifier):
            raise ValueError("搜索详情标识无效。")
        if not self.configured:
            raise ValueError("行情数据源尚未配置。")

        if entity_type == "index":
            index_basic = next((item for item in SEARCH_INDEXES if item["id"] == identifier), None)
            if not index_basic:
                raise ValueError("暂不支持该指数详情。")
            return self._index_search_detail(index_basic, normalized_date)

        directory = self._search_market_directory()
        basic = next(
            (
                item for item in directory
                if item.get("id") == identifier and item.get("type") == entity_type
            ),
            None,
        )
        if not basic:
            raise ValueError("未找到对应的板块或题材。")
        return self._ths_search_detail(basic, normalized_date)

    def get_intraday_chart(
        self, entity_type: str, identifier: str
    ) -> dict[str, Any]:
        entity_type = str(entity_type or "").strip().lower()
        identifier = str(identifier or "").strip().upper()
        if entity_type == "stock":
            code = validate_stock_code(identifier)
            chart = self.chart_data.stock_intraday(code)
            type_label = SEARCH_TYPE_LABELS["stock"]
        elif entity_type == "index":
            basic = next((item for item in SEARCH_INDEXES if item["id"] == identifier), None)
            if not basic:
                raise ValueError("暂不支持该指数分时行情。")
            chart = self.chart_data.index_intraday(identifier)
            type_label = SEARCH_TYPE_LABELS["index"]
        elif entity_type in {"sector", "theme"}:
            basic = next(
                (
                    item for item in self._search_market_directory()
                    if item.get("id") == identifier and item.get("type") == entity_type
                ),
                None,
            )
            if not basic:
                raise ValueError("未找到对应的板块或题材。")
            chart = self.chart_data.board_intraday(identifier, str(basic.get("name") or ""))
            type_label = SEARCH_TYPE_LABELS[entity_type]
        else:
            raise ValueError("分时行情类型不支持。")

        return {
            "meta": {
                "trade_date": str(chart.get("trade_date") or ""),
                "previous_close": float(chart.get("previous_close") or 0),
            },
            "entity": {
                "id": identifier,
                "code": str(chart.get("code") or identifier),
                "name": str(chart.get("name") or ""),
                "type": entity_type,
                "type_label": type_label,
            },
            "points": list(chart.get("points") or []),
        }

    def _ths_search_detail(
        self, basic: dict[str, Any], trade_date: str
    ) -> dict[str, Any]:
        client = self._tushare_client()
        resolved_date, _ = client.resolve_trade_context(trade_date)
        end = datetime.strptime(resolved_date, "%Y%m%d")
        start_date = (end - timedelta(days=190)).strftime("%Y%m%d")
        identifier = str(basic["id"])
        snapshot = client.sector_snapshot(identifier, resolved_date)
        rows = client.query(
            "ths_daily",
            {"ts_code": identifier, "start_date": start_date, "end_date": resolved_date},
            "ts_code,trade_date,open,high,low,close,pct_change,vol,turnover_rate,total_mv,float_mv",
        )
        rows.sort(key=lambda item: str(item.get("trade_date") or ""))
        series = [
            {
                "trade_date": self._display_compact_date(str(row.get("trade_date") or "")),
                "open": float(row.get("open") or 0),
                "high": float(row.get("high") or 0),
                "low": float(row.get("low") or 0),
                "close": float(row.get("close") or 0),
                "change": float(row.get("pct_change") or 0),
                "volume": float(row.get("vol") or 0),
                "turnover_rate": float(row.get("turnover_rate") or 0),
            }
            for row in rows[-90:]
        ]
        try:
            chart_series = self.chart_data.board_daily(identifier, resolved_date, 90)
            if chart_series:
                series = chart_series
        except (AttributeError, ChartDataError):
            pass
        latest = series[-1] if series else {}
        snapshot_is_current = str(snapshot.get("trade_date") or "").replace("-", "") == resolved_date
        change = float(
            snapshot.get("change")
            if snapshot_is_current and snapshot.get("change") is not None
            else latest.get("change") or 0
        )
        if latest.get("realtime"):
            change = float(latest.get("change") or 0)
        turnover_rate = float(
            snapshot.get("turnover_rate")
            if snapshot_is_current and snapshot.get("turnover_rate") is not None
            else latest.get("turnover_rate") or 0
        )
        metrics = [
            {"label": "涨跌幅", "value": round(change, 2), "unit": "%", "tone": "change"},
            {"label": "换手率", "value": round(turnover_rate, 2), "unit": "%"},
            {"label": "成份数量", "value": int(float(basic.get("member_count") or 0)), "unit": "只"},
        ]
        up_count = int(float(snapshot.get("up_count") or 0))
        down_count = int(float(snapshot.get("down_count") or 0))
        if up_count or down_count:
            metrics.extend(
                [
                    {"label": "上涨家数", "value": up_count, "unit": "家"},
                    {"label": "下跌家数", "value": down_count, "unit": "家"},
                ]
            )
        leader = str(snapshot.get("leader") or "").strip()
        if leader and leader != "--":
            metrics.extend(
                [
                    {"label": "领涨标的", "value": leader, "unit": ""},
                    {"label": "领涨幅", "value": round(float(snapshot.get("leading_pct") or 0), 2), "unit": "%", "tone": "change"},
                ]
            )
        return {
            "meta": {
                "trade_date": self._display_compact_date(resolved_date),
                "realtime": bool(snapshot.get("realtime")),
            },
            "entity": {
                "id": identifier,
                "code": identifier,
                "name": str(snapshot.get("name") or basic.get("name") or "--"),
                "type": str(basic.get("type") or "sector"),
                "type_label": SEARCH_TYPE_LABELS[str(basic.get("type") or "sector")],
                "subtitle": str(basic.get("subtitle") or ""),
                "value": float(latest.get("close") or 0),
                "change": change,
            },
            "series": series,
            "metrics": metrics,
        }

    def _index_search_detail(
        self, basic: dict[str, Any], trade_date: str
    ) -> dict[str, Any]:
        client = self._tushare_client()
        resolved_date, _ = client.resolve_trade_context(trade_date)
        payload = (
            client.realtime_market_indices(resolved_date)
            if client.should_use_realtime(trade_date, resolved_date)
            else client.market_indices(resolved_date, 90)
        )
        current = next(
            (item for item in payload.get("indices") or [] if item.get("ts_code") == basic["id"]),
            None,
        )
        if not current:
            raise ValueError("该指数暂无可用行情。")
        end = datetime.strptime(resolved_date, "%Y%m%d")
        rows = client.query(
            "index_daily",
            {
                "ts_code": basic["id"],
                "start_date": (end - timedelta(days=190)).strftime("%Y%m%d"),
                "end_date": resolved_date,
            },
            "ts_code,trade_date,open,high,low,close,pct_chg,vol,amount",
        )
        rows.sort(key=lambda item: str(item.get("trade_date") or ""))
        series = [
            {
                "trade_date": self._display_compact_date(str(row.get("trade_date") or "")),
                "open": float(row.get("open") or 0),
                "high": float(row.get("high") or 0),
                "low": float(row.get("low") or 0),
                "close": float(row.get("close") or 0),
                "change": float(row.get("pct_chg") or 0),
                "volume": float(row.get("vol") or 0),
            }
            for row in rows[-90:]
        ]
        try:
            chart_series = self.chart_data.index_daily(str(basic["id"]), resolved_date, 90)
            if chart_series:
                series = chart_series
        except (AttributeError, ChartDataError):
            pass
        latest = series[-1] if series else {}
        latest_close = float(latest.get("close") or current.get("close") or 0)
        latest_change = float(latest.get("change") or current.get("pct_chg") or 0)

        def series_return(days: int) -> float:
            if len(series) <= days:
                return 0.0
            previous = float(series[-days - 1].get("close") or 0)
            return (latest_close / previous - 1) * 100 if previous > 0 else 0.0
        return {
            "meta": {
                "trade_date": self._display_compact_date(str(current.get("trade_date") or resolved_date)),
                "realtime": bool(payload.get("realtime")),
            },
            "entity": {
                **basic,
                "type_label": SEARCH_TYPE_LABELS["index"],
                "value": latest_close,
                "change": latest_change,
            },
            "series": series,
            "metrics": [
                {"label": "涨跌幅", "value": round(latest_change, 2), "unit": "%", "tone": "change"},
                {"label": "近5日", "value": round(series_return(5), 2), "unit": "%", "tone": "change"},
                {"label": "近20日", "value": round(series_return(20), 2), "unit": "%", "tone": "change"},
                {"label": "成交额", "value": round(float(current.get("amount_billion") or 0), 2), "unit": "亿"},
            ],
        }

    def get_stock_detail(
        self, code: str, trade_date: str, force: bool = False
    ) -> dict[str, Any]:
        code = validate_stock_code(code)
        normalized_date = normalize_date(trade_date)
        cache_key = f"{code}:{normalized_date}"
        if not force:
            cached = self.database.get_data_snapshot("stock_detail", cache_key)
            if cached and str((cached.get("meta") or {}).get("source") or "") != "demo":
                if not self._stock_detail_cache_needs_refresh(cached, normalized_date):
                    cached["meta"] = {**cached.get("meta", {}), "cached": True}
                    return self._prepare_stock_detail(cached, code, normalized_date)

        name, sector = self._stock_identity(code, normalized_date)
        source = "tushare"
        if self.configured:
            try:
                payload = self._tushare_client().stock_detail(
                    tushare_code(code), normalized_date
                )
                if not payload.get("prices"):
                    raise TushareError("No price history returned")
            except TushareError as exc:
                payload = self.database.get_latest_data_snapshot(
                    "stock_detail", f"{code}:", cache_key, exclude_source="demo"
                )
                if not payload:
                    raise ValueError(f"暂无 {code} 的真实行情数据：{exc}") from exc
                payload = copy.deepcopy(payload)
                payload["meta"] = {
                    **payload.get("meta", {}),
                    "cached": True,
                    "notice": "最新行情暂不可用，已沿用最近真实收盘数据。",
                }
                return self._prepare_stock_detail(payload, code, normalized_date)
        else:
            payload = self.database.get_latest_data_snapshot(
                "stock_detail", f"{code}:", cache_key, exclude_source="demo"
            )
            if not payload:
                raise ValueError(f"暂无 {code} 的真实行情数据，请等待后台完成首次同步。")
            payload = copy.deepcopy(payload)
            payload["meta"] = {
                **payload.get("meta", {}),
                "cached": True,
                "notice": "公共行情尚未配置，已沿用最近真实收盘数据。",
            }
            return self._prepare_stock_detail(payload, code, normalized_date)
        payload["meta"]["source"] = source
        payload["meta"]["cached"] = False
        self.database.save_data_snapshot("stock_detail", cache_key, source, payload)
        return self._prepare_stock_detail(payload, code, normalized_date)

    @staticmethod
    def _stock_detail_bar_date(payload: dict[str, Any]) -> str:
        prices = list(payload.get("prices") or [])
        return str((prices[-1] if prices else {}).get("trade_date") or "").replace("-", "")

    def _stock_detail_cache_needs_refresh(
        self, payload: dict[str, Any], requested_date: str
    ) -> bool:
        now = datetime.now().astimezone()
        return (
            requested_date == now.strftime("%Y%m%d")
            and now.time().replace(tzinfo=None) >= dt_time(15, 0)
            and self._stock_detail_bar_date(payload) < requested_date
        )

    def _prepare_stock_detail(
        self, payload: dict[str, Any], code: str, requested_date: str
    ) -> dict[str, Any]:
        result = copy.deepcopy(payload)
        now = datetime.now().astimezone()
        try:
            result["prices"] = self.chart_data.stock_daily(code, requested_date, 90)
            result["meta"] = {**(result.get("meta") or {}), "chart_source": "market_chart"}
        except (AttributeError, ChartDataError):
            pass
        result = self._sanitize_stock_detail_prices(result, now)
        actual_date = self._stock_detail_bar_date(result)
        if actual_date:
            result["meta"] = {
                **(result.get("meta") or {}),
                "trade_date": f"{actual_date[:4]}-{actual_date[4:6]}-{actual_date[6:]}",
            }
        today = now.strftime("%Y%m%d")
        should_merge = (
            requested_date == today
            and actual_date <= today
            and now.weekday() < 5
            and now.time().replace(tzinfo=None) >= dt_time(9, 30)
        )
        if should_merge:
            quote = self._ifind_realtime_stock_quote(code)
            if quote and self._valid_realtime_stock_quote(quote, today):
                self._merge_realtime_stock_detail(result, quote, requested_date)
            elif self.configured and actual_date < today:
                client = self._tushare_client()
                try:
                    resolved_date, _ = client.resolve_trade_context(requested_date)
                    if resolved_date == today:
                        quote = client.realtime_stock_quote(tushare_code(code), requested_date)
                        if self._valid_realtime_stock_quote(quote, today):
                            self._merge_realtime_stock_detail(result, quote, requested_date)
                except TushareError:
                    pass
        return self._enrich_stock_detail(result)

    @staticmethod
    def _sanitize_stock_detail_prices(
        payload: dict[str, Any], market_now: datetime
    ) -> dict[str, Any]:
        result = copy.deepcopy(payload)
        raw_prices = list(result.get("prices") or [])
        raw_latest_date = str(
            (raw_prices[-1] if raw_prices else {}).get("trade_date") or ""
        ).replace("-", "")
        prices = []
        for bar in raw_prices:
            open_price = float(bar.get("open") or 0)
            high = float(bar.get("high") or 0)
            low = float(bar.get("low") or 0)
            close = float(bar.get("close") or 0)
            if (
                open_price > 0
                and high >= max(open_price, close)
                and 0 < low <= min(open_price, close)
                and close > 0
            ):
                prices.append(bar)

        today = market_now.strftime("%Y%m%d")
        market_open = (
            market_now.weekday() < 5
            and market_now.time().replace(tzinfo=None) >= dt_time(9, 30)
        )
        if prices and str(prices[-1].get("trade_date") or "").replace("-", "") == today:
            current = prices[-1]
            has_market_activity = (
                float(current.get("volume") or 0) > 0
                or float(current.get("amount_billion") or 0) > 0
            )
            if not market_open or not has_market_activity:
                prices.pop()

        if raw_latest_date == today and (
            not prices
            or str(prices[-1].get("trade_date") or "").replace("-", "") != today
        ):
            result["meta"] = {**(result.get("meta") or {}), "realtime": False}

        result["prices"] = prices
        if prices:
            latest = prices[-1]
            stock = dict(result.get("stock") or {})
            stock.update(
                {
                    "price": float(latest.get("close") or 0),
                    "change": float(latest.get("change") or 0),
                    "amount_billion": float(latest.get("amount_billion") or 0),
                }
            )
            result["stock"] = stock
        return result

    @staticmethod
    def _valid_realtime_stock_quote(quote: dict[str, Any], trade_date: str) -> bool:
        price = float(quote.get("price") or 0)
        open_price = float(quote.get("open") or 0)
        high = float(quote.get("high") or 0)
        low = float(quote.get("low") or 0)
        volume = float(quote.get("volume") or 0)
        amount = float(quote.get("amount_billion") or 0)
        quote_date = str(quote.get("quote_time") or "")[:10].replace("-", "")
        return (
            price > 0
            and open_price > 0
            and high >= max(open_price, price)
            and 0 < low <= min(open_price, price)
            and (volume > 0 or amount > 0)
            and (not quote_date or quote_date == trade_date)
        )

    def _ifind_realtime_stock_quote(self, code: str) -> dict[str, Any] | None:
        ifind = getattr(self, "ifind", None)
        if not ifind or not ifind.configured:
            return None
        try:
            rows = ifind.real_time(
                tushare_code(code),
                [
                    "open", "high", "low", "latest", "preClose",
                    "volume", "amount", "turnoverRatio",
                ],
                cache_ttl=10,
            )
        except IfindError:
            return None
        row = rows[0] if rows else {}
        price = float(row.get("latest") or 0)
        previous_close = float(row.get("preClose") or 0)
        if price <= 0:
            return None
        change = (price / previous_close - 1) * 100 if previous_close > 0 else 0.0
        stock = self._stock_identity(code, date.today().strftime("%Y%m%d"))
        return {
            "name": stock[0],
            "sector": stock[1],
            "price": price,
            "open": float(row.get("open") or price),
            "high": float(row.get("high") or price),
            "low": float(row.get("low") or price),
            "change": round(change, 4),
            "volume": float(row.get("volume") or 0),
            "volume_unit": "lots",
            "amount_billion": float(row.get("amount") or 0) / 100_000_000,
            "turnover_rate": float(row.get("turnoverRatio") or 0),
            "quote_time": str(row.get("time") or ""),
        }

    @staticmethod
    def _merge_realtime_stock_detail(
        payload: dict[str, Any], quote: dict[str, Any], trade_date: str
    ) -> None:
        display_date = f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:]}"
        realtime_bar = {
            "trade_date": display_date,
            "open": quote["open"],
            "high": quote["high"],
            "low": quote["low"],
            "close": quote["price"],
            "change": quote["change"],
            "volume": quote["volume"] if quote.get("volume_unit") == "lots" else quote["volume"] / 100,
            "amount_billion": quote["amount_billion"],
            "realtime": True,
        }
        prices = list(payload.get("prices") or [])
        if prices and str(prices[-1].get("trade_date") or "").replace("-", "") == trade_date:
            prices[-1] = realtime_bar
        else:
            prices.append(realtime_bar)
        payload["prices"] = prices[-90:]
        stock = dict(payload.get("stock") or {})
        stock.update(
            {
                "name": quote["name"],
                "industry": quote["sector"],
                "price": quote["price"],
                "change": quote["change"],
                "amount_billion": quote["amount_billion"],
                "turnover_rate": quote["turnover_rate"],
            }
        )
        payload["stock"] = stock
        payload["meta"] = {
            **(payload.get("meta") or {}),
            "trade_date": display_date,
            "realtime": True,
            "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
        }

    def get_stock_preview(
        self, code: str, trade_date: str, force: bool = False
    ) -> dict[str, Any]:
        code = validate_stock_code(code)
        # Hover previews deliberately follow the latest market day, independent
        # from the review date selected by the page.
        detail = self.get_stock_detail(code, date.today().strftime("%Y%m%d"), force)
        detail_meta = detail.get("meta") or {}
        resolved_date = str(detail_meta.get("trade_date") or trade_date)
        intraday_points: list[dict[str, Any]] = []
        intraday_status = "unavailable"
        intraday_notice = "分时行情暂不可用。"

        intraday_trade_date = ""
        intraday_previous_close = 0.0
        try:
            intraday = self.chart_data.stock_intraday(code)
            intraday_points = list(intraday.get("points") or [])
            intraday_trade_date = str(intraday.get("trade_date") or "")
            intraday_previous_close = float(intraday.get("previous_close") or 0)
            if intraday_points:
                intraday_status = "available"
                intraday_notice = ""
            else:
                intraday_status = "empty"
                intraday_notice = "最近交易日暂无分时数据。"
        except ChartDataError:
            intraday_status = "unavailable"
            intraday_notice = "分时行情暂不可用，请稍后重试。"

        prices = list(detail.get("prices") or [])[-60:]
        stock = dict(detail.get("stock") or {"code": code})
        realtime = bool(detail_meta.get("realtime"))
        return {
            "meta": {
                "trade_date": resolved_date,
                "source": detail_meta.get("source") or "unavailable",
                "notice": detail_meta.get("notice") or "",
                "intraday_status": intraday_status,
                "intraday_notice": intraday_notice,
                "intraday_trade_date": intraday_trade_date,
                "intraday_previous_close": intraday_previous_close,
                "realtime": realtime,
                "refresh_interval_seconds": 10 if realtime else 0,
            },
            "stock": stock,
            "prices": prices,
            "intraday": intraday_points,
        }

    def backfill(self, start_date: str, end_date: str) -> list[dict[str, Any]]:
        start = datetime.strptime(normalize_date(start_date), "%Y%m%d").date()
        end = datetime.strptime(normalize_date(end_date), "%Y%m%d").date()
        if start > end:
            raise ValueError("开始日期不能晚于结束日期。")
        weekdays = []
        current = start
        while current <= end:
            if current.weekday() < 5:
                weekdays.append(current)
            current += timedelta(days=1)
        if len(weekdays) > 15:
            raise ValueError("单次最多回补 15 个工作日。")
        results = []
        for day in weekdays:
            dashboard = self.sync_dashboard(day.strftime("%Y%m%d"))
            results.append(
                {
                    "requested_date": day.isoformat(),
                    "trade_date": dashboard["meta"]["trade_date"],
                    "source": dashboard["meta"]["source"],
                    "records": self._record_count(dashboard),
                }
            )
        return results

    def _stock_identity(self, code: str, trade_date: str) -> tuple[str, str]:
        snapshot = self.database.get_snapshot(trade_date) or {}
        for key in ("limits", "broken", "down_limits"):
            for row in snapshot.get(key) or []:
                if str(row.get("code")) == code:
                    return row.get("name") or "--", row.get("sector") or "其他"
        for item in self.database.list_watchlist(self.current_user_id):
            if item["code"] == code:
                return item["name"], item["sector"] or "其他"
        return "--", "其他"

    def _enrich_stock_detail(self, payload: dict[str, Any]) -> dict[str, Any]:
        result = dict(payload)
        stock = dict(payload.get("stock") or {})
        code = str(stock.get("code") or "")
        watched = {
            item["code"]: item
            for item in self.database.list_watchlist(self.current_user_id)
        }
        stock["watchlist"] = watched.get(code)
        result["stock"] = stock
        result["notes"] = self.database.list_notes(self.current_user_id, code=code)
        return result

    def _with_storage(self, dashboard: dict[str, Any], cached: bool) -> dict[str, Any]:
        result = dict(dashboard)
        result["meta"] = {
            **dashboard.get("meta", {}),
            "storage": "sqlite",
            "cached": cached,
        }
        return result

    @staticmethod
    def _record_count(dashboard: dict[str, Any]) -> int:
        return sum(
            len(dashboard.get(key) or [])
            for key in ("limits", "broken", "down_limits", "yesterday_limits")
        )

