"""LicenseManager — activation, cache, grace period, air-gap validation.

Manages the license lifecycle: activation (online + air-gapped),
cache read/write, grace period, auto-refresh.
"""

from __future__ import annotations

import base64
import hashlib
import json
import re
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING

from pydantic import BaseModel, ConfigDict

from complyform.core.config import ensure_complyform_dir
from complyform.core.errors import LicenseInvalidError

if TYPE_CHECKING:
    from complyform.core.auth.tier_enforcement import TierInfo
    from complyform.core.errors import ComplyFormError

_LICENSE_KEY_PATTERN = re.compile(r"^cf_(live|test)_[a-f0-9]{32}$")
_API_KEY_PATTERN = re.compile(r"^cfapi_[a-f0-9]{32}$")

_GRACE_PERIOD_DAYS = 7
_CACHE_FRESHNESS_SECONDS = 3600  # 1 hour

_PEM_BEGIN = "-----BEGIN COMPLYFORM LICENSE-----"
_PEM_END = "-----END COMPLYFORM LICENSE-----"


class LicenseCache(BaseModel):
    """Cached license metadata. Stored at ~/.complyform/license_cache.json."""

    model_config = ConfigDict(strict=True)

    cache_version: int = 1
    key_hash: str
    tier: str
    assess_frameworks: list[str]
    remediate_frameworks: list[str]
    features: list[str]
    project_limit: int | None
    expires_at: str | None
    last_validated_at: str
    profile_bundle_version: str | None = None
    api_key_hash: str | None = None
    batch_limit: int | None = None
    discover_limit: int | None = None
    scan_sources: list[str] | None = None


