"""sshj.selftest — `sshj --selftest`: probe env, degrade w/ a message, never crash.

Leaf (stdlib only). Six probes per worklog/rq6-selftest-probes.md (RQ6):
tty (the grid gate: stdin-isatty, RQ8) · size (TIOCGWINSZ, 10x50 min) ·
kernel (TIOCSTI on a throwaway pty — never the real tty, spec 5.3) ·
shell (the ACTUAL shell = the parent's /proc/<pid>/comm, $SHELL fallback,
spec 5.4) · relay (sshj-relay, REQUIRED whenever TIOCSTI is refused; the
/bin/bash child sub-check rides along, spec P5 note) · python (floor 3.8).
run(ver) prints the table to stdout (2-space indent, 4-char status col,
2-space gap, 9-char name col — exactly the spec section 4 sample), one degrade message per WARN/FAIL to stderr, and returns
the exit code: 0 all OK · 1 warnings only · 2 any FAIL. No global state,
no network, no writes.
"""

import os
import re
import shutil
import struct
import sys

try:
    import fcntl
    import termios
except ImportError:        # non-Linux: the probes degrade, never crash
    fcntl = None
    termios = None

FLOOR = (3, 8)

MSG_NO_TTY = ("sshj: no terminal — the grid can't open; run sshj <host> "
              "(direct mode) or use ssh -T host sshj")
MSG_NO_PREFILL = ("sshj: this kernel can't prefill the shell line — "
                  "two-stage Tab is off; Enter still connects, "
                  "Tab prints the command")
MSG_NO_RELAY = ("sshj: sshj-relay not found — two-stage Tab is off; "
                "run 'make install' or add ~/.local/bin to PATH")
MSG_NO_BASH = "sshj: no /bin/bash (relay child shell missing)"


def _termios_ok():
    return fcntl is not None and termios is not None


def _kernel_version():
    """(major.minor, full release) — first \\d+\\.\\d+ of os.uname().release."""
    rel = os.uname().release
    m = re.search(r"(\d+)\.(\d+)", rel)
    if m:
        return m.group(1) + "." + m.group(2), rel
    return rel, rel


def _ttiocsti_works():
    """True when TIOCSTI can stuff the input queue (kernel < 6.2).

    A throwaway pty (spec 5.3) — never ioctl the user's real tty, even
    though the refusal is harmless: we don't depend on refusal semantics."""
    if not _termios_ok():
        return False
    try:
        import pty
        m, s = pty.openpty()
    except Exception:
        return False
    try:
        fcntl.ioctl(s, termios.TIOCSTI, b"a")
        return True
    except OSError:
        return False
    finally:
        for fd in (m, s):
            try:
                os.close(fd)
            except OSError:
                pass


def _relay_path():
    """sshj-relay on PATH (deployed), else the dev tree (src/sshj-relay)."""
    r = shutil.which("sshj-relay")
    if r:
        return r
    dev = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        "sshj-relay")
    if os.path.exists(dev) and os.access(dev, os.X_OK):
        return dev
    return None


def probe_tty():
    """P1 — the grid gate: stdin must be a tty (RQ8: piped stdout with tty stdin still renders)."""
    tin = sys.stdin.isatty()
    tout = sys.stdout.isatty()
    if tin and tout:
        return "OK", "stdin+stdout on a terminal", None
    if tin:
        return ("WARN", "stdout piped (stdin is a terminal) — the "
                        "/dev/tty fallback covers the grid", None)
    return "FAIL", "no terminal (stdin not a tty)", MSG_NO_TTY


def probe_size():
    """P2 — terminal size; an unknown 0x0 (unconfigured pty) is a WARN, never a FAIL."""
    if not _termios_ok():
        return "WARN", "unknown (no termios)", None
    try:
        w = fcntl.ioctl(0, termios.TIOCGWINSZ, b"\0" * 8)
        rows, cols = struct.unpack("hh", w[:4])
    except OSError:
        return "WARN", "unknown (not a tty)", None
    if rows <= 0 or cols <= 0:
        return "WARN", "0x0 — unknown (unconfigured pty)", None
    if rows >= 10 and cols >= 50:
        return "OK", "%d rows x %d cols (min 10x50)" % (rows, cols), None
    return ("WARN", "%d rows x %d cols (min 10x50)" % (rows, cols),
            "sshj: terminal is small (%dx%d) — the grid will be cramped; "
            "try a wider terminal" % (rows, cols))


