from __future__ import annotations

import json
import urllib.error
import urllib.request
from collections.abc import Callable
from dataclasses import replace
from datetime import datetime, timedelta
from typing import Any
from zoneinfo import ZoneInfo

from backend.data.contracts import (
    DataSource,
    DataUsage,
    ObservationMetadata,
    ProviderResult,
    SnapshotState,
)
from backend.data.providers.base import ProviderError

SHANGHAI = ZoneInfo("Asia/Shanghai")


class TushareProvider:
    source = DataSource.TUSHARE
    url = "http://api.tushare.pro"

    def __init__(self, token: str | None | Callable[[], str | None], timeout: int = 20) -> None:
        self._token_provider = token if callable(token) else lambda: token
        self._timeout = timeout

    @property
    def configured(self) -> bool:
        return bool(self._token())

    def calendar(self, start_date: str, end_date: str) -> ProviderResult:
        result = self._query(
            "trade_cal",
            {"exchange": "SSE", "start_date": _compact(start_date), "end_date": _compact(end_date)},
            "cal_date,is_open,pretrade_date",
            unit="calendar_day",
        )
        days = (
            datetime.fromisoformat(end_date).date() - datetime.fromisoformat(start_date).date()
        ).days + 1
        return ProviderResult(
            result.rows,
            replace(result.metadata, coverage=min(len(result.rows) / max(days, 1), 1)),
        )

    def entities(self) -> ProviderResult:
        rows: list[dict[str, Any]] = []
        for status in ("L", "P", "D"):
            result = self._query(
                "stock_basic",
                {"exchange": "", "list_status": status},
                "ts_code,symbol,name,industry,list_status,list_date,delist_date",
                unit="entity",
            )
            rows.extend(result.rows)
        return ProviderResult(
            tuple(rows), _metadata(self.source, "entity", min(len(rows) / 5300, 1))
        )

    def daily(self, entity_type: str, identifier: str, end_date: str) -> ProviderResult:
        api_name = "index_daily" if entity_type == "index" else "daily"
        if entity_type in {"sector", "theme"}:
            api_name = "ths_daily"
        end = datetime.strptime(_compact(end_date), "%Y%m%d")
        start = (end - timedelta(days=380)).strftime("%Y%m%d")
        return self._query(
            api_name,
            {"ts_code": identifier, "start_date": start, "end_date": end.strftime("%Y%m%d")},
            "ts_code,trade_date,open,high,low,close,vol,amount,pct_chg",
            unit="yuan/share",
            adjustment="unadjusted",
        )

    def minute(self, entity_type: str, identifier: str, trade_date: str) -> ProviderResult:
        if entity_type != "stock":
            raise ProviderError("Tushare minute charts only support stocks")
        date = _display(trade_date)
        return self._query(
            "stk_mins",
            {
                "ts_code": identifier,
                "freq": "1min",
                "start_date": f"{date} 09:30:00",
                "end_date": f"{date} 15:00:00",
            },
            "ts_code,trade_time,open,high,low,close,vol,amount",
            unit="yuan/share",
        )

    def snapshot_inputs(
        self, trade_date: str, previous_trade_date: str
    ) -> dict[str, ProviderResult | dict[str, Any]]:
        current = _compact(trade_date)
        previous = _compact(previous_trade_date)
        daily = self._query(
            "daily",
            {"trade_date": current},
            "ts_code,trade_date,open,high,low,close,pre_close,pct_chg,vol,amount",
            unit="mixed",
        )
        event_fields = (
            "trade_date,ts_code,industry,name,close,pct_chg,amount,limit_amount,"
            "float_mv,total_mv,turnover_ratio,fd_amount,first_time,last_time,"
            "open_times,up_stat,limit_times"
        )
        datasets: dict[str, ProviderResult | dict[str, Any]] = {"daily": daily}
        datasets["price_limits"] = self._query(
            "stk_limit",
            {"trade_date": current},
            "ts_code,trade_date,up_limit,down_limit",
            unit="yuan/share",
        )
        for key, limit_type, date in (
            ("limit_up", "U", current),
            ("limit_down", "D", current),
            ("broken", "Z", current),
            ("previous_limit_up", "U", previous),
        ):
            datasets[key] = self._query(
                "limit_list_d",
                {"trade_date": date, "limit_type": limit_type},
                event_fields,
                unit="mixed",
                empty_is_complete=True,
            )
        return datasets

    def realtime_market_inputs(
        self,
        trade_date: str,
        previous_trade_date: str,
        identifiers: tuple[str, ...],
    ) -> dict[str, ProviderResult | dict[str, Any]]:
        current = _compact(trade_date)
        previous = _compact(previous_trade_date)
        quotes = self._query(
            "rt_k",
            {"ts_code": ",".join(identifiers)},
            "",
            unit="mixed",
        )
        normalized = tuple(
            {
                **row,
                "trade_date": current,
                "pct_chg": _change(row.get("close"), row.get("pre_close")),
                "amount_unit": "yuan",
            }
            for row in quotes.rows
            if _number(row.get("close")) > 0 and _number(row.get("pre_close")) > 0
        )
        daily = ProviderResult(
            normalized,
            replace(
                quotes.metadata,
                coverage=min(len(normalized) / max(len(identifiers), 1), 1),
                state=SnapshotState.REALTIME,
            ),
        )
        return {
            "daily": daily,
            "price_limits": self._query(
                "stk_limit",
                {"trade_date": current},
                "ts_code,trade_date,up_limit,down_limit",
                unit="yuan/share",
            ),
            "previous_limit_up": self._query(
                "limit_list_d",
                {"trade_date": previous, "limit_type": "U"},
                (
                    "trade_date,ts_code,industry,name,close,pct_chg,amount,limit_amount,"
                    "float_mv,total_mv,turnover_ratio,fd_amount,first_time,last_time,"
                    "open_times,up_stat,limit_times"
                ),
                unit="mixed",
                empty_is_complete=True,
            ),
        }

    def sector_members(self, representative: str, trade_date: str) -> ProviderResult:
        target = _compact(trade_date)
        industry, members = self._sector_memberships(representative, target)
        sector_code = str(industry.get("l2_code") or "")
        sector_name = str(industry.get("l2_name") or "").strip()
        daily = self._query(
            "daily",
            {"trade_date": target},
            "ts_code,trade_date,open,close,pct_chg,amount",
            unit="mixed",
        )
        quote_map = {str(row.get("ts_code") or ""): row for row in daily.rows}
        rows = []
        for member in members:
            code = str(member.get("ts_code") or "")
            quote = quote_map.get(code) or {}
            rows.append(
                {
                    "sector_code": sector_code,
                    "sector_name": sector_name,
                    "ts_code": code,
                    "name": str(member.get("name") or "").strip(),
                    "change": _number(quote.get("pct_chg")) if quote else None,
                    "open": _number(quote.get("open")) if quote else None,
                    "close": _number(quote.get("close")) if quote else None,
                    "amount": _number(quote.get("amount")) * 1000 if quote else None,
                    "quoted": bool(quote),
                }
            )
        coverage = sum(bool(row["quoted"]) for row in rows) / len(rows)
        return ProviderResult(tuple(rows), _metadata(self.source, "mixed", coverage))

    def heaven_inputs(
        self, representative: str, trade_date: str
    ) -> dict[str, ProviderResult | None]:
        target = _compact(trade_date)
        membership = self.sector_members(representative, trade_date)
        sector_code = str(membership.rows[0].get("sector_code") or "") if membership.rows else ""
        return {
            "members": membership,
            "daily": self._optional_query(
                "daily",
                {"trade_date": target},
                "ts_code,trade_date,open,high,low,close,pre_close,pct_chg,vol,amount",
            ),
            "daily_basic": self._optional_query(
                "daily_basic",
                {"trade_date": target},
                "ts_code,trade_date,turnover_rate,volume_ratio,total_mv,circ_mv",
            ),
            "sector_daily": self._optional_query(
                "sw_daily",
                {"ts_code": sector_code, "trade_date": target},
                "ts_code,trade_date,name,open,high,low,close,pct_change,vol,amount,pe,pb,float_mv,total_mv",
            ),
            "indices": self._index_rows(target),
        }

    def heaven_realtime_inputs(
        self, representative: str, trade_date: str, previous_trade_date: str
    ) -> dict[str, ProviderResult | None]:
        target = _compact(trade_date)
        previous = _compact(previous_trade_date)
        industry, members = self._sector_memberships(representative, target)
        sector_code = str(industry.get("l2_code") or "")
        directory = self._optional_query(
            "stock_basic",
            {"exchange": "", "list_status": "L"},
            "ts_code,name,industry,market,list_date",
        )
        active_codes = tuple(
            str(row.get("ts_code") or "")
            for row in (directory.rows if directory else ())
            if row.get("ts_code")
        )
        realtime_codes = (*active_codes, "000001.SH", "399001.SZ", "399006.SZ")
        member_rows = tuple(
            {
                "sector_code": sector_code,
                "sector_name": str(industry.get("l2_name") or "").strip(),
                "ts_code": str(row.get("ts_code") or ""),
                "name": str(row.get("name") or "").strip(),
            }
            for row in members
        )
        start = (datetime.strptime(target, "%Y%m%d") - timedelta(days=35)).strftime("%Y%m%d")
        return {
            "directory": directory,
            "members": ProviderResult(
                member_rows,
                _metadata(self.source, "membership", 1 if member_rows else 0),
            ),
            "realtime": self._optional_query(
                "rt_k",
                {"ts_code": ",".join(realtime_codes)},
                "ts_code,name,trade_time,open,high,low,close,pre_close,vol,amount,num,pct_chg",
            ),
            "capital": self._optional_query(
                "daily_basic",
                {"trade_date": previous},
                "ts_code,trade_date,total_share,float_share,free_share,total_mv,circ_mv",
            ),
            "stock_history": self._optional_query(
                "daily",
                {"ts_code": representative, "start_date": start, "end_date": previous},
                "ts_code,trade_date,vol,amount",
            ),
            "price_limits": self._optional_query(
                "stk_limit",
                {"trade_date": target},
                "ts_code,trade_date,up_limit,down_limit",
            ),
            "suspensions": self._optional_query(
                "suspend_d",
                {"suspend_date": target},
                "ts_code,suspend_date,resume_date,suspend_timing,suspend_type",
            ),
            "sector_realtime": self._optional_query(
                "rt_sw_k",
                {"ts_code": sector_code},
                "ts_code,name,trade_time,close,pre_close,high,open,low,vol,amount,pct_change",
            ),
        }

    def _index_rows(self, trade_date: str) -> ProviderResult | None:
        rows: list[dict[str, Any]] = []
        completed = 0
        for identifier in ("000001.SH", "399001.SZ", "399006.SZ"):
            result = self._optional_query(
                "index_daily",
                {"ts_code": identifier, "trade_date": trade_date},
                "ts_code,trade_date,close,pre_close,pct_chg",
            )
            if result is not None and result.rows:
                rows.extend(result.rows)
                completed += 1
        if not rows:
            return None
        return ProviderResult(tuple(rows), _metadata(self.source, "percent", completed / 3))

    def market_insight(
        self,
        kind: str,
        trade_date: str,
        previous_trade_date: str = "",
        identifier: str = "",
    ) -> dict[str, ProviderResult | None]:
        current = _compact(trade_date)
        previous = _compact(previous_trade_date) if previous_trade_date else current
        if kind == "auction":
            return {
                "auction": self._optional_query(
                    "stk_auction",
                    {"trade_date": current},
                    "ts_code,trade_date,vol,price,amount,pre_close,turnover_rate,"
                    "volume_ratio,float_share",
                ),
                "price_limits": self._optional_query(
                    "stk_limit",
                    {"trade_date": current},
                    "trade_date,ts_code,up_limit,down_limit",
                ),
                "ths_hot": self._optional_query("ths_hot", {"trade_date": previous}, ""),
                "dc_hot": self._optional_query("dc_hot", {"trade_date": previous}, ""),
            }
        if kind == "themes":
            return {
                "directory": self._optional_query(
                    "ths_index", {}, "ts_code,name,count,exchange,list_date,type"
                ),
                "daily": self._optional_query(
                    "ths_daily",
                    {"trade_date": current},
                    "ts_code,trade_date,open,high,low,close,pre_close,pct_change,vol,turnover_rate",
                ),
                "hot": self._optional_query("ths_hot", {"trade_date": current}, ""),
            }
        if kind == "theme-detail":
            return {
                "members": self._optional_query(
                    "ths_member",
                    {"ts_code": identifier, "is_new": "Y"},
                    "ts_code,con_code,con_name",
                ),
                "daily": self._optional_query(
                    "daily",
                    {"trade_date": current},
                    "ts_code,trade_date,open,high,low,close,pct_chg,vol,amount",
                ),
            }
        if kind == "popularity":
            return {
                "ths": self._optional_query("ths_hot", {"trade_date": current}, ""),
                "dc": self._optional_query("dc_hot", {"trade_date": current}, ""),
                "previous_ths": self._optional_query("ths_hot", {"trade_date": previous}, ""),
                "previous_dc": self._optional_query("dc_hot", {"trade_date": previous}, ""),
            }
        if kind == "dragon-list":
            return {
                "official": self._optional_query(
                    "hm_detail",
                    {"trade_date": current},
                    "trade_date,ts_code,ts_name,buy_amount,sell_amount,net_amount,"
                    "hm_name,hm_orgs,tag",
                ),
                "profiles": self._optional_query("hm_list", {}, "name,desc,orgs"),
                "stocks": self._optional_query(
                    "top_list",
                    {"trade_date": current},
                    "trade_date,ts_code,name,pct_change,reason",
                ),
                "seats": self._optional_query(
                    "top_inst",
                    {"trade_date": current},
                    "trade_date,ts_code,exalter,buy,sell,net_buy,side,reason",
                ),
            }
        raise ProviderError("不支持的市场洞察数据集")

    def realtime_snapshots(
        self, identifiers: tuple[str, ...], start_time: str, end_time: str
    ) -> ProviderResult:
        raise ProviderError("Tushare不提供动态竞价快照")

    def event_reasons(self, trade_date: str) -> ProviderResult:
        raise ProviderError("Tushare涨跌停榜单不提供可用的事件原因字段")

    def screener_inputs(self, trade_dates: tuple[str, ...]) -> dict[str, ProviderResult | None]:
        if len(trade_dates) < 21:
            raise ProviderError("选股因子至少需要21个交易日")
        compact_dates = tuple(_compact(value) for value in trade_dates)
        current = compact_dates[-1]
        quarters = _quarter_periods(current, 5)
        years = tuple(f"{int(current[:4]) - offset}1231" for offset in range(1, 6))
        return {
            "directory": self._optional_query(
                "stock_basic",
                {"exchange": "", "list_status": "L"},
                "ts_code,symbol,name,industry,market,list_date,list_status",
            ),
            "industry": self._optional_query(
                "index_member_all",
                {"is_new": "Y"},
                "l1_code,l1_name,l2_code,l2_name,ts_code,name,in_date,out_date,is_new",
            ),
            "daily": self._series_query(
                "daily",
                compact_dates,
                "ts_code,trade_date,open,high,low,close,pre_close,pct_chg,vol,amount",
            ),
            "daily_basic": self._series_query(
                "daily_basic",
                compact_dates[-5:],
                "ts_code,trade_date,turnover_rate,volume_ratio,pe_ttm,pb,ps_ttm,dv_ttm,"
                "total_mv,circ_mv",
            ),
            "moneyflow": self._series_query(
                "moneyflow",
                compact_dates[-5:],
                "ts_code,trade_date,buy_lg_amount,sell_lg_amount,buy_elg_amount,"
                "sell_elg_amount,net_mf_amount",
            ),
            "benchmark": self._optional_query(
                "index_daily",
                {
                    "ts_code": "000300.SH",
                    "start_date": compact_dates[0],
                    "end_date": current,
                },
                "ts_code,trade_date,close,pct_chg",
            ),
            "fundamentals": self._period_query(
                "fina_indicator",
                quarters,
                "ts_code,ann_date,end_date,roe,roa,roic,grossprofit_margin,"
                "netprofit_yoy,or_yoy,ocf_to_or",
            ),
            "dividends": self._period_query(
                "dividend",
                years,
                "ts_code,end_date,ann_date,div_proc,cash_div_tax,ex_date",
                parameter="end_date",
            ),
            "auction": self._optional_query(
                "stk_auction",
                {"trade_date": current},
                "ts_code,trade_date,price,pre_close,amount,turnover_rate,volume_ratio",
            ),
            "limit_events": self._series_query(
                "limit_list_d",
                compact_dates[-80:],
                "trade_date,ts_code,name,limit_type,limit_times",
                empty_is_complete=True,
            ),
            "forecast": self._period_query(
                "forecast",
                quarters,
                "ts_code,ann_date,end_date,type,p_change_min,p_change_max,"
                "net_profit_min,net_profit_max,last_parent_net",
            ),
            "express": self._period_query(
                "express",
                quarters,
                "ts_code,ann_date,end_date,revenue,operate_profit,total_profit,n_income,"
                "total_assets,diluted_roe,yoy_net_profit",
            ),
        }

    def _series_query(
        self,
        api_name: str,
        dates: tuple[str, ...],
        fields: str,
        *,
        empty_is_complete: bool = False,
    ) -> ProviderResult | None:
        rows: list[dict[str, Any]] = []
        completed = 0
        for trade_date in dates:
            try:
                result = self._query(
                    api_name,
                    {"trade_date": trade_date},
                    fields,
                    unit="mixed",
                    empty_is_complete=empty_is_complete,
                )
            except ProviderError:
                continue
            rows.extend(result.rows)
            completed += 1
        if completed == 0:
            return None
        return ProviderResult(
            tuple(rows),
            _metadata(self.source, "mixed", completed / len(dates)),
        )

    def _period_query(
        self,
        api_name: str,
        periods: tuple[str, ...],
        fields: str,
        *,
        parameter: str = "period",
    ) -> ProviderResult | None:
        rows: list[dict[str, Any]] = []
        completed = 0
        for period in periods:
            try:
                result = self._query(
                    api_name,
                    {parameter: period},
                    fields,
                    unit="mixed",
                    empty_is_complete=True,
                )
            except ProviderError:
                continue
            rows.extend(result.rows)
            completed += 1
        if completed == 0:
            return None
        return ProviderResult(
            tuple(rows),
            _metadata(self.source, "mixed", completed / len(periods)),
        )

    def _optional_query(
        self, api_name: str, params: dict[str, Any], fields: str
    ) -> ProviderResult | None:
        try:
            return self._query(
                api_name,
                params,
                fields,
                unit="mixed",
                empty_is_complete=True,
            )
        except ProviderError:
            return None

    def _membership_rows(self, params: dict[str, str]) -> list[dict[str, Any]]:
        rows: list[dict[str, Any]] = []
        fields = (
            "l1_code,l1_name,l2_code,l2_name,l3_code,l3_name,ts_code,name,in_date,out_date,is_new"
        )
        for is_new in ("Y", "N"):
            result = self._query(
                "index_member_all",
                {**params, "is_new": is_new},
                fields,
                unit="membership",
                empty_is_complete=True,
            )
            rows.extend(result.rows)
        return rows

    def _sector_memberships(
        self, representative: str, trade_date: str
    ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
        active = [
            row
            for row in self._membership_rows({"ts_code": representative})
            if _active_on(row, trade_date)
        ]
        if not active:
            raise ProviderError("未找到该股票在目标日期的申万行业")
        industry = max(
            active,
            key=lambda row: (str(row.get("in_date") or ""), str(row.get("l2_code") or "")),
        )
        sector_code = str(industry.get("l2_code") or "")
        if not sector_code:
            raise ProviderError("该股票缺少申万二级行业")
        rows = [
            row
            for row in self._membership_rows({"l2_code": sector_code})
            if _active_on(row, trade_date)
        ]
        deduplicated: dict[str, dict[str, Any]] = {}
        for row in rows:
            code = str(row.get("ts_code") or "")
            current = deduplicated.get(code)
            if code and (
                current is None or str(row.get("in_date") or "") > str(current.get("in_date") or "")
            ):
                deduplicated[code] = row
        if not deduplicated:
            raise ProviderError("该申万行业没有有效成分股")
        return industry, list(deduplicated.values())

    def _query(
        self,
        api_name: str,
        params: dict[str, Any],
        fields: str,
        *,
        unit: str,
        adjustment: str = "not_applicable",
        empty_is_complete: bool = False,
    ) -> ProviderResult:
        if not self.configured:
            raise ProviderError("行情服务尚未配置")
        token = self._token()
        if not token:
            raise ProviderError("行情服务尚未配置")
        body = json.dumps(
            {"api_name": api_name, "token": token, "params": params, "fields": fields},
            ensure_ascii=False,
        ).encode("utf-8")
        request = urllib.request.Request(
            self.url,
            data=body,
            headers={"Content-Type": "application/json", "User-Agent": "XiaobaiReview/2"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(request, timeout=self._timeout) as response:
                payload = json.loads(response.read().decode("utf-8"))
        except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
            raise ProviderError("行情服务请求失败") from exc
        if payload.get("code") not in (None, 0):
            raise ProviderError(str(payload.get("msg") or "行情服务拒绝请求"))
        data = payload.get("data") or {}
        columns = data.get("fields") or []
        rows = tuple(dict(zip(columns, item, strict=False)) for item in data.get("items") or [])
        coverage = 1 if rows or empty_is_complete else 0
        return ProviderResult(rows, _metadata(self.source, unit, coverage, adjustment))

    def _token(self) -> str:
        return str(self._token_provider() or "").strip()


def _metadata(
    source: DataSource, unit: str, coverage: float, adjustment: str = "not_applicable"
) -> ObservationMetadata:
    return ObservationMetadata(
        source=source,
        observed_at=datetime.now(SHANGHAI),
        unit=unit,
        adjustment=adjustment,
        freshness_seconds=0,
        coverage=coverage,
        state=SnapshotState.ARCHIVE,
        usage=DataUsage.CALCULATION,
    )


def _compact(value: str) -> str:
    normalized = value.replace("-", "")
    if len(normalized) != 8 or not normalized.isdigit():
        raise ProviderError("日期格式无效")
    return normalized


def _display(value: str) -> str:
    compact = _compact(value)
    return f"{compact[:4]}-{compact[4:6]}-{compact[6:]}"


def _change(close: Any, previous: Any) -> float:
    current = _number(close)
    prior = _number(previous)
    return round((current / prior - 1) * 100, 4) if current > 0 and prior > 0 else 0.0


def _quarter_periods(through: str, count: int) -> tuple[str, ...]:
    year = int(through[:4])
    quarter = (int(through[4:6]) - 1) // 3
    periods = []
    for offset in range(count + 4):
        index = year * 4 + quarter - offset
        period_year, period_quarter = divmod(index, 4)
        period = f"{period_year}{('0331', '0630', '0930', '1231')[period_quarter]}"
        if period <= through:
            periods.append(period)
        if len(periods) == count:
            break
    return tuple(reversed(periods))


def _active_on(row: dict[str, Any], trade_date: str) -> bool:
    start = str(row.get("in_date") or "")
    end = str(row.get("out_date") or "")
    return (not start or start <= trade_date) and (not end or end > trade_date)


def _number(value: Any) -> float:
    try:
        number = float(value)
        return number if number == number else 0.0
    except (TypeError, ValueError):
        return 0.0
