"""Read-only machine-activity inspection for dashboard chat diagnostics.

Backs the ``inspect_system_activity`` agent tool: answers "why is my computer
slow?" / "what is downloading right now?" with per-process evidence. Every
collector here is read-only, runs without sudo, and is bounded by short
timeouts so a tool call stays in the low single-digit seconds.

macOS specifics:
- ``nettop`` provides per-process network counters (two samples diffed for
  live rates) — psutil cannot attribute network traffic per process on macOS.
- ``pmset -g therm`` reports thermal throttling state.
Both degrade gracefully (rows omitted, note added) on other platforms.
"""

from __future__ import annotations

import os
import subprocess
import time

NETTOP_TIMEOUT_SECONDS = 15.0
_NAME_MAX = 48


# ---------------------------------------------------------------------------
# Collectors (structured dicts; formatting lives in the render_* helpers)
# ---------------------------------------------------------------------------


def _run_nettop(sample_seconds: int) -> str:
    """Two cumulative per-process samples, ``sample_seconds`` apart (CSV)."""
    out = subprocess.run(
        ["nettop", "-P", "-x", "-L", "2", "-s", str(sample_seconds)],
        capture_output=True,
        timeout=NETTOP_TIMEOUT_SECONDS,
    )
    if out.returncode != 0:
        raise RuntimeError(f"nettop exited {out.returncode}")
    return out.stdout.decode("utf-8", errors="replace")


def _resolve_process_name(pid: str, fallback: str) -> str:
    """nettop truncates names ("Cursor Helper ("); recover the full one via psutil."""
    if pid.isdigit():
        try:
            import psutil

            full = (psutil.Process(int(pid)).name() or "").strip()
            if full:
                return full[:_NAME_MAX]
        except Exception:
            pass
    return fallback[:_NAME_MAX]


def network_by_process(sample_seconds: int = 1, top_n: int = 8) -> dict:
    """Live per-process down/up rates by diffing two nettop samples."""
    interval = max(1, int(sample_seconds))
    try:
        raw = _run_nettop(interval)
    except FileNotFoundError:
        return {"error": "nettop is unavailable on this system.", "rows": []}
    except Exception as e:
        return {"error": f"nettop failed: {e}", "rows": []}

    # Row shape: time,process.pid,interface,state,bytes_in,bytes_out,...
    counters: dict[str, list[tuple[int, int]]] = {}
    for line in raw.splitlines():
        parts = line.split(",")
        if len(parts) < 6 or parts[0].strip().lower() == "time":
            continue
        proc = parts[1].strip()
        if not proc:
            continue
        try:
            b_in = int(parts[4] or 0)
            b_out = int(parts[5] or 0)
        except ValueError:
            continue
        counters.setdefault(proc, []).append((b_in, b_out))

    rows: list[dict] = []
    total_down = 0.0
    total_up = 0.0
    for proc, samples in counters.items():
        if len(samples) < 2:
            continue
        d_in = max(0, samples[-1][0] - samples[0][0])
        d_out = max(0, samples[-1][1] - samples[0][1])
        down_kbps = d_in / interval / 1024.0
        up_kbps = d_out / interval / 1024.0
        total_down += down_kbps
        total_up += up_kbps
        if d_in == 0 and d_out == 0:
            continue
        name, _, pid = proc.rpartition(".")
        rows.append(
            {
                "name": _resolve_process_name(pid, name or proc),
                "pid": pid if pid.isdigit() else "",
                "down_kbps": round(down_kbps, 1),
                "up_kbps": round(up_kbps, 1),
            }
        )
    rows.sort(key=lambda r: r["down_kbps"] + r["up_kbps"], reverse=True)
    return {
        "interval_seconds": interval,
        "rows": rows[: max(1, top_n)],
        "total_down_kbps": round(total_down, 1),
        "total_up_kbps": round(total_up, 1),
        "error": None,
    }


def thermal_state() -> str:
    """Thermal/throttle summary from `pmset -g therm` ("nominal" when healthy)."""
    try:
        out = subprocess.run(["pmset", "-g", "therm"], capture_output=True, timeout=5.0)
        text = out.stdout.decode("utf-8", errors="replace")
    except Exception:
        return ""
    limit = None
    for line in text.splitlines():
        if "CPU_Speed_Limit" in line:
            try:
                limit = int(line.split("=")[-1].strip())
            except ValueError:
                pass
    if limit is not None and limit < 100:
        return f"CPU is thermally throttled to {limit}% of max speed"
    if "No thermal warning" in text or limit is not None:
        return "nominal (no thermal throttling)"
    return ""


