"""
Licensing — 30-day trial, then a paid license key.

Security model (do read this):
  * A license key is a signed token. The billing server holds the Ed25519 PRIVATE
    key and signs each key; the app embeds only the PUBLIC key and *verifies*
    offline. Because the app never holds the private key, keys cannot be forged or
    generated by users — only the server can mint them.
  * Verification is offline (no phone-home required), so the app works without a
    network. An optional online re-check can revoke a key (see `online_status`).
  * The trial is recorded locally (install date). A determined user can reset local
    state to extend a trial — that is true of all offline trials; the paid path is
    what's cryptographically protected. Tie keys to the customer email so sharing
    is socially deterred and revocable.

Key format:  BSX1.<base64url(payload_json)>.<base64url(signature)>
payload = {email, license_id, plan, issued (date), expires (date)}
"""

from __future__ import annotations

import base64
import json
import time
from datetime import date, datetime
from pathlib import Path

from cryptography.hazmat.primitives.asymmetric.ed25519 import (
    Ed25519PublicKey, Ed25519PrivateKey)
from cryptography.exceptions import InvalidSignature

# Public key of the biosync billing server (safe to ship). Verifies keys; cannot mint them.
LICENSE_PUBLIC_KEY_HEX = "239701ee1886243502ae64501efc43a44d37a4489cdbec6d6e296b3da1981258"

TRIAL_DAYS = 30
PREFIX = "BSX1"


def _b64e(b: bytes) -> str:
    return base64.urlsafe_b64encode(b).decode().rstrip("=")


def _b64d(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


# --------------------------------------------------------------------------
# verification (app side — public key only)
# --------------------------------------------------------------------------
def verify_key(key: str, *, public_key_hex: str = LICENSE_PUBLIC_KEY_HEX) -> dict:
    """Return {'valid', 'expired', 'email', 'plan', 'expires', 'reason'}."""
    try:
        prefix, payload_b64, sig_b64 = key.strip().split(".")
        assert prefix == PREFIX
    except Exception:
        return {"valid": False, "reason": "malformed key"}
    payload_raw = _b64d(payload_b64)
    try:
        pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key_hex))
        pub.verify(_b64d(sig_b64), payload_raw)
    except (InvalidSignature, Exception):
        return {"valid": False, "reason": "bad signature"}
    try:
        p = json.loads(payload_raw)
        exp = datetime.strptime(p["expires"], "%Y-%m-%d").date()
    except Exception:
        return {"valid": False, "reason": "bad payload"}
    expired = date.today() > exp
    return {"valid": not expired, "expired": expired, "email": p.get("email"),
            "plan": p.get("plan"), "license_id": p.get("license_id"),
            "expires": p["expires"], "issued": p.get("issued"),
            "reason": "expired" if expired else "ok"}


# --------------------------------------------------------------------------
# signing (server side only — needs the PRIVATE key)
# --------------------------------------------------------------------------
def sign_key(private_key_hex: str, *, email: str, plan: str, days: int,
             license_id: str) -> str:
    priv = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex))
    issued = date.today()
    from datetime import timedelta
    payload = {"email": email, "license_id": license_id, "plan": plan,
               "issued": issued.isoformat(),
               "expires": (issued + timedelta(days=days)).isoformat()}
    raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode()
    sig = priv.sign(raw)
    return f"{PREFIX}.{_b64e(raw)}.{_b64e(sig)}"


# --------------------------------------------------------------------------
# app-side state: trial + activation
# --------------------------------------------------------------------------
class LicenseManager:
    def __init__(self, state_dir: str):
        self.dir = Path(state_dir)
        self.dir.mkdir(parents=True, exist_ok=True)
        self.path = self.dir / "license.json"
        self._state = self._load()
        if "install_ts" not in self._state:
            self._state["install_ts"] = time.time()
            self._save()

    def _load(self) -> dict:
        if self.path.exists():
            try:
                return json.load(open(self.path))
            except Exception:
                return {}
        return {}

    def _save(self):
        json.dump(self._state, open(self.path, "w"), indent=2)

    # --- activation ---
    def activate(self, key: str) -> dict:
        info = verify_key(key)
        if not info.get("valid"):
            return {"ok": False, "reason": info.get("reason", "invalid")}
        self._state["license_key"] = key.strip()
        self._save()
        return {"ok": True, **info}

    def deactivate(self):
        self._state.pop("license_key", None)
        self._save()

    # --- status ---
    def status(self) -> dict:
        key = self._state.get("license_key")
        if key:
            info = verify_key(key)
            if info.get("valid"):
                exp = datetime.strptime(info["expires"], "%Y-%m-%d").date()
                return {"mode": "licensed", "allowed": True,
                        "email": info["email"], "plan": info["plan"],
                        "expires": info["expires"],
                        "days_left": (exp - date.today()).days}
            # stored key expired/revoked -> fall through to trial state
            reason = info.get("reason", "invalid")
        else:
            reason = None
        # trial
        install = datetime.fromtimestamp(self._state["install_ts"]).date()
        used = (date.today() - install).days
        left = TRIAL_DAYS - used
        return {"mode": "trial" if left > 0 else "expired",
                "allowed": left > 0, "days_left": max(0, left),
                "trial_days": TRIAL_DAYS, "license_reason": reason}

    def allowed(self) -> bool:
        return self.status().get("allowed", False)