class LicenseManager:
    """Manages license activation, caching, and validation."""

    def __init__(self, config_dir: Path | None = None) -> None:
        self._config_dir = config_dir or (Path.home() / ".complyform")

    @property
    def cache_path(self) -> Path:
        return self._config_dir / "license_cache.json"

    @property
    def public_key_path(self) -> Path:
        return Path(__file__).parent / "ed25519_public.key"

    def load_cache(self) -> LicenseCache | None:
        """Load license cache. Returns None if not found or corrupt."""
        if not self.cache_path.exists():
            return None
        try:
            raw = self.cache_path.read_text(encoding="utf-8")
            data = json.loads(raw)
            return LicenseCache.model_validate(data)
        except Exception:
            return None

    def save_cache(self, cache: LicenseCache) -> None:
        """Write cache to disk. File permissions 0o600."""
        import os

        ensure_complyform_dir()
        self._config_dir.mkdir(parents=True, exist_ok=True)
        content = json.dumps(
            cache.model_dump(mode="json"),
            indent=2,
        )
        tmp = self.cache_path.with_suffix(".tmp")
        tmp.write_text(content, encoding="utf-8")
        os.chmod(tmp, 0o600)
        os.replace(tmp, self.cache_path)

    def clear_cache(self) -> None:
        """Delete license cache file."""
        if self.cache_path.exists():
            self.cache_path.unlink()

    def is_within_grace_period(self, cache: LicenseCache) -> bool:
        """True if now - last_validated_at < 7 days."""
        try:
            last = datetime.fromisoformat(cache.last_validated_at)
            return datetime.now(UTC) - last < timedelta(days=_GRACE_PERIOD_DAYS)
        except ValueError, TypeError:
            return False

    def is_cache_fresh(self, cache: LicenseCache) -> bool:
        """True if last_validated_at < 1 hour ago."""
        try:
            last = datetime.fromisoformat(cache.last_validated_at)
            return (datetime.now(UTC) - last).total_seconds() < _CACHE_FRESHNESS_SECONDS
        except ValueError, TypeError:
            return False

    def resolve_tier_info(self) -> TierInfo:
        """Load cache -> resolve to TierInfo. No cache or expired -> community."""
        from complyform.core.auth.tier_enforcement import resolve_tier_info

        cache = self.load_cache()
        return resolve_tier_info(cache)

    def validate_airgap_license(self, license_file: Path) -> LicenseCache:
        """Validate .complyform-license file per §F.3.2.

        1. Strip PEM headers
        2. Base64-decode outer -> {enc, sig, alg}
        3. Assert alg == "ed25519"
        4. Verify sig against enc using bundled public key
        5. Base64-decode enc -> JSON payload
        6. Check version field
        7. Validate expires_at > now
        8. Validate min_cli_version
        9. Return LicenseCache (key_hash = SHA-256 of full file content)
        """
        from cryptography.hazmat.primitives.asymmetric.ed25519 import (
            Ed25519PublicKey,
        )

        raw_content = license_file.read_text(encoding="utf-8")

        # Strip PEM headers
        lines = [
            line.strip()
            for line in raw_content.strip().splitlines()
            if line.strip() and line.strip() != _PEM_BEGIN and line.strip() != _PEM_END
        ]
        if not lines:
            raise LicenseInvalidError("Empty license file")

        # Check PEM markers were present
        if _PEM_BEGIN not in raw_content or _PEM_END not in raw_content:
            raise LicenseInvalidError("Missing PEM headers")

        b64_body = "".join(lines)
        try:
            outer_bytes = base64.b64decode(b64_body)
            outer = json.loads(outer_bytes)
        except Exception as exc:
            raise LicenseInvalidError(f"Cannot decode license: {exc}") from exc

        enc: str = outer.get("enc", "")
        sig: str = outer.get("sig", "")
        alg: str = outer.get("alg", "")

        if alg != "ed25519":
            raise LicenseInvalidError(f"Unsupported algorithm: {alg}")

        # Verify signature
        pub_key_bytes = self.public_key_path.read_bytes()
        try:
            public_key = Ed25519PublicKey.from_public_bytes(pub_key_bytes)
            sig_bytes = base64.b64decode(sig)
            enc_bytes = enc.encode("utf-8")
            public_key.verify(sig_bytes, enc_bytes)
        except Exception as exc:
            raise LicenseInvalidError(f"Signature verification failed: {exc}") from exc

        # Decode payload
        try:
            payload_bytes = base64.b64decode(enc)
            payload = json.loads(payload_bytes)
        except Exception as exc:
            raise LicenseInvalidError(f"Cannot decode payload: {exc}") from exc

        # Validate expiry
        expires_at = payload.get("expires_at")
        if expires_at:
            try:
                exp = datetime.fromisoformat(expires_at)
                if exp < datetime.now(UTC):
                    raise LicenseInvalidError("License has expired")
            except ValueError as exc:
                raise LicenseInvalidError(f"Invalid expires_at: {exc}") from exc

        # Validate min_cli_version
        min_cli = payload.get("min_cli_version")
        if min_cli:
            from packaging.version import InvalidVersion, Version

            from complyform import __version__

            try:
                if Version(__version__) < Version(min_cli):
                    raise LicenseInvalidError(
                        f"License requires CLI v{min_cli}+, running v{__version__}"
                    )
            except InvalidVersion:
                pass

        # Build key hash from full file content
        key_hash = "sha256:" + hashlib.sha256(raw_content.encode("utf-8")).hexdigest()

        now_iso = datetime.now(UTC).isoformat()

        return LicenseCache(
            cache_version=1,
            key_hash=key_hash,
            tier=payload.get("tier", "community"),
            assess_frameworks=payload.get("assess_frameworks", ["all"]),
            remediate_frameworks=payload.get("remediate_frameworks", []),
            features=payload.get("features", []),
            project_limit=payload.get("project_limit"),
            expires_at=expires_at,
            last_validated_at=now_iso,
            profile_bundle_version=payload.get("profile_bundle_version"),
            batch_limit=payload.get("batch_limit"),
            discover_limit=payload.get("discover_limit"),
            scan_sources=payload.get("scan_sources"),
        )

    def should_auto_refresh(self, cache: LicenseCache) -> bool:
        """True if cache is older than 1 hour."""
        return not self.is_cache_fresh(cache)

    def attempt_auto_refresh(self, original_error: ComplyFormError) -> TierInfo | None:
        """Try remote re-validation on tier gate errors.

        Triggered by RemediationTierError (#7), FeatureNotAvailableError
        (#24), or ScanSourceTierError (#23).

        Returns updated TierInfo if tier upgraded, None otherwise.
        """
        cache = self.load_cache()
        if cache is None:
            return None

        # Prevent retry loops: if cache < 1 hour old, don't refresh
        if self.is_cache_fresh(cache):
            return None

        result = self._remote_validate(cache.key_hash)
        if result is None:
            return None

        if result.tier == cache.tier:
            return None

        self.save_cache(result)

        from complyform.core.auth.tier_enforcement import resolve_tier_info

        return resolve_tier_info(result)

    def activate_online(self, key: str) -> LicenseCache:
        """Activate license via online validation.

        Delegates to ``complyform.core.license.activate_online`` for the
        actual HTTP call + bundle download.  Falls back to local stub if
        the remote module is unavailable (e.g. during unit tests without
        network dependencies installed).
        """
        if not _LICENSE_KEY_PATTERN.match(key):
            raise LicenseInvalidError("Invalid key format")

        key_hash = hashlib.sha256(key.encode("utf-8")).hexdigest()

        result = self._remote_validate(key_hash)
        if result is not None:
            return result

        # Fallback: construct cache from key hash only (offline / test)
        now_iso = datetime.now(UTC).isoformat()
        return LicenseCache(
            cache_version=1,
            key_hash=key_hash,
            tier="community",
            assess_frameworks=["all"],
            remediate_frameworks=[],
            features=[],
            project_limit=1,
            expires_at=None,
            last_validated_at=now_iso,
        )

    def _remote_validate(self, key_hash: str) -> LicenseCache | None:
        """POST /v1/license/validate. Returns cache or None on failure."""
        try:
            import httpx
        except ImportError:
            return None

        url = "https://api.complyform.dev/v1/license/validate"
        try:
            resp = httpx.post(url, json={"key_hash": key_hash}, timeout=15.0)
        except httpx.HTTPError:
            return None

        if resp.status_code != 200:
            return None

        body = resp.json()
        now_iso = datetime.now(UTC).isoformat()
        return LicenseCache(
            cache_version=1,
            key_hash=key_hash,
            tier=body.get("tier", "community"),
            assess_frameworks=["all"],
            remediate_frameworks=body.get("features", []),
            features=body.get("features", []),
            project_limit=None,
            expires_at=body.get("expiry") or None,
            last_validated_at=now_iso,
            profile_bundle_version=body.get("manifest", {}).get("bundle_version"),
        )


def validate_license_key_format(key: str) -> bool:
    """Validate license key format: cf_(live|test)_<32 hex chars>."""
    return bool(_LICENSE_KEY_PATTERN.match(key))


def validate_api_key_format(key: str) -> bool:
    """Validate API key format: cfapi_<32 hex chars>."""
    return bool(_API_KEY_PATTERN.match(key))


def _as_str_list(val: object) -> list[str]:
    if isinstance(val, list):
        return [str(v) for v in val]
    return []


def _as_optional_int(val: object) -> int | None:
    if val is None:
        return None
    if isinstance(val, int):
        return val
    return None
