from __future__ import annotations

import base64
import binascii
from datetime import UTC, datetime
import json
import os
from pathlib import Path

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from govoplan_core.security.http_fetch import fetch_http_text


def module_license_decision(required_features: list[str] | tuple[str, ...]) -> dict[str, object]:
    return _module_license_decision_from_validation(validate_module_license(), required_features)


def module_license_diagnostics(
    path: Path | None = None,
    *,
    required_features: list[str] | tuple[str, ...] = (),
    trusted_keys: dict[str, str] | None = None,
    require_trusted: bool | None = None,
) -> dict[str, object]:
    validation = validate_module_license(path, trusted_keys=trusted_keys, require_trusted=require_trusted)
    decision = _module_license_decision_from_validation(validation, required_features)
    guidance = _license_guidance(validation, decision)
    return {
        "configured": validation["configured"],
        "valid": validation["valid"],
        "path": validation["path"],
        "license_id": validation["license_id"],
        "subject": validation["subject"],
        "features": validation["features"],
        "valid_from": validation["valid_from"],
        "valid_until": validation["valid_until"],
        "signed": validation["signed"],
        "trusted": validation["trusted"],
        "key_id": validation["key_id"],
        "enforced": _license_enforcement_enabled(),
        "allowed": decision["allowed"],
        "required_features": decision["required_features"],
        "missing_features": decision["missing_features"],
        "expires_in_days": _expires_in_days(validation.get("valid_until")),
        "reason": decision.get("reason") or validation.get("error"),
        "guidance": guidance,
    }


def issue_module_license(
    *,
    path: Path,
    license_id: str,
    subject: str,
    features: list[str] | tuple[str, ...],
    valid_until: str | datetime,
    signing_key_id: str,
    signing_private_key_path: Path,
    valid_from: str | datetime | None = None,
    issuer: str | None = None,
    notes: str | None = None,
) -> Path:
    clean_license_id = str(license_id).strip()
    clean_subject = str(subject).strip()
    clean_key_id = str(signing_key_id).strip()
    clean_features = _normalize_required_features(features)
    if not clean_license_id:
        raise ValueError("License id is required.")
    if not clean_subject:
        raise ValueError("License subject is required.")
    if not clean_key_id:
        raise ValueError("License signing key id is required.")
    if not clean_features:
        raise ValueError("At least one license feature is required.")
    payload: dict[str, object] = {
        "license_id": clean_license_id,
        "subject": clean_subject,
        "features": list(clean_features),
        "valid_from": _datetime_text(valid_from or datetime.now(tz=UTC)),
        "valid_until": _datetime_text(valid_until),
    }
    clean_issuer = str(issuer).strip() if issuer else ""
    if clean_issuer:
        payload["issuer"] = clean_issuer
    clean_notes = str(notes).strip() if notes else ""
    if clean_notes:
        payload["notes"] = clean_notes
    private_key = _load_private_key(signing_private_key_path)
    signature = private_key.sign(_canonical_bytes(payload))
    payload["signature"] = {
        "algorithm": "ed25519",
        "key_id": clean_key_id,
        "value": base64.b64encode(signature).decode("ascii"),
    }
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    return path


def _module_license_decision_from_validation(
    validation: dict[str, object],
    required_features: list[str] | tuple[str, ...],
) -> dict[str, object]:
    required = tuple(dict.fromkeys(str(item).strip() for item in required_features if str(item).strip()))
    if not required:
        return {"allowed": True, "required_features": [], "missing_features": [], "enforced": False}
    enforced = _license_enforcement_enabled()
    if not validation["valid"]:
        return {
            "allowed": not enforced,
            "required_features": list(required),
            "missing_features": list(required),
            "enforced": enforced,
            "license": validation,
            "reason": validation["error"] or "No valid license is configured.",
        }
    licensed_features = set(validation["features"])
    missing = tuple(feature for feature in required if feature not in licensed_features)
    return {
        "allowed": not missing or not enforced,
        "required_features": list(required),
        "missing_features": list(missing),
        "enforced": enforced,
        "license": validation,
        "reason": "Missing licensed feature(s): " + ", ".join(missing) if missing else None,
    }


