# -*- coding: utf-8 -*-
"""调用 v1.apizero.cn。只请求本站 /api/<id>，不改写媒体地址。"""

from __future__ import annotations

import json
import os
import re
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

from catalog import by_id

API_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$")
DEFAULT_BASE = "https://v1.apizero.cn"
MAX_BODY = 8 * 1024 * 1024
MAX_RESPONSE = 80_000


class CallError(Exception):
    """调用方能直接读懂的失败，还没打到上游，或上游返回了非 JSON。"""


def load_local_env() -> None:
    path = Path(__file__).resolve().parent / ".env"
    if not path.is_file():
        return
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip().strip('"').strip("'")
        if key and key not in os.environ:
            os.environ[key] = value


def base_url() -> str:
    return os.environ.get("APIZERO_BASE", DEFAULT_BASE).strip().rstrip("/") or DEFAULT_BASE


def api_key() -> str:
    return os.environ.get("APIZERO_KEY", "").strip()


def _merge(params: dict | None, json_body: dict | None) -> dict:
    merged: dict = {}
    if isinstance(params, dict):
        merged.update(params)
    if isinstance(json_body, dict):
        merged.update(json_body)
    return {str(key): value for key, value in merged.items() if value is not None}


def prepare(api_id: str, params: dict | None, json_body: dict | None, method: str, confirm_sensitive: bool) -> dict:
    api_id = api_id.strip().lower()
    if not API_ID_RE.fullmatch(api_id):
        raise CallError("api_id 只能是小写字母、数字和短横线，例如 weather、company-search")
    row = by_id(api_id)
    if row and row.get("kyc") and not confirm_sensitive:
        raise CallError(
            f"{api_id} 是实名或车牌核验，会扣费。"
            "只有用户明确要求核验，并且姓名、证件号或车牌是用户自己提供的，才把 confirm_sensitive 设为 true 再调一次。"
        )
    chosen = (method or "AUTO").strip().upper()
    if chosen == "AUTO":
        chosen = str((row or {}).get("method") or "POST")
    if chosen not in {"GET", "POST"}:
        raise CallError("method 只能是 AUTO、GET 或 POST")
    payload = _merge(params, json_body)
    if chosen == "GET" and any(isinstance(value, (dict, list)) for value in payload.values()):
        raise CallError("GET 不能带数组或对象。改用 POST，或把这个接口的 method 设为 POST。")
    encoded = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    if len(encoded) > MAX_BODY:
        raise CallError("参数超过 8MB，图片请先确认 input_data 体积")
    return {
        "api_id": api_id,
        "row": row,
        "method": chosen,
        "url": f"{base_url()}/api/{api_id}",
        "payload": payload,
        "timeout": int((row or {}).get("timeout_sec") or 25),
    }


def call(api_id: str, params: dict | None = None, json_body: dict | None = None, method: str = "AUTO", confirm_sensitive: bool = False) -> dict:
    plan = prepare(api_id, params, json_body, method, confirm_sensitive)
    request = _request(plan)
    try:
        with urllib.request.urlopen(request, timeout=plan["timeout"]) as response:
            status = int(response.status)
            raw = response.read()
    except urllib.error.HTTPError as exc:
        status = int(exc.code)
        raw = exc.read()
    except urllib.error.URLError as exc:
        raise CallError(f"连不上 {plan['url']}：{exc.reason}") from exc
    except TimeoutError as exc:
        raise CallError(f"{plan['api_id']} 超过 {plan['timeout']} 秒还没返回") from exc
    text = raw.decode("utf-8", errors="replace")
    text = _redact(text)
    try:
        body = json.loads(text)
    except json.JSONDecodeError:
        return {
            "ok": False,
            "http": status,
            "api_id": plan["api_id"],
            "msg": "响应不是 JSON",
            "raw": text[:4000],
        }
    shaped = _shape(status, body)
    shaped["api_id"] = plan["api_id"]
    shaped["http"] = status
    row = plan["row"]
    if row is None:
        shaped["note"] = "本地目录没有这个接口，参数没有核对。返回内容原样转述。"
    elif row.get("media"):
        shaped["note"] = "媒体地址是接口原样返回的直链。不要改写成代理，也不要换成经本站中转的播放地址。"
    return _shrink(shaped)


def _request(plan: dict) -> urllib.request.Request:
    headers = {
        "Accept": "application/json",
        "User-Agent": "apizero-mcp/0.1",
    }
    key = api_key()
    if key:
        headers["Authorization"] = f"Bearer {key}"
    payload = plan["payload"]
    if plan["method"] == "GET":
        query = urllib.parse.urlencode({k: v for k, v in payload.items()}, doseq=True)
        url = plan["url"] + (("?" + query) if query else "")
        return urllib.request.Request(url, headers=headers, method="GET")
    headers["Content-Type"] = "application/json; charset=utf-8"
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    return urllib.request.Request(plan["url"], data=data, headers=headers, method="POST")


def _shape(status: int, body: object) -> dict:
    if isinstance(body, dict) and "code" in body and ("data" in body or "msg" in body or "desc" in body):
        code = body.get("code")
        return {
            "ok": code in (0, "0", 200, "200"),
            "code": code,
            "msg": body.get("msg") or body.get("desc") or "",
            "request_id": body.get("request_id") or "",
            "data": body.get("data"),
        }
    if isinstance(body, dict) and "choices" in body:
        return {"ok": True, "code": 0, "msg": "成功", "request_id": "", "data": body}
    if isinstance(body, dict) and isinstance(body.get("error"), dict):
        err = body["error"]
        apizero = err.get("apizero") if isinstance(err.get("apizero"), dict) else {}
        return {
            "ok": False,
            "code": apizero.get("code"),
            "msg": str(err.get("message") or "请求失败"),
            "request_id": str(apizero.get("request_id") or ""),
            "data": apizero.get("detail"),
        }
    return {"ok": 200 <= status < 300, "code": None, "msg": "", "request_id": "", "data": body}


def _redact(text: str) -> str:
    key = api_key()
    if key and key in text:
        text = text.replace(key, "sk_***")
    return text


def _shrink(payload: dict) -> dict:
    encoded = json.dumps(payload, ensure_ascii=False)
    if len(encoded) <= MAX_RESPONSE:
        return payload
    payload = dict(payload)
    payload["data"] = _trim(payload.get("data"))
    payload["truncated"] = True
    encoded = json.dumps(payload, ensure_ascii=False)
    if len(encoded) <= MAX_RESPONSE:
        return payload
    payload["data"] = {"preview": encoded[:4000], "truncated": True}
    return payload


def _trim(value: object, depth: int = 0) -> object:
    if isinstance(value, list):
        if len(value) > 12:
            return [_trim(item, depth + 1) for item in value[:12]] + [{"_omitted": len(value) - 12}]
        return [_trim(item, depth + 1) for item in value]
    if isinstance(value, dict):
        return {str(key): _trim(item, depth + 1) for key, item in value.items()}
    if isinstance(value, str) and len(value) > 4000:
        return value[:4000] + "…(已截断)"
    return value
