from __future__ import annotations

import json
import re
import threading
import urllib.error
import urllib.request
from collections.abc import Callable
from datetime import datetime
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 IfindProvider:
    source = DataSource.IFIND
    base_url = "https://quantapi.51ifind.com/api/v1"
    auth_error_codes = {-1302, -1303, -1304, -4302, -4303}

    def __init__(
        self,
        refresh_token: str | None | Callable[[], str | None],
        access_token: str | None | Callable[[], str | None],
        timeout: int = 15,
    ) -> None:
        self._refresh_provider = refresh_token if callable(refresh_token) else lambda: refresh_token
        self._access_provider = access_token if callable(access_token) else lambda: access_token
        self._issued_access = ""
        self._timeout = timeout
        self._lock = threading.Lock()

    @property
    def configured(self) -> bool:
        return bool(self._refresh() or self._configured_access())

    def calendar(self, start_date: str, end_date: str) -> ProviderResult:
        raise ProviderError("iFinD is not the calendar authority")

    def entities(self) -> ProviderResult:
        raise ProviderError("iFinD is not the entity-directory authority")

    def daily(self, entity_type: str, identifier: str, end_date: str) -> ProviderResult:
        end = datetime.strptime(_compact(end_date), "%Y%m%d")
        start = end.replace(year=end.year - 1).strftime("%Y-%m-%d")
        payload = self._request(
            "cmd_history_quotation",
            {
                "codes": identifier,
                "indicators": "open,high,low,close,volume,amount",
                "startdate": start,
                "enddate": end.strftime("%Y-%m-%d"),
                "functionpara": {"CPS": "forward1", "Fill": "Omit"},
            },
        )
        return _result(payload, "yuan/share", "forward", SnapshotState.ARCHIVE)

    def minute(self, entity_type: str, identifier: str, trade_date: str) -> ProviderResult:
        date = _display(trade_date)
        payload = self._request(
            "high_frequency",
            {
                "codes": identifier,
                "indicators": "open,high,low,close,volume,amount,avgPrice",
                "starttime": f"{date} 09:30:00",
                "endtime": f"{date} 15:00:00",
                "functionpara": {
                    "CPS": "forward1",
                    "Fill": "Previous",
                    "Timeformat": "LocalTime",
                    "Interval": "1",
                    "Limitstart": "09:30:00",
                    "Limitend": "15:00:00",
                },
            },
        )
        return _result(payload, "yuan/share", "forward", SnapshotState.REALTIME)

    def snapshot_inputs(
        self, trade_date: str, previous_trade_date: str
    ) -> dict[str, ProviderResult | dict[str, Any]]:
        raise ProviderError("iFinD is not the post-close snapshot authority")

    def realtime_market_inputs(
        self,
        trade_date: str,
        previous_trade_date: str,
        identifiers: tuple[str, ...],
    ) -> dict[str, ProviderResult | dict[str, Any]]:
        raise ProviderError("iFinD不承担全市场盘中快照计算")

    def sector_members(self, representative: str, trade_date: str) -> ProviderResult:
        raise ProviderError("iFinD is not the Shenwan constituent authority")

    def heaven_inputs(
        self, representative: str, trade_date: str
    ) -> dict[str, ProviderResult | None]:
        raise ProviderError("iFinD deterministic Heaven inputs are not enabled")

    def heaven_realtime_inputs(
        self, representative: str, trade_date: str, previous_trade_date: str
    ) -> dict[str, ProviderResult | None]:
        raise ProviderError("iFinD deterministic Heaven inputs are not enabled")

    def market_insight(
        self,
        kind: str,
        trade_date: str,
        previous_trade_date: str = "",
        identifier: str = "",
    ) -> dict[str, ProviderResult | None]:
        raise ProviderError("iFinD只承担许可范围内的动态竞价快照")

    def realtime_snapshots(
        self, identifiers: tuple[str, ...], start_time: str, end_time: str
    ) -> ProviderResult:
        rows: list[dict[str, Any]] = []
        for offset in range(0, len(identifiers), 80):
            batch = identifiers[offset : offset + 80]
            if not batch:
                continue
            payload = self._request(
                "snap_shot",
                {
                    "codes": ",".join(batch),
                    "indicators": "latest,volume,amount,preClose,turnoverRatio,volumeRatio,"
                    "bid1,bidSize1,ask1,askSize1",
                    "starttime": start_time,
                    "endtime": end_time,
                },
            )
            rows.extend(_result(payload, "mixed", "not_applicable", SnapshotState.REALTIME).rows)
        covered = {str(row.get("thscode") or "") for row in rows if row.get("thscode")}
        return ProviderResult(
            tuple(rows),
            ObservationMetadata(
                source=self.source,
                observed_at=datetime.now(SHANGHAI),
                unit="mixed",
                adjustment="not_applicable",
                freshness_seconds=0,
                coverage=min(len(covered) / max(len(identifiers), 1), 1),
                state=SnapshotState.REALTIME,
                usage=DataUsage.CALCULATION,
            ),
        )

    def event_reasons(self, trade_date: str) -> ProviderResult:
        date = datetime.fromisoformat(_display(trade_date)).date()
        display = f"{date.year}年{date.month}月{date.day}日"
        queries = {
            "limit_up": (
                f"{display}涨停股票，股票代码、股票简称、涨停原因、"
                "首次涨停时间、最终涨停时间、开板次数"
            ),
            "broken": (
                f"{display}曾涨停但收盘未涨停的股票，股票代码、股票简称、"
                "涨停原因、首次涨停时间、开板次数"
            ),
            "limit_down": f"{display}跌停股票，股票代码、股票简称、跌停原因",
        }
        rows: list[dict[str, Any]] = []
        completed = 0
        for event_type, query in queries.items():
            try:
                payload = self._request(
                    "smart_stock_picking",
                    {"searchstring": query, "searchtype": "stock"},
                )
            except ProviderError:
                continue
            completed += 1
            for raw in _result(
                payload, "event", "not_applicable", SnapshotState.FINAL
            ).rows:
                identifier = _event_identifier(raw)
                if not identifier:
                    continue
                reason_tokens = (
                    ("跌停原因", "风险线索", "原因")
                    if event_type == "limit_down"
                    else ("涨停原因类别", "涨停原因", "触板逻辑", "原因")
                )
                rows.append(
                    {
                        "event_type": event_type,
                        "identifier": identifier,
                        "reason": str(_event_field(raw, reason_tokens) or "").strip(),
                        "first_time": _event_time(
                            _event_field(
                                raw, ("首次涨停时间", "首次触板时间", "首次封板时间")
                            )
                        ),
                        "last_time": _event_time(
                            _event_field(
                                raw, ("最终涨停时间", "最后涨停时间", "最后封板时间")
                            )
                        ),
                        "open_times": _event_integer(
                            _event_field(raw, ("开板次数", "打开涨停次数"))
                        ),
                    }
                )
        return ProviderResult(
            tuple(rows),
            ObservationMetadata(
                source=self.source,
                observed_at=datetime.now(SHANGHAI),
                unit="event",
                adjustment="not_applicable",
                freshness_seconds=0,
                coverage=completed / len(queries),
                state=SnapshotState.FINAL,
                usage=DataUsage.CALCULATION,
            ),
        )

    def screener_inputs(self, trade_dates: tuple[str, ...]) -> dict[str, ProviderResult | None]:
        raise ProviderError("iFinD尚未批准用于盘后因子批量计算")

    def _request(self, endpoint: str, body: dict[str, Any]) -> dict[str, Any]:
        if not self.configured:
            raise ProviderError("实时行情服务尚未配置")
        payload = self._post(endpoint, body, self._access())
        code = _error_code(payload)
        if self._auth_error(payload) and self._refresh():
            payload = self._post(endpoint, body, self._refresh_access())
            code = _error_code(payload)
        if code != 0:
            raise ProviderError(
                str(payload.get("errmsg") or payload.get("message") or "实时行情服务拒绝请求")
            )
        return payload

    def _access(self) -> str:
        with self._lock:
            if self._issued_access:
                return self._issued_access
            configured = self._configured_access()
            if configured:
                return configured
            refresh = self._refresh()
            if not refresh:
                raise ProviderError("实时行情服务尚未配置")
            payload = self._post("get_access_token", {}, "", refresh)
            token = str((payload.get("data") or {}).get("access_token") or "").strip()
            if not token:
                raise ProviderError("实时行情服务授权失败")
            self._issued_access = token
            return token

    def _refresh_access(self) -> str:
        refresh = self._refresh()
        if not refresh:
            raise ProviderError("实时行情服务授权失败")
        with self._lock:
            payload = self._post("get_access_token", {}, "", refresh)
            token = str((payload.get("data") or {}).get("access_token") or "").strip()
            if not token:
                raise ProviderError("实时行情服务授权失败")
            self._issued_access = token
            return token

    def _auth_error(self, payload: dict[str, Any]) -> bool:
        message = str(payload.get("errmsg") or payload.get("message") or "").casefold()
        return (
            _error_code(payload) in self.auth_error_codes or "token" in message or "鉴权" in message
        )

    def _refresh(self) -> str:
        return str(self._refresh_provider() or "").strip()

    def _configured_access(self) -> str:
        return str(self._access_provider() or "").strip()

    def _post(
        self, endpoint: str, body: dict[str, Any], access: str, refresh: str = ""
    ) -> dict[str, Any]:
        headers = {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "User-Agent": "XiaobaiReview/2",
            "ifindlang": "cn",
        }
        if access:
            headers["access_token"] = access
        if refresh:
            headers["refresh_token"] = refresh
        request = urllib.request.Request(
            f"{self.base_url}/{endpoint}",
            data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
            headers=headers,
            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 not isinstance(payload, dict):
            raise ProviderError("实时行情服务返回格式无效")
        return payload


def _result(
    payload: dict[str, Any], unit: str, adjustment: str, state: SnapshotState
) -> ProviderResult:
    tables = payload.get("tables") or (payload.get("data") or {}).get("tables") or []
    if isinstance(tables, dict):
        tables = [tables]
    rows: list[dict[str, Any]] = []
    for block in tables:
        columns = block.get("table") or {}
        if not columns:
            continue
        times = block.get("time") or []
        codes = block.get("thscode") or block.get("thscodes") or []
        if isinstance(codes, str):
            codes = [codes]
        size = max(
            (len(value) for value in columns.values() if isinstance(value, list)),
            default=len(times) if isinstance(times, list) else 1,
        )
        for index in range(size):
            row = {
                key: values[index]
                if isinstance(values, list) and index < len(values)
                else values
                if index == 0
                else None
                for key, values in columns.items()
            }
            if isinstance(times, list) and index < len(times):
                row["time"] = times[index]
            if codes:
                row["thscode"] = codes[index] if index < len(codes) else codes[0]
            rows.append(row)
    metadata = ObservationMetadata(
        source=DataSource.IFIND,
        observed_at=datetime.now(SHANGHAI),
        unit=unit,
        adjustment=adjustment,
        freshness_seconds=0,
        coverage=1 if rows else 0,
        state=state,
        usage=DataUsage.DISPLAY,
    )
    return ProviderResult(tuple(rows), metadata)


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 _error_code(payload: dict[str, Any]) -> int:
    try:
        return int(payload.get("errorcode", payload.get("code", 0)) or 0)
    except (TypeError, ValueError):
        return -1


def _event_field(row: dict[str, Any], tokens: tuple[str, ...]) -> Any:
    for token in tokens:
        for key, value in row.items():
            if token in str(key) and value not in (None, ""):
                return value
    return None


def _event_identifier(row: dict[str, Any]) -> str:
    value = _event_field(row, ("股票代码", "证券代码", "代码", "thscode"))
    text = str(value or "").strip().upper()
    match = re.search(r"(?<!\d)(\d{6})(?!\d)", text)
    if not match:
        return ""
    code = match.group(1)
    if re.fullmatch(r"\d{6}\.(?:SH|SZ|BJ)", text):
        return text
    suffix = "BJ" if code.startswith(("4", "8", "9")) else "SH" if code.startswith("6") else "SZ"
    return f"{code}.{suffix}"


def _event_time(value: Any) -> str:
    text = str(value or "").strip()
    match = re.search(r"(?<!\d)(\d{1,2}):(\d{2})(?::\d{2})?(?!\d)", text)
    if match:
        return f"{int(match.group(1)):02d}:{match.group(2)}"
    compact = re.search(r"(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)", text)
    return f"{compact.group(1)}:{compact.group(2)}" if compact else ""


def _event_integer(value: Any) -> int | None:
    try:
        return max(0, int(float(value))) if value not in (None, "") else None
    except (TypeError, ValueError):
        return None
