"""24h session-based license manager with online validation.

Flow:
    1. App starts → check Keychain for valid session
    2. If no session → show activation dialog
    3. User enters token → POST /activate → get session_key + expires_at
    4. Store session in Keychain
    5. Every hour → POST /validate → if invalid → close app
    6. 1h before expiry → show renewal notification

Architecture:
    - Session stored in macOS Keychain (service: "MuraveyChina")
      or encrypted file fallback (Windows / no-keyring)
    - All validation calls go to Cloudflare Worker
    - Grace period: 30min offline before enforcement
"""

from __future__ import annotations

import base64
import hashlib
import json
import logging
import os
import sys
import time
import random
import urllib.error
import urllib.request
from dataclasses import dataclass
from enum import Enum
from typing import Any

from ..utils.helpers import get_device_id

try:
    import keyring

    _KEYRING_AVAILABLE = True
except ImportError:
    _KEYRING_AVAILABLE = False

try:
    from cryptography.fernet import Fernet, InvalidToken as _FernetInvalidToken

    _FERNET_AVAILABLE = True
except ImportError:
    _FERNET_AVAILABLE = False

log = logging.getLogger(__name__)

# ── Configuration ─────────────────────────────────────────────

LICENSE_SERVER_URL = "https://license.muravey.app"
KEYCHAIN_SERVICE = "MuraveyChina"
KEYCHAIN_ACCOUNT = "session"
SESSION_DURATION = 86400  # 24 hours in seconds
GRACE_PERIOD = 7200  # 2-hour offline grace (matches backend GRACE_PERIOD)
RENEWAL_WARNING_SECONDS = 3600  # warn 1h before expiry
VALIDATION_INTERVAL = 3600  # validate every hour
REQUEST_TIMEOUT = 15  # HTTP timeout (15s: GFW packet-loss can delay TLS handshakes)


# ── Data types ────────────────────────────────────────────────


class LicenseStatus(Enum):
    """Current license state."""

    VALID = "valid"
    EXPIRED = "expired"
    NO_SESSION = "no_session"
    GRACE_PERIOD = "grace_period"  # offline but within grace window
    INVALID = "invalid"


@dataclass
class ActivationResult:
    """Result of token activation attempt."""

    success: bool
    session_key: str = ""
    expires_at: int = 0
    time_remaining: int = 0
    error: str = ""


@dataclass
class ValidationResult:
    """Result of session validation."""

    valid: bool
    expires_at: int = 0
    time_remaining: int = 0
    reason: str = ""


# ── Session storage ───────────────────────────────────────────


def _get_session_file_path() -> str:
    """Return platform-aware path for session fallback file."""
    if sys.platform == "win32":
        appdata = os.environ.get("APPDATA", os.path.expanduser("~"))
        return os.path.join(appdata, "MuraveyChina", ".session")
    # macOS / Linux
    return os.path.expanduser("~/Library/Application Support/MuraveyChina/.session")


SESSION_FILE_PATH = _get_session_file_path()


def _derive_file_key() -> bytes:
    """Derive a Fernet key bound to this machine's hardware ID."""
    mid = get_device_id()
    raw = hashlib.sha256(b"muraveychina-session-key-v1:" + mid.encode()).digest()
    return base64.urlsafe_b64encode(raw)


def _save_session(token: str, session_key: str, expires_at: int) -> None:
    """Store session in macOS Keychain or fallback file."""
    data = json.dumps(
        {
            "token": token,
            "session_key": session_key,
            "expires_at": expires_at,
            "saved_at": int(time.time()),
        }
    )
    if _KEYRING_AVAILABLE:
        try:
            keyring.set_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, data)
            return
        except Exception as e:
            log.warning("Keychain save failed: %s", e)
    # Fallback: Fernet-encrypted file, key derived from device ID
    path = SESSION_FILE_PATH
    os.makedirs(os.path.dirname(path), exist_ok=True)
    if _FERNET_AVAILABLE:
        try:
            encrypted = Fernet(_derive_file_key()).encrypt(data.encode())
            with open(path, "wb") as fb:
                fb.write(encrypted)
            os.chmod(path, 0o600)
            return
        except Exception as e:
            log.error("Fernet encryption failed: %s — refusing to store session in plaintext", e)
            return  # Do not store plaintext session — security risk


def _load_session() -> dict[str, Any] | None:
    """Load session from Keychain or fallback file."""
    if _KEYRING_AVAILABLE:
        try:
            data = keyring.get_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
            if data:
                result: dict[str, Any] = json.loads(data)
                return result
        except Exception as e:
            log.debug("Keychain load failed: %s", e)

    path = SESSION_FILE_PATH
    if not os.path.exists(path):
        return None
    # Try Fernet-encrypted (binary) only — no plaintext fallback
    if _FERNET_AVAILABLE:
        try:
            with open(path, "rb") as fb:
                raw = fb.read()
            decrypted = Fernet(_derive_file_key()).decrypt(raw).decode()
            loaded: dict[str, Any] = json.loads(decrypted)
            return loaded
        except (_FernetInvalidToken, Exception):
            log.warning("Session file decryption failed — ignoring unreadable session")
    return None


