"""perf — rootless thermal/perf-mode reader + sudo-backed mode switch.

Ported from perf-tray.py (the PyQt tray) with the Qt dropped, so the same proven
logic backs the web Bridge. Reading thermals/mode is rootless (hwmon + cpufreq
sysfs); only *changing* mode needs root, done via `sudo -n` against a ROOT-OWNED,
sudoers-whitelisted copy of perf.sh at /usr/local/sbin/perf-audio (see
perf-audio.sudoers) — never the user-editable repo copy.

DRY note: perf-tray.py still has its own copy of Thermals/detect_mode; a later
pass can have it import this module. Kept separate now to avoid touching the
working tray.
"""
from __future__ import annotations

import glob
import os
import subprocess
import time

# Root-owned, sudoers-whitelisted deployment of perf.sh (same as perf-tray).
SCRIPT = os.environ.get("PERF_TRAY_SCRIPT", "/usr/local/sbin/perf-audio")

# mode key -> (label, perf.sh flag)
MODES = {
    "cool":     ("Cool",     "--cool"),
    "standard": ("Standard", "--optimize"),
    "extreme":  ("Extreme",  "--extreme"),
    "normal":   ("Normal",   "--stop"),
}

# temp thresholds (°C) → state name; the UI maps state→color (matches tray).
TEMP_STATES = ((55, "cool"), (70, "warm"), (85, "hot"), (10_000, "critical"))


def _read(path, default=None):
    try:
        with open(path) as f:
            return f.read().strip()
    except OSError:
        return default


def _read_int(path, default=None):
    v = _read(path)
    try:
        return int(v)
    except (TypeError, ValueError):
        return default


def _hwmon_by_name(name):
    for d in glob.glob("/sys/class/hwmon/hwmon*"):
        if _read(os.path.join(d, "name")) == name:
            return d
    return None


def temp_state(c):
    if c is None:
        return "unknown"
    for ceiling, state in TEMP_STATES:
        if c < ceiling:
            return state
    return "critical"


