"""Offline Ed25519-signed license verification for g-gremlin."""

from __future__ import annotations

import base64
import hashlib
import json
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

log = logging.getLogger(__name__)

LICENSE_PATH = Path.home() / ".g_gremlin" / "license.json"
KEYRING_DIR = Path(__file__).resolve().parent / "keyring"


@dataclass
class LicensePayload:
    schema_version: int
    issuer: str
    kid: str
    license_id: str
    account_id: str
    plan: str
    trial: bool
    not_before: str
    issued_at: str
    expires_at: str
    grace_period_hours: int
    entitlements: dict[str, bool]
    limits: dict[str, int | None]
    entitlements_hash: str | None
    signature: str


@dataclass
class LicenseVerificationResult:
    valid: bool
    payload: LicensePayload | None
    error: str | None
    warnings: list[str]


def canonical_json_bytes(payload_dict: dict[str, Any]) -> bytes:
    """Return deterministic UTF-8 canonical JSON bytes for signing/verification."""
    return json.dumps(
        payload_dict,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
        allow_nan=False,
    ).encode("utf-8")


def _parse_iso_utc(value: str) -> datetime:
    parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _to_iso_utc(value: datetime) -> str:
    return value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def load_public_keyring(keyring_dir: Path | None = None) -> dict[str, Ed25519PublicKey]:
    directory = keyring_dir or KEYRING_DIR
    keys: dict[str, Ed25519PublicKey] = {}
    if not directory.exists():
        return keys

    for pem_path in sorted(directory.glob("*.pem")):
        kid = pem_path.stem
        try:
            key = serialization.load_pem_public_key(pem_path.read_bytes())
            if not isinstance(key, Ed25519PublicKey):
                log.warning("Skipping non-Ed25519 key file %s", pem_path)
                continue
            keys[kid] = key
        except Exception as exc:
            log.warning("Failed loading public key %s: %s", pem_path, exc)
    return keys


def compute_entitlements_hash(entitlements: dict[str, bool], limits: dict[str, int | None]) -> str:
    payload = {
        "entitlements": entitlements,
        "limits": limits,
    }
    digest = hashlib.sha256(canonical_json_bytes(payload)).hexdigest()
    return digest[:16]


def _parse_payload(raw: dict[str, Any]) -> LicensePayload:
    return LicensePayload(
        schema_version=int(raw.get("schema_version", 1)),
        issuer=str(raw.get("issuer") or ""),
        kid=str(raw.get("kid") or ""),
        license_id=str(raw.get("license_id") or ""),
        account_id=str(raw.get("account_id") or ""),
        plan=str(raw.get("plan") or "local_free"),
        trial=bool(raw.get("trial", False)),
        not_before=str(raw.get("not_before") or raw.get("issued_at") or ""),
        issued_at=str(raw.get("issued_at") or ""),
        expires_at=str(raw.get("expires_at") or ""),
        grace_period_hours=int(raw.get("grace_period_hours", 0) or 0),
        entitlements={
            str(k): bool(v)
            for k, v in (raw.get("entitlements") or {}).items()
        },
        limits={
            str(k): (None if v is None else int(v))
            for k, v in (raw.get("limits") or {}).items()
        },
        entitlements_hash=(str(raw.get("entitlements_hash")) if raw.get("entitlements_hash") is not None else None),
        signature=str(raw.get("signature") or ""),
    )


def verify_license(
    path: Path | None = None,
    keyring_dir: Path | None = None,
) -> LicenseVerificationResult:
    target_path = path or LICENSE_PATH
    if not target_path.exists():
        return LicenseVerificationResult(valid=False, payload=None, error="License file not found.", warnings=[])

    try:
        payload_raw = json.loads(target_path.read_text(encoding="utf-8"))
    except Exception as exc:
        return LicenseVerificationResult(valid=False, payload=None, error=f"Invalid license JSON: {exc}", warnings=[])

    if not isinstance(payload_raw, dict):
        return LicenseVerificationResult(valid=False, payload=None, error="License payload must be a JSON object.", warnings=[])

    signature_b64 = str(payload_raw.get("signature") or "")
    if not signature_b64:
        return LicenseVerificationResult(valid=False, payload=None, error="License signature missing.", warnings=[])

    payload_without_sig = dict(payload_raw)
    payload_without_sig.pop("signature", None)

    payload = _parse_payload(payload_raw)
    if not payload.kid:
        return LicenseVerificationResult(valid=False, payload=None, error="License key id (kid) missing.", warnings=[])

    keys = load_public_keyring(keyring_dir)
    signing_key = keys.get(payload.kid)
    if signing_key is None:
        return LicenseVerificationResult(
            valid=False,
            payload=None,
            error=f"Unknown license key id: {payload.kid}",
            warnings=[],
        )

    try:
        signature = base64.b64decode(signature_b64, validate=True)
    except Exception as exc:
        return LicenseVerificationResult(valid=False, payload=None, error=f"Invalid license signature encoding: {exc}", warnings=[])

    canonical = canonical_json_bytes(payload_without_sig)
    try:
        signing_key.verify(signature, canonical)
    except InvalidSignature:
        return LicenseVerificationResult(valid=False, payload=None, error="License signature verification failed.", warnings=[])

    warnings: list[str] = []
    expected_hash = compute_entitlements_hash(payload.entitlements, payload.limits)
    if payload.entitlements_hash and payload.entitlements_hash != expected_hash:
        warnings.append("entitlements_hash mismatch")

    return LicenseVerificationResult(valid=True, payload=payload, error=None, warnings=warnings)


def effective_entitlements(
    payload: LicensePayload,
    local_free_defaults: dict[str, bool],
    now_utc: datetime | None = None,
) -> dict[str, bool]:
    now = now_utc or datetime.now(timezone.utc)
    expires = _parse_iso_utc(payload.expires_at)
    grace_deadline = expires + timedelta(hours=payload.grace_period_hours)
    if now <= grace_deadline:
        return dict(payload.entitlements)
    return dict(local_free_defaults)


def effective_limits(
    payload: LicensePayload,
    local_free_limits: dict[str, int | None],
    now_utc: datetime | None = None,
) -> dict[str, int | None]:
    now = now_utc or datetime.now(timezone.utc)
    expires = _parse_iso_utc(payload.expires_at)
    grace_deadline = expires + timedelta(hours=payload.grace_period_hours)
    if now <= grace_deadline:
        return dict(payload.limits)
    return dict(local_free_limits)


def evaluate_license_window(payload: LicensePayload, now_utc: datetime | None = None) -> tuple[bool, bool, str | None]:
    """
    Returns (within_valid_window, within_grace, error).

    within_valid_window: now between not_before and expires_at inclusive.
    within_grace: now after expiry but within grace_period_hours.
    """
    now = now_utc or datetime.now(timezone.utc)

    try:
        not_before = _parse_iso_utc(payload.not_before)
        expires = _parse_iso_utc(payload.expires_at)
    except Exception as exc:
        return False, False, f"Invalid license time fields: {exc}"

    if now < not_before:
        return False, False, f"License not active until {_to_iso_utc(not_before)}"

    if now <= expires:
        return True, False, None

    grace_deadline = expires + timedelta(hours=payload.grace_period_hours)
    if now <= grace_deadline:
        return False, True, None

    return False, False, f"License expired at {_to_iso_utc(expires)}"