def cpu_by_process(top_n: int = 8, interval: float = 0.5) -> dict:
    """Per-process CPU over a short measured window + load average + thermal."""
    import psutil

    procs = []
    for p in psutil.process_iter(["pid", "name"]):
        try:
            p.cpu_percent(interval=None)  # prime the per-process baseline
            procs.append(p)
        except Exception:
            continue
    time.sleep(max(0.1, interval))
    rows: list[dict] = []
    for p in procs:
        try:
            cpu = p.cpu_percent(interval=None)
            name = (p.info.get("name") or "").strip()
        except Exception:
            continue
        if not name or cpu <= 0.0:
            continue
        rows.append({"pid": int(p.info.get("pid") or 0), "name": name[:_NAME_MAX], "cpu_percent": round(cpu, 1)})
    rows.sort(key=lambda r: r["cpu_percent"], reverse=True)

    cores = psutil.cpu_count() or 1
    try:
        load1, load5, load15 = os.getloadavg()
    except OSError:
        load1 = load5 = load15 = 0.0
    return {
        "rows": rows[: max(1, top_n)],
        "total_percent": round(float(psutil.cpu_percent(interval=None)), 1),
        "cores": cores,
        "load_1m": round(load1, 2),
        "load_5m": round(load5, 2),
        "load_15m": round(load15, 2),
        "thermal": thermal_state(),
    }


def memory_breakdown(top_n: int = 8) -> dict:
    """Memory + swap pressure and the top RSS consumers."""
    import psutil

    vm = psutil.virtual_memory()
    try:
        swap = psutil.swap_memory()
    except Exception:
        swap = None
    rows: list[dict] = []
    gib = 1024.0**3
    for p in psutil.process_iter(["pid", "name", "memory_info"]):
        try:
            info = p.info
            mi = info.get("memory_info")
            name = (info.get("name") or "").strip()
        except Exception:
            continue
        if not name or mi is None:
            continue
        rss_gb = float(mi.rss) / gib
        if rss_gb < 0.05:
            continue
        rows.append({"pid": int(info.get("pid") or 0), "name": name[:_NAME_MAX], "rss_gb": round(rss_gb, 2)})
    rows.sort(key=lambda r: r["rss_gb"], reverse=True)

    # macOS swaps opportunistically, so swap alone only signals "elevated".
    if vm.percent >= 90.0:
        pressure = "high"
    elif vm.percent >= 75.0 or (swap is not None and swap.total > 0 and swap.percent >= 70.0):
        pressure = "elevated"
    else:
        pressure = "normal"
    return {
        "percent": round(float(vm.percent), 1),
        "used_gb": round(vm.used / gib, 1),
        "total_gb": round(vm.total / gib, 1),
        "swap_used_gb": round(swap.used / gib, 2) if swap is not None else None,
        "swap_total_gb": round(swap.total / gib, 2) if swap is not None else None,
        "pressure": pressure,
        "rows": rows[: max(1, top_n)],
    }


def disk_activity(interval: float = 1.0) -> dict:
    """Whole-machine read/write throughput (sampled) + per-volume usage."""
    import psutil

    io1 = psutil.disk_io_counters()
    time.sleep(max(0.2, interval))
    io2 = psutil.disk_io_counters()
    read_mbps = write_mbps = None
    if io1 is not None and io2 is not None:
        dt = max(0.2, interval)
        read_mbps = round(max(0, io2.read_bytes - io1.read_bytes) / dt / (1024.0**2), 2)
        write_mbps = round(max(0, io2.write_bytes - io1.write_bytes) / dt / (1024.0**2), 2)

    volumes: list[dict] = []
    gib = 1024.0**3
    seen_mounts: set[str] = set()
    try:
        parts = psutil.disk_partitions(all=False)
    except Exception:
        parts = []
    for part in parts:
        mount = part.mountpoint
        # Skip macOS system-volume and cryptex noise; keep / and real data volumes.
        if (
            mount in seen_mounts
            or (mount.startswith("/System/Volumes/") and mount != "/System/Volumes/Data")
            or mount.startswith("/private/var/run/com.apple.security.cryptexd/")
        ):
            continue
        seen_mounts.add(mount)
        try:
            u = psutil.disk_usage(mount)
        except Exception:
            continue
        volumes.append(
            {
                "mount": mount[:120],
                "used_gb": round(u.used / gib, 1),
                "total_gb": round(u.total / gib, 1),
                "percent": round(float(u.percent), 1),
            }
        )
    return {"read_mbps": read_mbps, "write_mbps": write_mbps, "volumes": volumes[:6]}


# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------


def _fmt_kbps(kbps: float) -> str:
    if kbps >= 1024.0:
        return f"{kbps / 1024.0:.1f} MB/s"
    return f"{kbps:.0f} KB/s"


def render_network(d: dict) -> str:
    if d.get("error"):
        return f"Per-process network data unavailable: {d['error']}"
    rows = d.get("rows") or []
    lines = [
        f"Per-process network activity (measured over {d.get('interval_seconds', 1)}s just now):",
        f"Machine total: down {_fmt_kbps(d.get('total_down_kbps') or 0.0)} / up {_fmt_kbps(d.get('total_up_kbps') or 0.0)}",
    ]
    if not rows:
        lines.append("No process moved measurable traffic during the sample window — the network is effectively idle right now.")
        return "\n".join(lines)
    for r in rows:
        pid = f" (pid {r['pid']})" if r.get("pid") else ""
        lines.append(f"- {r['name']}{pid}: down {_fmt_kbps(r['down_kbps'])} / up {_fmt_kbps(r['up_kbps'])}")
    return "\n".join(lines)


def render_cpu(d: dict) -> str:
    cores = d.get("cores") or 1
    lines = [
        f"CPU: {d.get('total_percent', '?')}% overall across {cores} cores.",
        f"Load average: {d.get('load_1m')} (1m) / {d.get('load_5m')} (5m) / {d.get('load_15m')} (15m)"
        f" — above {cores}.0 means work is queueing.",
    ]
    thermal = (d.get("thermal") or "").strip()
    if thermal:
        lines.append(f"Thermal: {thermal}.")
    rows = d.get("rows") or []
    if rows:
        lines.append("Top CPU consumers (measured just now; 100% = one full core):")
        for r in rows:
            lines.append(f"- {r['name']} (pid {r['pid']}): {r['cpu_percent']}%")
    else:
        lines.append("No process is using measurable CPU right now.")
    return "\n".join(lines)


def render_memory(d: dict) -> str:
    lines = [
        f"Memory: {d.get('used_gb')}/{d.get('total_gb')} GB used ({d.get('percent')}%) — pressure {d.get('pressure')}.",
    ]
    swap_total = d.get("swap_total_gb")
    if swap_total:
        lines.append(f"Swap: {d.get('swap_used_gb')}/{swap_total} GB in use (heavy swap = slowdowns).")
    rows = d.get("rows") or []
    if rows:
        lines.append("Top RAM consumers:")
        for r in rows:
            lines.append(f"- {r['name']} (pid {r['pid']}): {r['rss_gb']} GB")
    return "\n".join(lines)


def render_disk(d: dict) -> str:
    lines: list[str] = []
    if d.get("read_mbps") is not None:
        lines.append(
            f"Disk throughput right now: read {d['read_mbps']} MB/s / write {d['write_mbps']} MB/s (whole machine)."
        )
    vols = d.get("volumes") or []
    if vols:
        lines.append("Volumes:")
        for v in vols:
            lines.append(f"- {v['mount']}: {v['used_gb']}/{v['total_gb']} GB ({v['percent']}%)")
    return "\n".join(lines) if lines else "Disk data unavailable."


def inspect(focus: str = "overview") -> str:
    """Run the collectors for ``focus`` and return an agent-readable report."""
    f = (focus or "overview").strip().lower()
    if f == "network":
        return render_network(network_by_process())
    if f == "cpu":
        return render_cpu(cpu_by_process())
    if f == "memory":
        return render_memory(memory_breakdown())
    if f == "disk":
        return render_disk(disk_activity())
    # Overview: everything except the slower nettop pass.
    sections = [
        render_cpu(cpu_by_process(top_n=5)),
        render_memory(memory_breakdown(top_n=5)),
        render_disk(disk_activity(interval=0.5)),
        "(For per-process network rates, call again with focus=\"network\".)",
    ]
    return "\n\n".join(s for s in sections if s)
