"""MeowAlpha License 驗證 —— GUI 啟動時呼叫。

四道閘：
    1. 簽章 (ed25519 with public_key.pem)
    2. machine_id 一致
    3. email 一致（在用戶按登入時，由 GUI 比對 username_var）
    4. 過期日

License 檔結構（JSON）：
    {
        "schema": 1,
        "user_name": "Alice",
        "user_email": "alice@gmail.com",
        "user_uid": "U123456",
        "machine_id": "<sha256(MEOWALPHA_HOST_ID file)>",
        "issued_at": "2026-05-05T08:00:00Z",
        "expires_at": "2026-11-05T08:00:00Z",
        "signature": "<base64 ed25519 sig of payload>"
    }

machine_id 演算法（PM 推薦 C：第一次隨機生成存檔）：
    讀 ~/.meowalpha_host_id 檔內隨機 32 bytes hex；不存在就生成存進去。
    換 wifi / 換 router / 重灌系統都不影響；只有刪檔或換電腦才會變。
"""

from __future__ import annotations

import base64
import json
import os
import secrets
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional


def _resource_path(rel: str) -> Path:
    """PyInstaller frozen → sys._MEIPASS（Resources/）；dev → __file__ 旁邊。
    讓 keys/public_key.pem 等 read-only resource 在 .app 內也能被找到。"""
    if hasattr(sys, "_MEIPASS"):
        return Path(sys._MEIPASS) / rel
    return Path(__file__).resolve().parent / rel


def _candidate_license_paths() -> List[Path]:
    """搜尋順序（從容易到不容易找）。任一存在就用，前者優先。
    PyInstaller frozen 後 .app 內部不可讀寫，所以多了 home / Downloads / Desktop / .app 旁。"""
    home = Path.home()
    paths = [home / ".meowalpha_license.lic"]
    if hasattr(sys, "_MEIPASS"):
        try:
            exe = Path(sys.executable).resolve()
            if sys.platform == "darwin":
                # macOS .app bundle: Contents/MacOS/MeowAlpha → parents[2] = .app → parent = Applications/
                app_bundle = exe.parents[2]
                paths.append(app_bundle.parent / "license.lic")
            else:
                # Windows onedir: dist/MeowAlpha/MeowAlpha.exe
                # 使用者通常把 license.lic 放在 MeowAlpha\ 資料夾旁邊（解壓後的同層）
                paths.append(exe.parent / "license.lic")  # dist/MeowAlpha/license.lic
                paths.append(
                    exe.parent.parent / "license.lic"
                )  # dist/license.lic（zip 解壓後同層）
        except Exception:
            pass
    paths += [home / "Downloads" / "license.lic", home / "Desktop" / "license.lic"]
    if not hasattr(sys, "_MEIPASS"):
        # dev mode：腳本旁邊
        paths.append(Path(__file__).resolve().parent / "license.lic")
    return paths


# 公開符號（保留向後相容）
LICENSE_PATHS = _candidate_license_paths()
HOST_ID_PATH = Path.home() / ".meowalpha_host_id"
PUBLIC_KEY_PATH = _resource_path("keys/public_key.pem")
INSTALLED_LICENSE_PATH = Path.home() / ".meowalpha_license.lic"  # GUI 選檔後複製到這

WARN_DAYS_BEFORE_EXPIRY = 14  # PM Q4：到期前 14 天彈 banner


# ════════════════════════════════════════════════════════════════════
# Machine ID
# ════════════════════════════════════════════════════════════════════


def get_machine_id() -> str:
    """回傳本機固定 32-byte hex machine_id。第一次呼叫會生成並存檔。"""
    if HOST_ID_PATH.exists():
        try:
            mid = HOST_ID_PATH.read_text(encoding="utf-8").strip()
            if mid and len(mid) >= 32:
                return mid
        except OSError:
            pass
    # 生成新 machine_id
    mid = secrets.token_hex(32)
    try:
        # atomic 0o600
        flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
        fd = os.open(str(HOST_ID_PATH), flags, 0o600)
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(mid)
    except OSError as e:
        print(f"[license] 警告：無法寫 {HOST_ID_PATH}：{e}")
    return mid


