"""License management for PageFuse.

Trial:   30 days from first run, stored in ~/.pagefuse/license.json
Paid:    Activated via Lemon Squeezy API; re-validated every 7 days online.
         Offline use is allowed until 45 days before license expiry,
         at which point online validation is required.
"""

import json
import socket
from datetime import datetime, timedelta, timezone
from pathlib import Path

import requests

LEMON_SQUEEZY_API = "https://api.lemonsqueezy.com/v1/licenses"
PURCHASE_URL = "https://pagefuse.net"

CONFIG_DIR = Path.home() / ".pagefuse"
LICENSE_FILE = CONFIG_DIR / "license.json"

TRIAL_DAYS = 30
REVALIDATE_AFTER_DAYS = 7
# Must be online when within this many days of license expiry.
EXPIRY_WINDOW_DAYS = 45
# Countdown warning shown when this many days or fewer remain.
EXPIRY_WARN_DAYS = 30


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _load() -> dict:
    if LICENSE_FILE.exists():
        try:
            return json.loads(LICENSE_FILE.read_text())
        except Exception:
            return {}
    return {}


def _save(data: dict) -> None:
    CONFIG_DIR.mkdir(exist_ok=True)
    LICENSE_FILE.write_text(json.dumps(data, indent=2))


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _parse(ts: str) -> datetime:
    return datetime.fromisoformat(ts)


def _days_to_expiry(data: dict) -> int | None:
    """Return days until license expiry, or None if no expiry date stored."""
    expires_at = data.get("expires_at")
    if not expires_at:
        return None
    return (_parse(expires_at) - _now()).days


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def activate(key: str) -> dict:
    """Activate a license key via the Lemon Squeezy API.

    Stores the result in ~/.pagefuse/license.json.
    Returns the response metadata on success.
    Raises RuntimeError on failure.
    """
    try:
        r = requests.post(
            f"{LEMON_SQUEEZY_API}/activate",
            data={
                "license_key": key,
                "instance_name": socket.gethostname(),
            },
            timeout=10,
        )
        body = r.json()
    except requests.RequestException as e:
        raise RuntimeError(f"Could not reach activation server: {e}")

    if not body.get("activated"):
        error = body.get("error", "Unknown error")
        if "activation limit" in error.lower():
            raise RuntimeError(
                "Machine limit reached (3 machines per license).\n"
                "Run 'pagefuse deactivate' on an old machine to free up a slot,\n"
                "or manage your activations at https://app.lemonsqueezy.com"
            )
        raise RuntimeError(f"Activation failed: {error}")

    data = _load()
    data.update({
        "key": key,
        "instance_id": body["instance"]["id"],
        "email": body.get("meta", {}).get("customer_email", ""),
        "activated_at": _now().isoformat(),
        "last_validated": _now().isoformat(),
        "valid": True,
        "expires_at": body.get("license_key", {}).get("expires_at"),
    })
    _save(data)
    return body


def deactivate() -> None:
    """Deactivate the license on this machine, freeing up an activation slot.

    Raises RuntimeError if no license is stored or the API call fails.
    """
    data = _load()
    if not data.get("key") or not data.get("instance_id"):
        raise RuntimeError("No active license found on this machine.")

    try:
        r = requests.post(
            f"{LEMON_SQUEEZY_API}/deactivate",
            data={
                "license_key": data["key"],
                "instance_id": data["instance_id"],
            },
            timeout=10,
        )
        body = r.json()
    except requests.RequestException as e:
        raise RuntimeError(f"Could not reach activation server: {e}")

    if not body.get("deactivated"):
        error = body.get("error", "Unknown error")
        raise RuntimeError(f"Deactivation failed: {error}")

    # Remove license data but keep trial_started so the trial doesn't reset.
    trial_started = data.get("trial_started")
    new_data = {}
    if trial_started:
        new_data["trial_started"] = trial_started
    _save(new_data)


