# SPDX-License-Identifier: Apache-2.0
"""
Machine fingerprinting for license binding.

Generates unique identifiers based on hardware characteristics.
"""

import hashlib
import platform
import uuid
from enum import Enum
from typing import Optional, List

try:
    import psutil
except ImportError:
    psutil = None


class FingerprintPolicy(str, Enum):
    """Policy for machine fingerprint binding."""
    NONE = "none"  # No binding (rely on other protection)
    SINGLE = "single"  # Bind to single machine
    CLUSTER = "cluster"  # Bind to list of machines (for distributed)


def get_cpu_id() -> str:
    """
    Get CPU/machine identifier (platform-dependent).
    
    On Linux, prefers /etc/machine-id (systemd) or DMI product UUID.
    Falls back to platform info if hardware IDs unavailable.
    """
    system = platform.system()
    
    if system == "Linux":
        # Prefer /etc/machine-id (systemd) — stable across reboots, unique per install
        try:
            with open("/etc/machine-id", "r") as f:
                machine_id = f.read().strip()
                if machine_id:
                    return f"machine-id:{machine_id}"
        except Exception:
            pass
        
        # Fallback: DMI product UUID (requires root on some systems)
        try:
            with open("/sys/class/dmi/id/product_uuid", "r") as f:
                product_uuid = f.read().strip()
                if product_uuid:
                    return f"dmi-uuid:{product_uuid}"
        except Exception:
            pass
        
        # Last resort: try /proc/cpuinfo for actual CPU serial (rare on x86)
        try:
            with open("/proc/cpuinfo", "r") as f:
                for line in f:
                    if "Serial" in line:  # ARM/Pi have this
                        serial = line.split(":")[-1].strip()
                        if serial and serial != "0000000000000000":
                            return f"cpu-serial:{serial}"
        except Exception:
            pass
    
    elif system == "Windows":
        try:
            import subprocess
            result = subprocess.run(
                ["wmic", "cpu", "get", "ProcessorId"],
                capture_output=True,
                text=True,
                check=True
            )
            lines = result.stdout.strip().split("\n")
            if len(lines) > 1:
                return lines[1].strip()
        except Exception:
            pass
    
    # Fallback: use platform info (not unique, but better than nothing)
    return f"{platform.processor()}_{platform.machine()}_{platform.node()}"


def get_mac_addresses() -> List[str]:
    """Get MAC addresses of network interfaces."""
    macs = []
    
    if psutil:
        try:
            for interface, addrs in psutil.net_if_addrs().items():
                for addr in addrs:
                    if addr.family == psutil.AF_LINK:  # MAC address
                        macs.append(addr.address)
        except Exception:
            pass
    
    # Fallback: use uuid.getnode()
    if not macs:
        mac = uuid.getnode()
        macs.append(':'.join(f'{(mac >> i) & 0xff:02x}' for i in range(0, 48, 8)))
    
    return sorted(macs)  # Sort for consistency


def get_gpu_uuids() -> List[str]:
    """Get GPU UUIDs (NVIDIA only for now)."""
    uuids = []
    
    try:
        import subprocess
        result = subprocess.run(
            ["nvidia-smi", "--query-gpu=uuid", "--format=csv,noheader"],
            capture_output=True,
            text=True,
            check=True
        )
        uuids = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
    except Exception:
        pass
    
    return uuids


def get_machine_fingerprint(
    include_gpu: bool = False,
    salt: Optional[str] = None
) -> str:
    """
    Generate a unique machine fingerprint.
    
    Args:
        include_gpu: Whether to include GPU UUIDs (may cause issues in containers)
        salt: Optional salt for hashing
        
    Returns:
        Hex-encoded SHA-256 hash of machine characteristics
    """
    components = []
    
    # CPU ID
    cpu_id = get_cpu_id()
    components.append(f"cpu:{cpu_id}")
    
    # MAC addresses
    macs = get_mac_addresses()
    components.append(f"mac:{','.join(macs)}")
    
    # Hostname
    hostname = platform.node()
    components.append(f"host:{hostname}")
    
    # GPU UUIDs (optional)
    if include_gpu:
        gpu_uuids = get_gpu_uuids()
        if gpu_uuids:
            components.append(f"gpu:{','.join(gpu_uuids)}")
    
    # Add salt if provided
    if salt:
        components.append(f"salt:{salt}")
    
    # Hash all components
    fingerprint_str = "|".join(components)
    fingerprint_hash = hashlib.sha256(fingerprint_str.encode()).hexdigest()
    
    return fingerprint_hash


def verify_fingerprint(
    expected: str,
    include_gpu: bool = False,
    salt: Optional[str] = None
) -> bool:
    """
    Verify if current machine matches expected fingerprint.
    
    Args:
        expected: Expected fingerprint hash
        include_gpu: Whether GPU was included in original fingerprint
        salt: Salt used in original fingerprint
        
    Returns:
        True if fingerprints match
    """
    current = get_machine_fingerprint(include_gpu=include_gpu, salt=salt)
    return current == expected


def get_fingerprint_info() -> dict:
    """
    Get detailed fingerprint information for debugging.
    
    Returns:
        Dictionary with fingerprint components
    """
    return {
        "cpu_id": get_cpu_id(),
        "mac_addresses": get_mac_addresses(),
        "hostname": platform.node(),
        "gpu_uuids": get_gpu_uuids(),
        "platform": platform.platform(),
        "fingerprint": get_machine_fingerprint(),
        "fingerprint_with_gpu": get_machine_fingerprint(include_gpu=True),
    }
