"""
License Validation - Validate Rivet licenses against license server.

Features:
- Online validation against license server
- 7-day offline tolerance with cached validation
- Graceful degradation with user-friendly warnings
- Three validation modes: strict, lenient, development
"""

import os
import sys
import json
import base64
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional, Dict, Any
import requests

from app.services.device_fingerprint import DeviceFingerprintGenerator
from app.config.pricing import get_features_for_tier

# Ed25519 public key for offline license signature verification
try:
    from app.config.config import OFFLINE_PUBLIC_KEY as _OFFLINE_PUBLIC_KEY, LICENSE_SERVER_URL
except ImportError:
    _OFFLINE_PUBLIC_KEY = ""
    LICENSE_SERVER_URL = os.getenv("LICENSE_SERVER_URL", "https://license.cerebion.com")

# Import build configuration
try:
    from app.config.build_config import BUILD_TYPE
except ImportError:
    BUILD_TYPE = "development"

logger = logging.getLogger(__name__)

# Configuration from environment — fall back to baked-in build constant
try:
    from app.config.build_config import LICENSE_API_KEY as _BAKED_API_KEY
except ImportError:
    _BAKED_API_KEY = None

LICENSE_API_KEY = os.getenv("LICENSE_API_KEY") or _BAKED_API_KEY
LICENSE_KEY = os.getenv("RIVET_LICENSE_KEY")
PRODUCT_SLUG = "rivet"

# Note: OFFLINE_LICENSE_FILE is read dynamically via _get_offline_license_file()
# to support runtime updates when uploaded via API

# Cache configuration
CACHE_DIR = Path.home() / ".rivet"
CACHE_FILE = CACHE_DIR / "license_cache.json"
CACHE_DURATION_DAYS = 7  # Cache valid for 7 days
MAX_OFFLINE_DAYS = 7  # Maximum 7 days offline before requiring validation

# Validation mode: "strict" or "lenient"
# - strict: Requires online validation every 24 hours
# - lenient: Allows 7 days offline operation (RECOMMENDED)
VALIDATION_MODE = os.getenv("LICENSE_VALIDATION_MODE", "lenient")


def _restore_license_key_from_cache() -> None:
    """
    Restore LICENSE_KEY from cache if the env var is absent.

    In frozen (PyInstaller) builds the .env write silently fails, so
    RIVET_LICENSE_KEY is not set on the next launch.  We save the key
    alongside the validation cache and reload it here so the user does
    not have to re-enter their license key after every restart.
    """
    global LICENSE_KEY
    if LICENSE_KEY:
        return
    try:
        if not CACHE_FILE.exists():
            return
        with open(CACHE_FILE, "r", encoding="utf-8") as f:
            cache_data = json.load(f)
        saved_key = cache_data.get("license_key", "")
        if saved_key:
            LICENSE_KEY = saved_key
            os.environ["RIVET_LICENSE_KEY"] = saved_key
            logger.debug("Restored license key from cache (frozen build restart)")
    except Exception:
        pass


_restore_license_key_from_cache()


class LicenseValidationError(Exception):
    """Raised when license validation fails"""
    pass


