"""psutil-backed metrics provider for desktop platforms."""

from __future__ import annotations

import os
import platform
import subprocess
import threading
import time

from .types import MemorySnapshot, ProcessSnapshot, ThermalSnapshot


class PsutilMetricsProvider:
    """psutil-backed provider for Windows/macOS/Linux desktop hosts.

    Primes psutil's interval-based CPU counters at construction so the
    first real sample returns a meaningful value instead of 0.0. The
    logical CPU count is cached because it never changes during a
    process's lifetime.
    """

    available: bool = True

    def __init__(self, psutil_module) -> None:
        self._psutil = psutil_module
        psutil_module.cpu_percent(interval=None)
        self._process = psutil_module.Process(os.getpid())
        self._process.cpu_percent(interval=None)
        self._cpu_count = int(psutil_module.cpu_count(logical=True) or 1)
        # psutil has no sensors_temperatures() on Windows, so fall back to a
        # throttled WMI/LHM reader running in a daemon thread. Disabled in
        # tests via LEDGRAB_DISABLE_WIN_TEMP.
        self._windows_temp: _WindowsCpuTemp | None = (
            _WindowsCpuTemp()
            if platform.system() == "Windows" and not os.environ.get("LEDGRAB_DISABLE_WIN_TEMP")
            else None
        )

    def cpu_percent(self) -> float:
        return float(self._psutil.cpu_percent(interval=None))

    def cpu_count(self) -> int:
        return self._cpu_count

    def virtual_memory(self) -> MemorySnapshot:
        m = self._psutil.virtual_memory()
        return MemorySnapshot(
            used_bytes=int(m.used),
            total_bytes=int(m.total),
            percent=float(m.percent),
        )

    def process_snapshot(self) -> ProcessSnapshot:
        # psutil's Process.cpu_percent() returns 0–N*100%; normalize to 0–100%.
        cpu = self._process.cpu_percent(interval=None) / self._cpu_count
        rss = int(self._process.memory_info().rss)
        return ProcessSnapshot(cpu_percent=float(cpu), rss_bytes=rss)

    def thermals(self) -> ThermalSnapshot:
        battery_pct: float | None = None
        battery_temp: float | None = None
        cpu_temp: float | None = None

        # Battery: only some hosts report it (laptops, tablets); psutil
        # raises or returns None on desktops without a battery.
        sensors_battery = getattr(self._psutil, "sensors_battery", None)
        if sensors_battery is not None:
            try:
                bat = sensors_battery()
                if bat is not None:
                    battery_pct = float(bat.percent)
            except Exception:
                pass

        # CPU temperature: pick the hottest reading across all sensors.
        # sensors_temperatures() is Linux-only on psutil; absent on Win/macOS.
        sensors_temps = getattr(self._psutil, "sensors_temperatures", None)
        if sensors_temps is not None:
            try:
                temps = sensors_temps()
                hottest = None
                for entries in (temps or {}).values():
                    for entry in entries:
                        current = getattr(entry, "current", None)
                        if current is None:
                            continue
                        if hottest is None or current > hottest:
                            hottest = float(current)
                if hottest is not None:
                    cpu_temp = hottest
            except Exception:
                pass

        # Windows fallback: psutil exposes no CPU temperature there, so the
        # reading would always be None without this. Other platforms keep
        # the psutil result as-is.
        if cpu_temp is None and self._windows_temp is not None:
            cpu_temp = self._windows_temp.get()

        return ThermalSnapshot(
            battery_percent=battery_pct,
            battery_temp_c=battery_temp,
            cpu_temp_c=cpu_temp,
        )


# ── Windows CPU temperature helper ───────────────────────────────────────

