"""支付宝当面付（扫码支付）—— FEAT-013 envelope-buy-real。

复用 vault `alipay-kongxuanpin` credential：
    app_id              应用 ID
    app_private_key     RSA 2048 PEM 私钥
    alipay_public_key   支付宝平台公钥 PEM（验签 webhook 用）

流程：
    1. create_precreate(order_id, amount_cents, subject) → qr_code URL
       （CLI 用 qrcode 库渲染成 ASCII 让用户手机扫）
    2. 用户扫码支付
    3. 支付宝异步 POST /envelope/webhook/alipay（form-urlencoded）
    4. verify_webhook(body) → (order_id, status) 验签 + 解析状态
    5. 路由据此更新 envelope_orders + 充值 envelope_balance

参考:
    https://opendocs.alipay.com/open/194/105170  precreate
    https://opendocs.alipay.com/common/02khjm    RSA2 签名
"""

from __future__ import annotations

import base64
import json
import os
from datetime import datetime
from typing import Any, Literal

import httpx
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

from . import credentials as creds

ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do"
ALIPAY_GATEWAY_SANDBOX = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"

OrderStatus = Literal["pending", "paid", "failed", "cancelled"]


class AlipayError(Exception):
    pass


def _is_sandbox() -> bool:
    return os.environ.get("XIANGQIN_ALIPAY_SANDBOX", "").strip().lower() in ("1", "true", "yes")


def _get_gateway() -> str:
    return ALIPAY_GATEWAY_SANDBOX if _is_sandbox() else ALIPAY_GATEWAY_PROD


def _get_conf() -> dict[str, str]:
    """从 vault 读 alipay credential（运行时查，便于测试 monkey-patch）。

    XIANGQIN_ALIPAY_SANDBOX=1 时读 `alipay_sandbox` credential（vault.json 声明）；
    否则读生产 `alipay`（对应 vault slug alipay-kongxuanpin）。
    """
    cred_key = "alipay_sandbox" if _is_sandbox() else "alipay"
    c = creds.get(cred_key)
    return {
        "app_id": c["app_id"],
        "app_private_key": _normalize_pem(c["app_private_key"]),
        "alipay_public_key": _normalize_pem(c["alipay_public_key"]),
    }


def create_precreate(order_id: str, amount_cents: int, subject: str) -> str:
    """调 alipay.trade.precreate，返回 qr_code URL。

    Raises:
        AlipayError: HTTP / 解码 / 业务错误码
    """
    conf = _get_conf()
    yuan = f"{amount_cents / 100:.2f}"
    biz_content = {
        "out_trade_no": order_id,
        "total_amount": yuan,
        "subject": subject[:256],
    }
    params = _build_public_params("alipay.trade.precreate", biz_content, conf["app_id"])
    params["sign"] = _sign(params, conf["app_private_key"])

    try:
        r = httpx.post(
            _get_gateway(),
            params={"charset": params["charset"]},
            data=params,
            timeout=15.0,
            trust_env=False,
        )
        r.raise_for_status()
        resp = json.loads(r.content.decode("utf-8"))
    except httpx.HTTPError as e:
        raise AlipayError(f"alipay precreate HTTP 失败: {e}") from e
    except (UnicodeDecodeError, json.JSONDecodeError) as e:
        raise AlipayError(
            f"alipay precreate 响应解码失败（content-type={r.headers.get('content-type')}）: "
            f"{r.content[:200]!r}"
        ) from e

    payload = resp.get("alipay_trade_precreate_response") or {}
    if payload.get("code") != "10000":
        raise AlipayError(
            f"alipay precreate 业务错误 code={payload.get('code')} "
            f"msg={payload.get('msg')} sub_code={payload.get('sub_code')} "
            f"sub_msg={payload.get('sub_msg')}"
        )

    qr = payload.get("qr_code")
    if not qr:
        raise AlipayError("alipay precreate 响应无 qr_code 字段")
    return qr


def verify_webhook(body: bytes) -> tuple[str, OrderStatus]:
    """支付宝异步回调。

    body: x-www-form-urlencoded 的 k=v&k=v（非 JSON）
    返回 (our_order_id, status)
    Raises:
        AlipayError: 验签失败 / 缺字段
    """
    from urllib.parse import parse_qsl

    conf = _get_conf()
    pairs = parse_qsl(body.decode("utf-8"), keep_blank_values=True)
    data = dict(pairs)

    sign = data.pop("sign", None)
    data.pop("sign_type", None)
    if not sign:
        raise AlipayError("alipay webhook 缺少 sign 字段")

    canonical = "&".join(f"{k}={v}" for k, v in sorted(data.items()))

    public_key = serialization.load_pem_public_key(conf["alipay_public_key"].encode())
    try:
        public_key.verify(  # type: ignore[union-attr]
            base64.b64decode(sign),
            canonical.encode("utf-8"),
            padding.PKCS1v15(),
            hashes.SHA256(),
        )
    except Exception as e:  # noqa: BLE001
        raise AlipayError(f"alipay webhook 验签失败: {e}") from e

    order_id = data.get("out_trade_no")
    if not order_id:
        raise AlipayError("alipay webhook 缺 out_trade_no")

    trade_status = data.get("trade_status", "")
    status: OrderStatus
    if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"):
        status = "paid"
    elif trade_status == "WAIT_BUYER_PAY":
        status = "pending"
    elif trade_status == "TRADE_CLOSED":
        status = "cancelled"
    else:
        status = "failed"
    return order_id, status


# ---- 内部工具 ----


def _build_public_params(method: str, biz_content: dict[str, Any], app_id: str) -> dict[str, str]:
    params = {
        "app_id": app_id,
        "method": method,
        "format": "JSON",
        "charset": "utf-8",
        "sign_type": "RSA2",
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "version": "1.0",
        "biz_content": json.dumps(biz_content, ensure_ascii=False, separators=(",", ":")),
    }
    base = os.environ.get("XIANGQIN_BASE_URL", "").rstrip("/")
    if base:
        params["notify_url"] = f"{base}/envelope/webhook/alipay"
    return params


def _sign(params: dict[str, str], private_key_pem: str) -> str:
    """RSA2（RSA-SHA256）签名。除 sign 外字段 key 升序拼 k=v&k=v，base64 输出。"""
    filtered = {k: v for k, v in params.items() if k != "sign"}
    canonical = "&".join(f"{k}={v}" for k, v in sorted(filtered.items()))

    private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None)
    signature = private_key.sign(  # type: ignore[union-attr]
        canonical.encode("utf-8"),
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    return base64.b64encode(signature).decode()


def _normalize_pem(pem: str) -> str:
    """vault 返回的 PEM 可能带字面 \\n 转义，转真换行；确保末尾有 \\n。"""
    if "\\n" in pem and "\n" not in pem:
        pem = pem.replace("\\n", "\n")
    if not pem.endswith("\n"):
        pem += "\n"
    return pem
