"""kicad_diagnostics — read-only self-inspection so debugging the bridge never
needs Bridge's approval-gated `run_script`/shell.

Everything here runs INSIDE the bridge process, which already has full filesystem
+ subprocess access on the user's desktop (no approval prompt). During the
ADOMBASELINE bring-up we kept reaching for Bridge's `run_script` (powershell) just to
answer "where did KiCad install?", "what env does the bridge see?", "is pcbnew
running?" — all of which the bridge can answer directly. This verb exposes that,
so an agent can diagnose a detection/config/env problem with zero shell approval.

Args (all optional):
  probePaths: [str]  — for each path report exists/isDir/isFile/size + (dirs) a
                       capped child listing. Read-only metadata.
  readFile:   str    — return the text content of a single file if it's small
                       (<256 KB) and decodes as UTF-8. For peeking config/output.
  maxEntries: int    — cap on dir-listing length per probed dir (default 60).
"""

import os
import sys
import platform
import subprocess
from pathlib import Path

_READ_LIMIT = 256 * 1024
_ENV_KEYS = (
    "LOCALAPPDATA", "APPDATA", "USERPROFILE", "HOMEDRIVE", "HOMEPATH",
    "PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMDATA", "TEMP", "TMP",
    "ADOM_BIND_HOST", "PATH",
)
_PROC_NAMES = ("kicad", "kicad-cli", "pcbnew", "eeschema", "gerbview",
               "pl_editor", "bitmap2component", "pcb_calculator")


def _list_processes() -> list:
    """Running KiCad-family processes (name + pid + exe). On Windows uses the
    ctypes EnumProcesses API (instant, in-process) instead of spawning tasklist,
    which timed out dumping the full process table on a busy VM."""
    procs = []
    exes = {n + ".exe" for n in _PROC_NAMES}
    if sys.platform == "win32":
        try:
            import ctypes
            import ctypes.wintypes
            psapi = ctypes.windll.psapi
            kernel32 = ctypes.windll.kernel32
            PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
            arr = (ctypes.wintypes.DWORD * 8192)()
            br = ctypes.wintypes.DWORD()
            if not psapi.EnumProcesses(ctypes.byref(arr), ctypes.sizeof(arr), ctypes.byref(br)):
                return [{"error": "EnumProcesses failed"}]
            count = br.value // ctypes.sizeof(ctypes.wintypes.DWORD)
            buf = ctypes.create_unicode_buffer(1024)
            for i in range(count):
                pid = arr[i]
                if not pid:
                    continue
                h = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
                if not h:
                    continue
                try:
                    size = ctypes.wintypes.DWORD(1024)
                    if kernel32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
                        p = buf.value
                        if os.path.basename(p).lower() in exes:
                            procs.append({"name": os.path.basename(p), "pid": pid, "exe": p})
                finally:
                    kernel32.CloseHandle(h)
        except Exception as e:  # pylint: disable=broad-except
            return [{"error": f"EnumProcesses failed: {e}"}]
    else:
        try:
            out = subprocess.run(["ps", "-eo", "pid,comm"], capture_output=True,
                                 text=True, timeout=10).stdout
            for line in out.splitlines()[1:]:
                bits = line.split(None, 1)
                if len(bits) == 2 and any(n in bits[1].lower() for n in _PROC_NAMES):
                    procs.append({"name": bits[1].strip(), "pid": bits[0]})
        except Exception as e:  # pylint: disable=broad-except
            return [{"error": f"ps failed: {e}"}]
    return procs


def _probe(path_str: str, max_entries: int) -> dict:
    p = Path(path_str)
    info = {"path": str(p), "exists": False, "isDir": False, "isFile": False}
    try:
        info["exists"] = p.exists()
        if not info["exists"]:
            return info
        info["isDir"] = p.is_dir()
        info["isFile"] = p.is_file()
        if info["isFile"]:
            info["size"] = p.stat().st_size
        elif info["isDir"]:
            names = []
            for i, entry in enumerate(sorted(p.iterdir(), key=lambda e: e.name)):
                if i >= max_entries:
                    info["truncated"] = True
                    break
                names.append(entry.name + ("/" if entry.is_dir() else ""))
            info["entries"] = names
    except Exception as e:  # pylint: disable=broad-except
        info["error"] = str(e)
    return info


