"""Beta license module — machine lock + 30-day expiry + server activation.

On first run: generate machine fingerprint (SHA256 of MAC + hostname + disk serial + OS).
POST to license server to activate. Store license at ~/.greymatter/license.json.
On every startup: verify license exists, not expired, machine matches, HMAC signature valid.
If license server unreachable: use cached license (grace period 7 days).
If expired: print message and exit.
"""
from __future__ import annotations

import hashlib
import hmac
import json
import logging
import os
import platform
import socket
import sys
import time
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional

import httpx

logger = logging.getLogger("greymatter.license")

# --- Constants ---

LICENSE_DIR = Path.home() / ".greymatter"
LICENSE_PATH = LICENSE_DIR / "license.json"
LICENSE_SERVER_URL = os.environ.get(
    "GM_LICENSE_SERVER", "https://license.greymatter.dev"
)
BETA_DURATION_DAYS = 30
GRACE_PERIOD_DAYS = 7
HMAC_KEY_ENV = "GM_LICENSE_HMAC_KEY"
_DEFAULT_HMAC_KEY = b"greymatter-beta-2026"


# --- Machine Fingerprint ---


def _get_mac_address() -> str:
    """Get the primary MAC address as a hex string.

    Uses system network interface commands for a stable address, since
    uuid.getnode() can return random MACs across process invocations on macOS.
    Falls back to uuid.getnode() if interface lookup fails.
    """
    try:
        import subprocess as _sp

        if platform.system() == "Darwin":
            # en0 is the primary interface on macOS
            result = _sp.run(
                ["ifconfig", "en0"], capture_output=True, text=True, timeout=5
            )
            for line in result.stdout.splitlines():
                if "ether" in line:
                    return line.strip().split()[1].replace(":", "")
        elif platform.system() == "Linux":
            # Try common physical interfaces
            import pathlib

            for iface in sorted(pathlib.Path("/sys/class/net").iterdir()):
                if iface.name in ("lo",):
                    continue
                addr_file = iface / "address"
                if addr_file.exists():
                    addr = addr_file.read_text().strip()
                    if addr and addr != "00:00:00:00:00:00":
                        return addr.replace(":", "")
    except Exception:
        pass
    # Fallback — may be non-deterministic on some systems
    mac = uuid.getnode()
    return f"{mac:012x}"


