# client.py (仅展示“需要替换的整文件最终版”，你可直接覆盖)
from __future__ import annotations
from typing import Any, Dict, Callable, Optional
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from .vendor import default_base, patch_hdr, info, laybot_error_meta
from .exceptions import (
    LayBotError, HttpError,
    LayBotApiError,
    ModelNotAllowedError, ParamInvalidError, FunctionUnsupportedError,
    CapabilityForbiddenError, DuplicateRequestError,
    ApiKeyInvalidError, IpNotAllowedError,
    InsufficientCreditError, PayloadTooLargeError, UnsupportedMediaTypeError,
    JobNotFinishedError, RateLimitedError,
    InternalError, DependencyError, UpstreamTimeoutError,
    ServiceUnavailableError, MaintenanceModeError,
)

_LAYBOT_EXC_MAP = {
    40001: ModelNotAllowedError,
    40002: ParamInvalidError,
    40003: FunctionUnsupportedError,
    40004: CapabilityForbiddenError,
    40005: DuplicateRequestError,
    40101: ApiKeyInvalidError,
    40102: IpNotAllowedError,
    40200: InsufficientCreditError,
    41300: PayloadTooLargeError,
    41500: UnsupportedMediaTypeError,
    42300: JobNotFinishedError,
    42900: RateLimitedError,
    50000: InternalError,
    50001: DependencyError,
    50002: UpstreamTimeoutError,
    50300: ServiceUnavailableError,
    50301: MaintenanceModeError,
}

_DETAIL_STR_TO_CODE = {
    "INSUFFICIENT_CREDIT": 40200,
    "RATE_LIMITED": 42900,
    "API_KEY_INVALID": 40101,
    "IP_NOT_ALLOWED": 40102,
    "MODEL_NOT_ALLOWED": 40001,
    "PARAM_INVALID": 40002,
    "FUNCTION_UNSUPPORTED": 40003,
    "CAPABILITY_FORBIDDEN": 40004,
    "DUPLICATE_REQUEST": 40005,
    "PAYLOAD_TOO_LARGE": 41300,
    "UNSUPPORTED_MEDIA_TYPE": 41500,
    "JOB_NOT_FINISHED": 42300,
    "INTERNAL_ERROR": 50000,
    "DEPENDENCY_ERROR": 50001,
    "TIMEOUT": 50002,
    "SERVICE_UNAVAILABLE": 50300,
    "MAINTENANCE_MODE": 50301,
}