def handle_diagnostics(kicad_info: dict, args: dict) -> dict:
    args = args or {}
    out = {"success": True}

    # 1. What environment does THE BRIDGE see (not the desktop user — Bridge strips it).
    out["environment"] = {
        "vars": {k: os.environ.get(k) for k in _ENV_KEYS},
        "homeResolved": str(Path.home()),
        "pythonExe": sys.executable,
        "pythonVersion": sys.version.split()[0],
        "platform": platform.platform(),
        "pid": os.getpid(),
        "cwd": os.getcwd(),
    }

    # 2. KiCad-detection breakdown: every base the detector scans, whether it
    #    exists, and which version dirs / kicad.exe it finds. This is the direct
    #    lens on the "%LOCALAPPDATA% stripped → per-user install invisible" class.
    try:
        import kicad_detect
        bases = []
        for b in getattr(kicad_detect, "WIN_KICAD_BASES", []):
            bp = Path(b)
            entry = {"base": str(bp), "exists": bp.exists(), "versionDirs": []}
            if entry["exists"]:
                try:
                    for d in sorted(bp.iterdir()):
                        if d.is_dir():
                            exe = d / "bin" / "kicad.exe"
                            entry["versionDirs"].append(
                                {"dir": d.name, "kicadExe": str(exe), "exeExists": exe.exists()})
                except Exception as e:  # pylint: disable=broad-except
                    entry["error"] = str(e)
            bases.append(entry)
        out["kicadSearch"] = {
            "bases": bases,
            "detected": kicad_detect.detect_all_kicad_versions(),
        }
    except Exception as e:  # pylint: disable=broad-except
        out["kicadSearch"] = {"error": str(e)}

    # 3. Running KiCad-family processes.
    out["processes"] = _list_processes()

    # 4. The kicad_info the bridge is currently operating with + dir existence.
    ki = {k: kicad_info.get(k) for k in
          ("version", "activeVersion", "kicad_exe", "base_dir",
           "config_dir", "user_dir", "installed")}
    for key in ("config_dir", "user_dir", "base_dir"):
        v = kicad_info.get(key)
        ki[key + "_exists"] = bool(v and Path(v).exists())
    out["kicadInfo"] = ki

    # 5. Optional scoped path probes + a small-file read (read-only).
    probe_paths = args.get("probePaths") or []
    if isinstance(probe_paths, str):
        probe_paths = [probe_paths]
    max_entries = int(args.get("maxEntries", 60))
    if probe_paths:
        out["probes"] = [_probe(p, max_entries) for p in probe_paths]

    read_file = args.get("readFile")
    if read_file:
        rp = Path(read_file)
        fr = {"path": str(rp)}
        try:
            if not rp.is_file():
                fr["error"] = "not a file"
            elif rp.stat().st_size > _READ_LIMIT:
                fr["error"] = f"too large ({rp.stat().st_size} bytes > {_READ_LIMIT})"
            else:
                fr["content"] = rp.read_text(encoding="utf-8", errors="replace")
        except Exception as e:  # pylint: disable=broad-except
            fr["error"] = str(e)
        out["fileRead"] = fr

    out["_hint"] = (
        "Read-only bridge self-diagnostics — use this INSTEAD of Bridge's approval-gated "
        "run_script/shell when debugging the bridge. `environment` shows what env the "
        "BRIDGE sees (Bridge strips it — a missing LOCALAPPDATA here explains a per-user "
        "KiCad going undetected). `kicadSearch.bases` shows every path the detector "
        "scans and what it finds. `processes` lists running KiCad exes. Pass "
        "probePaths:[...] to stat/list any path and readFile:\"...\" to peek a small "
        "config/output file — all without a shell-approval prompt."
    )
    return out