def validate_module_license(
    path: Path | None = None,
    *,
    trusted_keys: dict[str, str] | None = None,
    require_trusted: bool | None = None,
) -> dict[str, object]:
    license_path = path or _configured_license_path()
    effective_require_trusted = _license_enforcement_enabled() if require_trusted is None else require_trusted
    if license_path is None:
        return _license_result(valid=not effective_require_trusted, configured=False, path=None, error="No license file configured.")
    if not license_path.exists():
        return _license_result(valid=False, configured=True, path=license_path, error=f"License file does not exist: {license_path}")
    try:
        payload = json.loads(license_path.read_text(encoding="utf-8"))
        if not isinstance(payload, dict):
            raise ValueError("License file must contain a JSON object.")
        features = _string_list(payload.get("features"))
        valid_from = _parse_datetime(payload.get("valid_from"))
        valid_until = _parse_datetime(payload.get("valid_until"))
        now = datetime.now(tz=UTC)
        if valid_from is not None and now < valid_from:
            return _license_result(valid=False, configured=True, path=license_path, payload=payload, features=features, error=f"License is not valid before {valid_from.isoformat()}.")
        if valid_until is not None and now > valid_until:
            return _license_result(valid=False, configured=True, path=license_path, payload=payload, features=features, error=f"License expired at {valid_until.isoformat()}.")
        signature_state = _license_signature_state(payload, trusted_keys=trusted_keys if trusted_keys is not None else _configured_trusted_keys())
        if signature_state.get("fatal"):
            return _license_result(valid=False, configured=True, path=license_path, payload=payload, features=features, signature_state=signature_state, error=str(signature_state["error"]))
        if effective_require_trusted and not signature_state["trusted"]:
            return _license_result(valid=False, configured=True, path=license_path, payload=payload, features=features, signature_state=signature_state, error=str(signature_state["error"] or "License must be signed by a trusted key."))
        return _license_result(valid=True, configured=True, path=license_path, payload=payload, features=features, signature_state=signature_state)
    except Exception as exc:
        return _license_result(valid=False, configured=True, path=license_path, error=str(exc))


def _license_result(
    *,
    valid: bool,
    configured: bool,
    path: Path | None,
    error: str | None = None,
    payload: dict[str, object] | None = None,
    features: list[str] | None = None,
    signature_state: dict[str, object] | None = None,
) -> dict[str, object]:
    state = signature_state or {"signed": False, "trusted": False, "key_id": None}
    return {
        "valid": valid,
        "configured": configured,
        "path": str(path) if path is not None else None,
        "license_id": str(payload.get("license_id")) if payload and payload.get("license_id") else None,
        "subject": str(payload.get("subject")) if payload and payload.get("subject") else None,
        "features": features or [],
        "valid_from": str(payload.get("valid_from")) if payload and payload.get("valid_from") else None,
        "valid_until": str(payload.get("valid_until")) if payload and payload.get("valid_until") else None,
        "signed": bool(state.get("signed")),
        "trusted": bool(state.get("trusted")),
        "key_id": state.get("key_id") if isinstance(state.get("key_id"), str) else None,
        "error": error,
    }


def _license_guidance(validation: dict[str, object], decision: dict[str, object]) -> list[str]:
    guidance: list[str] = []
    if not validation.get("configured"):
        guidance.append("Configure GOVOPLAN_LICENSE_FILE with the current signed license file.")
    elif validation.get("path") and not validation.get("valid") and validation.get("error"):
        guidance.append("Import a renewed license file or correct the configured license path before enforcing installs.")
    if validation.get("configured") and not validation.get("signed"):
        guidance.append("Use a signed license for production and configure GOVOPLAN_LICENSE_TRUSTED_KEYS_FILE.")
    elif validation.get("signed") and not validation.get("trusted"):
        guidance.append("Add the signing public key to the trusted license keyring, or rotate to a trusted issuer key.")
    expires_in_days = _expires_in_days(validation.get("valid_until"))
    if expires_in_days is not None:
        if expires_in_days < 0:
            guidance.append("Renew the license; the validity window has ended.")
        elif expires_in_days <= 30:
            guidance.append(f"Renew the license soon; it expires in {expires_in_days} day(s).")
    missing = decision.get("missing_features")
    if isinstance(missing, list) and missing:
        guidance.append("Request a renewal or entitlement update for: " + ", ".join(str(item) for item in missing))
    if not _license_enforcement_enabled():
        guidance.append("License enforcement is observe-only until GOVOPLAN_LICENSE_ENFORCEMENT=true is set.")
    return guidance


