"""
wivexx_perf.py — System resource monitor + power-mode manager  (v1.6)
═══════════════════════════════════════════════════════════════════════
Watches CPU, RAM and GPU usage in a background thread.  When any of them
stays above its configured limit for PERF_HIGH_SECS, WIVEXX is switched to
LOW-POWER mode (smaller CPU Whisper model, memory consolidation paused).
When everything stays back under the limits for PERF_RECOVER_SECS, WIVEXX
returns to FULL-POWER mode automatically.

GPU readings come from pynvml when available, otherwise from `nvidia-smi`.
If neither works (AMD/Intel GPU, no driver tools) GPU monitoring is simply
skipped — CPU and RAM limits still apply.

Public API (called from wivexx_api.py):
    start(cfg, on_low_power, on_full_power, logger=None) -> bool
    stop()
    is_low_power() -> bool
    snapshot() -> dict     # {"cpu": .., "ram": .., "gpu": .. or None}
"""

import subprocess
import threading
import time
from typing import Callable, Optional

_thread: Optional[threading.Thread] = None
_stop_evt = threading.Event()
_low_power = False
_last_snapshot: dict = {"cpu": 0.0, "ram": 0.0, "gpu": None}

# ── GPU backends ───────────────────────────────────────────────────────────────
_nvml_handle = None
_gpu_backend = None   # "nvml" | "smi" | None


def _init_gpu_backend() -> None:
    """Pick the cheapest working GPU-utilisation source."""
    global _nvml_handle, _gpu_backend
    try:
        import pynvml
        pynvml.nvmlInit()
        _nvml_handle = pynvml.nvmlDeviceGetHandleByIndex(0)
        _gpu_backend = "nvml"
        return
    except Exception:
        _nvml_handle = None

    try:
        out = subprocess.run(
            ["nvidia-smi", "--query-gpu=utilization.gpu",
             "--format=csv,noheader,nounits"],
            capture_output=True, timeout=4,
            creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
        )
        if out.returncode == 0 and out.stdout.strip():
            _gpu_backend = "smi"
            return
    except Exception:
        pass
    _gpu_backend = None


def _read_gpu_percent() -> Optional[float]:
    try:
        if _gpu_backend == "nvml" and _nvml_handle is not None:
            import pynvml
            util = pynvml.nvmlDeviceGetUtilizationRates(_nvml_handle)
            return float(util.gpu)
        if _gpu_backend == "smi":
            out = subprocess.run(
                ["nvidia-smi", "--query-gpu=utilization.gpu",
                 "--format=csv,noheader,nounits"],
                capture_output=True, timeout=4,
                creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
            )
            if out.returncode == 0:
                return float(out.stdout.decode().strip().splitlines()[0])
    except Exception:
        pass
    return None


# Foreground activities whose heavy GPU use is the USER's app (a game, a video, a
# render) — NOT Wivexx straining. While one of these is active we ignore the GPU
# limit so the perf guard doesn't "fall asleep" (sleepy cap + lite STT) mid-game.
_HEAVY_ACTIVITIES = frozenset({"gaming", "video", "design"})


# ── Monitor loop ───────────────────────────────────────────────────────────────
def _monitor_loop(cfg, on_low: Callable, on_full: Callable, logger,
                  activity_getter: Optional[Callable] = None) -> None:
    global _low_power, _last_snapshot
    import psutil

    interval     = float(getattr(cfg, "PERF_CHECK_INTERVAL", 5))
    cpu_limit    = float(getattr(cfg, "PERF_CPU_LIMIT", 85))
    gpu_limit    = float(getattr(cfg, "PERF_GPU_LIMIT", 85))
    ram_limit    = float(getattr(cfg, "PERF_RAM_LIMIT", 90))
    high_secs    = float(getattr(cfg, "PERF_HIGH_SECS", 15))
    recover_secs = float(getattr(cfg, "PERF_RECOVER_SECS", 45))

    over_since:  Optional[float] = None
    under_since: Optional[float] = None

    psutil.cpu_percent(interval=None)   # prime the counter

    while not _stop_evt.wait(interval):
        try:
            cpu = psutil.cpu_percent(interval=None)
            ram = psutil.virtual_memory().percent
            gpu = _read_gpu_percent()
            _last_snapshot = {"cpu": cpu, "ram": ram, "gpu": gpu}

            # a game/video/render pegging the GPU isn't Wivexx overloading — skip
            # the GPU term while one of those is the foreground activity
            gpu_counts = gpu is not None
            try:
                if activity_getter and activity_getter() in _HEAVY_ACTIVITIES:
                    gpu_counts = False
            except Exception:
                pass

            overloaded = (
                cpu >= cpu_limit
                or ram >= ram_limit
                or (gpu_counts and gpu >= gpu_limit)
            )
            now = time.time()

            if not _low_power:
                if overloaded:
                    over_since = over_since or now
                    if now - over_since >= high_secs:
                        _low_power = True
                        over_since = None
                        under_since = None
                        if logger:
                            logger.info(
                                "PERF: entering LOW-POWER mode (cpu=%.0f%% ram=%.0f%% gpu=%s)",
                                cpu, ram, f"{gpu:.0f}%" if gpu is not None else "n/a")
                        try:
                            on_low()
                        except Exception as e:
                            if logger:
                                logger.warning("PERF on_low callback failed: %s", e)
                else:
                    over_since = None
            else:
                if not overloaded:
                    under_since = under_since or now
                    if now - under_since >= recover_secs:
                        _low_power = False
                        under_since = None
                        over_since = None
                        if logger:
                            logger.info(
                                "PERF: back to FULL-POWER mode (cpu=%.0f%% ram=%.0f%% gpu=%s)",
                                cpu, ram, f"{gpu:.0f}%" if gpu is not None else "n/a")
                        try:
                            on_full()
                        except Exception as e:
                            if logger:
                                logger.warning("PERF on_full callback failed: %s", e)
                else:
                    under_since = None
        except Exception as e:
            if logger:
                logger.debug("PERF monitor tick failed: %s", e)


# ── Public API ─────────────────────────────────────────────────────────────────
def start(cfg, on_low_power: Callable, on_full_power: Callable, logger=None,
          activity_getter: Optional[Callable] = None) -> bool:
    """Start the monitor thread. Returns False when disabled or psutil missing.
    `activity_getter` (optional) returns the current mascot activity string so a
    game/video's GPU load doesn't get mistaken for Wivexx overloading."""
    global _thread
    if not getattr(cfg, "PERF_MONITOR_ENABLED", True):
        return False
    try:
        import psutil  # noqa: F401
    except ImportError:
        if logger:
            logger.warning("PERF: psutil not installed — performance monitor disabled.")
        return False
    if _thread is not None and _thread.is_alive():
        return True

    _init_gpu_backend()
    if logger:
        logger.info("PERF: monitor started (gpu backend: %s)", _gpu_backend or "none")
    _stop_evt.clear()
    _thread = threading.Thread(
        target=_monitor_loop,
        args=(cfg, on_low_power, on_full_power, logger, activity_getter),
        daemon=True, name="wivexx-perf-monitor",
    )
    _thread.start()
    return True


def stop() -> None:
    _stop_evt.set()


def is_low_power() -> bool:
    return _low_power


def snapshot() -> dict:
    return dict(_last_snapshot)
