"""
Power / performance backend — CPU/GPU/DMC governors, battery, thermals — read
from sysfs and (for governors) written via the device's own knobs. Same in-shell,
pure-core + tolerant-live + gated pattern as netops/storageops.

Grounding (from dArkOS's sleep_governors.sh, captured in
docs/DEVICE_DARKOS_SCRIPTS.md): the R36S is an **RK3326**, so:
  • CPU governor : /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
  • GPU governor : /sys/devices/platform/ff400000.gpu/devfreq/ff400000.gpu/governor
  • DMC governor : /sys/devices/platform/dmc/devfreq/dmc/governor
Battery/thermal paths vary by board, so the live layer SCANS the standard
sysfs classes and degrades to None — never assumes a fixed node.

All writes go through `sudo -n` (passwordless `ark`, like perfmax in launch.sh)
with captured output → nothing ever reaches a console.
"""

from __future__ import annotations

import glob
import os
import subprocess
from typing import Optional

_SUDO = [] if os.environ.get("AAHEAD_SUDO") == "0" else ["sudo", "-n"]

_CPU_GOV = "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor"
_CPU_AVAIL = "/sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors"
_GPU_GOV = "/sys/devices/platform/ff400000.gpu/devfreq/ff400000.gpu/governor"  # RK3326
_DMC_GOV = "/sys/devices/platform/dmc/devfreq/dmc/governor"

# User-facing profile → preferred CPU governor, best-match against what the
# kernel actually offers (scaling_available_governors).
PROFILES = ("powersave", "balanced", "performance")
_PROFILE_GOV = {
    "powersave":   ["powersave"],
    "balanced":    ["schedutil", "ondemand", "interactive", "conservative"],
    "performance": ["performance"],
}


# ── pure parsers (testable, no device) ────────────────────────────────

def parse_governors(text: str) -> list[str]:
    """scaling_available_governors → list of governor names."""
    return text.split()


def governor_for_profile(profile: str, available: list[str]) -> Optional[str]:
    """Pick the best real governor for a profile given what the kernel offers."""
    for g in _PROFILE_GOV.get(profile, []):
        if g in available:
            return g
    return None


def profile_for_governor(gov: str) -> str:
    """Reverse map a live governor → the profile bucket it belongs to."""
    for prof, govs in _PROFILE_GOV.items():
        if gov in govs:
            return prof
    return "balanced"


def parse_temp_c(milli) -> Optional[float]:
    """Thermal-zone temp (milli-°C as in sysfs) → °C."""
    try:
        v = int(str(milli).strip())
    except (TypeError, ValueError):
        return None
    # sysfs thermal is milli-°C; guard against already-°C nodes
    return round(v / 1000.0, 1) if v > 1000 else float(v)


def parse_battery(capacity, status, voltage_uv) -> dict:
    """Raw power_supply values → {percent, status, mv}. Tolerant of blanks."""
    out = {"percent": None, "status": None, "mv": None}
    try:
        out["percent"] = max(0, min(100, int(str(capacity).strip())))
    except (TypeError, ValueError):
        pass
    s = (status or "").strip()
    out["status"] = s or None
    try:
        out["mv"] = round(int(str(voltage_uv).strip()) / 1000)   # µV → mV
    except (TypeError, ValueError):
        pass
    return out


# ── human temperature language (no numbers in the UI by default) ──────
# Rough thresholds tuned for the RK3326 (idles ~45-55 °C, loads to 70-85 °C
# under heavy cores). The point is a FEELING — cool / warm / hot — not
# precision; refine on-device (P1.E) if the verdict ever feels off.
_WARM_C = 60.0
_HOT_C = 78.0


def temp_verdict(c: Optional[float]) -> Optional[str]:
    """A temperature (°C) → a human word: 'cool' / 'warm' / 'hot'. None when
    there's no reading at all."""
    if c is None:
        return None
    if c >= _HOT_C:
        return "hot"
    if c >= _WARM_C:
        return "warm"
    return "cool"


