"""Python 3.10+：pip install cryptography
python client.py credentials.json 1.00 ORDER_20260928_001 https://glzf.top
同一笔业务重试时保持业务订单号不变。私钥只在你的服务端使用。
"""
import base64
import hashlib
import json
import secrets
import sys
import time
from pathlib import Path
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import Request, urlopen

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


def sign_headers(app_id, private_key, method, target, body=b"", timestamp=None, nonce=None):
    timestamp = str(int(time.time()) if timestamp is None else timestamp)
    nonce = nonce or secrets.token_hex(16)
    message = "\n".join((app_id, method.upper(), target, timestamp, nonce,
                          hashlib.sha256(body).hexdigest())).encode("utf-8")
    key = serialization.load_pem_private_key(private_key.encode(), password=None)
    signature = key.sign(message, padding.PKCS1v15(), hashes.SHA256())
    return {"X-App-Id": app_id, "X-Timestamp": timestamp, "X-Nonce": nonce,
            "X-Signature": base64.b64encode(signature).decode(), "Content-Type": "application/json"}


class PaymentClient:
    def __init__(self, base_url, credentials):
        parsed = urlsplit(base_url)
        if (parsed.scheme != "https" and not (parsed.scheme == "http" and parsed.hostname in {"localhost", "127.0.0.1"})
                or parsed.path not in {"", "/"} or parsed.query or parsed.fragment or parsed.username):
            raise ValueError("base_url 应为 HTTPS 域名（本机可用 HTTP），不要包含路径")
        self.base_url = base_url.rstrip("/")
        self.credentials = credentials

    def request(self, method, target, payload=None):
        if not target.startswith("/api/v1/") or "#" in target:
            raise ValueError("只接受 /api/v1/ 下的相对接口路径")
        body = b"" if payload is None else json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()
        headers = sign_headers(self.credentials["app_id"], self.credentials["private_key"], method, target, body)
        request = Request(self.base_url + target, data=body if payload is not None else None, headers=headers, method=method)
        try:
            with urlopen(request, timeout=25) as response:
                data = response.read()
                return data if response.headers.get_content_type() == "image/png" else json.loads(data)
        except HTTPError as error:
            raise RuntimeError(f"HTTP {error.code}: {error.read().decode('utf-8', errors='replace')}") from None


def main():
    if len(sys.argv) != 5:
        raise SystemExit("用法：python client.py 凭证.json 金额 业务订单号 API域名")
    credentials = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
    client = PaymentClient(sys.argv[4], credentials)
    order = client.request("POST", "/api/v1/orders", {"amount": sys.argv[2], "out_trade_no": sys.argv[3]})
    print(json.dumps(order, ensure_ascii=False, indent=2))
    if order["qrcode_url"] and not order["paid"] and order["status"] == "pending":
        filename = Path("payment-" + order["order_id"] + ".png")
        filename.write_bytes(client.request("GET", order["qrcode_url"]))
        print("收款码已保存：", filename.resolve())
    for _ in range(180):
        if order["paid"]:
            print("支付成功，回执：", order["receipt_id"])
            return
        if order["status"] in {"expired", "closed", "failed"}:
            print("当前状态：", order["status"], "；如需核对晚到结果，请再次查询原订单。")
            return
        time.sleep(5)
        try:
            order = client.request("GET", order["status_url"])
            print("当前状态：", order["status"])
        except (RuntimeError, OSError) as error:
            print("查单暂时失败，继续核对原订单：", error)
    print("等待结束。请保存原订单号，稍后继续查询，勿用新业务订单号盲目重试。")


if __name__ == "__main__":
    main()
