"""``mgf-secrets`` CLI — P1 verbs: list / show / doctor / probe.

Every verb takes ``--json`` and returns stable exit codes (SPEC §4):
``0`` clean · ``1`` findings · ``2`` usage / credential / load error. No verb
ever prints a secret value — ``list`` and ``show`` emit metadata only, ``doctor``
and ``probe`` emit findings/evidence only. The redaction property test drives this
whole surface with planted canaries and asserts their absence (SPEC §7.4).
"""

from __future__ import annotations

import argparse
import json
import sys
from typing import TYPE_CHECKING, Any

from . import __version__
from .context import Context, build_context
from .doctor import ALL_SURFACES, DEFAULT_SURFACES, Doctor
from .errors import EXIT_ERROR, EXIT_OK, CredentialError, SecretsError, UsageError
from .probes import run_probe
from .registry import Registry, RegistryError, load

if TYPE_CHECKING:
    from collections.abc import Sequence

    from .model import Entry


def entry_to_json(e: Entry) -> dict[str, Any]:
    """Metadata-only serialization of an entry (never a value)."""
    return {
        "id": e.id,
        "family": e.family,
        "kind": e.kind,
        "state": e.state,
        "purpose": e.purpose,
        "surfaces": [{"type": s.type, **s.data} for s in e.surfaces],
        "consumers": list(e.consumers),
        "policy": {"allowed_repos": e.policy.allowed_repos, "blast_radius": e.policy.blast_radius},
        "rotation": dict(e.rotation),
        "verify": dict(e.verify),
        "recovery": e.recovery,
    }


# --------------------------------------------------------------------------- #
# Verbs.
# --------------------------------------------------------------------------- #


def _cmd_list(args: argparse.Namespace, reg: Registry, _ctx: Context | None) -> int:
    rows = reg.select(family=args.family, kind=args.kind, state=args.state)
    if args.json:
        print(json.dumps([entry_to_json(e) for e in rows], indent=2, sort_keys=True))
        return EXIT_OK
    if not rows:
        print("(no matching registry entries)")
        return EXIT_OK
    width = max(len(e.id) for e in rows)
    print(f"{'ID'.ljust(width)}  FAMILY  STATE      KIND               SURFACES  LAST-ROTATED")
    for e in rows:
        last = str(e.rotation.get("last", "-"))
        print(
            f"{e.id.ljust(width)}  {e.family:<6}  {e.state:<9}  {e.kind:<17}  "
            f"{len(e.surfaces):<8}  {last}"
        )
    print(f"\n{len(rows)} entr{'y' if len(rows) == 1 else 'ies'}.")
    return EXIT_OK


def _cmd_show(args: argparse.Namespace, reg: Registry, _ctx: Context | None) -> int:
    entry = reg.get(args.id)
    if entry is None:
        raise UsageError(f"unknown id {args.id!r} — run `mgf-secrets list` to see ids")
    if args.json:
        print(json.dumps(entry_to_json(entry), indent=2, sort_keys=True))
        return EXIT_OK
    print(f"id:        {entry.id}")
    print(f"family:    {entry.family}")
    print(f"kind:      {entry.kind}")
    print(f"state:     {entry.state}")
    print(f"purpose:   {entry.purpose}")
    print(
        f"policy:    allowed_repos={entry.policy.allowed_repos!r} "
        f"blast_radius={entry.policy.blast_radius!r}"
    )
    print(f"rotation:  {dict(entry.rotation)}")
    print(f"verify:    {dict(entry.verify)}")
    print(f"recovery:  {entry.recovery}")
    print(f"consumers: {list(entry.consumers)}")
    print("surfaces:")
    for s in entry.surfaces:
        print(f"  - {s.type}: {s.data}")
    return EXIT_OK