# Public alias — used by proxy provider to read the active session_key
load_session = _load_session


def _get_app_version() -> str:
    try:
        from .. import __version__
        return __version__
    except Exception:
        return "unknown"


def _clear_session() -> None:
    """Remove session from storage."""
    if _KEYRING_AVAILABLE:
        try:
            keyring.delete_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
        except Exception:
            pass
    path = SESSION_FILE_PATH
    try:
        os.remove(path)
    except OSError:
        pass


# ── HTTP helpers ──────────────────────────────────────────────


def _post_json(url: str, data: dict[str, Any]) -> dict[str, Any]:
    """POST JSON to URL. Retries on DNS/network errors (GFW resilience)."""
    from ..utils.proxied_urllib import proxied_urlopen

    body = json.dumps(data).encode()
    last_exc: Exception = RuntimeError("No attempts made")
    for attempt in range(3):
        try:
            req = urllib.request.Request(
                url,
                data=body,
                method="POST",
                headers={
                    "Content-Type": "application/json",
                    "User-Agent": f"MuraveyChina/{_get_app_version()}",
                },
            )
            with proxied_urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
                parsed: dict[str, Any] = json.loads(resp.read().decode())
                return parsed
        except urllib.error.HTTPError:
            raise  # HTTP errors (4xx/5xx) should not be retried
        except (urllib.error.URLError, OSError, TimeoutError) as e:
            last_exc = e
            if attempt < 2:
                # Exponential backoff + random jitter (avoid CSCA pattern detection)
                base_delay = 1.0 * (2.0 ** attempt)
                delay = base_delay * random.uniform(0.5, 1.5)
                time.sleep(delay)
    raise last_exc


# ── Main manager ──────────────────────────────────────────────


