"""license_check.py — client-side license gate for the shared Flyff bot.

On launch the bot calls a small AWS API (`API_URL/validate`) with the user's license key. The server replies
with a payload SIGNED by an AWS KMS key; this module verifies the signature against a PINNED public key
(`license_config.PUBLIC_KEY_PEM`) so a fake server / hosts-file redirect can't forge a "valid" reply. If the
key is unknown or **revoked**, the bot refuses to start. A signed token is cached so brief internet outages
don't lock users out (offline grace = the token's `exp`, set server-side).

⚠️ This runs on the user's machine, so a determined person can patch it out — it's a strong deterrent + gives
you instant revocation, not unbreakable DRM. See license_server/README.md.

Dev copy: if a file named `LICENSE_DEV` sits next to this module, the check is SKIPPED (so your working copy
isn't gated). `prune_for_share.bat` excludes that marker, so the shared copy enforces the license.
"""
import base64
import hashlib
import json
import os
import sys
import time
import urllib.error
import urllib.request
import uuid
from pathlib import Path

_DIR = Path(__file__).resolve().parent
_KEY_FILE = _DIR / "license.key"          # the user pastes their key here (one line)
_CACHE_FILE = _DIR / ".license_cache"     # last good SIGNED token → offline grace
_DEV_MARKER = _DIR / "LICENSE_DEV"        # present ONLY in the dev copy → skip the check

try:                                      # written by license_server/configure_client.py after you deploy
    from license_config import API_URL, PUBLIC_KEY_PEM
except Exception:
    API_URL, PUBLIC_KEY_PEM = "", ""


def _machine_id():
    """A stable-ish anonymous fingerprint. NOT used to BIND the key (licenses are floating) — sent so the
    admin page can show how many distinct machines use a key (spot obvious key-sharing)."""
    raw = f"{uuid.getnode()}|{os.environ.get('COMPUTERNAME', '')}|{os.environ.get('USERNAME', '')}"
    return hashlib.sha256(raw.encode()).hexdigest()[:16]


def _read_key():
    try:
        return _KEY_FILE.read_text(encoding="utf-8").strip()
    except Exception:
        return ""


def _verify(payload_b64, sig_b64):
    """Verify the ECDSA-P256/SHA-256 signature (KMS) over the payload with the pinned public key. Raises on
    a bad signature."""
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import ec
    pub = serialization.load_pem_public_key(PUBLIC_KEY_PEM.encode())
    pub.verify(base64.b64decode(sig_b64), base64.b64decode(payload_b64), ec.ECDSA(hashes.SHA256()))


def _accept(payload_b64, sig_b64, key):
    """Verify signature + payload contents. Returns (ok, reason)."""
    try:
        _verify(payload_b64, sig_b64)
    except Exception:
        return False, "license signature invalid (untrusted response)"
    try:
        payload = json.loads(base64.b64decode(payload_b64))
    except Exception:
        return False, "malformed license payload"
    if payload.get("key") != key or not payload.get("valid"):
        return False, payload.get("reason") or "license invalid"
    if payload.get("exp", 0) < time.time():
        return False, "license token expired — connect to re-validate"
    return True, "ok"


def _call_api(key):
    body = json.dumps({"key": key, "machineId": _machine_id(), "product": "flyffbot"}).encode()
    req = urllib.request.Request(API_URL.rstrip("/") + "/validate", data=body,
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=10) as r:
        return json.loads(r.read())


def check():
    """Return (ok: bool, message: str)."""
    if _DEV_MARKER.exists():
        return True, "dev copy — license check skipped"
    if not API_URL or not PUBLIC_KEY_PEM:
        return False, "license system not configured (missing license_config.py)"
    key = _read_key()
    if not key:
        return False, f"no license key — paste your key into {_KEY_FILE.name}"
    try:
        resp = _call_api(key)
        p, sig = resp.get("payload"), resp.get("signature")
        if not p or not sig:
            return False, resp.get("reason") or "license rejected"
        ok, reason = _accept(p, sig, key)
        if ok:
            try: _CACHE_FILE.write_text(json.dumps({"payload": p, "signature": sig}))
            except Exception: pass
        return ok, ("license valid" if ok else reason)
    except urllib.error.HTTPError as e:                 # server explicitly rejected (unknown/revoked)
        try: reason = json.loads(e.read()).get("reason")
        except Exception: reason = None
        return False, reason or f"license rejected (HTTP {e.code})"
    except Exception:                                   # network/other error → offline grace from the cache
        return _offline_grace(key)


def _offline_grace(key):
    try:
        c = json.loads(_CACHE_FILE.read_text())
    except Exception:
        return False, "can't reach the license server and no cached license"
    ok, reason = _accept(c.get("payload", ""), c.get("signature", ""), key)
    return (True, "license valid (offline)") if ok else (False, reason)


def require():
    """Enforce the license: print + sys.exit(1) if it's not valid. Call this at launch before running."""
    ok, msg = check()
    if not ok:
        print("\n" + "=" * 60)
        print("  LICENSE CHECK FAILED")
        print("  " + msg)
        print("  Contact the provider for a valid license key, then put it in")
        print(f"  {_KEY_FILE}")
        print("=" * 60 + "\n")
        sys.exit(1)
    if "dev copy" not in msg:
        print(f"[license] {msg}")
