"""License manager with HTTP server validation and offline cache.

Validation flow
---------------
validate(key):
    1. Minimal format check (length, charset)
    2. POST to server_url with {key, device_id}   ← if server_url configured
       or stub-accept any 8+ char key              ← if server_url is empty
    3. On success → save key in keyring + update cache
    4. Return LicenseResult

is_licensed():
    1. Load stored key from keyring
    2. If server_url configured:
       a. Check cache (valid + not expired + same key_hash) → return cached result
       b. Try fresh HTTP check → update cache → return result
       c. Offline fallback: return cached valid (up to 2× TTL) or False
    3. If no server_url (stub mode): trust stored key

Cache format (stored as JSON in keyring):
    {
        "valid":      bool,
        "cached_at":  float,       # time.time()
        "key_hash":   str,         # SHA-256[:16] of the license key
        "expires":    str | None,  # ISO-8601 or None
    }
"""

from __future__ import annotations

import hashlib
import json
import logging
import time
import urllib.error
from dataclasses import dataclass
from typing import Any

from ..security import privacy_manager
from ..utils.helpers import get_device_id

log = logging.getLogger(__name__)

_APP_NAME = "MuraveyChina"
_REQUEST_TIMEOUT = 15  # seconds (15s: GFW packet-loss can delay TLS handshakes)
_MIN_KEY_LENGTH = 8


@dataclass
class LicenseResult:
    """Result of a license validation attempt."""

    valid: bool
    expires: str | None = None  # ISO-8601 date or None
    error: str | None = None
    from_cache: bool = False

    @property
    def ok(self) -> bool:
        return self.valid


