"""License validation logic — cache, grace period, remote refresh.

Orchestrates the full license validation lifecycle used by every
gated CLI command:

1. Read cache from ``~/.complyform/license_cache.json``
2. No cache → free tier
3. Air-gapped (key_hash starts with ``sha256:``) → check expiry only
4. Cache fresh (<24 hr) → return cached tier info
5. Try remote validation → update cache
6. Remote fails + within 7-day grace → return cached tier
7. Remote fails + grace expired → free tier fallback
"""

from __future__ import annotations

import hashlib
import logging
import tarfile
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

import httpx

from complyform.core.auth.license import LicenseCache, LicenseManager
from complyform.core.errors import (
    LicenseInvalidError,
    NetworkUnreachableError,
    ProfileBundleError,
    ProfileBundleVersionError,
)

_log = logging.getLogger(__name__)

_API_BASE = "https://api.complyform.dev"
_VALIDATE_URL = f"{_API_BASE}/v1/license/validate"
_CACHE_FRESHNESS_SECONDS = 86400  # 24 hours for gated commands
_GRACE_PERIOD_DAYS = 7


def _hash_key(key: str) -> str:
    """SHA-256 hash of a plaintext license key."""
    return hashlib.sha256(key.encode("utf-8")).hexdigest()


async def activate_online(key: str, mgr: LicenseManager) -> LicenseCache:
    """Activate license via POST /v1/license/validate.

    Downloads profile bundle, verifies checksums, caches license
    metadata.  Raises on failure.
    """
    key_hash = _hash_key(key)

    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.post(_VALIDATE_URL, json={"key_hash": key_hash})
    except httpx.HTTPError as exc:
        remaining = _grace_days_remaining(mgr)
        raise NetworkUnreachableError(remaining) from exc

    if resp.status_code == 401:
        data = resp.json()
        raise LicenseInvalidError(data.get("error", "invalid_key"))

    if resp.status_code != 200:
        remaining = _grace_days_remaining(mgr)
        raise NetworkUnreachableError(remaining)

    body: dict[str, Any] = resp.json()
    tier = body.get("tier", "community")
    expiry = body.get("expiry", "")
    features = body.get("features", [])
    profile_url = body.get("profile_url", "")
    manifest = body.get("manifest", {})

    # Check min_cli_version from manifest
    min_cli_version = manifest.get("min_cli_version")
    if min_cli_version:
        from packaging.version import InvalidVersion, Version

        from complyform import __version__

        try:
            if Version(__version__) < Version(min_cli_version):
                raise ProfileBundleVersionError(
                    bundle_version=manifest.get("bundle_version", "unknown"),
                    min_cli_version=min_cli_version,
                    current_cli_version=__version__,
                )
        except InvalidVersion:
            pass

    # Download and install profile bundle
    if profile_url:
        await _download_and_install_bundle(profile_url, manifest, mgr)

    now_iso = datetime.now(UTC).isoformat()
    cache = LicenseCache(
        cache_version=1,
        key_hash=key_hash,
        tier=tier,
        assess_frameworks=["all"],
        remediate_frameworks=features,
        features=features,
        project_limit=None,
        expires_at=expiry or None,
        last_validated_at=now_iso,
        profile_bundle_version=manifest.get("bundle_version"),
    )
    mgr.save_cache(cache)
    return cache


async def remote_validate(key_hash: str) -> dict[str, Any] | None:
    """Call POST /v1/license/validate. Returns response body or None."""
    try:
        async with httpx.AsyncClient(timeout=15.0) as client:
            resp = await client.post(_VALIDATE_URL, json={"key_hash": key_hash})
        if resp.status_code == 200:
            return resp.json()  # type: ignore[no-any-return]
    except Exception:
        _log.debug("Remote validation failed for key_hash=%s...", key_hash[:8])
    return None


async def auto_refresh_if_needed(
    mgr: LicenseManager,
) -> LicenseCache | None:
    """Re-validate remotely if cache is stale (>1 hr).

    Called when RemediationTierError (#7), FeatureNotAvailableError (#24),
    or ScanSourceTierError (#23) is raised.

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

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

    result = await remote_validate(cache.key_hash)
    if result is None:
        return None

    new_tier = result.get("tier", "community")
    if new_tier == cache.tier:
        return None

    # Tier changed — update cache
    now_iso = datetime.now(UTC).isoformat()
    updated = LicenseCache(
        cache_version=1,
        key_hash=cache.key_hash,
        tier=new_tier,
        assess_frameworks=["all"],
        remediate_frameworks=result.get("features", []),
        features=result.get("features", []),
        project_limit=cache.project_limit,
        expires_at=result.get("expiry") or cache.expires_at,
        last_validated_at=now_iso,
        profile_bundle_version=cache.profile_bundle_version,
    )
    mgr.save_cache(updated)
    return updated


async def _download_and_install_bundle(
    url: str,
    manifest: dict[str, Any],
    mgr: LicenseManager,
) -> None:
    """Download profile bundle, verify checksums, install."""
    profiles_dir = mgr._config_dir / "profiles"
    profiles_dir.mkdir(parents=True, exist_ok=True)

    try:
        async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
            resp = await client.get(url)
            resp.raise_for_status()
    except Exception as exc:
        raise ProfileBundleError(f"Download failed: {exc}") from exc

    bundle_bytes = resp.content

    # Verify SHA-256 if provided in manifest
    expected_sha = manifest.get("sha256")
    if expected_sha:
        actual_sha = hashlib.sha256(bundle_bytes).hexdigest()
        if actual_sha != expected_sha:
            raise ProfileBundleError(
                f"Checksum mismatch: expected {expected_sha}, got {actual_sha}"
            )

    # Extract tar.gz to profiles directory
    with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
        tmp_path = Path(tmp.name)
        tmp.write(bundle_bytes)

    try:
        with tarfile.open(tmp_path, "r:gz") as tar:
            # Security: reject paths that escape the target directory
            for member in tar.getmembers():
                resolved = (profiles_dir / member.name).resolve()
                if not str(resolved).startswith(str(profiles_dir.resolve())):
                    raise ProfileBundleError(
                        f"Bundle contains path traversal: {member.name}"
                    )
            tar.extractall(path=profiles_dir)  # noqa: S202
    except tarfile.TarError as exc:
        raise ProfileBundleError(f"Bundle extraction failed: {exc}") from exc
    finally:
        tmp_path.unlink(missing_ok=True)


def _grace_days_remaining(mgr: LicenseManager) -> int:
    """Compute remaining grace period days from cached license."""
    cache = mgr.load_cache()
    if cache is None:
        return 0
    try:
        last = datetime.fromisoformat(cache.last_validated_at)
        elapsed = (datetime.now(UTC) - last).days
        return max(0, _GRACE_PERIOD_DAYS - elapsed)
    except ValueError, TypeError:
        return 0
