"""
gearbox — Python client for the thermal gearbox (SRE/thermal, GEARBOX.md).

ONE source of the gearbox vocabulary for every face: the Qt tray (perf-tray.py,
the face PLN actually uses mid-set), the web Bridge, and anything else that grows
later. The engine itself is shell and lives in the SRE repo — this module never
duplicates its POLICY, it only names the gears and shells out.

The model, in one paragraph (the long version is SRE/thermal/GEARBOX.md):

  Two ORTHOGONAL axes, not one list of modes.
    GEAR — how hard the machine may work: off / standard / aggressive
    TRIM — how the heat gets paid for:    quiet / auto / cool
  Trim applies in EVERY gear. Quiet and cool are in tension: you cannot be fast
  AND quiet AND cool, and the whole point of splitting the axes is that the UI
  stops pretending you can.

Reading is unprivileged (a plain file); only SWITCHING needs root, via
`sudo -n` against the root-owned, sudoers-whitelisted helper — the same contract
perf.py uses for perf-audio. `-n` matters: the tray has no tty, so an
interactive sudo would hang a menu click forever instead of failing loudly.
"""

import os
import subprocess
from pathlib import Path

# Root-owned helper installed by SRE/thermal/install.sh. Every one of the argv
# forms built below is an exact-args NOPASSWD rule in /etc/sudoers.d/thermal-mode
# — that exactness is the security model, so DO NOT interpolate anything into
# these commands beyond a key validated against the tables below.
HELPER = os.environ.get("THERMAL_MODE_HELPER", "/usr/local/sbin/thermal-mode-apply")
CONF = Path(os.environ.get("THERMAL_MODE_CONF", "/etc/thermal-mode.conf"))

# gear key -> (label, blurb). Order = the lever's travel, idle → hard.
GEARS = {
    "off": ("Off", "stop steering — ppd gets the knobs back"),
    "standard": ("Standard", "the everyday livecoding regime"),
    "aggressive": ("Aggressive", "full watts the source can give, turbo, RT audio"),
}

# trim key -> (label, blurb). Order = quiet → cool, the axis it actually is.
TRIMS = {
    "quiet": ("Quiet", "minimise noise; heat and sustain pay for it"),
    "auto": ("Auto", "let the platform balance it"),
    "cool": ("Cool", "minimise temperature; fans are allowed to work"),
}

# What the helper does when nothing has ever set a trim.
DEFAULT_TRIM = "auto"


def installed():
    """True when the root helper is present and executable.

    Absent is a legitimate state (the gearbox lives in a sibling repo and may
    simply not be installed on this box), so every caller must be able to render
    "not installed" instead of implying a gear.
    """
    return os.access(HELPER, os.X_OK)


def read_conf(conf=None):
    """The helper's persisted state as a dict. Never raises."""
    out = {}
    try:
        for line in Path(conf or CONF).read_text().splitlines():
            if "=" in line and not line.lstrip().startswith("#"):
                k, v = line.split("=", 1)
                out[k.strip()] = v.strip()
    except OSError:
        pass
    return out


def state(conf=None):
    """Where the lever is, read unprivileged.

    `gear`/`trim` are None when never set — that is NOT the same as "off" (a
    deliberate off) and the distinction is the whole reason `off` had to become
    an explicit gear rather than the absence of one.
    """
    c = read_conf(conf)
    gear = c.get("GEAR") or None
    trim = c.get("TRIM") or None
    return {
        "installed": installed(),
        "gear": gear if gear in GEARS else None,
        "trim": trim if trim in TRIMS else None,
        # The finer five-mode name the gear resolved to, and the Dell/ACPI
        # profile last pushed. Diagnostics: they explain a gear, they are not it.
        "mode": c.get("MODE") or None,
        "bios": c.get("BIOS_PROFILE") or None,
    }


def gear_argv(key):
    """argv to engage a gear. Raises ValueError on anything not in the table —
    the sudoers rules are exact-args, so an unvalidated key can only produce a
    confusing sudo refusal instead of an honest error."""
    if key not in GEARS:
        raise ValueError(f"unknown gear: {key!r}")
    return ["sudo", "-n", HELPER, key]


def trim_argv(key):
    """argv to set the trim. `--trim <key>` is two words on purpose: that is the
    exact pair the sudoers rule whitelists."""
    if key not in TRIMS:
        raise ValueError(f"unknown trim: {key!r}")
    return ["sudo", "-n", HELPER, "--trim", key]


def apply(kind, key, timeout=25):
    """Blocking switch, for the web face and tests. Returns (ok, message).

    The Qt tray does NOT use this — it drives the same argv through QProcess so a
    menu click cannot freeze the tray while RAPL zones and fan profiles settle
    (measured: an aggressive→standard switch takes about a second, and the RT
    ladder walk can add more).
    """
    argv = gear_argv(key) if kind == "gear" else trim_argv(key)
    if not installed():
        return False, f"{HELPER} not installed (see SRE/thermal/install.sh)"
    try:
        r = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)
    except (OSError, subprocess.SubprocessError) as e:
        return False, str(e)
    out = (r.stdout or "").strip() or (r.stderr or "").strip()
    if r.returncode != 0:
        return False, out or "sudo -n refused (check /etc/sudoers.d/thermal-mode)"
    return True, out


def label(kind, key):
    """Human label for a key, or the key itself when it is unknown/None. Faces
    show what the file SAYS even if this module has never heard of it — a
    gearbox newer than its UI must not render a blank."""
    table = GEARS if kind == "gear" else TRIMS
    if key in table:
        return table[key][0]
    return key or "—"


def summary(conf=None):
    """One line for a tooltip or a status row: "standard · cool"."""
    s = state(conf)
    if not s["installed"] and not s["gear"]:
        return "not installed"
    g = label("gear", s["gear"]) if s["gear"] else "unset"
    t = label("trim", s["trim"] or DEFAULT_TRIM)
    return f"{g} · {t}"
