"""GPU status for arbo's device-aware scheduler (GET /gpu-status).

arbo steers a lean train OFF the 3090 when TTS is live there (the 3090/TTS co-OOM concern,
contract §7). The worker can't decide that — it just reports the raw per-device VRAM plus a
`tts_on_3090` boolean derived from nvidia-smi compute-apps. Everything is nvidia-smi CSV
parsing; no torch import.

Device indices are reported under CUDA_DEVICE_ORDER=PCI_BUS_ID (index 0 = RTX 3090,
index 1 = RTX A6000) — the same ordering the training recipe pins, so `device_index` in a
/train request and the indices here mean the same physical card.
"""

from __future__ import annotations

import subprocess

from . import config

_SMI_ENV = {"CUDA_DEVICE_ORDER": "PCI_BUS_ID"}


def _run(args: list[str]) -> str:
    return subprocess.run(
        ["nvidia-smi", *args], capture_output=True, text=True, timeout=15, env={**_SMI_ENV}
    ).stdout


def _query_devices() -> list[dict]:
    # index,name,memory.total,memory.used,memory.free (MiB) under PCI_BUS_ID ordering
    out = _run(["--query-gpu=index,name,memory.total,memory.used,memory.free",
                "--format=csv,noheader,nounits"])
    devices = []
    for line in out.strip().splitlines():
        parts = [p.strip() for p in line.split(",")]
        if len(parts) != 5:
            continue
        idx, name, total, used, free = parts
        devices.append({
            "index": int(idx), "name": name,
            "total_mb": int(total), "used_mb": int(used), "free_mb": int(free),
            "procs": [],
        })
    return devices


def _query_compute_apps() -> list[dict]:
    # pid,used_memory,gpu_bus_id — map each compute app to a device by bus id
    out = _run(["--query-compute-apps=pid,used_memory,gpu_bus_id", "--format=csv,noheader,nounits"])
    apps = []
    for line in out.strip().splitlines():
        parts = [p.strip() for p in line.split(",")]
        if len(parts) != 3:
            continue
        pid, mem, bus = parts
        if not pid.isdigit():
            continue
        apps.append({"pid": int(pid), "used_mb": int(mem) if mem.isdigit() else 0, "bus_id": bus})
    return apps


def _bus_id_by_index() -> dict[int, str]:
    out = _run(["--query-gpu=index,gpu_bus_id", "--format=csv,noheader"])
    mapping = {}
    for line in out.strip().splitlines():
        parts = [p.strip() for p in line.split(",")]
        if len(parts) == 2 and parts[0].isdigit():
            mapping[int(parts[0])] = parts[1]
    return mapping


def _cmdline(pid: int) -> str:
    try:
        with open(f"/proc/{pid}/cmdline", "rb") as fh:
            return fh.read().replace(b"\x00", b" ").decode("utf-8", "replace").lower()
    except (OSError, ValueError):
        return ""


def gpu_status() -> dict:
    """Return `{devices: [...], tts_on_3090: bool}`. Degrades to an error field on nvidia-smi failure."""
    try:
        devices = _query_devices()
        apps = _query_compute_apps()
        bus_by_idx = _bus_id_by_index()
    except (subprocess.SubprocessError, OSError, ValueError) as exc:
        return {"devices": [], "tts_on_3090": False, "error": f"nvidia-smi failed: {exc}"}

    idx_by_bus = {bus: idx for idx, bus in bus_by_idx.items()}
    dev_by_idx = {d["index"]: d for d in devices}

    # Attach compute apps (with a marker flag) to their device.
    for app in apps:
        idx = idx_by_bus.get(app["bus_id"])
        cmd = _cmdline(app["pid"])
        app["is_tts"] = any(m in cmd for m in config.TTS_CMDLINE_MARKERS)
        if idx is not None and idx in dev_by_idx:
            dev_by_idx[idx]["procs"].append(
                {"pid": app["pid"], "used_mb": app["used_mb"], "is_tts": app["is_tts"]}
            )

    # tts_on_3090 — the co-OOM-risk signal for arbo's device-aware steering. Honest derivation
    # (transparent via `reason`), because the worker can't reliably fingerprint TTS: the TTS
    # containers run generic cmdlines (`uvicorn app:app`, `python app.py`) so only the container
    # NAME identifies them, and the worker's llmuser isn't in the docker group. So:
    #   (a) proc_marker — a compute app on device 0 whose cmdline matches a TTS marker (best effort), else
    #   (b) used_mb_floor — device 0 used >= floor (a busy 3090 = lean-train co-OOM risk, TTS/STT/audio alike).
    # The PRECISE lever arbo should key on is `devices[0].free_mb`; `tts_on_3090` is the convenience boolean.
    dev0 = dev_by_idx.get(config.DEVICE_3090)
    tts_on_3090, reason = False, "idle"
    free_3090 = None
    if dev0 is not None:
        free_3090 = dev0["free_mb"]
        if any(p["is_tts"] for p in dev0["procs"]):
            tts_on_3090, reason = True, "proc_marker"
        elif dev0["used_mb"] >= config.TTS_3090_USED_MB_FLOOR:
            tts_on_3090, reason = True, "used_mb_floor"
    return {
        "devices": devices,
        "tts_on_3090": tts_on_3090,
        "tts_on_3090_reason": reason,
        "gpu3090_free_mb": free_3090,  # the precise co-OOM lever; steer lean off the 3090 when low
    }