def probe_kernel(ttiocsti=None):
    """P3 — TIOCSTI direct stuff, or (refused) the relay route; run() finalizes against P5."""
    kver, _rel = _kernel_version()
    if ttiocsti is None:
        ttiocsti = _ttiocsti_works()
    if ttiocsti:
        return "OK", kver + " — TIOCSTI works (direct stuff)", None
    return "WARN", kver + " — TIOCSTI unavailable", None


def probe_shell():
    """P4 — the ACTUAL shell running sshj: its parent's /proc/<pid>/comm (RQ9's
    truth — sshj is launched by the shell, whose $SHELL may lie; after the
    widget's $() subshell or a direct type, the parent IS the shell)."""
    shell = ""
    try:
        with open("/proc/%d/comm" % os.getppid()) as f:
            shell = f.read().strip()
    except OSError:
        shell = os.path.basename(os.environ.get("SHELL") or "")
    if not shell:
        return ("WARN", "unknown (no /proc comm, no $SHELL)",
                "sshj: shell unknown — the Tab widget is bash-only; type "
                "the command or use direct mode (sshj <host>)")
    if shell == "bash":
        return "OK", "bash (widget: two-stage Tab active)", None
    return ("WARN", shell + " detected — the Tab widget is bash-only",
            "sshj: %s detected — the Tab widget is bash-only; type the "
            "command or use direct mode (sshj <host>)" % shell)


def probe_relay(ttiocsti=None):
    """P5 — the relay (REQUIRED whenever TIOCSTI is refused) + the /bin/bash sub-check."""
    if ttiocsti is None:
        ttiocsti = _ttiocsti_works()
    relay = _relay_path()
    if relay:
        if os.path.exists("/bin/bash") and os.access("/bin/bash", os.X_OK):
            return "OK", relay + " (child /bin/bash present)", None
        return "WARN", relay + " (child /bin/bash missing)", MSG_NO_BASH
    if ttiocsti:
        return "OK", "not found — not needed (TIOCSTI direct stuff works)", None
    return "FAIL", "not found (TIOCSTI refused — the Tab path is dead)", \
        MSG_NO_RELAY


def probe_python():
    """P6 — the python floor (3.8); the only probe that FAILs below it."""
    v = sys.version_info
    vstr = "%d.%d.%d" % (v.major, v.minor, v.micro)
    if (v.major, v.minor) < FLOOR:
        return "FAIL", vstr + " (floor 3.8)", \
            "sshj: needs Python 3.8+ (found %s)" % vstr
    return "OK", vstr + " (floor 3.8)", None


def _safe(name, fn):
    """C3: a probe must never crash selftest — a broken probe degrades to WARN."""
    try:
        return (name, fn())
    except Exception as e:
        return (name, ("WARN", "probe error: %r" % (e,), None))


def run(ver):
    """Print the probe table (stdout) + degrade messages (stderr); return 0/1/2."""
    ttiocsti = _ttiocsti_works()
    rows = [_safe("tty", probe_tty),
            _safe("size", probe_size),
            _safe("kernel", lambda: probe_kernel(ttiocsti)),
            _safe("shell", probe_shell),
            _safe("relay", lambda: probe_relay(ttiocsti)),
            _safe("python", probe_python)]
    # P3 finalizes against P5: refused + relay OK -> OK via the relay
    # route; refused + relay WARN/FAIL -> WARN (the Tab path is off).
    if not ttiocsti:
        _k_st, k_det, _k_msg = rows[2][1]
        if rows[4][1][0] == "OK":
            rows[2] = ("kernel", ("OK", k_det + ", using relay", None))
        else:
            rows[2] = ("kernel", ("WARN", k_det, MSG_NO_PREFILL))
    out = ["  sshj --selftest  (v%s, python %d.%d.%d)" %
           (ver, sys.version_info.major, sys.version_info.minor,
            sys.version_info.micro)]
    warns = 0
    fails = []
    for name, (st, det, msg) in rows:
        out.append("  " + st.ljust(4) + "  " + name.ljust(9) + det)
        if st == "WARN":
            warns += 1
        elif st == "FAIL":
            fails.append(name)
        if msg:
            sys.stderr.write(msg + "\n")
    if fails:
        verdict, code = "FAILED: " + ", ".join(fails), 2
    elif warns:
        verdict, code = "OK with warnings: %d warn" % warns, 1
    else:
        verdict, code = "ALL OK", 0
    out.append("  -- selftest: " + verdict)
    print("\n".join(out))
    return code