class Client:
    _DEF_TMO = {"connect": 10, "idle": 180}

    def __init__(
        self,
        api_key: str,
        *,
        vendor: str = "laybot",
        base: str | None = None,
        timeout: Dict[str, int] | None = None,
        on_req: Callable | None = None,
        on_resp: Callable | None = None,
        proxies: dict | None = None,
        verify: bool | str | None = None,
        headers: dict | None = None,
        query: dict | None = None,
    ):
        self.vendor = vendor
        self.base = (base or default_base(vendor)).rstrip("/") + "/"

        self.headers = {
            "Content-Type": "application/json",
            **patch_hdr(vendor, api_key),
            **(headers or {}),
        }

        self.query_default = dict(query or {})

        qname = info(vendor).get("query_key_name")
        if qname and api_key and qname not in self.query_default:
            self.query_default[qname] = api_key

        self.tmo = {**self._DEF_TMO, **(timeout or {})}
        self.on_req, self.on_resp = on_req, on_resp
        self.proxies = proxies
        self.verify = verify

        retry = Retry(
            total=3,
            backoff_factor=.2,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=False,
        )
        self.session = requests.Session()
        self.session.mount("http://", HTTPAdapter(max_retries=retry))
        self.session.mount("https://", HTTPAdapter(max_retries=retry))

    @staticmethod
    def _safe_json(resp: requests.Response) -> Optional[dict]:
        try:
            return resp.json()
        except Exception:
            return None

    def _make_laybot_error(
        self,
        *,
        status: int,
        url: str,
        raw_json: Optional[dict],
        raw_text: str,
    ) -> LayBotApiError | None:
        code: Optional[int] = None
        msg: str = ""
        rid: str = ""
        err_str: str = ""

        if isinstance(raw_json, dict):
            if isinstance(raw_json.get("code"), int) and raw_json.get("code") != 200:
                code = int(raw_json["code"])
                msg = str(raw_json.get("message") or "")
                rid = str(raw_json.get("request_id") or raw_json.get("request_uid") or "")
                meta = laybot_error_meta(code)
                err_str = (meta or {}).get("err", "")
            elif "detail" in raw_json:
                detail = raw_json.get("detail")
                if isinstance(detail, int):
                    code = int(detail)
                    meta = laybot_error_meta(code)
                    err_str = (meta or {}).get("err", "")
                elif isinstance(detail, str):
                    mapped = _DETAIL_STR_TO_CODE.get(detail.strip().upper())
                    if mapped:
                        code = mapped
                        meta = laybot_error_meta(code)
                        err_str = (meta or {}).get("err", detail)
                    else:
                        return None
                elif isinstance(detail, dict):
                    if isinstance(detail.get("code"), int):
                        code = int(detail["code"])
                        msg = str(detail.get("message") or "")
                        meta = laybot_error_meta(code)
                        err_str = (meta or {}).get("err", "")
                    else:
                        return None
            else:
                return None
        else:
            return None

        if code is None:
            return None

        if not msg:
            msg = raw_text.strip()
        if not err_str:
            meta = laybot_error_meta(code)
            err_str = (meta or {}).get("err", "")

        exc_cls = _LAYBOT_EXC_MAP.get(code, LayBotApiError)
        return exc_cls(
            status_code=status,
            message=msg,
            url=url,
            raw=raw_json if raw_json is not None else raw_text,
            code=code,
            err=err_str,
            request_id=rid,
        )

    def _req(self, m: str, path: str, **opt):
        url = self.base + path.lstrip("/")

        params = {**self.query_default, **(opt.pop("params", {}) or {})}
        if params:
            opt["params"] = params
        if self.proxies is not None and "proxies" not in opt:
            opt["proxies"] = self.proxies
        if self.verify is not None and "verify" not in opt:
            opt["verify"] = self.verify

        if self.on_req:
            self.on_req(m, url, opt)

        try:
            r: requests.Response = self.session.request(
                m, url,
                headers=self.headers,
                timeout=(self.tmo["connect"], self.tmo["idle"]),
                **opt
            )

            if self.on_resp:
                self.on_resp(r)

            if r.status_code < 400:
                return r

            raw_text = ""
            try:
                raw_text = r.text or ""
            except Exception:
                raw_text = ""

            raw_json = self._safe_json(r)

            is_laybot = (self.vendor == "laybot") or ("api.laybot.cn" in (self.base or ""))
            if is_laybot:
                lb_err = self._make_laybot_error(
                    status=r.status_code,
                    url=url,
                    raw_json=raw_json,
                    raw_text=raw_text,
                )
                if lb_err:
                    raise lb_err

            if isinstance(raw_json, dict) and isinstance(raw_json.get("error"), dict):
                err = raw_json["error"]
                msg = str(err.get("message") or raw_text)
                raise HttpError(status_code=r.status_code, message=msg, url=url, raw=raw_json)

            raise HttpError(status_code=r.status_code, message=raw_text.strip(), url=url, raw=raw_json or raw_text)

        except requests.RequestException as exc:
            raise LayBotError(str(exc)) from exc

    def post(self, path: str, body: Dict[str, Any], stream=False, params: dict | None = None):
        return self._req("POST", path, json=body, stream=stream, params=params)

    def get(self, path: str, **params):
        return self._req("GET", path, params=params)

    def delete(self, path: str):
        return self._req("DELETE", path)

    def patch(self, path: str, body: Dict[str, Any], params: dict | None = None):
        return self._req("PATCH", path, json=body, params=params)