def _expires_in_days(value: object) -> int | None:
    if not value:
        return None
    try:
        expires_at = _parse_datetime(value)
    except (TypeError, ValueError):
        return None
    if expires_at is None:
        return None
    seconds = (expires_at - datetime.now(tz=UTC)).total_seconds()
    return int(seconds // 86400)


def _configured_license_path() -> Path | None:
    value = os.getenv("GOVOPLAN_LICENSE_FILE", "").strip()
    return Path(value).expanduser() if value else None


def _license_enforcement_enabled() -> bool:
    return os.getenv("GOVOPLAN_LICENSE_ENFORCEMENT", "").strip().lower() in {"1", "true", "yes", "on"}


def _configured_trusted_keys() -> dict[str, str]:
    file_value = os.getenv("GOVOPLAN_LICENSE_TRUSTED_KEYS_FILE", "").strip()
    if file_value:
        path = Path(file_value).expanduser()
        if not path.exists():
            raise ValueError(f"Trusted license key file does not exist: {path}")
        return _parse_trusted_keys(path.read_text(encoding="utf-8"))
    url_value = os.getenv("GOVOPLAN_LICENSE_TRUSTED_KEYS_URL", "").strip()
    if url_value:
        return _parse_trusted_keys(_read_trusted_keys_url(url_value))
    value = os.getenv("GOVOPLAN_LICENSE_TRUSTED_KEYS", "").strip()
    return _parse_trusted_keys(value) if value else {}


def _configured_trusted_keys_cache_path() -> Path | None:
    value = os.getenv("GOVOPLAN_LICENSE_TRUSTED_KEYS_CACHE", "").strip()
    return Path(value).expanduser() if value else None


def _read_trusted_keys_url(url: str) -> str:
    cache_path = _configured_trusted_keys_cache_path()
    try:
        body = fetch_http_text(url, timeout=15, label="Trusted license key URL")
        if cache_path is not None:
            cache_path.parent.mkdir(parents=True, exist_ok=True)
            cache_path.write_text(body, encoding="utf-8")
        return body
    except OSError:
        if cache_path is not None and cache_path.exists():
            return cache_path.read_text(encoding="utf-8")
        raise


def _parse_trusted_keys(value: str) -> dict[str, str]:
    payload = json.loads(value)
    if isinstance(payload, dict) and isinstance(payload.get("keys"), list):
        keys: dict[str, str] = {}
        now = datetime.now(tz=UTC)
        for item in payload["keys"]:
            if not isinstance(item, dict):
                continue
            if str(item.get("status") or "active").strip().lower() in {"revoked", "disabled", "retired"}:
                continue
            not_before = _parse_datetime(item.get("not_before"))
            not_after = _parse_datetime(item.get("not_after"))
            if not_before is not None and now < not_before:
                continue
            if not_after is not None and now > not_after:
                continue
            key_id = str(item.get("key_id") or "").strip()
            public_key = str(item.get("public_key") or item.get("public_key_base64") or "").strip()
            if key_id and public_key:
                keys[key_id] = public_key
        return keys
    if not isinstance(payload, dict):
        raise ValueError("Trusted license keys must be a JSON object.")
    return {str(key): str(item) for key, item in payload.items() if str(key).strip() and str(item).strip()}


def _license_signature_state(payload: dict[str, object], *, trusted_keys: dict[str, str]) -> dict[str, object]:
    signature = payload.get("signature")
    if signature is None:
        return {"signed": False, "trusted": False, "key_id": None, "error": "License is unsigned.", "fatal": False}
    if not isinstance(signature, dict):
        return {"signed": True, "trusted": False, "key_id": None, "error": "License signature must be an object.", "fatal": True}
    algorithm = str(signature.get("algorithm") or "").strip().lower()
    key_id = str(signature.get("key_id") or "").strip()
    value = str(signature.get("value") or "").strip()
    if algorithm != "ed25519":
        return {"signed": True, "trusted": False, "key_id": key_id or None, "error": f"Unsupported license signature algorithm: {algorithm!r}", "fatal": True}
    if not key_id or not value:
        return {"signed": True, "trusted": False, "key_id": key_id or None, "error": "License signature requires key_id and value.", "fatal": True}
    trusted_key = trusted_keys.get(key_id)
    if not trusted_key:
        return {"signed": True, "trusted": False, "key_id": key_id, "error": f"License signature key is not trusted: {key_id}", "fatal": False}
    try:
        public_key = Ed25519PublicKey.from_public_bytes(base64.b64decode(trusted_key, validate=True))
        signed_payload = dict(payload)
        signed_payload.pop("signature", None)
        public_key.verify(base64.b64decode(value, validate=True), _canonical_bytes(signed_payload))
    except (ValueError, binascii.Error, InvalidSignature) as exc:
        return {"signed": True, "trusted": False, "key_id": key_id, "error": f"License signature verification failed: {exc}", "fatal": True}
    return {"signed": True, "trusted": True, "key_id": key_id, "error": None, "fatal": False}


def _canonical_bytes(payload: object) -> bytes:
    return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")


def _datetime_text(value: str | datetime) -> str:
    parsed = _parse_datetime(value)
    if parsed is None:
        raise ValueError("License validity timestamp is required.")
    return parsed.isoformat().replace("+00:00", "Z")


def _parse_datetime(value: object) -> datetime | None:
    if value is None:
        return None
    text = str(value).strip()
    if not text:
        return None
    if text.endswith("Z"):
        text = text[:-1] + "+00:00"
    parsed = datetime.fromisoformat(text)
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=UTC)
    return parsed.astimezone(UTC)


def _string_list(value: object) -> list[str]:
    if value is None:
        return []
    if not isinstance(value, list):
        raise ValueError("License features must be a list.")
    return [str(item).strip() for item in value if str(item).strip()]


def _normalize_required_features(value: list[str] | tuple[str, ...]) -> tuple[str, ...]:
    return tuple(dict.fromkeys(str(item).strip() for item in value if str(item).strip()))


def _load_private_key(path: Path) -> Ed25519PrivateKey:
    private_key = serialization.load_pem_private_key(path.expanduser().read_bytes(), password=None)
    if not isinstance(private_key, Ed25519PrivateKey):
        raise ValueError("License signing key must be an Ed25519 private key.")
    return private_key