# ════════════════════════════════════════════════════════════════════
# License load + verify
# ════════════════════════════════════════════════════════════════════


class LicenseInvalidError(RuntimeError):
    """License 任何驗證失敗都拋這個。msg 給用戶看；reason 內部分類。"""

    def __init__(self, msg: str, reason: str = "unknown"):
        super().__init__(msg)
        self.reason = reason


def _find_license_file() -> Optional[Path]:
    # 每次呼叫都重新算（frozen 時 sys.executable 才有值）
    for p in _candidate_license_paths():
        if p.exists():
            return p
    return None


def _load_public_key():
    """延遲 import cryptography（用戶第一次跑可能還沒裝）。"""
    from cryptography.hazmat.primitives.serialization import load_pem_public_key

    if not PUBLIC_KEY_PATH.exists():
        raise LicenseInvalidError(
            f"找不到公鑰檔：{PUBLIC_KEY_PATH}（zip 不完整？）", reason="no_public_key"
        )
    return load_pem_public_key(PUBLIC_KEY_PATH.read_bytes())


def _canonical_payload(lic: dict) -> bytes:
    """要和 backend 簽章端一致：sort_keys + 排除 signature 欄位 + UTF-8 + 無空格。"""
    payload = {k: v for k, v in lic.items() if k != "signature"}
    return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")