# Windows has no user-space API for real per-core CPU temperature without
# a vendor driver or third-party monitoring service, so we only try sources
# that reflect the actual CPU die rather than a motherboard/chassis zone:
#
#  1. LibreHardwareMonitor / OpenHardwareMonitor WMI — °C. Only usable when
#     the monitoring app is running, but reads Intel DTS / AMD SMN directly
#     so the reading actually tracks load.
#  2. ``MSAcpi_ThermalZoneTemperature`` WMI — Kelvin × 10. Some OEM boards
#     wire this to the CPU; many require admin or expose a chassis zone
#     instead. Only used as a last resort.
#
# The ``\Thermal Zone Information(*)\Temperature`` perf counter is
# deliberately NOT queried: on most consumer desktops it returns ACPI
# TZxx zones that are pinned at ~27–30 °C regardless of CPU load — a
# misleading stable reading is worse than no reading at all.
#
# Emits a single numeric line on stdout and exits.
_WIN_TEMP_POWERSHELL = (
    "$ErrorActionPreference='SilentlyContinue';"
    "foreach ($ns in 'root/LibreHardwareMonitor','root/OpenHardwareMonitor') {"
    " $lhm = Get-CimInstance -Namespace $ns -ClassName Sensor"
    "  -Filter \"SensorType='Temperature'\";"
    " if ($lhm) {"
    "  $cpu = $lhm | Where-Object { $_.Parent -match 'cpu' -or $_.Name -match 'CPU' }"
    "   | Sort-Object Value -Descending | Select-Object -First 1;"
    "  if ($cpu) { '{0:N2}' -f $cpu.Value; exit }"
    " }"
    "}"
    "$acpi = Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature;"
    "if ($acpi) {"
    " $t = ($acpi | Measure-Object -Property CurrentTemperature -Maximum).Maximum;"
    " if ($t) { '{0:N2}' -f ($t / 10.0 - 273.15); exit }"
    "}"
)


def _query_windows_cpu_temp() -> float | None:
    """Run the PowerShell WMI probe once and parse the single-line result.

    Returns None on any failure. Rejects wildly out-of-range values to
    guard against sensors that report raw (un-scaled) Kelvin or 0.
    """
    if platform.system() != "Windows":
        return None
    try:
        creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
        result = subprocess.run(
            ["powershell", "-NoProfile", "-NonInteractive", "-Command", _WIN_TEMP_POWERSHELL],
            capture_output=True,
            text=True,
            timeout=4.0,
            creationflags=creationflags,
        )
    except (OSError, subprocess.TimeoutExpired):
        return None
    if result.returncode != 0:
        return None
    line = (result.stdout or "").strip().splitlines()
    if not line:
        return None
    try:
        # Locale may use comma as decimal separator (e.g. ru-RU).
        temp = float(line[0].replace(",", ".").strip())
    except ValueError:
        return None
    if -20.0 <= temp <= 150.0:
        return temp
    return None


class _WindowsCpuTemp:
    """Throttled background reader for Windows CPU temperature.

    Spawning PowerShell costs hundreds of ms per call, so we refresh in a
    daemon thread at most once every ``REFRESH_INTERVAL_S`` seconds and
    return the most recent cached value from ``get()``. After
    ``MAX_FAILURES`` consecutive empty results we self-disable to avoid
    launching PowerShell forever on hosts without any usable sensor.
    """

    REFRESH_INTERVAL_S = 5.0
    MAX_FAILURES = 3

    def __init__(self) -> None:
        self._cached_c: float | None = None
        self._last_refresh: float = 0.0
        self._refreshing: bool = False
        self._disabled: bool = False
        self._failures: int = 0
        self._lock = threading.Lock()

    def get(self) -> float | None:
        if self._disabled:
            return None
        now = time.monotonic()
        with self._lock:
            due = now - self._last_refresh >= self.REFRESH_INTERVAL_S
            should_start = due and not self._refreshing
            if should_start:
                self._refreshing = True
        if should_start:
            threading.Thread(target=self._refresh, daemon=True).start()
        return self._cached_c

    def _refresh(self) -> None:
        try:
            value = _query_windows_cpu_temp()
        finally:
            now = time.monotonic()
            with self._lock:
                self._last_refresh = now
                self._refreshing = False
                if value is not None:
                    self._cached_c = value
                    self._failures = 0
                else:
                    self._failures += 1
                    if self._failures >= self.MAX_FAILURES:
                        self._disabled = True