class Thermals:
    """Rootless reader for package/core temps, fans, freq and throttle counts."""

    def __init__(self):
        self.coretemp = _hwmon_by_name("coretemp")
        self.dell = _hwmon_by_name("dell_smm")
        self._pkg_input = self._find_label_input(self.coretemp, "Package id 0")
        self._throttle_base = self._throttle_sum()

    def _find_label_input(self, chip, label):
        if not chip:
            return None
        for lbl in glob.glob(os.path.join(chip, "temp*_label")):
            if _read(lbl) == label:
                return lbl.replace("_label", "_input")
        t1 = os.path.join(chip, "temp1_input")
        return t1 if os.path.exists(t1) else None

    def package_c(self):
        v = _read_int(self._pkg_input)
        return round(v / 1000) if v is not None else None

    def cores_c(self):
        out = []
        if self.coretemp:
            for lbl in sorted(glob.glob(os.path.join(self.coretemp, "temp*_label"))):
                if (_read(lbl) or "").startswith("Core"):
                    v = _read_int(lbl.replace("_label", "_input"))
                    if v is not None:
                        out.append(round(v / 1000))
        return out

    def fans_rpm(self):
        out = []
        if self.dell:
            for fi in sorted(glob.glob(os.path.join(self.dell, "fan*_input"))):
                v = _read_int(fi)
                if v is not None:
                    out.append(v)
        return out

    def freq_mhz(self):
        freqs = [_read_int(p) for p in
                 glob.glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq")]
        freqs = [f for f in freqs if f]
        if not freqs:
            return (None, None)
        return (round(max(freqs) / 1000), round(sum(freqs) / len(freqs) / 1000))

    def _throttle_sum(self):
        total = 0
        for p in glob.glob(
                "/sys/devices/system/cpu/cpu*/thermal_throttle/core_throttle_count"):
            total += _read_int(p, 0) or 0
        return total

    def throttle_delta(self):
        return self._throttle_sum() - self._throttle_base


def detect_mode():
    gov = _read("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
    epp = _read("/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference")
    maxp = _read_int("/sys/devices/system/cpu/intel_pstate/max_perf_pct", 100)
    minp = _read_int("/sys/devices/system/cpu/intel_pstate/min_perf_pct", 0)
    if gov == "performance" and minp >= 100:
        return "extreme"
    if gov == "performance":
        return "standard"
    if epp == "balance_performance" and (maxp or 100) < 100:
        return "cool"
    return "normal"


def script_available():
    """True if the sudoers-whitelisted root perf.sh is deployed."""
    return os.path.exists(SCRIPT)


def set_mode(mode):
    """Switch perf mode via `sudo -n`. Returns (ok, message)."""
    if mode not in MODES:
        return False, f"unknown mode {mode!r}; have {sorted(MODES)}"
    if not script_available():
        return False, (f"{SCRIPT} not deployed — install the root-owned perf.sh + "
                       "sudoers rule (see README 'Deploy mode-switching')")
    flag = MODES[mode][1]
    try:
        r = subprocess.run(["sudo", "-n", SCRIPT, flag],
                           capture_output=True, text=True, timeout=30)
    except (OSError, subprocess.SubprocessError) as e:
        return False, str(e)
    if r.returncode != 0:
        return False, (r.stderr or r.stdout or f"exit {r.returncode}").strip()
    write_desired(mode)  # record intent so the watcher can reassert it (below)
    return True, (r.stdout or "ok").strip()


# --- power-aware desired-state maintenance -----------------------------------
# One chosen regime, reasserted whenever the platform silently resets the pstate
# caps underneath us. The trigger that started this: plugging in on AC lifts the
# battery turbo limit, so a "cool" set is quietly undone and temps jump ~25°C.
# Also fires on resume-from-suspend. The Bridge daemon runs run_watcher().
#
# "desired" is the mode the user actually chose (written by set_mode on both the
# tray and web faces). desired absent or "normal" == maintenance off, so we never
# fight a deliberate switch back to platform defaults.

DESIRED_PATH = os.environ.get(
    "PERF_DESIRED_FILE", os.path.expanduser("~/.cache/parvagues/perf-desired"))


def read_desired():
    """The mode to keep asserted, or None if maintenance is off."""
    m = _read(DESIRED_PATH)
    return m if m in MODES else None


def write_desired(mode):
    """Persist the chosen regime (called on every successful mode switch)."""
    if mode not in MODES:
        return
    try:
        os.makedirs(os.path.dirname(DESIRED_PATH) or ".", exist_ok=True)
        with open(DESIRED_PATH, "w") as f:
            f.write(mode)
    except OSError:
        pass


def on_ac():
    """True on AC, False on battery, None if no mains adapter is exposed."""
    for p in sorted(glob.glob("/sys/class/power_supply/A*/online")):
        v = _read_int(p)
        if v is not None:
            return bool(v)
    return None


def reconcile(force=False):
    """Reassert the desired mode if live state has drifted from it. Returns
    (acted, message). desired absent/"normal" == maintenance off."""
    desired = read_desired()
    if not desired or desired == "normal":
        return False, "maintenance off"
    if not force and detect_mode() == desired:
        return False, f"{desired} holding"
    ok, msg = set_mode(desired)
    return ok, (f"reasserted {desired} ({msg})" if ok else f"reassert FAILED ({msg})")


def run_watcher(interval=5.0, drift_every=6, log=None):
    """Daemon loop: keep the desired perf regime across AC/battery flips and
    resume-from-suspend. Reasserts on a power-source edge and on resume (both
    force), plus a periodic drift check as the safety net. Blocks; run in a
    thread. `log` is a callable(str); defaults to stdout."""
    emit = log or (lambda s: print(f"[perf-watch] {s}", flush=True))
    if read_desired() is None:              # seed from whatever is live right now
        cur = detect_mode()
        if cur in MODES and cur != "normal":
            write_desired(cur)
    emit(f"watching (every {interval}s) on_ac={on_ac()} desired={read_desired()}")
    last_ac = on_ac()
    last_clock = time.monotonic()
    i = 0
    while True:
        time.sleep(interval)
        i += 1
        now = time.monotonic()
        resumed = (now - last_clock) > interval * 3   # slept far longer than asked
        last_clock = now
        ac = on_ac()
        edge = ac is not None and last_ac is not None and ac != last_ac
        if edge or resumed:
            reason = ("power→" + ("AC" if ac else "battery")) if edge else "resume"
            time.sleep(2)                             # let the platform settle
            _, msg = reconcile(force=True)
            emit(f"{reason}: {msg}")
        elif i % drift_every == 0:
            acted, msg = reconcile(force=False)
            if acted:
                emit(f"drift: {msg}")
        last_ac = ac if ac is not None else last_ac


def snapshot(therm: "Thermals | None" = None) -> dict:
    """One JSON-ready reading of mode + thermals (the /api/perf payload)."""
    t = therm or Thermals()
    pkg = t.package_c()
    fmax, favg = t.freq_mhz()
    return {
        "mode": detect_mode(),
        "package_c": pkg,
        "temp_state": temp_state(pkg),
        "cores_c": t.cores_c(),
        "fans_rpm": t.fans_rpm(),
        "freq_max_mhz": fmax,
        "freq_avg_mhz": favg,
        "throttle_delta": t.throttle_delta(),
        "script_available": script_available(),
        "modes": {k: v[0] for k, v in MODES.items()},
    }