def friendly_zone(label: str) -> tuple[str, str]:
    """A raw thermal-zone type ('soc-thermal', 'gpu-thermal', …) → a
    human (name, kind). kind ∈ {cpu, gpu, battery, other} is used to dedupe
    and order; name is what the user reads."""
    s = (label or "").lower()
    if "gpu" in s or "mali" in s:
        return ("Graphics", "gpu")
    if "cpu" in s or "soc" in s or "core" in s:
        return ("Processor", "cpu")
    if "batt" in s:
        return ("Battery", "battery")
    clean = ((label or "").replace("-thermal", "").replace("-", " ")
             .replace("_", " ").strip().title())
    return (clean or "Sensor", "other")


def summarize_thermals(zones: list[dict]) -> dict:
    """Reduce raw zones [{label, c}] to a human summary:
        {verdict, hottest, parts: [{name, kind, c, verdict}]}
    One part per kind (hottest wins), ordered Processor → Graphics → Battery →
    other. `verdict`/`hottest` are None when nothing readable is present."""
    by_kind: dict[str, dict] = {}
    hottest = None
    for z in zones or []:
        c = z.get("c")
        if c is None:
            continue
        hottest = c if hottest is None else max(hottest, c)
        name, kind = friendly_zone(z.get("label", ""))
        if kind not in by_kind or c > by_kind[kind]["c"]:
            by_kind[kind] = {"name": name, "kind": kind, "c": c,
                             "verdict": temp_verdict(c)}
    order = {"cpu": 0, "gpu": 1, "battery": 2, "other": 3}
    parts = sorted(by_kind.values(), key=lambda p: order.get(p["kind"], 9))
    return {"verdict": temp_verdict(hottest), "hottest": hottest, "parts": parts}


# ── live layer (device; tolerant; never raises) ──────────────────────

def _read(path: str) -> Optional[str]:
    try:
        with open(path, encoding="utf-8", errors="replace") as fh:
            return fh.read().strip()
    except OSError:
        return None


def _write_sudo(path: str, value: str) -> bool:
    """Write `value` to a root-owned sysfs node via `sudo -n tee`. Captured."""
    try:
        r = subprocess.run([*_SUDO, "tee", path], input=value + "\n",
                           capture_output=True, text=True, timeout=10)
        return r.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return False


def available_governors() -> list[str]:
    return parse_governors(_read(_CPU_AVAIL) or "")


def governor() -> Optional[str]:
    return _read(_CPU_GOV)


def set_governor(profile: str) -> bool:
    """Apply a profile by writing the matching governor to CPU (and best-effort
    GPU/DMC). True if the CPU governor was set."""
    gov = governor_for_profile(profile, available_governors())
    if not gov:
        return False
    ok = _write_sudo(_CPU_GOV, gov)
    # GPU/DMC are best-effort: they may not accept the same names; ignore failure
    for node in (_GPU_GOV, _DMC_GOV):
        if os.path.exists(node):
            _write_sudo(node, gov)
    return ok


def battery() -> Optional[dict]:
    """Scan /sys/class/power_supply for a battery → {percent, status, mv},
    or None when nothing battery-like is present (dev box / desktop)."""
    for base in sorted(glob.glob("/sys/class/power_supply/*")):
        kind = _read(os.path.join(base, "type")) or ""
        cap = _read(os.path.join(base, "capacity"))
        if kind.lower() == "battery" and cap is not None:
            return parse_battery(cap, _read(os.path.join(base, "status")),
                                 _read(os.path.join(base, "voltage_now")))
    return None


def thermals() -> list[dict]:
    """All thermal zones → [{label, c}], hottest first. [] if none."""
    out = []
    for base in sorted(glob.glob("/sys/class/thermal/thermal_zone*")):
        c = parse_temp_c(_read(os.path.join(base, "temp")))
        if c is None:
            continue
        label = (_read(os.path.join(base, "type"))
                 or os.path.basename(base))
        out.append({"label": label, "c": c})
    out.sort(key=lambda z: z["c"], reverse=True)
    return out


def status() -> dict:
    """One-shot snapshot for the Power screens."""
    gov = governor()
    return {
        "governor": gov,
        "profile": profile_for_governor(gov) if gov else None,
        "available": available_governors(),
        "battery": battery(),
        "thermals": thermals(),
    }
