"""sshj.doctor — `sshj doctor`: machine-readable self-diagnostics (atuin doctor pattern).

Leaf (stdlib only): the JSON output surface over the EXISTING probes —
reuses selftest's probe functions and relay lookup, adds no probe logic.
Tiers:
  sshj doctor           full: sshj identity + probes (tty/kernel/relay) +
                        env/config/state + warnings; a human header line
                        above the JSON ("include this output with any
                        bug report") unless --json.
  sshj doctor --info    tier 2: env/config/state only — no probes.
Exit codes: 0 OK (warnings allowed) · 1 a hard-unavailable: the relay
route is dead (TIOCSTI refused with no relay, or no /bin/bash on the
relay route) or python below the 3.8 floor. A no-tty run is a WARNING,
not a hard — doctor must stay runnable from a pipe (that's how a bug
report collects it).
"""
import json
import os
import platform
import re
import shutil
import sys

from . import selftest
from .discover import config_files
from .jsonio import STATE_DIR
from .theme import VER

# The per-launch var the grid exports to the relay — diagnostic noise,
# never report it.
GRID_ROW_ENV = "SSHJ_RELAY_GRID_ROW"
# The relay's prompt-render deadline (src/sshj-relay reads the same var).
PROMPT_WAIT_ENV = "SSHJ_RELAY_PROMPT_WAIT"
PROMPT_WAIT_DEFAULT = 5.0
# The state files doctor reports on (jsonio owns the dir, these the names).
STATE_FILES = ("last_used.json", "identity.json", "update.flag")


def _identity():
    """The sshj identity block (version/python/os/arch) — no probes."""
    v = sys.version_info
    return {"version": VER,
            "python": "%d.%d.%d" % (v.major, v.minor, v.micro),
            "os": sys.platform, "arch": platform.machine()}


def _kernel(ttiocsti):
    """The kernel block: release + TIOCSTI state + the resulting route."""
    _kver, rel = selftest._kernel_version()
    return {"release": rel,
            "tiocsti": "available" if ttiocsti else "unavailable",
            "route": "direct" if ttiocsti else "relay"}


def _relay():
    """The relay block: presence, the /bin/bash child, starship, prompt wait."""
    path = selftest._relay_path()
    # starship lookup mirrors the relay itself (SSHJ_STARSHIP, then
    # ~/.local/bin, then PATH — the relay draws the prompt, not us).
    star = os.environ.get("SSHJ_STARSHIP") or ""
    if not star:
        local = os.path.expanduser("~/.local/bin/starship")
        if os.path.exists(local) and os.access(local, os.X_OK):
            star = local
    if not star:
        star = shutil.which("starship") or ""
    try:
        wait = float(os.environ.get(PROMPT_WAIT_ENV, PROMPT_WAIT_DEFAULT))
    except ValueError:
        wait = PROMPT_WAIT_DEFAULT
    return {"present": path is not None,
            "path": path,
            "shell": ("present"
                      if os.path.exists("/bin/bash")
                      and os.access("/bin/bash", os.X_OK)
                      else "missing"),
            "starship": star or "none",
            "prompt_wait": wait}


def _env():
    """Every SSHJ_* var set (with its value), minus the per-launch grid-row var."""
    return dict(sorted((k, os.environ[k]) for k in os.environ
                       if k.startswith("SSHJ_") and k != GRID_ROW_ENV))


def _config():
    """The ssh config files actually read + the SSHJ_CONFIG/SSH_CONFIG extras."""
    return {"files": config_files(),
            "extra": {"SSHJ_CONFIG": os.environ.get("SSHJ_CONFIG"),
                      "SSH_CONFIG": os.environ.get("SSH_CONFIG")}}


def _state():
    """The state dir + which of its files are present."""
    return {"dir": STATE_DIR,
            "files": [n for n in STATE_FILES
                      if os.path.exists(os.path.join(STATE_DIR, n))]}


def _classify(t_status, s_status, s_det, sh_status, sh_det,
              ttiocsti, relay):
    """Split findings into (soft warnings, hard unavailables)."""
    soft, hard = [], []
    if t_status == "FAIL":
        soft.append("no tty (stdin not a terminal) — the grid can't open; "
                    "use sshj <host> (direct mode) or ssh -T host sshj")
    if not ttiocsti:
        if not relay["present"]:
            hard.append("TIOCSTI unavailable and sshj-relay missing — "
                        "two-stage Tab is dead (make install)")
        elif relay["shell"] == "missing":
            hard.append("relay route active but /bin/bash missing — the "
                        "relay child shell is unavailable")
        if relay["starship"] == "none":
            soft.append("starship not found — the relay prompt prefix "
                        "will be blank")
    elif relay["shell"] == "missing":
        soft.append("/bin/bash missing — the relay fallback won't work "
                    "(TIOCSTI direct stuff covers it)")
    if s_status == "WARN":
        soft.append("terminal size: " + s_det)
    if sh_status == "WARN":
        soft.append("shell: " + sh_det)
    return soft, hard


def run(args):
    """Build the document, print header (unless --json) + JSON, return the exit code (0/1)."""
    info = "--info" in args
    json_only = "--json" in args
    doc = {"sshj": _identity()}
    hard = []
    if info:
        doc["env"] = _env()
        doc["config"] = _config()
        doc["state"] = _state()
    else:
        ttiocsti = selftest._ttiocsti_works()
        t_status, _t_det, _t_msg = selftest.probe_tty()
        s_status, s_det, _s_msg = selftest.probe_size()
        sh_status, sh_det, _sh_msg = selftest.probe_shell()
        py_status, py_det, _py_msg = selftest.probe_python()
        relay = _relay()
        # the size [rows, cols] is parsed out of probe_size's detail
        # string — the probe is the single measurement (no duplicate
        # TIOCGWINSZ here)
        m = re.search(r"(\d+) rows x (\d+) cols", s_det)
        doc["tty"] = {"stdin_isatty": sys.stdin.isatty(),
                      "stdout_isatty": sys.stdout.isatty(),
                      "size": [int(m.group(1)), int(m.group(2))] if m
                      else None}
        doc["kernel"] = _kernel(ttiocsti)
        doc["relay"] = relay
        soft, hard = _classify(t_status, s_status, s_det, sh_status,
                               sh_det, ttiocsti, relay)
        if py_status == "FAIL":
            hard.append("python " + py_det)
        doc["env"] = _env()
        doc["config"] = _config()
        doc["state"] = _state()
        doc["warnings"] = soft
        doc["hard"] = hard
    if not json_only:
        print("sshj doctor (v%s) — include this output with any bug report"
              % VER)
    print(json.dumps(doc, indent=2, sort_keys=True))
    return 1 if hard else 0
