"""Sand 卡密授权客户端：机器码生成、联网激活/校验、本地卡密存储。

每次启动都会联网到授权服务器校验（严格模式），服务端可随时吊销。
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import platform
import secrets
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
import uuid
from pathlib import Path
from typing import Callable, Dict, Optional

import securebox

# 仅开发（非打包）环境允许用环境变量指向测试服务器；正式包固定为线上地址。
_DEV_SERVER = None if getattr(sys, "frozen", False) else os.environ.get("SAND_LICENSE_SERVER")
LICENSE_SERVER = (_DEV_SERVER or "https://sd.yueyun.co").rstrip("/")
# 必须与服务端 LICENSE_SIGN_SECRET 一致，用于校验服务端响应签名。
SIGN_SECRET = "byTqWy3WFBFE7UKZYZ0nPUH6QVQzPSOVGz9adYfY76c"
# 必须与服务端 LICENSE_PAYLOAD_SECRET 一致，用于解密下发的补丁负载。
PAYLOAD_SECRET = "6JAmyhXCEdCBDTo6Wh1J5RPrpit6mipyHTm3DuTPsjw"
REQUEST_TIMEOUT = 8
PURCHASE_URL = "https://sand.yueyun.co"

STATUS_FILE = "license.json"

# 记录本进程内已解密的补丁负载（key -> dict），供 ensure_licensed 复用
_session_ok: Dict[str, bool] = {}
_session_payloads: Dict[str, Dict] = {}


class LicenseError(RuntimeError):
    def __init__(self, message: str, code: str = "ERROR", recoverable: bool = False) -> None:
        super().__init__(message)
        self.code = code
        # recoverable=True 表示换一个卡密即可解决（卡密无效/吊销/绑定他机）
        self.recoverable = recoverable


# ------------------------------------------------------------------ 平台信息


def product_name() -> str:
    if sys.platform == "win32":
        return "windows"
    if sys.platform == "darwin":
        return "macos"
    return "linux"


def _run(cmd: list) -> str:
    try:
        out = subprocess.run(cmd, capture_output=True, text=True, timeout=5, check=False)
        return out.stdout or ""
    except Exception:
        return ""


def _hardware_id() -> str:
    if sys.platform == "win32":
        try:
            import winreg  # type: ignore

            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography", 0, winreg.KEY_READ | winreg.KEY_WOW64_64KEY) as key:
                value, _ = winreg.QueryValueEx(key, "MachineGuid")
                if value:
                    return f"win:{value}"
        except Exception:
            pass
        out = _run(["wmic", "csproduct", "get", "uuid"])
        lines = [l.strip() for l in out.splitlines() if l.strip() and "UUID" not in l.upper()]
        if lines:
            return f"win-smbios:{lines[0]}"
    elif sys.platform == "darwin":
        out = _run(["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"])
        for line in out.splitlines():
            if "IOPlatformUUID" in line:
                return "mac:" + line.split('"')[-2]
    else:
        for path in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
            try:
                value = Path(path).read_text(encoding="utf-8").strip()
                if value:
                    return f"linux:{value}"
            except Exception:
                continue
    return f"mac-addr:{uuid.getnode():012x}"


def machine_code() -> str:
    raw = "|".join([_hardware_id(), platform.machine(), product_name()])
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:32]


def machine_meta(app_version: str) -> Dict[str, str]:
    return {
        "hostname": socket.gethostname()[:128],
        "platform": f"{platform.system()} {platform.release()} {platform.machine()}"[:128],
        "app_version": app_version,
    }


# ------------------------------------------------------------------ 本地存储


def _config_dir() -> Path:
    if sys.platform == "win32":
        base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
        root = Path(base) if base else Path.home() / "AppData" / "Local"
        return root / "SandClientModeStream" / "sand-client-cli"
    if sys.platform == "darwin":
        return Path.home() / "Library" / "Application Support" / "SandClientModeStream" / "sand-client-cli"
    return Path.home() / ".config" / "SandClientModeStream" / "sand-client-cli"


def _status_path() -> Path:
    return _config_dir() / STATUS_FILE


def load_saved_key() -> Optional[str]:
    try:
        data = json.loads(_status_path().read_text(encoding="utf-8"))
        key = str(data.get("license_key", "")).strip()
        return key or None
    except Exception:
        return None


def save_key(license_key: str, extra: Optional[Dict[str, object]] = None) -> None:
    path = _status_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    payload: Dict[str, object] = {"license_key": license_key, "machine_code": machine_code(), "saved_at": int(time.time())}
    if extra:
        payload.update(extra)
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


def clear_saved_key() -> None:
    try:
        _status_path().unlink()
    except FileNotFoundError:
        pass
    _session_ok.clear()


def normalize_key(raw: str) -> str:
    return "".join(ch for ch in raw.strip().upper() if ch.isalnum() or ch == "-")


# ------------------------------------------------------------------ 网络请求


def _post(path: str, payload: Dict[str, object]) -> Dict[str, object]:
    body = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        LICENSE_SERVER + path,
        data=body,
        headers={"Content-Type": "application/json", "User-Agent": "SandToolkit/license"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        try:
            return json.loads(exc.read().decode("utf-8"))
        except Exception:
            raise LicenseError(f"授权服务器返回错误（HTTP {exc.code}）", "HTTP_ERROR")
    except (urllib.error.URLError, socket.timeout, OSError) as exc:
        raise LicenseError(f"无法连接授权服务器 {LICENSE_SERVER}：{getattr(exc, 'reason', exc)}\n请检查网络后重试。", "NETWORK")
    except ValueError:
        raise LicenseError("授权服务器响应格式异常", "BAD_RESPONSE")


def _verify_signature(data: Dict[str, object], license_key: str, machine: str, nonce: str) -> None:
    if str(data.get("nonce", "")) != nonce:
        raise LicenseError("授权响应校验失败（nonce 不匹配）", "BAD_SIGNATURE")
    ok_flag = "1" if data.get("ok") else "0"
    signed_machine = str(data.get("machine_code") or "") if data.get("ok") else ""
    message = "|".join([ok_flag, license_key, signed_machine, nonce, str(data.get("server_time", ""))]).encode("utf-8")
    expected = hmac.new(SIGN_SECRET.encode("utf-8"), message, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, str(data.get("sig", ""))):
        raise LicenseError("授权响应签名无效，可能存在中间人或服务器配置错误", "BAD_SIGNATURE")
    if data.get("ok") and signed_machine != machine:
        raise LicenseError("授权响应机器码不匹配", "BAD_SIGNATURE")


_RECOVERABLE = {"NOT_FOUND", "BAD_FORMAT", "REVOKED", "EXPIRED", "PRODUCT_MISMATCH", "MACHINE_MISMATCH"}


def _call(path: str, license_key: str, app_version: str, with_meta: bool) -> Dict[str, object]:
    key = normalize_key(license_key)
    machine = machine_code()
    nonce = secrets.token_hex(8)
    payload: Dict[str, object] = {"license_key": key, "machine_code": machine, "product": product_name(), "nonce": nonce, "app_version": app_version}
    if with_meta:
        payload.update(machine_meta(app_version))
    data = _post(path, payload)
    if "sig" not in data:
        detail = data.get("detail") or data.get("message") or "未知错误"
        raise LicenseError(f"授权服务器拒绝请求：{detail}", "REJECTED")
    _verify_signature(data, key, machine, nonce)
    if not data.get("ok"):
        code = str(data.get("code", "ERROR"))
        raise LicenseError(str(data.get("message", "授权失败")), code, recoverable=code in _RECOVERABLE)
    return data


def _decrypt_payload(data: Dict[str, object], key: str, machine: str) -> Optional[Dict]:
    blob_b64 = data.get("payload")
    if not blob_b64:
        return None
    try:
        blob = base64.b64decode(str(blob_b64))
        plain = securebox.open_box(PAYLOAD_SECRET, "sand-payload", key, machine, blob)
        return json.loads(plain.decode("utf-8"))
    except Exception as exc:
        raise LicenseError(f"补丁数据解密失败：{exc}", "BAD_PAYLOAD")


def activate(license_key: str, app_version: str = "") -> Dict[str, object]:
    key = normalize_key(license_key)
    data = _call("/api/v1/activate", key, app_version, with_meta=True)
    save_key(key, {"activated_at": data.get("activated_at"), "product": data.get("product")})
    payloads = _decrypt_payload(data, key, machine_code())
    if payloads is not None:
        _session_payloads[key] = payloads
    _session_ok[key] = True
    return {**data, "payloads": payloads or _session_payloads.get(key)}


def validate(license_key: str, app_version: str = "") -> Dict[str, object]:
    key = normalize_key(license_key)
    try:
        data = _call("/api/v1/validate", key, app_version, with_meta=False)
    except LicenseError as exc:
        if exc.code == "NOT_ACTIVATED":
            # 服务端已解绑（或首次在本机使用），重新激活绑定
            return activate(key, app_version)
        raise
    payloads = _decrypt_payload(data, key, machine_code())
    if payloads is not None:
        _session_payloads[key] = payloads
    _session_ok[key] = True
    return {**data, "payloads": payloads or _session_payloads.get(key)}


# ------------------------------------------------------------------ 门禁入口


def ensure_licensed(
    app_version: str = "",
    prompt: Optional[Callable[[str], Optional[str]]] = None,
    notify: Optional[Callable[[str], None]] = None,
    max_attempts: int = 3,
) -> Dict[str, object]:
    """确保本机已授权。成功返回服务端响应，失败抛 LicenseError。

    prompt(message) -> 用户输入的卡密或 None（取消）。缺省使用终端 input。
    notify(message) -> 提示信息输出。缺省 print。
    """
    notify = notify or (lambda m: print(m))
    saved = load_saved_key()
    if saved and _session_ok.get(saved) and saved in _session_payloads:
        return {"ok": True, "license_key": saved, "cached": True, "payloads": _session_payloads[saved]}

    last_error: Optional[LicenseError] = None
    key = saved
    for _ in range(max_attempts):
        if not key:
            if prompt is None:
                key = _default_prompt(notify)
            else:
                key = prompt(_prompt_message(last_error))
            if not key:
                raise LicenseError("未输入卡密，已取消。", "CANCELLED")
            key = normalize_key(key)
        try:
            data = validate(key, app_version) if saved == key else activate(key, app_version)
            if data.get("code") == "OK" and str(data.get("message", "")).startswith("激活成功"):
                notify(f"✓ 激活成功，本机已绑定卡密 {key}")
            return data
        except LicenseError as exc:
            last_error = exc
            if not exc.recoverable:
                raise
            notify(f"✗ {exc}")
            if exc.code in ("NOT_FOUND", "BAD_FORMAT", "REVOKED", "EXPIRED") and saved == key:
                clear_saved_key()
                saved = None
            key = None
    raise last_error or LicenseError("授权失败", "ERROR")


def _prompt_message(last_error: Optional[LicenseError]) -> str:
    if last_error is None:
        return f"请输入卡密（购买地址：{PURCHASE_URL}）"
    return f"{last_error}\n请重新输入卡密（购买地址：{PURCHASE_URL}）"


def _default_prompt(notify: Callable[[str], None]) -> Optional[str]:
    notify(f"本工具需要卡密授权，购买地址：{PURCHASE_URL}")
    try:
        return input("请输入卡密> ").strip() or None
    except (EOFError, KeyboardInterrupt):
        return None


def status_text() -> str:
    key = load_saved_key()
    if not key:
        return "未激活"
    return f"已绑定卡密 {key[:10]}…  机器码 {machine_code()[:12]}…"