class LicenseValidator:
    """Validates Rivet licenses against the license server"""

    @staticmethod
    def _get_offline_license_file() -> Optional[str]:
        """
        Get offline license file path dynamically.
        
        Reads from environment variable each time to support
        runtime updates when uploaded via API.
        
        Returns:
            Path to offline license file or None
        """
        return os.getenv("OFFLINE_LICENSE_FILE")

    @staticmethod
    def validate(force_online: bool = False) -> Dict[str, Any]:
        """
        Validate license against server with offline tolerance.

        Behavior by mode:
        - strict: Must validate online, cache only 24 hours
        - lenient: Try online, fall back to cache up to 7 days
        - development: Always allow (logs warning)

        Args:
            force_online: If True, skip cache and force online validation

        Returns:
            Validation response dict with keys:
            - valid (bool): Whether license is valid
            - license_type (str): Type of license (trial/professional/enterprise)
            - features (dict): Enabled features
            - expires_at (str): License expiration date
            - seats_remaining (int): Available seats
            - message (str): Status message
            - offline_mode (bool): True if using cached/offline validation
            - days_since_last_check (int): Days since last successful online check

        Raises:
            LicenseValidationError: If validation fails in strict mode
        """
        # Development mode removed — use offline license files for testing
        # Check if license key is configured
        if not LICENSE_KEY:
            # Check for offline license file before falling back to no-license
            offline_file = LicenseValidator._get_offline_license_file()
            if offline_file and Path(offline_file).exists():
                try:
                    logger.info(f"No LICENSE_KEY configured, checking offline license file: {offline_file}")
                    result = LicenseValidator._validate_offline_file(offline_file)
                    result["offline_mode"] = True
                    result["days_since_last_check"] = 0
                    logger.info("[OK] Offline license file validation successful")
                    return result
                except Exception as offline_error:
                    logger.warning(f"Offline license file validation failed: {offline_error}")

            # No license configured and no valid offline file
            if VALIDATION_MODE == "lenient":
                logger.warning("No license configured")
                return {
                    "valid": False,
                    "license_type": "none",
                    "features": {},
                    "expires_at": None,
                    "seats_remaining": 0,
                    "message": "No license found. Please activate your license in Settings.",
                    "offline_mode": True,
                    "days_since_last_check": 0,
                    "is_trial_active": False,
                    "trial_days_remaining": 0,
                }
            raise LicenseValidationError(
                "No license configured. Set OFFLINE_LICENSE_FILE or RIVET_LICENSE_KEY."
            )

        # Try online validation first (unless using recent cache)
        if not force_online:
            cached = LicenseValidator._get_cached_validation(strict=False)
            if cached and cached.get("days_since_last_check", 0) < 2:
                # Use cache if less than 2 days old (fast path)
                logger.info("[OK] Using cached license validation (%s days old)",
                           cached.get("days_since_last_check", 0))
                return cached

        # Attempt online validation
        try:
            logger.info("Attempting online license validation...")
            result = LicenseValidator._validate_online()
            result["offline_mode"] = False
            result["days_since_last_check"] = 0
            LicenseValidator._cache_validation(result)
            logger.info("[OK] Online validation successful")
            return result

        except Exception as e:
            logger.warning(f"Online validation failed: {e}")

            # Try offline license file before falling back to cache
            # Air-gap: if offline file is configured but fails, do NOT fall back to cache
            offline_file = LicenseValidator._get_offline_license_file()
            if offline_file and Path(offline_file).exists():
                try:
                    logger.info("Attempting offline license file validation...")
                    result = LicenseValidator._validate_offline_file(offline_file)
                    result["offline_mode"] = True
                    result["days_since_last_check"] = 0
                    logger.info("[OK] Offline license file validation successful")
                    return result
                except LicenseValidationError as offline_error:
                    # Offline file present but invalid — hard fail, no cache fallback
                    logger.error(f"Offline license file validation failed (no cache fallback): {offline_error}")
                    raise LicenseValidationError(str(offline_error))

            # Strict mode: fail immediately if cache is too old
            if VALIDATION_MODE == "strict":
                cached = LicenseValidator._get_cached_validation(strict=True)
                if cached and cached.get("days_since_last_check", 0) < 1:
                    logger.warning("WARNING: Using 24-hour cache in strict mode")
                    return cached
                raise LicenseValidationError(f"License validation failed (strict mode): {e}")

            # Lenient mode: try to use cached validation
            cached = LicenseValidator._get_cached_validation(strict=False)
            if cached:
                days_offline = cached.get("days_since_last_check", 0)

                if days_offline <= MAX_OFFLINE_DAYS:
                    # Determine warning level based on days offline
                    if days_offline >= 6:
                        logger.warning(
                            f"WARNING: OFFLINE MODE: License check required soon ({days_offline}/{MAX_OFFLINE_DAYS} days offline)"
                        )
                    elif days_offline >= 3:
                        logger.info(
                            f"Using cached validation ({days_offline} days old, please connect to internet soon)"
                        )

                    cached["offline_mode"] = True
                    return cached
                else:
                    logger.error(
                        f"License validation required: {days_offline} days since last check (max {MAX_OFFLINE_DAYS} days)"
                    )
                    raise LicenseValidationError(
                        f"License validation required. Last successful check was {days_offline} days ago. "
                        f"Please connect to internet."
                    )

            # No cache available
            raise LicenseValidationError(f"License validation failed and no cache available: {e}")

    @staticmethod
    def _validate_online() -> Dict[str, Any]:
        """Perform online license validation against server"""
        if not LICENSE_API_KEY:
            raise LicenseValidationError(
                "No API key configured. Set LICENSE_API_KEY environment variable."
            )

        fingerprint = DeviceFingerprintGenerator.generate()
        metadata = DeviceFingerprintGenerator.generate_metadata()

        response = requests.post(
            f"{LICENSE_SERVER_URL}/api/validate",
            headers={
                "X-API-Key": LICENSE_API_KEY,
                "X-Product-ID": PRODUCT_SLUG
            },
            json={
                "license_key": LICENSE_KEY,
                "product_slug": PRODUCT_SLUG,
                "device_fingerprint": fingerprint,
                "device_name": metadata.get("hostname", "unknown"),
                "device_metadata": metadata
            },
            timeout=10
        )

        if response.status_code != 200:
            error_detail = response.json().get("detail", "Unknown error")
            raise LicenseValidationError(f"Validation failed: {error_detail}")

        result = response.json()

        # Kill switch: if server explicitly revokes this license, write a local
        # lock file so revocation survives the 7-day offline cache window.
        if result.get("revoked"):
            LicenseValidator._write_kill_switch()

        if not result.get("valid"):
            raise LicenseValidationError(result.get("message", "License is invalid"))

        # Fetch and cache the public key so key rotation works without app updates
        LicenseValidator._refresh_public_key_cache()

        return result

    @staticmethod
    def _refresh_public_key_cache() -> None:
        """Fetch the current public key from the license server and cache it locally.
        Called after every successful online validation so key rotation takes effect
        without requiring an app update."""
        try:
            response = requests.get(
                f"{LICENSE_SERVER_URL}/api/config",
                timeout=5
            )
            if response.status_code == 200:
                public_key = response.json().get("offline_public_key", "")
                if public_key:
                    CACHE_DIR.mkdir(parents=True, exist_ok=True)
                    key_cache_file = CACHE_DIR / "public_key.json"
                    import json as _json
                    key_cache_file.write_text(
                        _json.dumps({"offline_public_key": public_key}),
                        encoding="utf-8"
                    )
                    logger.debug("Cached public key from license server")
        except Exception as e:
            logger.debug(f"Could not refresh public key cache: {e}")

    @staticmethod
    def _get_cached_public_key() -> str:
        """Return the public key from local cache, falling back to the compiled-in default."""
        try:
            key_cache_file = CACHE_DIR / "public_key.json"
            if key_cache_file.exists():
                import json as _json
                data = _json.loads(key_cache_file.read_text(encoding="utf-8"))
                cached = data.get("offline_public_key", "")
                if cached:
                    return cached
        except Exception:
            pass
        return _OFFLINE_PUBLIC_KEY

    @staticmethod
    def _cache_validation(result: Dict[str, Any]) -> None:
        """Save validation result to cache"""
        try:
            CACHE_DIR.mkdir(parents=True, exist_ok=True)

            cache_data = {
                "validation": result,
                "cached_at": datetime.now(timezone.utc).isoformat(),
                "last_online_check": datetime.now(timezone.utc).isoformat(),
                # Persist key so frozen builds can restore it on restart
                # (frozen builds cannot write .env, so the env var is gone after exit)
                "license_key": LICENSE_KEY or "",
            }

            with open(CACHE_FILE, "w") as f:
                json.dump(cache_data, f, indent=2)

            # Restrict file permissions on non-Windows (user-only read/write)
            if sys.platform != "win32":
                try:
                    CACHE_FILE.chmod(0o600)
                except Exception:
                    pass

            logger.debug(f"Cached validation result to {CACHE_FILE}")
        except Exception as e:
            logger.warning(f"Failed to cache validation: {e}")

    @staticmethod
    def _write_kill_switch() -> None:
        """
        Write a local kill switch file when the server explicitly revokes a license.

        The file persists across cache deletions. Even if a user deletes
        license_cache.json, the lock file keeps access blocked until a new
        valid license is successfully activated (which clears it).
        """
        try:
            CACHE_DIR.mkdir(parents=True, exist_ok=True)
            kill_switch_file = CACHE_DIR / "license_revoked.lock"
            kill_switch_file.write_text(
                datetime.now(timezone.utc).isoformat(), encoding="utf-8"
            )
            logger.warning("Kill switch written — license has been revoked by the server")
        except Exception as e:
            logger.error(f"Failed to write kill switch file: {e}")

    @staticmethod
    def _get_cached_validation(strict: bool = False) -> Optional[Dict[str, Any]]:
        """
        Get cached validation result with client-side expiration checking.

        SECURITY: Even when offline, we check:
        1. License expiration date (prevents using expired licenses offline)
        2. Cache age (max 7 days offline)
        3. License status from last validation (if was revoked, stays revoked)

        This prevents users from:
        - Using expired licenses while offline
        - Keeping access after subscription cancellation
        - Bypassing revocation by going offline

        Args:
            strict: If True, enforce 24-hour cache expiration (strict mode)
                   If False, return cache regardless of age up to MAX_OFFLINE_DAYS

        Returns:
            Cached validation result or None if not available/expired
        """
        try:
            if not CACHE_FILE.exists():
                return None

            with open(CACHE_FILE, "r") as f:
                cache_data = json.load(f)

            last_check = datetime.fromisoformat(cache_data["last_online_check"])
            if last_check.tzinfo is None:
                last_check = last_check.replace(tzinfo=timezone.utc)
            now = datetime.now(timezone.utc)
            days_since_check = (now - last_check).total_seconds() / 86400

            # Strict mode: enforce 24-hour cache duration
            if strict and days_since_check > 1:
                logger.debug(f"Cached validation expired in strict mode ({days_since_check:.1f} days old)")
                return None

            # Lenient mode: enforce MAX_OFFLINE_DAYS
            if not strict and days_since_check > MAX_OFFLINE_DAYS:
                logger.debug(f"Cached validation expired ({days_since_check:.1f} days old, max {MAX_OFFLINE_DAYS})")
                return None

            result = cache_data["validation"]
            result["days_since_last_check"] = int(days_since_check)
            result["cached"] = True

            # SECURITY CHECK 1: Verify license wasn't revoked in last online check
            # If license was marked invalid/revoked when cached, it stays invalid
            if not result.get("valid", False):
                logger.warning("Cached license shows as invalid/revoked - blocking access")
                raise LicenseValidationError(
                    result.get("message", "License is invalid (from last online check)")
                )

            # SECURITY CHECK 2: Client-side expiration check
            # Even with offline cache, respect license expiration date
            expires_at = result.get("expires_at")
            if expires_at:
                try:
                    expiry_date = datetime.fromisoformat(expires_at.replace('Z', '+00:00'))

                    # Check if license has expired (including grace period)
                    grace_period_until = result.get("grace_period_until")
                    if grace_period_until:
                        grace_date = datetime.fromisoformat(grace_period_until.replace('Z', '+00:00'))
                        if now > grace_date:
                            logger.error(f"License expired {(now - grace_date).days} days ago (beyond grace period)")
                            raise LicenseValidationError(
                                f"License expired. Please renew your subscription or connect to internet."
                            )
                        elif now > expiry_date:
                            logger.warning(f"License expired but within grace period (until {grace_period_until})")
                    else:
                        # No grace period - hard expiration
                        if now > expiry_date:
                            logger.error(f"License expired {(now - expiry_date).days} days ago")
                            raise LicenseValidationError(
                                f"License expired on {expires_at}. Please renew your subscription."
                            )

                except (ValueError, AttributeError) as e:
                    logger.warning(f"Failed to parse expiration date: {e}")

            # SECURITY CHECK 3: Check for "kill switch" file
            # If server sets this, license is immediately revoked even offline
            kill_switch_file = CACHE_DIR / "license_revoked.lock"
            if kill_switch_file.exists():
                logger.error("License has been revoked (kill switch active)")
                raise LicenseValidationError(
                    "License has been revoked. Please contact support."
                )

            return result

        except LicenseValidationError:
            # Re-raise license validation errors
            raise
        except Exception as e:
            logger.warning(f"Failed to read cache: {e}")
            return None

    @staticmethod
    def check_feature(feature_name: str) -> bool:
        try:
            validation = LicenseValidator.validate()
            if not validation.get("valid", False):
                return False
            features = validation.get("features", {})
            return features.get(feature_name, False)
        except LicenseValidationError:
            return False

    @staticmethod
    def get_status() -> Dict[str, Any]:
        """
        Get current license status for display in UI.

        Returns:
            Status dict with user-friendly information:
            - valid (bool): Whether license is valid
            - license_type (str): Type of license
            - expires_at (str): Expiration date
            - offline_mode (bool): True if using cached validation
            - days_offline (int): Days since last online check
            - enabled_features (list): List of enabled feature names
            - message (str): Status message
            - warning (str, optional): Warning message if applicable
        """
        try:
            validation = LicenseValidator.validate()

            features = validation.get("features", {})
            status = {
                "valid": validation["valid"],
                "license_type": validation["license_type"],
                "expires_at": validation.get("expires_at"),
                "offline_mode": validation.get("offline_mode", False),
                "days_offline": validation.get("days_since_last_check", 0),
                "features": features,
                "enabled_features": [k for k, v in features.items() if v],
                "seats_remaining": validation.get("seats_remaining", 0),
                "is_trial_active": validation.get("is_trial_active", False),
                "trial_days_remaining": validation.get("trial_days_remaining", 0),
                "message": validation.get("message", "License valid"),
            }

            # Add warning based on offline duration
            days = status["days_offline"]
            if days >= 6:
                status["warning"] = (
                    f"License check required soon. Last verified {days} days ago. "
                    f"Please connect to internet."
                )
            elif days >= 3:
                status["warning"] = (
                    f"Please connect to internet for license verification. "
                    f"Last check was {days} days ago."
                )

            return status

        except LicenseValidationError as e:
            return {
                "valid": False,
                "error": str(e),
                "message": "License validation failed",
                "offline_mode": False,
                "days_offline": 0
            }

    @staticmethod
    def get_status_fresh() -> Dict[str, Any]:
        """
        Fresh status check — bypasses ALL cache.
        Used after detach/deactivate to get true current state.
        Does NOT fall back to cache on failure.
        """
        # No key and no offline file → definitely no license
        if not LICENSE_KEY:
            offline_file = LicenseValidator._get_offline_license_file()
            if offline_file and Path(offline_file).exists():
                try:
                    result = LicenseValidator._validate_offline_file(offline_file)
                    features = result.get("features", {})
                    return {
                        "valid": True,
                        "license_type": result["license_type"],
                        "expires_at": result.get("expires_at"),
                        "offline_mode": True,
                        "days_offline": 0,
                        "features": features,
                        "enabled_features": [k for k, v in features.items() if v],
                        "seats_remaining": result.get("seats_remaining", 0),
                        "is_trial_active": False,
                        "trial_days_remaining": 0,
                        "message": "Offline license validated successfully",
                    }
                except LicenseValidationError as e:
                    return {
                        "valid": False, "license_type": "none", "features": {},
                        "expires_at": None, "offline_mode": True, "days_offline": 0,
                        "seats_remaining": 0, "is_trial_active": False,
                        "trial_days_remaining": 0, "message": str(e),
                    }
            return {
                "valid": False, "license_type": "none", "features": {},
                "expires_at": None, "offline_mode": True, "days_offline": 0,
                "seats_remaining": 0, "is_trial_active": False,
                "trial_days_remaining": 0,
                "message": "No license found. Please activate your license in Settings.",
            }

        # Key is set — attempt online validation, no cache fallback
        try:
            result = LicenseValidator._validate_online()
            result["offline_mode"] = False
            result["days_since_last_check"] = 0
            LicenseValidator._cache_validation(result)
        except Exception as e:
            logger.warning(f"get_status_fresh online validation failed: {e}")
            return {
                "valid": False, "license_type": "none", "features": {},
                "expires_at": None, "offline_mode": False, "days_offline": 0,
                "seats_remaining": 0, "is_trial_active": False,
                "trial_days_remaining": 0,
                "message": f"License validation failed: {e}",
            }

        features = result.get("features", {})
        return {
            "valid": result["valid"],
            "license_type": result["license_type"],
            "expires_at": result.get("expires_at"),
            "offline_mode": False,
            "days_offline": 0,
            "features": features,
            "enabled_features": [k for k, v in features.items() if v],
            "seats_remaining": result.get("seats_remaining", 0),
            "is_trial_active": result.get("is_trial_active", False),
            "trial_days_remaining": result.get("trial_days_remaining", 0),
            "message": result.get("message", "License valid"),
        }

    @staticmethod
    def force_refresh() -> Dict[str, Any]:
        """
        Force license validation (bypass cache).
        
        Behavior:
        - If offline license file configured: validate from file
        - Otherwise: attempt online validation
        - Falls back to cached if online fails

        Useful for:
        - Manual refresh button in UI
        - After license key change
        - Troubleshooting

        Returns:
            Fresh validation result
        """
        logger.info("Forcing license validation refresh...")
        
        # If offline license file is configured, use it (air-gapped environment)
        offline_file = LicenseValidator._get_offline_license_file()
        if offline_file and Path(offline_file).exists():
            try:
                logger.info("Using offline license file for refresh...")
                result = LicenseValidator._validate_offline_file(offline_file)
                result["offline_mode"] = True
                result["days_since_last_check"] = 0
                # Do NOT cache offline file results — each access must re-validate the file
                logger.info("[OK] Offline license file validated successfully")
                return result
            except LicenseValidationError as e:
                logger.error(f"Offline license file validation failed: {e}")
                raise  # Hard fail — no cache fallback for air-gap
        
        # Try online validation
        return LicenseValidator.validate(force_online=True)

    @staticmethod
    def _verify_signature(license_data: dict, signature_b64: str, public_key_b64: str, algorithm: str = "Ed25519") -> bool:
        """Verify license signature. Algorithm is abstracted in license_signer."""
        from app.services.license_signer import verify
        return verify(license_data, signature_b64, public_key_b64, algorithm)

    @staticmethod
    def _validate_offline_file(license_file_path: str) -> Dict[str, Any]:
        """
        Validate an offline license file.

        Supports two formats:
        1. Signed format (new): { "license_data": {...}, "signature": "...", "public_key": "...", "algorithm": "Ed25519" }
        2. Flat format (legacy): { "license_key": "...", "expires_at": "...", ... }

        For signed files, verifies the Ed25519 signature before reading any data.

        Args:
            license_file_path: Path to license-{uuid}.json file

        Returns:
            dict: Validation result with license details

        Raises:
            LicenseValidationError: If validation fails
        """
        try:
            with open(license_file_path, 'r') as f:
                file_contents = json.load(f)

            # Signature required — no legacy unsigned format
            if "license_data" not in file_contents or "signature" not in file_contents:
                raise LicenseValidationError(
                    "Invalid license file format. "
                    "Please obtain a signed license file from Cerebion support."
                )

            signature_b64 = file_contents["signature"]
            public_key_b64 = file_contents.get("public_key", "") or LicenseValidator._get_cached_public_key()

            if not public_key_b64:
                raise LicenseValidationError(
                    "No public key available for signature verification. "
                    "Set OFFLINE_PUBLIC_KEY in rivet_server environment."
                )

            license_data = file_contents["license_data"]

            algorithm = file_contents.get("algorithm", "Ed25519")

            if not LicenseValidator._verify_signature(license_data, signature_b64, public_key_b64, algorithm):
                raise LicenseValidationError(
                    "License file signature is invalid. "
                    "The file may have been tampered with."
                )

            logger.info("Offline license signature verified")

            # Hardware fingerprint required — all offline licenses must be device-locked
            locked_fingerprint = license_data.get("device_fingerprint")
            if not locked_fingerprint:
                raise LicenseValidationError(
                    "This license file is not hardware-locked. "
                    "Please contact support to generate a device-locked offline license."
                )

            fingerprint_candidates = DeviceFingerprintGenerator.generate_candidates()
            if locked_fingerprint not in fingerprint_candidates:
                raise LicenseValidationError(
                    "This license file is locked to a different device. "
                    "Contact support to request a new offline license for this machine."
                )

            # Check product slug
            product_slug = license_data.get("product", {}).get("slug")
            if product_slug != PRODUCT_SLUG:
                raise LicenseValidationError(f"License not for this product (expected {PRODUCT_SLUG})")

            # Check status
            status = license_data.get("status")
            if status != "active":
                raise LicenseValidationError(f"License status is '{status}' (expected 'active')")

            # Check expiration
            now = datetime.now(timezone.utc)
            expires_at_str = license_data.get("expires_at")
            grace_period_until = None

            if expires_at_str:
                try:
                    expires_at = datetime.fromisoformat(expires_at_str.replace('Z', '+00:00'))
                    if expires_at.tzinfo is None:
                        expires_at = expires_at.replace(tzinfo=timezone.utc)

                    if now > expires_at:
                        grace_days = license_data.get("grace_period_days", 0)
                        if grace_days > 0:
                            grace_end = expires_at + timedelta(days=grace_days)
                            grace_period_until = grace_end.isoformat()
                            if now > grace_end:
                                raise LicenseValidationError(f"License expired on {expires_at.date()}")
                        else:
                            raise LicenseValidationError(f"License expired on {expires_at.date()}")
                except (ValueError, AttributeError) as e:
                    logger.warning(f"Failed to parse expiration date: {e}")

            return {
                "valid": True,
                "product_id": license_data.get("product", {}).get("id"),
                "product_name": license_data.get("product", {}).get("name"),
                "license_type": license_data.get("license_type"),
                "features": license_data.get("features", {}),
                "expires_at": expires_at_str,
                "grace_period_until": grace_period_until,
                "seats_remaining": license_data.get("max_seats", 0),
                "message": "Offline license validated successfully",
                "activation_id": None,
                "offline_file": True,
                "is_trial_active": False,
                "trial_days_remaining": 0,
            }

        except FileNotFoundError:
            raise LicenseValidationError("License file not found")
        except json.JSONDecodeError:
            raise LicenseValidationError("Invalid license file format")
        except LicenseValidationError:
            raise
        except Exception as e:
            raise LicenseValidationError(f"License validation error: {str(e)}")

    @staticmethod
    def set_license_key(license_key: str) -> Dict[str, Any]:
        """
        Set a new license key and validate it.
        On network error, accepts the key provisionally and re-validates next time.
        """
        global LICENSE_KEY
        old_key = LICENSE_KEY

        LICENSE_KEY = license_key
        os.environ["RIVET_LICENSE_KEY"] = license_key

        try:
            result = LicenseValidator.validate(force_online=True)
            LicenseValidator._update_env_file("RIVET_LICENSE_KEY", license_key)
            # Clear any kill switch from a previously revoked license on this machine
            kill_switch_file = CACHE_DIR / "license_revoked.lock"
            kill_switch_file.unlink(missing_ok=True)
            logger.info("License key updated and validated successfully")
            return result

        except Exception as e:
            # Restore old key — no access granted for unverified keys, including
            # network failures. The user must have connectivity to activate.
            LICENSE_KEY = old_key
            if old_key:
                os.environ["RIVET_LICENSE_KEY"] = old_key
            else:
                os.environ.pop("RIVET_LICENSE_KEY", None)
            raise
            
    @staticmethod
    def set_offline_license_file(file_path: str) -> Dict[str, Any]:
        """
        Set and validate an offline license file.
        
        This method:
        1. Validates the offline license file
        2. Updates the configuration if valid
        3. Returns validation result
        
        Args:
            file_path: Path to offline license JSON file
            
        Returns:
            dict: Validation result
            
        Raises:
            LicenseValidationError: If file is invalid
        """
        # Validate the file first
        result = LicenseValidator._validate_offline_file(file_path)
        
        # Update environment variable and .env file
        os.environ["OFFLINE_LICENSE_FILE"] = file_path
        LicenseValidator._update_env_file("OFFLINE_LICENSE_FILE", file_path)
        
        # Cache the validation
        LicenseValidator._cache_validation(result)
        
        logger.info(f"Offline license file set: {file_path}")
        return result

    @staticmethod
    def start_license_polling():
        """
        Start background license polling for online licenses.
        
        Only polls if:
        1. Online license key is configured (not offline license file)
        2. Not in air-gapped environment
        """
        import threading
        import time
        
        def poll_license():
            while True:
                try:
                    # Only poll if using online license (not offline file)
                    offline_file = LicenseValidator._get_offline_license_file()
                    if offline_file and Path(offline_file).exists():
                        logger.debug("Offline license detected - skipping polling")
                        time.sleep(3600)  # Check every hour for offline->online transition
                        continue
                    
                    if not LICENSE_KEY:
                        logger.debug("No license key - skipping polling")
                        time.sleep(3600)
                        continue
                    
                    # Check for license updates every 4 hours
                    logger.debug("Polling license server for updates...")
                    LicenseValidator.validate(force_online=True)
                    
                except Exception as e:
                    logger.debug(f"License polling failed: {e}")
                
                time.sleep(14400)  # 4 hours
        
        # Start polling in background thread
        if LICENSE_KEY and not LicenseValidator._get_offline_license_file():
            thread = threading.Thread(target=poll_license, daemon=True)
            thread.start()
            logger.info("Started license polling for online license")
    @staticmethod
    def _update_env_file(key: str, value: str) -> None:
        from pathlib import Path
        project_root = Path(__file__).parent.parent.parent.parent
        env_file = project_root / ".env"
        if not env_file.exists():
            logger.warning(f".env file not found at {env_file}")
            return
        try:
            with open(env_file, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            # Replace ALL occurrences, keep only last replacement
            new_lines = []
            replaced = False
            for line in lines:
                if line.strip().startswith(f"{key}="):
                    if not replaced:
                        new_lines.append(f"{key}={value}\n")
                        replaced = True
                    # drop duplicate occurrences
                else:
                    new_lines.append(line)

            if not replaced:
                if new_lines and not new_lines[-1].endswith('\n'):
                    new_lines[-1] += '\n'
                new_lines.append(f"{key}={value}\n")

            with open(env_file, 'w', encoding='utf-8') as f:
                f.writelines(new_lines)
            logger.info(f"Updated .env file: {key}")
        except Exception as e:
            logger.error(f"Failed to update .env file: {e}")