class LicenseManager:
    """Validates, caches, and stores the application license key."""

    MIN_KEY_LENGTH = _MIN_KEY_LENGTH

    def __init__(self, server_url: str = "", cache_ttl: int = 86400) -> None:
        """
        Args:
            server_url: Full URL of the POST /verify endpoint.
                        Empty string → stub mode (any 8+ char key is valid).
            cache_ttl:  Seconds before the cached result is considered stale.
                        Defaults to 86 400 (24 hours).
        """
        self._server_url = server_url.strip()
        self._cache_ttl = cache_ttl

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

    def validate(self, key: str) -> LicenseResult:
        """Validate *key* (via HTTP if server_url set, else stub).

        On success the key is saved to keyring and the cache is updated.
        Returns a LicenseResult.
        """
        key = key.strip()

        if len(key) < self.MIN_KEY_LENGTH:
            msg = f"Key too short ({len(key)} chars, min {self.MIN_KEY_LENGTH})"
            log.warning("License: %s", msg)
            return LicenseResult(valid=False, error=msg)

        result = self._do_validate(key)

        if result.valid:
            privacy_manager.save_license_key(key)
            self._save_cache(key, result)
            log.info("License activated (expires=%s)", result.expires)
        else:
            log.warning("License rejected: %s", result.error)

        return result

    def is_licensed(self) -> bool:
        """Return True if the application has a valid license.

        Uses cache when available; falls back to last known value if offline.
        """
        key = privacy_manager.load_license_key()
        if not key or len(key) < self.MIN_KEY_LENGTH:
            return False

        if not self._server_url:
            # Stub mode — trust stored key
            return True

        # 1. Fresh cache hit?
        cache = privacy_manager.get_license_cache()
        if cache and self._cache_fresh(cache, key):
            log.debug("License: cache hit (valid=%s)", cache.get("valid"))
            return bool(cache.get("valid", False))

        # 2. Try fresh HTTP check
        result = self._check_remote(key)
        if result is not None:
            self._save_cache(key, result)
            return result.valid

        # 3. Offline fallback — accept cached result up to 2× TTL
        if cache and self._cache_usable_offline(cache, key):
            log.info("License: offline fallback (valid=%s, cached)", cache.get("valid"))
            return bool(cache.get("valid", False))

        log.warning("License: no valid cache and server unreachable — denying")
        return False

    def get_key(self) -> str | None:
        """Return the stored license key, or None."""
        return privacy_manager.load_license_key()

    def revoke(self) -> None:
        """Remove the stored license key and cache."""
        privacy_manager.delete_api_key("license")
        privacy_manager.clear_license_cache()
        log.info("License revoked")

    def get_status(self) -> dict[str, Any]:
        """Return a dict with current license status for display in UI."""
        key = privacy_manager.load_license_key()
        cache = privacy_manager.get_license_cache()
        return {
            "has_key": bool(key),
            "key_preview": f"{key[:4]}…{key[-4:]}" if key and len(key) >= 8 else None,
            "valid": self.is_licensed(),
            "expires": cache.get("expires") if cache else None,
            "cached_at": cache.get("cached_at") if cache else None,
            "from_cache": bool(cache),
            "server_url": self._server_url or "(stub mode)",
        }

    # ------------------------------------------------------------------ #
    # Internal: validation
    # ------------------------------------------------------------------ #

    def _do_validate(self, key: str) -> LicenseResult:
        """Route to HTTP or stub based on server_url."""
        if not self._server_url:
            return LicenseResult(valid=True, expires=None)

        result = self._check_remote(key)
        if result is None:
            return LicenseResult(
                valid=False,
                error="Cannot reach license server. Check internet connection.",
            )
        return result

    def _check_remote(self, key: str) -> LicenseResult | None:
        """POST to license server. Returns None on network error."""
        device_id = get_device_id()
        payload = json.dumps({"key": key, "device_id": device_id}).encode()

        try:
            from src import __version__
        except ImportError:
            __version__ = "1.0.0"

        import urllib.request
        from ..utils.proxied_urllib import proxied_urlopen

        req = urllib.request.Request(
            self._server_url,
            data=payload,
            headers={
                "Content-Type": "application/json",
                "User-Agent": f"{_APP_NAME}/{__version__}",
            },
            method="POST",
        )

        try:
            with proxied_urlopen(req, timeout=_REQUEST_TIMEOUT) as resp:
                data = json.loads(resp.read())

            valid = bool(data.get("valid", False))
            expires = data.get("expires")
            error = data.get("error") if not valid else None
            log.debug("License server response: valid=%s expires=%s", valid, expires)
            return LicenseResult(valid=valid, expires=expires, error=error)

        except urllib.error.HTTPError as exc:
            log.error("License server HTTP %d: %s", exc.code, exc.reason)
            try:
                body = json.loads(exc.read())
                error_msg = body.get("error", f"HTTP {exc.code}")
            except Exception:
                error_msg = f"HTTP {exc.code}: {exc.reason}"
            return LicenseResult(valid=False, error=error_msg)

        except (urllib.error.URLError, OSError, TimeoutError) as exc:
            log.warning("License server unreachable: %s", exc)
            return None  # caller handles offline case

        except Exception as exc:
            log.error("Unexpected error checking license: %s", exc)
            return None

    # ------------------------------------------------------------------ #
    # Internal: cache helpers
    # ------------------------------------------------------------------ #

    @staticmethod
    def _key_hash(key: str) -> str:
        return hashlib.sha256(key.encode()).hexdigest()[:16]

    def _save_cache(self, key: str, result: LicenseResult) -> None:
        privacy_manager.store_license_cache(
            {
                "valid": result.valid,
                "cached_at": time.time(),
                "key_hash": self._key_hash(key),
                "expires": result.expires,
            }
        )

    def _cache_fresh(self, cache: dict, key: str) -> bool:
        """Return True if cache is within TTL and matches the current key."""
        if cache.get("key_hash") != self._key_hash(key):
            return False
        age = time.time() - float(cache.get("cached_at", 0))
        return age < self._cache_ttl

    def _cache_usable_offline(self, cache: dict, key: str) -> bool:
        """Return True if cache matches key and is within 2× TTL (offline grace)."""
        if cache.get("key_hash") != self._key_hash(key):
            return False
        age = time.time() - float(cache.get("cached_at", 0))
        return age < self._cache_ttl * 2