def revalidate(data: dict) -> tuple[bool, dict]:
    """Call Lemon Squeezy to re-check the license and update cached state.

    Offline behaviour:
      - If > EXPIRY_WINDOW_DAYS days until expiry: use cached result (no block).
      - If ≤ EXPIRY_WINDOW_DAYS days until expiry: must be online; returns False if not.

    Returns (valid, updated_data).
    """
    try:
        r = requests.post(
            f"{LEMON_SQUEEZY_API}/validate",
            data={
                "license_key": data["key"],
                "instance_id": data["instance_id"],
            },
            timeout=10,
        )
        body = r.json()
        valid = body.get("valid", False)
        expires_at = body.get("license_key", {}).get("expires_at")
        if expires_at:
            data["expires_at"] = expires_at
    except requests.RequestException:
        # Offline — allow if far enough from expiry, block if close.
        dtx = _days_to_expiry(data)
        if dtx is None or dtx > EXPIRY_WINDOW_DAYS:
            return data.get("valid", True), data
        return False, data

    data["valid"] = valid
    data["last_validated"] = _now().isoformat()
    _save(data)
    return valid, data


def check() -> tuple[bool, str]:
    """Check whether the current install is allowed to run.

    Returns (allowed, message) where message is non-empty when there is
    something to show the user (trial countdown, expiry warning, etc.).
    """
    data = _load()

    # ------------------------------------------------------------------ #
    # Licensed                                                             #
    # ------------------------------------------------------------------ #
    if data.get("key") and data.get("instance_id"):
        now = _now()
        last = data.get("last_validated")
        dtx = _days_to_expiry(data)

        # Re-validate if: cache is stale OR we're within the expiry window.
        stale = True
        if last:
            age = now - _parse(last)
            stale = age > timedelta(days=REVALIDATE_AFTER_DAYS)

        needs_revalidate = stale or (dtx is not None and dtx <= EXPIRY_WINDOW_DAYS)

        if needs_revalidate:
            valid, data = revalidate(data)
        else:
            valid = data.get("valid", True)

        if not valid:
            return False, (
                f"License check failed. Please connect to the internet to re-validate,\n"
                f"or manage your subscription at {PURCHASE_URL}"
            )

        # Show countdown only when within EXPIRY_WARN_DAYS of expiry.
        dtx = _days_to_expiry(data)
        if dtx is not None and 0 < dtx <= EXPIRY_WARN_DAYS:
            day_word = "day" if dtx == 1 else "days"
            return True, f"License expires in {dtx} {day_word} — renew at {PURCHASE_URL}"

        return True, ""

    # ------------------------------------------------------------------ #
    # Trial                                                                #
    # ------------------------------------------------------------------ #
    now = _now()
    if "trial_started" not in data:
        data["trial_started"] = now.isoformat()
        _save(data)

    days_used = (now - _parse(data["trial_started"])).days
    days_left = TRIAL_DAYS - days_used

    if days_left > 0:
        day_word = "day" if days_left == 1 else "days"
        return True, (
            f"Trial: {days_left} {day_word} remaining — "
            f"activate at {PURCHASE_URL}"
        )

    return False, (
        f"Your 30-day trial has expired.\n"
        f"Purchase a license at {PURCHASE_URL}"
    )


def status() -> dict:
    """Return a dict describing the current license state (for display)."""
    data = _load()
    now = _now()

    if data.get("key") and data.get("instance_id"):
        dtx = _days_to_expiry(data)
        return {
            "type": "licensed",
            "email": data.get("email", ""),
            "key": data["key"],
            "last_validated": data.get("last_validated", ""),
            "expires_at": data.get("expires_at", ""),
            "days_to_expiry": dtx,
            "valid": data.get("valid", True),
        }

    if "trial_started" in data:
        days_used = (now - _parse(data["trial_started"])).days
        days_left = TRIAL_DAYS - days_used
        return {
            "type": "trial",
            "days_left": max(days_left, 0),
            "expires_at": (_parse(data["trial_started"]) + timedelta(days=TRIAL_DAYS)).isoformat(),
        }

    return {"type": "none"}