class LicenseManagerV2:
    """24h session-based license with online validation.

    Usage:
        mgr = LicenseManagerV2()
        status = mgr.get_status()
        if status == LicenseStatus.NO_SESSION:
            result = mgr.activate(token)
        elif status == LicenseStatus.VALID:
            # Start hourly validation timer
    """

    def __init__(self, server_url: str = LICENSE_SERVER_URL) -> None:
        self._server_url = server_url.rstrip("/")
        self._last_online_check: int = 0
        self._cached_session: dict[str, Any] | None = None
        self._load_cached_session()

    def _load_cached_session(self) -> None:
        """Load session from storage into memory cache."""
        self._cached_session = _load_session()

    def get_status(self) -> LicenseStatus:
        """Get current license status (fast local check)."""
        session = self._cached_session
        if not session:
            return LicenseStatus.NO_SESSION

        now = int(time.time())
        expires_at = session.get("expires_at", 0)

        if now < expires_at:
            return LicenseStatus.VALID

        # Check grace period
        if now < expires_at + GRACE_PERIOD:
            return LicenseStatus.GRACE_PERIOD

        return LicenseStatus.EXPIRED

    def get_time_remaining(self) -> int:
        """Return seconds remaining in session, or 0 if expired."""
        session = self._cached_session
        if not session:
            return 0
        expires_at: int = int(session.get("expires_at", 0))
        return max(0, expires_at - int(time.time()))

    def get_expires_at(self) -> int:
        """Return session expiry timestamp, or 0."""
        if not self._cached_session:
            return 0
        return int(self._cached_session.get("expires_at", 0))

    def is_valid(self) -> bool:
        """Quick local validity check including RSA signature verification."""
        session = self._cached_session
        if session:
            cached_token = session.get("token", "")
            if cached_token and cached_token.startswith("MC-v1-"):
                try:
                    from .token_validator import validate_token
                    from .machine_fingerprint import get_machine_id
                    sig_valid, _payload, _err = validate_token(
                        cached_token, get_machine_id()
                    )
                    if not sig_valid:
                        log.warning("Cached token RSA check failed: %s", _err)
                        return False
                except Exception as exc:
                    log.warning("RSA cached-token check error: %s — failing safe", exc)
                    return False

        status = self.get_status()
        return status in (LicenseStatus.VALID, LicenseStatus.GRACE_PERIOD)

    def activate(self, token: str) -> ActivationResult:
        """Activate a token on this machine.

        Args:
            token: The license token from email/Telegram.

        Returns:
            ActivationResult with success/error details.
        """
        token = token.strip()

        if len(token) < 16:
            return ActivationResult(success=False, error="Invalid token format")

        # Verify RSA token signature fully offline before touching the network.
        # MC-v1 tokens carry a payload+signature; legacy tokens are passed through.
        if token.startswith("MC-v1-"):
            try:
                from .token_validator import validate_token
                from .machine_fingerprint import get_machine_id
                machine_id = get_machine_id()
                sig_valid, _payload, sig_error = validate_token(token, machine_id)
                if not sig_valid:
                    return ActivationResult(
                        success=False, error=f"Invalid token signature: {sig_error}"
                    )
            except Exception as exc:
                log.warning("RSA pre-validation failed unexpectedly: %s — continuing", exc)

        try:
            machine_id = get_device_id()
        except RuntimeError as e:
            return ActivationResult(success=False, error=f"Failed to get device ID: {e}")

        try:
            resp = _post_json(
                f"{self._server_url}/activate",
                {
                    "token": token,
                    "machine_id": machine_id,
                    "app_version": _get_app_version(),
                },
            )
        except urllib.error.HTTPError as e:
            try:
                err_body = json.loads(e.read().decode())
                msg = err_body.get("error", str(e))
            except Exception:
                msg = str(e)
            if e.code == 402:
                return ActivationResult(success=False, error="Token expired. Please purchase a new one.")
            if e.code == 403:
                return ActivationResult(success=False, error=f"Error: {msg}")
            if e.code == 404:
                return ActivationResult(success=False, error="Token not found")
            return ActivationResult(success=False, error=f"Server error ({e.code}): {msg}")
        except (urllib.error.URLError, OSError, TimeoutError):
            # GFW-aware error message: suggest VPN or direct provider keys
            return ActivationResult(
                success=False,
                error=(
                    "无法连接服务器。如果您在中国大陆，请使用VPN激活，"
                    "或直接使用 DeepSeek/阿里云 API密钥 (无需激活)。\n"
                    "Cannot reach server. If in China, use VPN to activate, "
                    "or use DeepSeek/Aliyun API keys directly (no activation needed)."
                ),
            )
        except Exception as e:
            log.exception("Activation error")
            return ActivationResult(success=False, error=f"Unknown error: {e}")

        if resp.get("status") != "ok":
            return ActivationResult(success=False, error=resp.get("error", "Unknown error"))

        session_key = resp["session_key"]
        expires_at = resp["expires_at"]
        time_remaining = resp.get("time_remaining", SESSION_DURATION)

        _save_session(token, session_key, expires_at)
        self._cached_session = {
            "token": token,
            "session_key": session_key,
            "expires_at": expires_at,
            "saved_at": int(time.time()),
        }
        self._last_online_check = int(time.time())

        log.info("Session activated. Expires at %d (in %dh)", expires_at, time_remaining // 3600)
        return ActivationResult(
            success=True,
            session_key=session_key,
            expires_at=expires_at,
            time_remaining=time_remaining,
        )

    def validate_online(self) -> ValidationResult:
        """Validate session with license server.

        Call this every hour from a timer. Returns ValidationResult.
        On network error, falls back to local expiry check (grace period).
        """
        session = self._cached_session
        if not session:
            return ValidationResult(valid=False, reason="no_session")

        try:
            machine_id = get_device_id()
        except RuntimeError:
            return ValidationResult(valid=False, reason="no_machine_id")

        try:
            resp = _post_json(
                f"{self._server_url}/validate",
                {
                    "token": session["token"],
                    "session_key": session["session_key"],
                    "machine_id": machine_id,
                },
            )
            self._last_online_check = int(time.time())

            if not resp.get("valid"):
                reason = resp.get("reason", "invalid")
                if reason == "expired":
                    _clear_session()
                    self._cached_session = None
                log.warning("Online validation failed: %s", reason)
                return ValidationResult(valid=False, reason=reason)

            log.debug(
                "Online validation OK. Time remaining: %ds",
                resp.get("time_remaining", 0),
            )
            return ValidationResult(
                valid=True,
                expires_at=resp.get("expires_at", session["expires_at"]),
                time_remaining=resp.get("time_remaining", 0),
            )

        except (urllib.error.URLError, OSError, TimeoutError) as e:
            # Network error — apply grace period
            log.warning("Validation network error (grace period active): %s", e)
            now = int(time.time())
            expires_at = session.get("expires_at", 0)

            if now < expires_at:
                return ValidationResult(
                    valid=True,
                    expires_at=expires_at,
                    time_remaining=expires_at - now,
                    reason="offline_grace",
                )
            if now < expires_at + GRACE_PERIOD:
                return ValidationResult(
                    valid=True,
                    expires_at=expires_at,
                    time_remaining=0,
                    reason="grace_period",
                )
            # Grace period exhausted
            return ValidationResult(valid=False, reason="grace_exhausted")

        except Exception as e:
            log.exception("Unexpected validation error")
            return ValidationResult(valid=False, reason=f"error: {e}")

    def clear_session(self) -> None:
        """Remove current session (for testing or manual reset)."""
        _clear_session()
        self._cached_session = None

    @property
    def needs_renewal_warning(self) -> bool:
        """True if session expires within RENEWAL_WARNING_SECONDS."""
        remaining = self.get_time_remaining()
        return 0 < remaining <= RENEWAL_WARNING_SECONDS
