"""mgf-hrb doctor — broker preconditions, structured + rendered.

Thin renderer over :mod:`mgf.hrb.diagnostics`, which holds the shared
check engine (FEEDBACK TOOLS-02). The CLI here is *behaviour*, not
business logic: it runs the engine, picks an exit code, and prints
the polished operator-facing report. SDK consumers reach the same
findings via :py:meth:`mgf.hrb.Client.doctor` — same engine, same
codes, no drift.

Exit codes:

* ``0`` — every wired check passed.
* ``1`` — at least one ``FAIL`` (the broker won't function).
* ``78`` — ``EX_CONFIG``; at least one ``WARN`` and no ``FAIL``
  (the broker may still work, but with reduced capability).
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from mgf.hrb import diagnostics
from mgf.hrb.diagnostics import Category, DoctorFinding, Severity

if TYPE_CHECKING:
    # Typed only for the renderer signatures + return-tuple shapes.
    # Runtime use happens via attribute access (snap.uptime_human,
    # tail.outcome_summary()); no module-load-time import needed.
    from mgf.hrb.domain import AuditTailResult, StateSnapshot

logger = logging.getLogger(__name__)


def doctor_command(*, socket_path: str) -> int:
    """Run the doctor check suite and print the polished report.

    Opens a short-lived client connection ONLY when the socket is
    reachable, so the local checks all run cleanly on a broker-absent
    host (no spurious "ConnectionRefused" surfaces from the
    capabilities check).
    """
    logger.debug("doctor starting", extra={"socket_path": socket_path})

    # Decide up-front whether to lift a Client for the broker-side
    # check. The diagnostics engine accepts a None client and emits
    # DEFERRED for the broker-side rows in that case; we only pay the
    # connect cost when the SOCKET_REACHABLE check would have passed.
    findings, backends, snapshot, tail = _run_with_optional_client(socket_path)
    _render_report(
        findings,
        socket_path=socket_path,
        backends=backends,
        snapshot=snapshot,
        tail=tail,
    )

    fail_count = sum(1 for r in findings if r.severity == Severity.FAIL)
    warn_count = sum(1 for r in findings if r.severity == Severity.WARN)
    if fail_count:
        return 1
    if warn_count:
        return 78  # EX_CONFIG — configuration issue, broker may still work
    return 0


def _run_with_optional_client(
    socket_path: str,
) -> tuple[
    list[DoctorFinding],
    list[dict[str, object]] | None,
    StateSnapshot | None,
    AuditTailResult | None,
]:
    """Try to connect; on success run the full suite (broker-side
    check included) AND capture the raw backends list, plus the
    broker's self-state snapshot + recent audit-tail rows so the
    renderer can show operator-facing health detail. On failure
    run the local-only suite and return ``None`` for the broker-
    side captures."""
    socket_finding = diagnostics.check_socket(socket_path)
    if socket_finding.severity != Severity.OK:
        return diagnostics.run_all(socket_path, client=None), None, None, None
    from mgf.hrb import Client

    try:
        with Client.connect(socket_path) as c:
            findings = diagnostics.run_all(socket_path, client=c)
            try:
                backends = c.capabilities_raw()
            except Exception:
                backends = None
            # Best-effort broker-health captures. Either may fail
            # if the cap doesn't carry `audit_read` (today's root
            # cap does; future attenuated caps may not). On
            # failure we render the existing report shape.
            snapshot = None
            tail = None
            try:
                snapshot = c.state_snapshot()
            except Exception as exc:
                logger.debug("state_snapshot fetch failed", exc_info=exc)
            try:
                # A4 (presentation-layer plan) — keep the full
                # AuditTailResult (not just .records). The renderer
                # now calls tail.outcome_summary() to get the
                # typed (ok, err, other, newest_error) tally
                # instead of looping inline.
                tail = c.audit_tail(n=20)
            except Exception as exc:
                logger.debug("audit_tail fetch failed", exc_info=exc)
            return findings, backends, snapshot, tail
    except Exception:
        return diagnostics.run_all(socket_path, client=None), None, None, None


# === rendering ============================================================

_GLYPH = {
    Severity.OK: "  OK ",
    Severity.WARN: " WARN",
    Severity.FAIL: " FAIL",
    Severity.DEFERRED: " ... ",
}


def _render_report(
    findings: list[DoctorFinding],
    *,
    socket_path: str,
    backends: list[dict[str, object]] | None,
    snapshot: StateSnapshot | None = None,
    tail: AuditTailResult | None = None,
) -> None:
    """Print the polished doctor report: header, grouped checks,
    optional per-worker ops grid, summary box."""
    ok = sum(1 for r in findings if r.severity == Severity.OK)
    warn = sum(1 for r in findings if r.severity == Severity.WARN)
    fail = sum(1 for r in findings if r.severity == Severity.FAIL)
    deferred = sum(1 for r in findings if r.severity == Severity.DEFERRED)

    # ---- Header ----
    print(f"mgf-hrb doctor  (broker socket: {socket_path})")
    print()
    print(
        "Diagnoses preconditions the broker needs before it can claim or report\n"
        "on hardware. Each row carries the check, its status, what it means, and\n"
        "— if applicable — a copy-pastable fix line."
    )
    print()
    print(
        "Statuses:  OK = passed  ·  WARN = degraded but usable  ·  "
        "FAIL = blocks the broker  ·  ... = check not yet wired (deferred)."
    )
    print()

    # ---- Grouped rows ----
    by_category: dict[Category, list[DoctorFinding]] = {}
    for r in findings:
        by_category.setdefault(r.category, []).append(r)

    for category in Category:
        bucket = by_category.get(category, [])
        if not bucket:
            continue
        print(f"── {category.value} " + "─" * max(2, 70 - len(category.value)))
        for r in bucket:
            print(f"  [{_GLYPH[r.severity]}] {r.summary}")
            print(f"          {r.detail}")
            if r.why:
                print(f"     why: {r.why}")
            if r.remediation:
                print(f"     fix: {r.remediation}")
            print()

    # ---- Optional worker-constellation detail ----
    # The diagnostics engine reports BACKEND_CAPABILITIES as a single
    # OK/WARN finding; the CLI shows the operator the per-worker ops
    # grid as additional context since they're already looking at it.
    if backends:
        _render_backends_detail(backends)

    # ---- Optional broker self-health pane (Part 13 §5.6.2 / §5.6.4) ----
    # state_snapshot + audit_tail were shipped in 5C / 5B but had no
    # operator-facing surface until now. The pane is best-effort: it
    # appears only when the client could fetch them (cap permitted +
    # broker reachable).
    if snapshot is not None:
        _render_broker_health(snapshot, tail)

    # ---- Summary box ----
    print("Summary")
    print("───────")
    print(
        f"  {ok} ok  ·  {warn} warn  ·  {fail} fail  ·  {deferred} deferred  "
        f"({len(findings)} checks total)"
    )

    if fail > 0:
        print()
        print("ACTION REQUIRED — the broker will not function until these are fixed:")
        for r in findings:
            if r.severity == Severity.FAIL:
                print(f"  ✗ {r.summary}: {r.detail}")
                if r.remediation:
                    print(f"      → {r.remediation}")
    elif warn > 0:
        print()
        print("Warnings — broker will run, but with reduced capability:")
        for r in findings:
            if r.severity == Severity.WARN:
                print(f"  ⚠ {r.summary}: {r.detail.splitlines()[0]}")
    else:
        print()
        print("All wired checks passed. Deferred rows are by design (the relevant")
        print("substrate isn't yet checked in code) and do not indicate a problem.")

    print()
    print(
        "Run `mgf-hrb explain doctor` for what each check is actually probing\nand why it matters."
    )


def _render_broker_health(
    snapshot: StateSnapshot,
    tail: AuditTailResult | None,
) -> None:
    """Operator-facing broker self-state pane (Part 13 §5.6).

    Single screen of "is the broker healthy?" — uptime, op
    throughput, lease/subscription/connection counts, audit
    ring fill, memory. Plus a short recent-op summary so the
    operator can see what the broker just handled (and how many
    of those were errors)."""
    s = snapshot  # typed StateSnapshot from mgf.hrb.domain
    print("── Broker health " + "─" * 55)
    # A4 (presentation-layer plan) — uptime + RSS now come from the
    # SDK's computed properties. Pre-A4 we hand-rolled both formats
    # here (Hh Mm Ss math + MB conversion). Centralised so any
    # future GUI / TUI / web view of the same broker health pane
    # renders identically by construction.
    uptime_str = getattr(s, "uptime_human", None) or "?"
    rss_str = getattr(s, "memory_rss_human", None) or "?"
    print(f"  broker {getattr(s, 'broker_version', '?')}  ·  uptime {uptime_str}  ·  rss {rss_str}")
    print(
        f"  ops {getattr(s, 'ops_total', 0):>6}  "
        f"({getattr(s, 'ops_per_second_1m', 0.0):.2f}/sec over last 60s)"
    )
    print(
        f"  active:  {getattr(s, 'active_connections', 0)} connections  ·  "
        f"{getattr(s, 'active_leases', 0)} leases  ·  "
        f"{getattr(s, 'active_subscriptions', 0)} subscriptions"
    )
    ring_used = getattr(s, "audit_ring_used", 0)
    ring_cap = getattr(s, "audit_ring_capacity", 0)
    cap_size = getattr(s, "cap_table_size", 0)
    dropped = getattr(s, "audit_dropped_total", 0)
    dropped_tag = f"  ·  dropped {dropped}" if dropped > 0 else ""
    print(
        f"  audit ring: {ring_used}/{ring_cap}{dropped_tag}  ·  "
        f"cap table: {cap_size} live capabilit{'y' if cap_size == 1 else 'ies'}"
    )
    if dropped > 0:
        print(
            f"     warn: {dropped} audit record(s) overwritten by ring overflow — "
            "consider a larger ring or higher inspection cadence"
        )
    print()

    # ---- Recent activity summary ----
    #
    # A4 (presentation-layer plan) — the inline tally loop is gone.
    # `tail` is now an AuditTailResult (typed) and its
    # `outcome_summary()` method does the same `(ok, err, other,
    # newest_error)` walk. Any future hrb-app/view/doctor.py reads
    # the same summary; consumers can't accidentally split the
    # logic across UIs.
    if tail is not None and len(tail.records) > 0:
        summary = tail.outcome_summary()
        print(
            f"  recent {len(tail.records)} op(s):  "
            f"{summary.ok} ok  ·  {summary.err} err  ·  {summary.other} other"
        )
        if summary.newest_error is not None:
            last_err = summary.newest_error
            urn_part = f" (urn {last_err.urn})" if last_err.urn else ""
            print(f"    newest error: {last_err.op} → {last_err.err or '?'}{urn_part}")
    elif tail is not None:
        print("  recent activity: (audit ring empty)")
    print()


def _render_backends_detail(backends: list[dict[str, object]]) -> None:
    """Per-worker constellation — one summary line + an ops grid each.

    Pulled out of the doctor check itself so the engine's
    BACKEND_CAPABILITIES finding stays a single structured row
    (consumable by GUIs) while the CLI keeps its operator-facing
    detail."""
    print("── Worker constellation " + "─" * 48)
    for b in backends:
        mode = b.get("mode", "?")
        mode_tag = f" [{mode}]" if mode and mode != "full" else ""
        print(
            f"  • `{b.get('scope_prefix', '?')}` → "
            f"{b.get('backend_name', '?')} v{b.get('backend_version', '?')}"
            f"{mode_tag}"
            f"  ({b.get('resource_count', 0)} resource(s))"
        )
        if mode and mode != "full" and b.get("mode_reason"):
            print(f"      reason: {b['mode_reason']}")
        ops = b.get("ops", {})
        if isinstance(ops, dict):
            # A4 (presentation-layer plan) — single-source the op-name
            # list via BackendCapability.KNOWN_OPS. Pre-A4 the list was
            # hard-coded here; adding a new broker-side op required
            # editing both the SDK AND the CLI. Now: edit SDK only,
            # every consumer's grid widens automatically. Late import
            # to keep _doctor.py's import-time surface tight (it
            # otherwise doesn't need domain.py at module scope).
            from mgf.hrb.domain import BackendCapability

            op_glyphs = " ".join(
                f"{name}={'✓' if ops.get(name, False) else '✗'}"
                for name in BackendCapability.KNOWN_OPS
            )
            print(f"      ops: {op_glyphs}")
    print()
