"""
35pay Python SDK
================

35pay 的官方 Python SDK，对应 npm `@35m/sdk`。

Install:
    pip install 35m-sdk            # 同步用法，零依赖（stdlib urllib）
    pip install '35m-sdk[async]'   # 含 httpx，支持 AsyncPay

同步用法（FastAPI 同步路由 / Flask / Django sync 视图）:
    from pay35 import Pay

    pay = Pay(api_key=os.environ["PAY_KEY"])
    session = pay.create_checkout(
        amount=9900, currency="CNY",
        description="追思视频套餐",
        metadata={"order_id": str(order.id)},
        success_url="https://yoursite.com/orders/123",
    )
    return redirect(session["url"])

异步用法（FastAPI / Starlette / Litestar / aiohttp）:
    from pay35 import AsyncPay

    async with AsyncPay(api_key=os.environ["PAY_KEY"]) as pay:
        session = await pay.create_checkout(amount=9900, currency="CNY", ...)
        return {"url": session["url"]}

    # 或长期复用，shutdown 时显式 aclose：
    pay = AsyncPay(api_key=...)
    try:
        ...
    finally:
        await pay.aclose()

校验 webhook 签名（同步 / 异步通用）:
    if not Pay.verify_webhook_signature(payload, sig, WEBHOOK_SECRET):
        return "invalid", 401

文档：https://pay.35team.com/docs
"""
from __future__ import annotations

import hashlib
import hmac
import json
import time
import urllib.error
import urllib.request
from typing import Any, Optional

__version__ = "0.2.0"
__all__ = ["Pay", "AsyncPay", "PayError", "__version__"]


class PayError(Exception):
    def __init__(self, status: int, body: str):
        super().__init__(f"35pay {status}: {body[:120]}")
        self.status = status
        self.body = body


class Pay:
    def __init__(
        self,
        api_key: str,
        base_url: str = "https://pay.35team.com",
        timeout: float = 15.0,
    ) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    # ---- Checkout ----

    def create_checkout(
        self,
        amount: int,
        currency: str = "CNY",
        description: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        success_url: Optional[str] = None,
        expires_in_seconds: Optional[int] = None,
    ) -> dict[str, Any]:
        """
        创建支付 session。

        success_url：客户付款成功后跳回的地址。
            不传则落到 35pay 收据页 /r/{recordId}（适合不需业务回调的简单收款）。
            传了的话，客户落地时本地 webhook 已处理完毕（同步中转），可直接读取 paid 状态，
            无需前端 polling。
        """
        body = _build_checkout_body(
            amount, currency, description, metadata, success_url, expires_in_seconds
        )
        return self._request("POST", "/api/v1/checkout", body)

    def get_session(self, session_id: str) -> dict[str, Any]:
        return self._request("GET", f"/api/v1/sessions/{session_id}")

    def refund(
        self,
        record_id: str,
        amount: Optional[int] = None,
        reason: Optional[str] = None,
    ) -> dict[str, Any]:
        body = _build_refund_body(record_id, amount, reason)
        return self._request("POST", "/api/v1/refunds", body)

    # ---- Webhook ----

    @staticmethod
    def verify_webhook_signature(
        payload: str,
        signature_header: str,
        secret: str,
        tolerance_seconds: int = 300,
    ) -> bool:
        """
        校验 35pay 转发到商户 URL 的 webhook 签名。
        header 格式: "t=<unix_ts>,v1=<hex_hmac>"
        签名 message: f"{ts}.{payload}"
        """
        parts: dict[str, str] = {}
        for kv in signature_header.split(","):
            if "=" in kv:
                k, v = kv.split("=", 1)
                parts[k.strip()] = v.strip()
        if "t" not in parts or "v1" not in parts:
            return False
        try:
            ts = int(parts["t"])
        except ValueError:
            return False
        if abs(time.time() - ts) > tolerance_seconds:
            return False
        expected = hmac.new(
            secret.encode("utf-8"),
            f"{ts}.{payload}".encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()
        return hmac.compare_digest(expected, parts["v1"])

    # ---- internal ----

    def _request(
        self,
        method: str,
        path: str,
        body: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]:
        url = self.base_url + path
        data: Optional[bytes] = None
        if body is not None:
            data = json.dumps(body).encode("utf-8")
        req = urllib.request.Request(
            url,
            data=data,
            method=method,
            headers=_auth_headers(self.api_key),
        )
        try:
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                return json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            raise PayError(e.code, e.read().decode("utf-8", errors="replace")) from None


def _auth_headers(api_key: str) -> dict[str, str]:
    return {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}",
        "User-Agent": f"35m-sdk-python/{__version__}",
    }


def _build_checkout_body(
    amount: int,
    currency: str,
    description: Optional[str],
    metadata: Optional[dict[str, Any]],
    success_url: Optional[str],
    expires_in_seconds: Optional[int],
) -> dict[str, Any]:
    body: dict[str, Any] = {"amount": amount, "currency": currency}
    if description is not None:
        body["description"] = description
    if metadata is not None:
        body["metadata"] = metadata
    if success_url is not None:
        body["successUrl"] = success_url
    if expires_in_seconds is not None:
        body["expiresInSeconds"] = expires_in_seconds
    return body


def _build_refund_body(
    record_id: str,
    amount: Optional[int],
    reason: Optional[str],
) -> dict[str, Any]:
    body: dict[str, Any] = {"recordId": record_id}
    if amount is not None:
        body["amount"] = amount
    if reason is not None:
        body["reason"] = reason
    return body


class AsyncPay:
    """
    异步版本，给 FastAPI / Starlette / Litestar / aiohttp 等 async 框架用。

    需要 httpx：`pip install '35m-sdk[async]'` 或 `pip install httpx`。

    资源管理：
        pay = AsyncPay(api_key=...)
        try:
            session = await pay.create_checkout(amount=9900, currency="CNY")
        finally:
            await pay.aclose()

    或用 async context manager：
        async with AsyncPay(api_key=...) as pay:
            session = await pay.create_checkout(...)
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://pay.35team.com",
        timeout: float = 15.0,
    ) -> None:
        try:
            import httpx
        except ImportError as e:
            raise ImportError(
                "AsyncPay requires httpx. Install with: pip install '35m-sdk[async]'"
            ) from e
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            timeout=timeout,
            headers=_auth_headers(api_key),
        )

    async def create_checkout(
        self,
        amount: int,
        currency: str = "CNY",
        description: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        success_url: Optional[str] = None,
        expires_in_seconds: Optional[int] = None,
    ) -> dict[str, Any]:
        body = _build_checkout_body(
            amount, currency, description, metadata, success_url, expires_in_seconds
        )
        return await self._request("POST", "/api/v1/checkout", body)

    async def get_session(self, session_id: str) -> dict[str, Any]:
        return await self._request("GET", f"/api/v1/sessions/{session_id}")

    async def refund(
        self,
        record_id: str,
        amount: Optional[int] = None,
        reason: Optional[str] = None,
    ) -> dict[str, Any]:
        body = _build_refund_body(record_id, amount, reason)
        return await self._request("POST", "/api/v1/refunds", body)

    verify_webhook_signature = staticmethod(Pay.verify_webhook_signature)

    async def aclose(self) -> None:
        await self._client.aclose()

    async def __aenter__(self) -> "AsyncPay":
        return self

    async def __aexit__(self, *_: Any) -> None:
        await self.aclose()

    async def _request(
        self,
        method: str,
        path: str,
        body: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]:
        resp = await self._client.request(method, path, json=body)
        if resp.status_code >= 400:
            raise PayError(resp.status_code, resp.text)
        return resp.json()
