#!/usr/bin/env python3
"""GPU Monitor factory and vendor detection."""

import json
import logging
import os
import platform
import shutil
import subprocess
from pathlib import Path
from typing import List

from .amd import AMDGPUMonitor
from .amd_npu import AMDNPUMonitor
from .apple import AppleGPUMonitor
from .base import BaseGPUMonitor
from .intel import IntelGPUMonitor
from .intel_npu import IntelNPUMonitor
from .nvidia import NvidiaGPUMonitor


class GPUMonitorFactory:
    """Factory class for creating appropriate GPU monitor instances."""

    @staticmethod
    def _append_vendor(vendors: list[str], vendor: str) -> None:
        """Append vendor once while preserving detection order."""
        if vendor not in vendors:
            vendors.append(vendor)

    @staticmethod
    def _is_macos() -> bool:
        """Return whether the current platform is macOS."""
        return platform.system().lower() == "darwin"

    @staticmethod
    def _has_system_profiler() -> bool:
        """Return whether ``system_profiler`` is available."""
        resolved = shutil.which("system_profiler")
        if resolved:
            return True
        fallback = Path("/usr/sbin/system_profiler")
        return fallback.exists() and os.access(fallback, os.X_OK)

    @staticmethod
    def _macos_live_metrics_requested() -> bool:
        """Return whether opt-in macOS live metrics are enabled."""
        raw_value = os.environ.get("AITOP_ENABLE_MACOS_POWERMETRICS", "")
        return raw_value.strip().lower() in {"1", "true", "yes", "on"}

    @staticmethod
    def _has_drm_vendor(vendor_ids: set[str]) -> bool:
        """Check DRM sysfs for any vendor ID in ``vendor_ids``."""
        drm_root = Path("/sys/class/drm")
        if not drm_root.exists():
            return False

        normalized_ids = {vendor.lower() for vendor in vendor_ids}
        for vendor_file in drm_root.glob("card*/device/vendor"):
            try:
                vendor_id = vendor_file.read_text(encoding="utf-8").strip().lower()
                if vendor_id in normalized_ids:
                    return True
            except Exception:
                continue
        return False

    @staticmethod
    def _has_intel_drm_device() -> bool:
        """Check DRM sysfs for Intel vendor IDs."""
        return GPUMonitorFactory._has_drm_vendor({"0x8086"})

    @staticmethod
    def _has_amd_drm_device() -> bool:
        """Check DRM sysfs for AMD GPU vendor IDs."""
        return GPUMonitorFactory._has_drm_vendor({"0x1002"})

    @staticmethod
    def _has_nvidia_gpu_device() -> bool:
        """Detect NVIDIA GPUs through procfs or DRM vendor IDs."""
        try:
            nvidia_proc_path = Path("/proc/driver/nvidia/gpus")
            if nvidia_proc_path.exists() and any(nvidia_proc_path.iterdir()):
                return True
        except Exception:
            pass
        return GPUMonitorFactory._has_drm_vendor({"0x10de"})

    @staticmethod
    def _has_intel_npu_accel_device() -> bool:
        """Check accel sysfs for Intel NPU devices."""
        accel_root = Path("/sys/class/accel")
        if not accel_root.exists():
            return False

        for vendor_file in accel_root.glob("accel*/device/vendor"):
            try:
                vendor_id = vendor_file.read_text(encoding="utf-8").strip().lower()
                if vendor_id == "0x8086":
                    return True
            except Exception:
                continue
        return False

    @staticmethod
    def _has_amd_npu_accel_device() -> bool:
        """Check accel sysfs for AMD NPU devices."""
        accel_root = Path("/sys/class/accel")
        if not accel_root.exists():
            return False

        for accel_path in accel_root.glob("accel*"):
            try:
                vendor_file = accel_path / "device" / "vendor"
                vendor_id = vendor_file.read_text(encoding="utf-8").strip().lower()
                if vendor_id in {"0x1022", "0x1002"}:
                    return True
            except Exception:
                pass

            try:
                driver_name = (accel_path / "device" / "driver").resolve().name.lower()
                if "amdxdna" in driver_name:
                    return True
            except Exception:
                continue

        return False

    @staticmethod
    def detect_dependency_warnings() -> list[str]:
        """Detect missing system tools for available accelerators."""
        warnings: list[str] = []

        if GPUMonitorFactory._is_macos():
            if not GPUMonitorFactory._has_system_profiler():
                warnings.append(
                    "macOS detected but system_profiler is unavailable "
                    "(required for Apple GPU inventory)."
                )
            if (
                GPUMonitorFactory._macos_live_metrics_requested()
                and shutil.which("powermetrics") is None
            ):
                warnings.append(
                    "macOS live GPU telemetry is limited without powermetrics "
                    "(optional; elevated privileges may be required)."
                )
            return warnings

        if GPUMonitorFactory._has_nvidia_gpu_device() and not shutil.which(
            "nvidia-smi"
        ):
            warnings.append(
                "NVIDIA GPU detected but nvidia-smi is missing "
                "(install NVIDIA driver tools)."
            )

        if GPUMonitorFactory._has_amd_drm_device() and not (
            shutil.which("rocm-smi") or shutil.which("amd-smi")
        ):
            warnings.append(
                "AMD GPU detected but neither rocm-smi nor amd-smi is available "
                "(install ROCm/AMD SMI tools)."
            )

        if GPUMonitorFactory._has_intel_drm_device() and not shutil.which(
            "intel_gpu_top"
        ):
            warnings.append(
                "Intel GPU detected but intel_gpu_top is missing "
                "(install intel-gpu-tools)."
            )

        if GPUMonitorFactory._has_amd_npu_accel_device() and not shutil.which(
            "xrt-smi"
        ):
            warnings.append(
                "AMD NPU detected without xrt-smi; telemetry is limited to "
                "accel sysfs counters."
            )

        return warnings

    @staticmethod
    def detect_vendors() -> list[str]:
        """Detect all available GPU vendors."""
        vendors: list[str] = []

        logger = logging.getLogger(__name__)
        logger.debug("Starting GPU vendor detection")

        if GPUMonitorFactory._is_macos():
            if GPUMonitorFactory._has_system_profiler():
                GPUMonitorFactory._append_vendor(vendors, "apple")
                logger.info("macOS Apple GPU support enabled via system_profiler")
                return vendors
            logger.warning("macOS detected but system_profiler was not found")
            return ["none"]

        # Add common GPU tool locations to PATH
        os.environ["PATH"] = (
            os.environ["PATH"] + ":/opt/rocm/bin:/usr/local/bin:/usr/bin"
        )
        logger.debug(f"Updated PATH: {os.environ['PATH']}")

        # Enhanced NVIDIA detection - more robust to handle edge cases
        logger.info("Checking for NVIDIA GPUs...")

        # Try several different methods to detect NVIDIA GPUs
        nvidia_detected = False

        # Method 1: Try nvidia-smi directly (most common case)
        try:
            # Just check if nvidia-smi runs at all first
            result = subprocess.run(
                ["nvidia-smi"], capture_output=True, text=True, timeout=3
            )

            # If we got here without an exception, we likely have NVIDIA GPUs
            if "NVIDIA-SMI" in result.stdout:
                logger.info("NVIDIA GPU detected via direct nvidia-smi call")
                GPUMonitorFactory._append_vendor(vendors, "nvidia")
                nvidia_detected = True
            else:
                logger.debug("nvidia-smi ran but didn't return expected output")
        except Exception as e:
            logger.debug(f"nvidia-smi direct call failed: {str(e)}")

        # Method 2: If first method failed, try checking common paths
        if not nvidia_detected:
            nvidia_smi_paths = [
                Path("/usr/bin/nvidia-smi"),
                Path("/usr/local/bin/nvidia-smi"),
                Path("/opt/nvidia/bin/nvidia-smi"),
                # Add any other common paths here
            ]

            for nvidia_smi in nvidia_smi_paths:
                if nvidia_smi.exists():
                    try:
                        # Try running with the full path
                        result = subprocess.run(
                            [str(nvidia_smi)], capture_output=True, text=True, timeout=3
                        )

                        if "NVIDIA-SMI" in result.stdout:
                            logger.info(f"NVIDIA GPU detected via path: {nvidia_smi}")
                            GPUMonitorFactory._append_vendor(vendors, "nvidia")
                            nvidia_detected = True
                            break
                    except Exception as e:
                        logger.debug(
                            f"Failed to run nvidia-smi at {nvidia_smi}: {str(e)}"
                        )

        # Method 3: If all else failed, check if the device exists in /proc
        if not nvidia_detected:
            try:
                # Check for NVIDIA devices in /proc/driver/nvidia/gpus
                nvidia_proc_path = Path("/proc/driver/nvidia/gpus")
                if nvidia_proc_path.exists() and any(nvidia_proc_path.iterdir()):
                    logger.info("NVIDIA GPU detected via /proc filesystem")
                    GPUMonitorFactory._append_vendor(vendors, "nvidia")
                    nvidia_detected = True
            except Exception as e:
                logger.debug(f"Failed to check /proc for NVIDIA devices: {str(e)}")

        # Then check AMD (prefer amd-smi, fallback to rocm-smi)
        amd_detected = False

        try:
            result = subprocess.run(
                ["amd-smi", "list", "--json"],
                capture_output=True,
                text=True,
                timeout=3,
            )
            if result.returncode == 0 and result.stdout.strip():
                try:
                    payload = json.loads(result.stdout)
                    payload_text = json.dumps(payload).lower()
                except json.JSONDecodeError:
                    payload_text = result.stdout.lower()

                if "gpu" in payload_text or "card" in payload_text:
                    GPUMonitorFactory._append_vendor(vendors, "amd")
                    amd_detected = True
                    logger.info("AMD GPU detected via amd-smi")
                else:
                    logger.debug("amd-smi list output did not indicate GPU presence")
        except Exception as e:
            logger.debug(f"Failed to detect AMD GPU via amd-smi: {str(e)}")

        if not amd_detected:
            try:
                result = subprocess.run(
                    ["rocm-smi", "-i"],
                    capture_output=True,
                    text=True,
                    timeout=3,
                )
                if "GPU ID" in result.stdout or "GPU[" in result.stdout:
                    GPUMonitorFactory._append_vendor(vendors, "amd")
                    amd_detected = True
                    logger.info("AMD GPU detected via rocm-smi")
                else:
                    logger.debug("rocm-smi output did not indicate GPU presence")
            except Exception as e:
                logger.debug(f"Failed to detect AMD GPU via rocm-smi: {str(e)}")

        # Finally check Intel (non-interactive probes only)
        intel_detected = False
        intel_gpu_top = shutil.which("intel_gpu_top")
        if intel_gpu_top:
            try:
                result = subprocess.run(
                    [intel_gpu_top, "-L"],
                    capture_output=True,
                    text=True,
                    timeout=3,
                    check=False,
                )
                output = f"{result.stdout}\n{result.stderr}".lower()
                if result.returncode == 0 and (
                    "card" in output or "/dev/dri" in output or "intel" in output
                ):
                    GPUMonitorFactory._append_vendor(vendors, "intel")
                    intel_detected = True
                    logger.info("Intel GPU detected via intel_gpu_top -L")
                else:
                    logger.debug(
                        "intel_gpu_top -L probe did not confirm devices "
                        "(rc=%s, output=%s)",
                        result.returncode,
                        output.strip()[:200],
                    )
            except Exception as e:
                logger.debug(f"Failed Intel probe via intel_gpu_top -L: {str(e)}")

        if (
            not intel_detected
            and intel_gpu_top
            and GPUMonitorFactory._has_intel_drm_device()
        ):
            GPUMonitorFactory._append_vendor(vendors, "intel")
            intel_detected = True
            logger.info("Intel GPU detected via DRM sysfs vendor ID")
        elif (
            not intel_detected
            and not intel_gpu_top
            and GPUMonitorFactory._has_intel_drm_device()
        ):
            logger.info(
                "Intel DRM GPU present but intel_gpu_top is unavailable; "
                "skipping Intel GPU monitor"
            )

        # Experimental AMD NPU detection
        amd_npu_detected = False
        xrt_smi = shutil.which("xrt-smi")
        if xrt_smi:
            try:
                result = subprocess.run(
                    [xrt_smi, "examine"],
                    capture_output=True,
                    text=True,
                    timeout=3,
                    check=False,
                )
                output = f"{result.stdout}\n{result.stderr}".lower()
                if result.returncode == 0 and "npu" in output:
                    GPUMonitorFactory._append_vendor(vendors, "amd_npu")
                    amd_npu_detected = True
                    logger.info("AMD NPU detected via xrt-smi examine")
            except Exception as e:
                logger.debug(f"Failed AMD NPU probe via xrt-smi: {str(e)}")

        if not amd_npu_detected and GPUMonitorFactory._has_amd_npu_accel_device():
            GPUMonitorFactory._append_vendor(vendors, "amd_npu")
            amd_npu_detected = True
            logger.info("AMD NPU detected via accel sysfs")

        # Experimental Intel NPU detection
        if GPUMonitorFactory._has_intel_npu_accel_device():
            GPUMonitorFactory._append_vendor(vendors, "intel_npu")
            logger.info("Intel NPU detected via accel sysfs")

        if not vendors:
            logger.warning("No GPUs detected")
            return ["none"]

        logger.info(f"Detected GPU vendors: {vendors}")
        return vendors

    @classmethod
    def create_monitors(cls) -> List[BaseGPUMonitor]:
        """Create appropriate GPU monitors for all detected vendors."""
        logger = logging.getLogger(__name__)
        logger.debug("Creating GPU monitors")

        monitors: List[BaseGPUMonitor] = []
        vendors = cls.detect_vendors()

        for vendor in vendors:
            try:
                if vendor == "nvidia":
                    monitors.append(NvidiaGPUMonitor())
                    logger.debug("Created NVIDIA GPU monitor")
                elif vendor == "amd":
                    monitors.append(AMDGPUMonitor())
                    logger.debug("Created AMD GPU monitor")
                elif vendor == "intel":
                    monitors.append(IntelGPUMonitor())
                    logger.debug("Created Intel GPU monitor")
                elif vendor == "amd_npu":
                    monitors.append(AMDNPUMonitor())
                    logger.debug("Created AMD NPU monitor")
                elif vendor == "intel_npu":
                    monitors.append(IntelNPUMonitor())
                    logger.debug("Created Intel NPU monitor")
                elif vendor == "apple":
                    monitors.append(AppleGPUMonitor())
                    logger.debug("Created Apple GPU monitor")
            except Exception as e:
                logger.error(f"Failed to create monitor for {vendor}: {str(e)}")

        logger.debug(f"Created {len(monitors)} GPU monitors")
        return monitors