def load_and_verify_license() -> dict:
    """讀 license 檔 + 驗章 + 驗 machine_id + 驗 expires。

    回傳：dict（含 user_email 等欄位）給後續 GUI 比對 email 用。
    任何失敗 → 拋 LicenseInvalidError。
    """
    from cryptography.exceptions import InvalidSignature

    lic_path = _find_license_file()
    if lic_path is None:
        raise LicenseInvalidError(
            f"找不到 License 檔。\n\n"
            f"請把下方 machine_id 連同你的 姓名 / WQ Email / WQ UID / 電話 寄給作者：\n\n"
            f"    machine_id = {get_machine_id()}\n\n"
            f"拿到 license.lic 後，把它放到下列任一處（二擇一）：\n\n"
            f"  方式 1（推薦）：和 install_and_run 同一個資料夾，檔名 license.lic\n"
            f"  方式 2：放到家目錄，檔名 .meowalpha_license.lic（注意開頭那個點）\n",
            reason="no_license",
        )

    try:
        lic = json.loads(lic_path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as e:
        raise LicenseInvalidError(f"License 檔格式錯誤：{e}", reason="parse")

    # 1. signature
    sig_b64 = lic.get("signature")
    if not sig_b64:
        raise LicenseInvalidError("License 缺 signature 欄位", reason="no_sig")
    try:
        sig = base64.b64decode(sig_b64)
    except Exception:
        raise LicenseInvalidError(
            "License signature 不是合法 base64", reason="sig_format"
        )

    pub = _load_public_key()
    payload = _canonical_payload(lic)
    try:
        pub.verify(sig, payload)
    except InvalidSignature:
        raise LicenseInvalidError(
            "License 簽章無效（可能被竄改、或非本工具發行的 License）",
            reason="sig_invalid",
        )

    # 2. machine_id
    expected_mid = lic.get("machine_id", "")
    actual_mid = get_machine_id()
    if expected_mid != actual_mid:
        raise LicenseInvalidError(
            f"License 是發給其他電腦的（不允許跨機使用）。\n\n"
            f"本機 machine_id：{actual_mid}\n"
            f"License 內   ：{expected_mid}\n\n"
            f"請在本機重新申請。",
            reason="machine_mismatch",
        )

    # 3. expires
    exp_str = lic.get("expires_at", "")
    try:
        if exp_str.endswith("Z"):
            exp = datetime.fromisoformat(exp_str.replace("Z", "+00:00"))
        else:
            exp = datetime.fromisoformat(exp_str)
        if exp.tzinfo is None:
            exp = exp.replace(tzinfo=timezone.utc)
    except ValueError:
        raise LicenseInvalidError(
            f"License expires_at 格式錯誤：{exp_str}", reason="date_format"
        )

    now = datetime.now(timezone.utc)
    if exp < now:
        days_ago = (now - exp).days
        raise LicenseInvalidError(
            f'License 已於 {exp.strftime("%Y-%m-%d")} 過期（{days_ago} 天前）。\n'
            f"請聯絡作者續約。",
            reason="expired",
        )

    return lic


def days_until_expiry(lic: dict) -> int:
    """剩餘天數；負數代表已過期。"""
    try:
        exp_str = lic.get("expires_at", "")
        if exp_str.endswith("Z"):
            exp = datetime.fromisoformat(exp_str.replace("Z", "+00:00"))
        else:
            exp = datetime.fromisoformat(exp_str)
        if exp.tzinfo is None:
            exp = exp.replace(tzinfo=timezone.utc)
        return (exp - datetime.now(timezone.utc)).days
    except Exception:
        return -1


# ════════════════════════════════════════════════════════════════════
# GUI 啟動入口
# ════════════════════════════════════════════════════════════════════


def verify_or_exit(quiet: bool = False) -> dict:
    """GUI 啟動時呼叫。任何失敗：跳 modal 顯示原因 + 退出。

    quiet=True：給 install_and_run 預檢用，失敗直接 sys.exit 不開 GUI。
    回傳 license dict 供 GUI 後續驗 email 用。
    """
    try:
        lic = load_and_verify_license()
    except LicenseInvalidError as e:
        if quiet:
            print(f"[license] {e}", file=sys.stderr)
            sys.exit(2)
        # 用 Tk 跳一個 modal（避開主 GUI 啟動）
        _show_license_error_modal(str(e), e.reason)
        sys.exit(2)
    except Exception as e:
        if quiet:
            print(f"[license] 未預期錯誤：{e}", file=sys.stderr)
            sys.exit(3)
        _show_license_error_modal(f"License 檢查發生未預期錯誤：{e}", "unknown")
        sys.exit(3)
    return lic


def _show_license_error_modal(msg: str, reason: str) -> None:
    """用一個獨立 Tk root 跳 modal（不開主 GUI）。
    no_license 時提供：① 複製 machine_id ② 從任意位置選 license.lic 檔案安裝
    任意路徑安裝後會複製到 ~/.meowalpha_license.lic（朋友不必懂 Mac 隱藏路徑）。"""
    import shutil
    import webbrowser
    import tkinter as tk
    from tkinter import scrolledtext, messagebox, filedialog

    root = tk.Tk()
    root.title("🔐 MeowAlpha License 驗證失敗")
    root.geometry("680x520")

    tk.Label(
        root,
        text="⚠️  無法啟動 — License 驗證未通過",
        font=("微軟正黑體", 14, "bold"),
        fg="#e74c3c",
        pady=10,
    ).pack()

    txt = scrolledtext.ScrolledText(root, height=11, wrap="word", font=("Consolas", 10))
    txt.pack(fill="both", expand=True, padx=12, pady=8)
    txt.insert("1.0", msg)
    txt.configure(state="disabled")

    if reason == "no_license":
        mid = get_machine_id()

        # 步驟提示
        tk.Label(
            root,
            text="📋 沒有 License？跟著做：\n"
            "  ① 點下方「複製 machine_id」→ 貼到 https://meowalpha.zeabur.app 申請\n"
            "  ② 收到 email 內的 license.lic 檔案後，點「選擇 license.lic」→ 選那個檔\n"
            "  ③ 重新打開 MeowAlpha",
            font=("微軟正黑體", 10),
            fg="#2c3e50",
            justify="left",
            anchor="w",
            pady=4,
        ).pack(fill="x", padx=20)

        btn_frame = tk.Frame(root)
        btn_frame.pack(pady=10)

        def copy_mid() -> None:
            root.clipboard_clear()
            root.clipboard_append(mid)
            messagebox.showinfo(
                "已複製",
                f"machine_id 已複製到剪貼簿：\n{mid}\n\n"
                f"請貼到 https://meowalpha.zeabur.app 申請 License",
            )

        def open_apply_url() -> None:
            webbrowser.open("https://meowalpha.zeabur.app/")

        def pick_license() -> None:
            initial_dir = str(Path.home() / "Downloads")
            file_path = filedialog.askopenfilename(
                title="選擇 license.lic 檔案",
                initialdir=initial_dir,
                filetypes=[("License 檔", "*.lic"), ("全部檔案", "*.*")],
            )
            if not file_path:
                return
            try:
                # 簡單驗一下是不是 license JSON
                import json as _json

                with open(file_path, "r", encoding="utf-8") as f:
                    data = _json.load(f)
                if "signature" not in data or "user_email" not in data:
                    messagebox.showerror(
                        "檔案格式錯",
                        "這不是有效的 license.lic 檔案。\n"
                        "請選 email 附件那個 license.lic。",
                    )
                    return
                # 複製到 ~/.meowalpha_license.lic
                target = INSTALLED_LICENSE_PATH
                shutil.copyfile(file_path, str(target))
                try:
                    target.chmod(0o600)
                except OSError:
                    pass
                messagebox.showinfo(
                    "✅ License 已安裝",
                    f"License 已安裝到：\n{target}\n\n"
                    f'發給：{data.get("user_email", "?")}\n\n'
                    f"請關閉這個視窗，重新打開 MeowAlpha 即可。",
                )
                root.destroy()
            except Exception as e:
                messagebox.showerror("安裝失敗", f"無法讀取檔案：{e}")

        tk.Button(
            btn_frame,
            text="📋 複製 machine_id",
            command=copy_mid,
            bg="#3498db",
            fg="white",
            font=("微軟正黑體", 11, "bold"),
            padx=14,
            pady=6,
        ).pack(side="left", padx=4)

        tk.Button(
            btn_frame,
            text="🌐 開啟申請網站",
            command=open_apply_url,
            bg="#95a5a6",
            fg="white",
            font=("微軟正黑體", 11),
            padx=14,
            pady=6,
        ).pack(side="left", padx=4)

        tk.Button(
            btn_frame,
            text="📂 選擇 license.lic",
            command=pick_license,
            bg="#27ae60",
            fg="white",
            font=("微軟正黑體", 11, "bold"),
            padx=14,
            pady=6,
        ).pack(side="left", padx=4)

    close_frame = tk.Frame(root)
    close_frame.pack(pady=8)
    tk.Button(close_frame, text="關閉", command=root.destroy, padx=20, pady=6).pack()
    root.mainloop()


if __name__ == "__main__":
    # CLI 自測：python3 license.py
    print(f"machine_id: {get_machine_id()}")
    print("license 搜尋順序（會找下列任一）：")
    print(
        f'  1. ~/.meowalpha_license.lic    {"✓ exists" if LICENSE_PATHS[0].exists() else "✗ missing"}'
    )
    print(
        f'  2. ./license.lic（同程式目錄） {"✓ exists" if LICENSE_PATHS[1].exists() else "✗ missing"}'
    )
    print(f'public key: {"✓ exists" if PUBLIC_KEY_PATH.exists() else "✗ missing"}')
    try:
        lic = load_and_verify_license()
        days = days_until_expiry(lic)
        print(f'✅ License 有效，發給 {lic.get("user_email")}，剩 {days} 天')
    except LicenseInvalidError as e:
        print(f"❌ {e.reason}: {e}")