def _get_disk_serial() -> str:
    """Get a disk serial number (best-effort, falls back to empty string)."""
    try:
        if platform.system() == "Darwin":
            import subprocess

            result = subprocess.run(
                ["system_profiler", "SPSerialATADataType", "-detailLevel", "mini"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            for line in result.stdout.splitlines():
                if "Serial Number" in line:
                    return line.split(":")[-1].strip()
            # Try NVMe on Apple Silicon
            result = subprocess.run(
                ["system_profiler", "SPNVMeDataType", "-detailLevel", "mini"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            for line in result.stdout.splitlines():
                if "Serial Number" in line:
                    return line.split(":")[-1].strip()
        elif platform.system() == "Linux":
            for name in sorted(os.listdir("/sys/block")):
                serial_path = Path(f"/sys/block/{name}/device/serial")
                if serial_path.exists():
                    return serial_path.read_text().strip()
    except Exception:
        pass
    return ""


def generate_fingerprint() -> str:
    """Generate a SHA256 machine fingerprint from hardware identifiers.

    Components: MAC address + hostname + disk serial + OS platform.
    """
    components = [
        _get_mac_address(),
        socket.gethostname(),
        _get_disk_serial(),
        platform.system(),
    ]
    raw = "|".join(components)
    return hashlib.sha256(raw.encode()).hexdigest()


# --- HMAC Signing ---


def _get_hmac_key() -> bytes:
    """Get the HMAC signing key from environment or default."""
    env_key = os.environ.get(HMAC_KEY_ENV)
    if env_key:
        return env_key.encode()
    return _DEFAULT_HMAC_KEY


def sign_license(data: dict[str, Any], key: Optional[bytes] = None) -> str:
    """Compute HMAC-SHA256 signature over canonical license fields.

    Args:
        data: License data dict (must contain machine_id, activated_at, expires_at).
        key: HMAC key. Uses default if not provided.

    Returns:
        Hex-encoded HMAC signature.
    """
    if key is None:
        key = _get_hmac_key()
    payload = f"{data['machine_id']}|{data['activated_at']}|{data['expires_at']}"
    return hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()


def verify_signature(data: dict[str, Any], key: Optional[bytes] = None) -> bool:
    """Verify HMAC signature on license data.

    Args:
        data: License data dict with 'signature' field.
        key: HMAC key. Uses default if not provided.

    Returns:
        True if signature is valid.
    """
    if "signature" not in data:
        return False
    expected = sign_license(data, key)
    return hmac.compare_digest(data["signature"], expected)


# --- License Data ---


@dataclass
class LicenseStatus:
    """Result of a license verification check."""

    valid: bool
    message: str
    days_remaining: int = 0
    grace: bool = False


def _load_cached_license() -> Optional[dict[str, Any]]:
    """Load license from disk cache."""
    if not LICENSE_PATH.exists():
        return None
    try:
        with open(LICENSE_PATH, encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError) as exc:
        logger.warning("Failed to read cached license: %s", exc)
        return None


def _save_license(data: dict[str, Any]) -> None:
    """Save license data to disk."""
    LICENSE_DIR.mkdir(parents=True, exist_ok=True)
    with open(LICENSE_PATH, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)
    LICENSE_PATH.chmod(0o600)


# --- Activation ---


def activate(
    server_url: Optional[str] = None,
    fingerprint: Optional[str] = None,
) -> dict[str, Any]:
    """Activate a beta license by posting machine fingerprint to license server.

    Args:
        server_url: License server base URL. Defaults to GM_LICENSE_SERVER env.
        fingerprint: Machine fingerprint. Generated if not provided.

    Returns:
        License data dict from server.

    Raises:
        httpx.HTTPError: If the server request fails.
        RuntimeError: If the server returns an error response.
    """
    if server_url is None:
        server_url = LICENSE_SERVER_URL
    if fingerprint is None:
        fingerprint = generate_fingerprint()

    url = f"{server_url.rstrip('/')}/api/activate"
    payload = {
        "machine_id": fingerprint,
        "hostname": socket.gethostname(),
        "platform": platform.system(),
    }

    response = httpx.post(url, json=payload, timeout=15.0)
    response.raise_for_status()
    data = response.json()

    if data.get("error"):
        raise RuntimeError(f"License activation failed: {data['error']}")

    _save_license(data)
    logger.info("License activated, expires at %s", data.get("expires_at"))
    return data


# --- Verification ---


def verify(
    fingerprint: Optional[str] = None,
    now: Optional[float] = None,
) -> LicenseStatus:
    """Verify the current license.

    Checks: license exists, machine matches, signature valid, not expired.
    Grace period of 7 days allowed when license server was unreachable at
    activation but a cached license exists.

    Args:
        fingerprint: Machine fingerprint. Generated if not provided.
        now: Current timestamp (for testing). Defaults to time.time().

    Returns:
        LicenseStatus with validity info.
    """
    if fingerprint is None:
        fingerprint = generate_fingerprint()
    if now is None:
        now = time.time()

    data = _load_cached_license()
    if data is None:
        return LicenseStatus(valid=False, message="No license found. Run activation first.")

    # Machine lock check
    if data.get("machine_id") != fingerprint:
        return LicenseStatus(
            valid=False,
            message="License is bound to a different machine.",
        )

    # Signature check
    if not verify_signature(data):
        return LicenseStatus(valid=False, message="License signature is invalid.")

    # Expiry check
    expires_at = data.get("expires_at", 0)
    if now > expires_at:
        grace_expires = expires_at + (GRACE_PERIOD_DAYS * 86400)
        if now <= grace_expires:
            days_left = int((grace_expires - now) / 86400)
            return LicenseStatus(
                valid=True,
                message=f"License expired but in grace period ({days_left} days remaining).",
                days_remaining=days_left,
                grace=True,
            )
        return LicenseStatus(
            valid=False,
            message="License has expired. Please renew your GreyMatter beta license.",
        )

    days_left = int((expires_at - now) / 86400)
    return LicenseStatus(valid=True, message="License valid.", days_remaining=days_left)


# --- Startup Gate ---


def check_license_or_exit() -> None:
    """Verify license at startup; exit if invalid.

    Called from MCP server and CLI entry points. On first run (no cached
    license), attempts activation against the license server.

    Set GM_LICENSE_SKIP=1 to bypass (development/container use).
    """
    if os.environ.get("GM_LICENSE_SKIP", "").lower() in ("1", "true", "yes"):
        logger.info("License check skipped (GM_LICENSE_SKIP)")
        return

    fingerprint = generate_fingerprint()

    # If no cached license, try to activate
    if not LICENSE_PATH.exists():
        logger.info("No license found — attempting activation...")
        try:
            activate(fingerprint=fingerprint)
        except Exception as exc:
            print(
                f"GreyMatter license activation failed: {exc}\n"
                "Set GM_LICENSE_SERVER if using a custom license server.",
                file=sys.stderr,
            )
            sys.exit(1)

    status = verify(fingerprint=fingerprint)

    if not status.valid:
        print(f"GreyMatter license error: {status.message}", file=sys.stderr)
        sys.exit(1)

    if status.grace:
        logger.warning("License grace period: %s", status.message)
    elif status.days_remaining <= 7:
        logger.warning(
            "License expires in %d day(s). Please renew soon.", status.days_remaining
        )