def _cmd_doctor(args: argparse.Namespace, reg: Registry, ctx: Context | None) -> int:
    surfaces = set(args.surface) if args.surface else set(DEFAULT_SURFACES)
    if ctx is None:
        ctx = build_context(surfaces, root=args.root, best_effort=False)
    doctor = Doctor(
        reg, fed_root=ctx.fed_root, woodpecker=ctx.woodpecker, forge=ctx.forge, hosts=ctx.hosts
    )
    report = doctor.run(sorted(surfaces))
    if args.json:
        print(json.dumps(report.to_json(), indent=2, sort_keys=True))
        return report.exit_code()
    findings = report.sorted_findings()
    print(f"doctor — surfaces: {', '.join(report.surfaces)}")
    if not findings:
        print("clean — declared == deployed == consumed across the checked surfaces.")
    else:
        current = None
        for f in findings:
            if f.severity != current:
                current = f.severity
                print(f"\n[{f.severity.upper()}]")
            print(f"  {f.check} {f.id}: {f.finding}")
        print(f"\n{len(findings)} finding(s).")
    if report.skipped:
        print("\nskipped:")
        for s in report.skipped:
            print(f"  - {s}")
    return report.exit_code()


def _cmd_probe(args: argparse.Namespace, reg: Registry, ctx: Context | None) -> int:
    entry = reg.get(args.id)
    if entry is None:
        raise UsageError(f"unknown id {args.id!r}")
    if ctx is None:
        ctx = build_context(set(ALL_SURFACES), root=args.root, best_effort=True)
    result = run_probe(entry, ctx)
    if args.json:
        print(json.dumps(result.to_json(), indent=2, sort_keys=True))
    else:
        print(f"{result.status.upper():<8} {result.id} ({result.kind}): {result.message}")
        if result.evidence:
            print(f"  evidence: {result.evidence}")
    return result.exit_code()


_VERBS = {
    "list": _cmd_list,
    "show": _cmd_show,
    "doctor": _cmd_doctor,
    "probe": _cmd_probe,
}


# --------------------------------------------------------------------------- #
# Parser + entry point.
# --------------------------------------------------------------------------- #


def _build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        prog="mgf-secrets", description="Foundation credential registry + doctor (P1)."
    )
    p.add_argument("--version", action="version", version=f"mgf-secrets {__version__}")
    p.add_argument("--root", default=None, help="federation root ($MGF_ROOT / ~/PycharmProjects)")
    sub = p.add_subparsers(dest="verb", required=True)

    lst = sub.add_parser("list", help="registry table (metadata)")
    lst.add_argument("--family")
    lst.add_argument("--kind")
    lst.add_argument("--state")
    lst.add_argument("--json", action="store_true")

    show = sub.add_parser("show", help="full metadata for one entry (never a value)")
    show.add_argument("id")
    show.add_argument("--json", action="store_true")

    doc = sub.add_parser("doctor", help="declared == deployed == consumed (read-only)")
    doc.add_argument(
        "--surface",
        action="append",
        choices=list(ALL_SURFACES),
        help="restrict/opt-in surfaces (default: woodpecker + local)",
    )
    doc.add_argument("--json", action="store_true")

    prb = sub.add_parser("probe", help="one entry's liveness probe")
    prb.add_argument("id")
    prb.add_argument("--json", action="store_true")
    return p


def main(
    argv: Sequence[str] | None = None,
    *,
    registry: Registry | None = None,
    context: Context | None = None,
) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)
    try:
        reg = registry if registry is not None else load(root=args.root)
        return _VERBS[args.verb](args, reg, context)
    except RegistryError as exc:
        print("mgf-secrets: registry failed to validate (load error):", file=sys.stderr)
        for problem in exc.problems:
            print(f"  - {problem}", file=sys.stderr)
        return EXIT_ERROR
    except (CredentialError, UsageError) as exc:
        print(f"mgf-secrets: {exc}", file=sys.stderr)
        return EXIT_ERROR
    except SecretsError as exc:  # pragma: no cover - defensive catch-all
        print(f"mgf-secrets: {exc}", file=sys.stderr)
        return EXIT_ERROR


if __name__ == "__main__":  # pragma: no cover
    raise SystemExit(main())
