"""License manager for ApiPosturePro."""

import hashlib
import json
import os
import socket
from datetime import datetime, timedelta
from pathlib import Path

import httpx
import jwt

from apiposture_pro.licensing.models import (
    LicenseContext,
    LicenseErrorCode,
    LicenseFeature,
    LicenseType,
    LicenseValidationResult,
    StoredLicense,
)


class ProLicenseManager:
    """Manages Pro license activation, validation, and storage."""

    LICENSE_API_URL = "https://www.apiposture.com/api/license"
    LICENSE_FILE = Path.home() / ".apiposture" / "license.json"
    GRACE_PERIOD_DAYS = 7
    VALIDATION_INTERVAL = timedelta(hours=24)

    def __init__(self) -> None:
        """Initialize license manager."""
        self.LICENSE_FILE.parent.mkdir(parents=True, exist_ok=True)

    def get_machine_id(self) -> str:
        """Generate unique machine identifier."""
        hostname = socket.gethostname()
        username = os.getenv("USER", os.getenv("USERNAME", "unknown"))
        machine_string = f"{hostname}:{username}"
        return hashlib.sha256(machine_string.encode()).hexdigest()

    def validate(self) -> LicenseValidationResult:
        """
        Validate the current license with throttled online re-validation.

        Returns:
            LicenseValidationResult with detailed status information
        """
        stored = self._load_stored_license()

        if not stored:
            # Check environment variable
            env_key = os.getenv("APIPOSTURE_LICENSE_KEY")
            if env_key:
                try:
                    context = self._parse_jwt(env_key)
                    if context.expires_at < datetime.utcnow():
                        return LicenseValidationResult(
                            is_valid=False,
                            error_code=LicenseErrorCode.EXPIRED,
                            error_message=f"License expired on {context.expires_at.strftime('%Y-%m-%d')}",
                            context=context,
                        )
                    return LicenseValidationResult(is_valid=True, context=context)
                except Exception:
                    return LicenseValidationResult(
                        is_valid=False,
                        error_code=LicenseErrorCode.INVALID_KEY,
                        error_message="Invalid license key format",
                    )
            return LicenseValidationResult(
                is_valid=False,
                error_code=LicenseErrorCode.INVALID_KEY,
                error_message="No license key found",
            )

        if not stored.context:
            return LicenseValidationResult(
                is_valid=False,
                error_code=LicenseErrorCode.INVALID_KEY,
                error_message="License context is missing",
            )

        context = stored.context
        now = datetime.utcnow()

        # Check if machine ID matches (skip for CI licenses)
        if context.license_type != LicenseType.CI:
            current_machine = self.get_machine_id()
            if context.machine_id and context.machine_id != current_machine:
                return LicenseValidationResult(
                    is_valid=False,
                    error_code=LicenseErrorCode.MACHINE_MISMATCH,
                    error_message="License is bound to a different machine",
                    context=context,
                )

        # Check if we need online re-validation (throttled to 24h)
        time_since_validation = now - stored.last_validated_at
        if time_since_validation > self.VALIDATION_INTERVAL:
            online_result = self._online_validate(stored)
            if online_result is not None:
                return online_result
            # Online validation failed (network error) — fall through to offline check

        # Offline validation: check expiry + grace period
        if context.expires_at < now:
            grace_cutoff = stored.last_validated_at + timedelta(days=self.GRACE_PERIOD_DAYS)
            if now > grace_cutoff:
                return LicenseValidationResult(
                    is_valid=False,
                    error_code=LicenseErrorCode.GRACE_PERIOD_EXPIRED,
                    error_message="License expired and grace period has ended",
                    context=context,
                )
            # In grace period
            days_remaining = (grace_cutoff - now).days
            return LicenseValidationResult(
                is_valid=True,
                context=context,
                is_in_grace_period=True,
                grace_period_days_remaining=max(0, days_remaining),
            )

        return LicenseValidationResult(is_valid=True, context=context)

    def is_valid(self) -> bool:
        """Check if current license is valid."""
        return self.validate().is_valid

    def get_context(self) -> LicenseContext | None:
        """Get current license context."""
        result = self.validate()
        return result.context if result.is_valid else None

    def has_feature(self, feature: str | LicenseFeature) -> bool:
        """
        Check if the license includes a specific feature.

        Args:
            feature: Feature name or LicenseFeature enum

        Returns:
            True if the feature is available
        """
        context = self.get_context()
        if not context:
            return False

        feature_str = feature.value if isinstance(feature, LicenseFeature) else feature
        return "all" in context.features or feature_str in context.features

    def activate(self, license_key: str) -> LicenseContext:
        """
        Activate a license key.

        Sends the raw key to the activation server, which returns a JWT token.
        The token is then parsed to extract the license context.

        Args:
            license_key: License key to activate (e.g. "2405-5B13-510E-8D9D")

        Returns:
            LicenseContext with license information

        Raises:
            ValueError: If activation fails (invalid key, network error, etc.)
        """
        machine_id = self.get_machine_id()
        machine_name = socket.gethostname()

        token: str | None = None

        # Call activation API — server validates the key and returns a JWT token
        try:
            with httpx.Client(timeout=10.0) as client:
                response = client.post(
                    f"{self.LICENSE_API_URL}/activate",
                    json={
                        "licenseKey": license_key,
                        "machineId": machine_id,
                        "machineName": machine_name,
                    },
                )
                if response.is_success:
                    data = response.json()
                    token = data.get("token")
                    if not token:
                        raise ValueError("Invalid response from license server: missing token")
                else:
                    try:
                        error_data = response.json()
                        error_msg = error_data.get("error") or error_data.get("message", "")
                        error_code = error_data.get("code", "")
                    except Exception:
                        error_msg = response.text
                        error_code = ""
                    detail = f" ({error_code})" if error_code else ""
                    raise ValueError(f"Activation failed: {error_msg}{detail}")
        except httpx.HTTPError as exc:
            raise ValueError(f"Network error during activation: {exc}") from exc

        # Parse the JWT token returned by the server
        context = self._parse_jwt(token)

        # CI licenses skip machine binding
        if context.license_type != LicenseType.CI:
            context.machine_id = machine_id

        # Store license
        stored = StoredLicense(
            license_key=license_key,
            activated_at=datetime.utcnow(),
            last_validated_at=datetime.utcnow(),
            machine_id=machine_id,
            token=token,
            context=context,
        )
        self._save_stored_license(stored)

        return context

    def deactivate(self) -> None:
        """Deactivate current license."""
        if self.LICENSE_FILE.exists():
            self.LICENSE_FILE.unlink()

    def get_stored_license(self) -> StoredLicense | None:
        """Get the stored license data (public accessor)."""
        return self._load_stored_license()

    def _online_validate(self, stored: StoredLicense) -> LicenseValidationResult | None:
        """
        Perform online re-validation against the license server.

        Returns:
            LicenseValidationResult on success/rejection, None on network error (fallback to offline).
        """
        try:
            with httpx.Client(timeout=10.0) as client:
                response = client.post(
                    f"{self.LICENSE_API_URL}/validate",
                    json={
                        "token": stored.token,
                        "licenseKey": stored.license_key,
                        "machineId": stored.machine_id,
                    },
                )

                if response.status_code == 200:
                    data = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
                    # Accept a refreshed token from the server if provided
                    new_token = data.get("token")
                    if new_token:
                        stored.token = new_token
                        try:
                            stored.context = self._parse_jwt(new_token)
                        except Exception:
                            pass

                    # Update last_validated_at and persist
                    stored.last_validated_at = datetime.utcnow()
                    self._save_stored_license(stored)
                    return LicenseValidationResult(is_valid=True, context=stored.context)

                # Server rejected the license
                data = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
                error_code_str = data.get("error_code", "")
                error_message = data.get("error_message", f"Server rejected license (HTTP {response.status_code})")

                error_code_map = {
                    "expired": LicenseErrorCode.EXPIRED,
                    "revoked": LicenseErrorCode.REVOKED,
                    "machine_mismatch": LicenseErrorCode.MACHINE_MISMATCH,
                    "too_many_activations": LicenseErrorCode.TOO_MANY_ACTIVATIONS,
                    "invalid_signature": LicenseErrorCode.INVALID_SIGNATURE,
                }
                error_code = error_code_map.get(error_code_str, LicenseErrorCode.INVALID_KEY)

                return LicenseValidationResult(
                    is_valid=False,
                    error_code=error_code,
                    error_message=error_message,
                    context=stored.context,
                )

        except (httpx.HTTPError, Exception):
            # Network error — return None to fall through to offline validation
            return None

    def _parse_jwt(self, license_key: str) -> LicenseContext:
        """Parse JWT token to extract license information."""
        try:
            # Decode without verification for development
            # In production, use proper JWT verification with public key
            payload = jwt.decode(license_key, options={"verify_signature": False})

            # Parse license type
            type_str = payload.get("type", "developer")
            try:
                license_type = LicenseType(type_str)
            except ValueError:
                license_type = LicenseType.DEVELOPER

            # Normalise feature names: the server uses hyphens (e.g. "risk-scoring")
            # while the Python enum uses underscores ("risk_scoring").
            raw_features: list[str] = payload.get("features", [])
            features = [f.replace("-", "_") for f in raw_features]

            # The server JWT uses "org" rather than "organization".
            organization = payload.get("org") or payload.get("organization")

            # Parse seats — server sends as a string
            seats_raw = payload.get("seats")
            seats: int | None = None
            if seats_raw is not None:
                try:
                    seats = int(seats_raw)
                except (ValueError, TypeError):
                    pass

            return LicenseContext(
                license_key=license_key,
                tier=payload.get("tier", "pro"),
                features=features,
                expires_at=datetime.fromtimestamp(payload.get("exp", 0)),
                customer_name=payload.get("customer_name", ""),
                customer_email=payload.get("customer_email", ""),
                organization=organization,
                seats=seats,
                license_type=license_type,
            )
        except Exception as e:
            raise ValueError(f"Invalid license key format: {e}") from e

    def _load_stored_license(self) -> StoredLicense | None:
        """Load stored license from disk."""
        if not self.LICENSE_FILE.exists():
            return None

        try:
            with open(self.LICENSE_FILE) as f:
                data = json.load(f)

            context = None
            if data.get("context"):
                ctx_data = data["context"]

                # Parse license type with fallback
                try:
                    license_type = LicenseType(ctx_data.get("license_type", "developer"))
                except ValueError:
                    license_type = LicenseType.DEVELOPER

                context = LicenseContext(
                    license_key=ctx_data["license_key"],
                    tier=ctx_data["tier"],
                    features=ctx_data["features"],
                    expires_at=datetime.fromisoformat(ctx_data["expires_at"]),
                    customer_name=ctx_data["customer_name"],
                    customer_email=ctx_data["customer_email"],
                    machine_id=ctx_data.get("machine_id"),
                    organization=ctx_data.get("organization"),
                    seats=ctx_data.get("seats"),
                    license_type=license_type,
                )

            stored = StoredLicense(
                license_key=data["license_key"],
                activated_at=datetime.fromisoformat(data["activated_at"]),
                last_validated_at=datetime.fromisoformat(data["last_validated_at"]),
                machine_id=data["machine_id"],
                token=data.get("token"),
                context=context,
            )

            # If context is missing but we have a token, re-derive context from the token.
            # This handles licenses stored by the new flow where context is embedded in the
            # JWT, as well as forward compatibility.
            if stored.context is None and stored.token:
                try:
                    stored.context = self._parse_jwt(stored.token)
                except Exception:
                    pass

            return stored
        except Exception:
            return None

    def _save_stored_license(self, stored: StoredLicense) -> None:
        """Save license to disk."""
        data: dict = {
            "license_key": stored.license_key,
            "activated_at": stored.activated_at.isoformat(),
            "last_validated_at": stored.last_validated_at.isoformat(),
            "machine_id": stored.machine_id,
            "token": stored.token,
            "context": None,
        }

        if stored.context:
            data["context"] = {
                "license_key": stored.context.license_key,
                "tier": stored.context.tier,
                "features": stored.context.features,
                "expires_at": stored.context.expires_at.isoformat(),
                "customer_name": stored.context.customer_name,
                "customer_email": stored.context.customer_email,
                "machine_id": stored.context.machine_id,
                "organization": stored.context.organization,
                "seats": stored.context.seats,
                "license_type": stored.context.license_type.value,
            }

        with open(self.LICENSE_FILE, "w") as f:
            json.dump(data, f, indent=2)
