"""零依赖数据接口客户端。

    from mq_data import MQData

    client = MQData(token="...")
    rows = client.limit_list_d(trade_date="20261106", limit=100)

``limit`` 表示期望取到的总行数，客户端自动翻页取满；传 ``all_pages=False``
可只取当前页并自行按 ``offset`` 翻页。安装 pandas 时返回 DataFrame；未安装
时返回 ``{"fields": [...], "items": [...], "has_more": bool}``。
"""

from __future__ import annotations

import json
import os
import urllib.error
import urllib.parse
import urllib.request

try:  # 可选依赖
    import pandas as pd
except ImportError:  # pragma: no cover - 多数数据环境有 pandas
    pd = None


class MQDataError(RuntimeError):
    def __init__(self, code: int, message: str):
        super().__init__(f"[{code}] {message}")
        self.code = code
        self.message = message


class MQData:
    def __init__(self, base_url: str | None = None, token: str | None = None,
                 timeout: float = 30.0):
        self.base_url = (base_url
                         or os.environ.get("MQ_DATA_API_URL")
                         or "https://data.qxdt.cn").rstrip("/")
        self.token = token or os.environ.get("MQ_DATA_API_TOKEN", "")
        self.timeout = float(timeout)

    def _request(self, url: str, *, data: bytes | None = None,
                 method: str = "GET") -> dict:
        headers = {"Accept": "application/json",
                   "User-Agent": "mq-data-api-python/1.0"}
        if self.token:
            headers["X-API-Token"] = self.token
        if data is not None:
            headers["Content-Type"] = "application/json; charset=utf-8"
        request = urllib.request.Request(url, data=data, headers=headers,
                                         method=method)
        try:
            with urllib.request.urlopen(request, timeout=self.timeout) as response:
                body = response.read().decode("utf-8", errors="replace")
        except urllib.error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="replace")
            try:
                payload = json.loads(body)
            except ValueError:
                raise MQDataError(exc.code, body[:200]) from exc
            raise MQDataError(int(payload.get("code") or exc.code),
                              str(payload.get("msg") or "请求失败")) from exc
        except urllib.error.URLError as exc:
            raise MQDataError(0, f"无法连接数据接口：{exc.reason}") from exc
        try:
            return json.loads(body)
        except ValueError as exc:
            raise MQDataError(0, f"响应不是 JSON：{body[:200]}") from exc

    @staticmethod
    def _unwrap(payload: dict):
        code = int(payload.get("code") or 0)
        if code != 0:
            raise MQDataError(code, str(payload.get("msg") or "请求失败"))
        return payload.get("data") or {}

    @staticmethod
    def _columns(fields) -> list[str]:
        if not fields:
            return []
        if isinstance(fields, str):
            return [part.strip() for part in fields.split(",") if part.strip()]
        return list(fields)

    @staticmethod
    def _as_int(value) -> int | None:
        try:
            return int(value)
        except (TypeError, ValueError):
            return None

    def _collect(self, fetch, fields, params: dict, *, all_pages: bool,
                 max_pages: int):
        offset = self._as_int(params.get("offset")) or 0
        total = self._as_int(params.get("limit"))
        if total is not None and total < 1:
            total = None
        columns = self._columns(fields)
        rows: list[list] = []
        has_more = False
        pages = 0
        while True:
            page_params = dict(params)
            page_params["offset"] = offset
            if total is not None:
                page_params["limit"] = max(total - len(rows), 1)
            data = fetch(page_params)
            columns = list(data.get("fields") or columns)
            page_rows = list(data.get("items") or [])
            rows.extend(page_rows)
            has_more = bool(data.get("has_more"))
            pages += 1
            if not all_pages or not has_more or not page_rows:
                break
            if total is not None and len(rows) >= total:
                break
            if pages >= max(1, int(max_pages)):
                break
            offset += len(page_rows)
        return self._frame({"fields": columns, "items": rows,
                            "has_more": has_more})

    def query(self, api_name: str, fields=None, *, all_pages: bool = True,
              max_pages: int = 100, **params):
        def fetch(page_params: dict) -> dict:
            body = {"api_name": api_name, "params": page_params}
            if fields:
                body["fields"] = (fields if isinstance(fields, str)
                                  else list(fields))
            payload = self._request(
                f"{self.base_url}/data/v1",
                data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
                method="POST")
            return self._unwrap(payload)

        return self._collect(fetch, fields, params, all_pages=all_pages,
                             max_pages=max_pages)

    def get(self, api_name: str, *, all_pages: bool = True,
            max_pages: int = 100, **params):
        def fetch(page_params: dict) -> dict:
            query = urllib.parse.urlencode(
                {"api_name": api_name, **page_params}, doseq=True)
            payload = self._request(f"{self.base_url}/data/v1?{query}")
            return self._unwrap(payload)

        return self._collect(fetch, params.get("fields"), params,
                             all_pages=all_pages, max_pages=max_pages)

    def apis(self) -> list[dict]:
        payload = self._request(f"{self.base_url}/data/v1/apis")
        return list((self._unwrap(payload) or {}).get("apis") or [])

    def health(self) -> dict:
        return self._request(f"{self.base_url}/health")

    @staticmethod
    def _frame(data: dict):
        fields = list(data.get("fields") or [])
        items = list(data.get("items") or [])
        if pd is not None:
            return pd.DataFrame(items, columns=fields)
        return {"fields": fields, "items": items,
                "has_more": bool(data.get("has_more"))}

    def __getattr__(self, name: str):
        if name.startswith("_"):
            raise AttributeError(name)

        def call(fields=None, **params):
            return self.query(name, fields=fields, **params)

        return call
