"""mgf-fed CLI entry point.

Subcommands: ``status``, ``drift-check``, ``local-index``, ``gates``, ``local-ci``,
``release-local``, ``release-remote``, ``scaffold``, ``registry`` (plan Phases 0-4, all built).
"""

from __future__ import annotations

import argparse
import platform
import shutil
import subprocess
import sys
from pathlib import Path

from mgf.fed import __version__


def _rich_version() -> str:
    """OP-01 rich version: package + git SHA + Python + composed-tool availability."""
    sha = ""
    try:
        cmd = ["git", "-C", str(Path(__file__).resolve().parent), "rev-parse", "--short", "HEAD"]
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=5)  # noqa: S603
        sha = proc.stdout.strip() if proc.returncode == 0 else ""
    except (OSError, subprocess.SubprocessError):
        sha = ""
    tools = ", ".join(t for t in ("uv", "ruff", "git", "tea", "woodpecker-cli") if shutil.which(t))
    head = f"mgf-fed {__version__}" + (f" ({sha})" if sha else "")
    return f"{head} | Python {platform.python_version()} | tools: {tools or 'none'}"


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        prog="mgf-fed", description="Magogi Foundation federation orchestrator."
    )
    p.add_argument("--version", action="version", version=_rich_version())
    sub = p.add_subparsers(dest="command", required=True)

    s = sub.add_parser("status", help="read-only federation tracking matrix")
    s.add_argument(
        "--root",
        type=Path,
        default=None,
        help="federation root (default: $MGF_ROOT or ~/PycharmProjects)",
    )
    s.add_argument("--shape", choices=["all", "python", "zig", "other"], default="all")
    s.add_argument("--json", action="store_true", help="emit MGF_FED_STATUS.json to stdout")

    dc = sub.add_parser(
        "drift-check", help="diff each repo against the canonical scaffolding policy"
    )
    dc.add_argument("--root", type=Path, default=None, help="federation root")
    dc.add_argument("--shape", choices=["all", "python", "zig", "other"], default="python")

    sub.add_parser(
        "local-index", help="inspect the local package index (release-local publishes here)"
    )

    g = sub.add_parser(
        "gates", help="run the fast static gates (ruff lint+format) across a selection"
    )
    g.add_argument("--root", type=Path, default=None, help="federation root")
    g.add_argument("--shape", choices=["all", "python", "zig", "other"], default="python")
    g.add_argument(
        "--fix", action="store_true", help="apply ruff --fix + format instead of just checking"
    )

    lc = sub.add_parser(
        "local-ci", help="run the real .woodpecker pipeline for repos (woodpecker-cli exec)"
    )
    lc.add_argument("repos", nargs="+", help="repo name(s), e.g. mgf-http")
    lc.add_argument("--root", type=Path, default=None, help="federation root")

    rl = sub.add_parser(
        "release-local", help="local-release gate → publish to the local index (wave-ordered)"
    )
    rl.add_argument("repos", nargs="+", help="repo name(s), e.g. mgf-standard")
    rl.add_argument("--root", type=Path, default=None, help="federation root")

    rr = sub.add_parser(
        "release-remote",
        help="promote local-validated wheels to the devpi index (tag→publish→verify; "
        "$DEVPI_URL-derived endpoint, password in $DEVPI_PASSWORD)",
    )
    rr.add_argument("repos", nargs="+", help="repo name(s), e.g. mgf-standard")
    rr.add_argument(
        "--dry-run", action="store_true", help="show the plan + pre-req checks; don't tag/publish"
    )
    rr.add_argument(
        "--skip-federation-gate",
        action="store_true",
        help=(
            "publish even though REL-13's federation consistency gate is red or "
            "unverifiable — deliberate override; the publish is irreversible"
        ),
    )
    rr.add_argument("--root", type=Path, default=None, help="federation root")

    sc = sub.add_parser(
        "scaffold", help="apply the template's byte-exact files to a repo (inverse of drift-check)"
    )
    sc.add_argument("repos", nargs="+", help="repo name(s) to scaffold")
    sc.add_argument("--dry-run", action="store_true", help="preview without writing")
    sc.add_argument("--root", type=Path, default=None, help="federation root")

    rg = sub.add_parser(
        "registry", help="regenerate (+ --check) the federation registry (MGF_LIBRARIES)"
    )
    rg.add_argument("--check", action="store_true", help="REL-13 consistency + currency gate")
    rg.add_argument("--root", type=Path, default=None, help="federation root")
    rg.add_argument(
        "--source",
        choices=["origin", "worktree"],
        default=None,
        help=(
            "what to resolve each member against. `origin` reads every member's "
            "committed files at its origin default branch (the fleet as it EXISTS); "
            "`worktree` reads this bench's working trees (a deliberate local "
            "preview — its verdict describes no remote). Default: origin for "
            "--check, worktree for a regen. PAPER-152."
        ),
    )
    rg.add_argument(
        "--fetch",
        action="store_true",
        help=(
            "with --source=origin: `git fetch origin` each member first, so the "
            "origin refs are current. Off by default — it MUTATES your clones and "
            "this gate is registered offline-capable; a CI workspace is already fresh."
        ),
    )

    cf = sub.add_parser(
        "conformance",
        help="authoritative fleet conformance audit (the honest scorer status points at)",
    )
    cf.add_argument("--json-only", action="store_true", help="skip the markdown tracker")
    cf.add_argument(
        "--generated", default=None, help="stamp the tracker YYYY-MM-DD (else UNSTAMPED)"
    )
    cf.add_argument(
        "--extra-root",
        action="append",
        default=[],
        metavar="DIR",
        help="additional search root for mgf-* repos (repeatable)",
    )
    cf.add_argument("--root", type=Path, default=None, help="federation root")

    ct = sub.add_parser(
        "court",
        help="court-round cadence — did a FLEET-WIDE ACT land without a round? (CRT-01)",
    )
    ct.add_argument(
        "--check",
        action="store_true",
        help="gate: every fleet-wide act in the window has a complete round",
    )
    ct.add_argument(
        "--since",
        default=None,
        metavar="DATE",
        # NOT "or ref". Git's `--since` is approxidate only: it turns `HEAD~5`
        # into a silent nonsense timestamp (measured: seven days ago) rather than
        # rejecting it, and --help is what a caller reads BEFORE choosing a value.
        help="window start (a git DATE, not a ref), e.g. 2026-08-04 or 3.days.ago — "
        "required; absent is exit 2, never a pass",
    )
    ct.add_argument(
        "--write", metavar="SLUG", default=None, help="emit a round skeleton to fill in"
    )
    ct.add_argument("--trigger", default="wave", help="wave|canon|gate|credentials|release")
    ct.add_argument("--act", default="", help="one line naming the act this round judges")
    ct.add_argument("--root", type=Path, default=None, help="federation root")

    au = sub.add_parser(
        "audit",
        help="cross-layer audit — declared vs committed vs published vs installed vs live",
    )
    au.add_argument(
        "--check",
        action="append",
        default=[],
        metavar="NAME",
        help="run only this check (repeatable); --list-checks to see them",
    )
    au.add_argument(
        "--list-checks", action="store_true", help="list the checks and the layers each compares"
    )
    au.add_argument(
        "--offline",
        action="store_true",
        help="skip the checks that need the network — they are still NAMED, never silently passed",
    )
    au.add_argument("--json", dest="json_only", action="store_true", help="machine-readable")
    au.add_argument(
        "--write",
        nargs="?",
        const="",
        default=None,
        metavar="PATH",
        help="write the committed artifact (default mgf-standard/AUDIT.json) — the run "
        "records what did NOT run, so an offline artifact can never read as a full audit",
    )
    au.add_argument(
        "--check-artifact",
        action="store_true",
        help="CI half: gate the COMMITTED artifact — stamped, fresh, COMPLETE, clean. "
        "Needs no clones, no network, no credentials",
    )
    au.add_argument(
        "--max-age-days", type=int, default=None, help="staleness limit for --check-artifact"
    )
    au.add_argument("--root", type=Path, default=None, help="federation root")

    dr = sub.add_parser(
        "doctor", help="self-diagnostic — mgf-fed's own operational prerequisites (OP-02)"
    )
    dr.add_argument("--root", type=Path, default=None, help="federation root")

    fb = sub.add_parser(
        "feedback", help="relay cross-repo feedback outboxes into target inboxes (FB-05)"
    )
    fb.add_argument(
        "--check",
        action="store_true",
        help="scan outboxes; exit non-zero if any un-relayed (the gate; default)",
    )
    fb.add_argument(
        "--relay",
        action="store_true",
        help="relay outbox papers into target FEEDBACK.md + move source to relayed/",
    )
    fb.add_argument("--target", default=None, help="restrict to one target repo (e.g. mgf-common)")
    fb.add_argument(
        "--dry-run", action="store_true", help="with --relay: show the plan; write nothing"
    )
    fb.add_argument("--root", type=Path, default=None, help="federation root")
    fb.add_argument(
        "--in-repo",
        nargs="?",
        const=".",
        default=None,
        metavar="PATH",
        help="check-only: scan THIS repo's pending-feedback/ outbox (default cwd) regardless of "
        "registry membership — the consumer-app gate (CIR-16)",
    )
    fb.add_argument(
        "--fresh",
        action="store_true",
        help="read the outbox from origin/main via git, not the dirty working tree (CIR-16)",
    )
    fb.add_argument(
        "--papers",
        action="store_true",
        help="check THIS repo's own FEEDBACK.md papers against FB-02 (metadata table + "
        "Status enum) and FB-03 (SHIPPED carries `Shipped in`); exit non-zero on violations",
    )
    fb.add_argument(
        "--all",
        action="store_true",
        help="with --papers: check every FEEDBACK.md under the federation root, not just this repo",
    )

    ph = sub.add_parser(
        "pr-health",
        help="is an open PR substantive and current? (empty PRs + behind-base staleness)",
    )
    from mgf.fed._prhealth import add_pr_health_args

    add_pr_health_args(ph)

    pc = sub.add_parser(
        "provision-ci-host",
        help="DEPRECATED — moved to `mgf-cloud-provision ci-host`; this shim delegates "
        "(or errors with instructions) and renders nothing itself",
    )
    pc.add_argument("--hostname", default="woodpecker", help="host base name (default: woodpecker)")
    pc.add_argument("--env", default="prod", help="environment suffix (default: prod)")
    pc.add_argument("--flavor", default=None, help="VM flavour (default: the Infomaniak nano)")
    pc.add_argument("--image", default=None, help="OS image (default: Ubuntu 24.04 LTS)")
    pc.add_argument("--network", default=None, help="network (default: ext-net1)")
    pc.add_argument(
        "--volume-size", type=int, default=20, help="boot-volume GB (0 = ephemeral disk)"
    )
    pc.add_argument(
        "--floating-ip",
        action="store_true",
        help="allocate a floating IP from ext-floating1 (only if NOT on ext-net1, "
        "whose VMs already get a public IP directly)",
    )
    pc.add_argument("--key-name", default=None, help="existing SSH keypair to inject")
    pc.add_argument("--bundle-dir", type=Path, default=None, help="override deploy/woodpecker/")
    pc.add_argument("--provider", default="infomaniak", help="cloud provider (default: infomaniak)")
    pc.add_argument("--clouds-yaml", default=None, help="path to an OpenStack clouds.yaml")
    pc.add_argument("--cloud", default="openstack", help="clouds.yaml entry (e.g. PCP-…-dc3-a)")
    pc.add_argument("--dry-run", action="store_true", help="plan only; touch no cloud")
    pc.add_argument("--yes", action="store_true", help="confirm — actually provision")

    # `hooks` is NATIVE, not registry-merged, and that is load-bearing. The shim
    # `mgf-fed hooks install` writes runs at the start of every session in every
    # adopting repo — including repos on a bare `pip install mgf-fed`. Reaching it
    # through the registry would make the hook path depend on the `[cli]` extra
    # (importing mgf.fed.cli needs mgf-common), so on a base install the verb would
    # not exist and every session would silently lose its re-teach: the exact
    # invisible-absence failure the kit exists to end. mgf.fed.sessionhooks is
    # stdlib-only, so this import is free of the extra.
    from mgf.fed.sessionhooks._cli import add_hooks_args

    hk = sub.add_parser(
        "hooks",
        help=(
            "Session-hook kit (observe mode): install/status/uninstall the "
            "SessionStart shim, emit events, read the trail."
        ),
    )
    add_hooks_args(hk)

    # v0.47 reorg: the moved consumer/governance CLI (explain, validate,
    # app-doctor, standards, agents, catch-up, version, logs, …) lives in
    # mgf.fed.cli and self-registers into mgf-common's subcommand registry.
    # Surface each as a `mgf-fed <verb>` subparser. Imported lazily + guarded
    # so the zero-dep orchestration core (status/release/…) still parses
    # without the `[cli]` extra installed.
    degraded = _add_registered_subcommands(sub)
    if degraded is not None:
        # Loud, in the one place every user of a broken install already looks.
        p.epilog = (
            "DEGRADED INSTALL — the extended CLI could not be loaded, so roughly "
            "half the verbs (app-doctor, standards, catch-up, agents, explain, "
            "validate, logs, migrate, …) are MISSING from the list above and will "
            f"be reported as 'invalid choice'. Reason: {degraded}. "
            "Fix: install the CLI extra, e.g. "
            "`uv tool install --with 'mgf-common[standards]' mgf-fed`. "
            "This is a broken environment, not a smaller tool."
        )
        p.formatter_class = argparse.RawDescriptionHelpFormatter
    return p


def _native_commands() -> frozenset[str]:
    """The fleet-orchestration verbs handled by main()'s native ladder."""
    return frozenset(
        {
            "status",
            "drift-check",
            "local-index",
            "gates",
            "local-ci",
            "release-local",
            "release-remote",
            "scaffold",
            "registry",
            "conformance",
            "doctor",
            "feedback",
            "provision-ci-host",
            # Native so the hook path survives a base install (see build_parser).
            # Listing it here also stops _add_registered_subcommands adding a second
            # parser for the same name when the [cli] extra IS installed.
            "hooks",
        }
    )


def _add_registered_subcommands(
    sub: argparse._SubParsersAction[argparse.ArgumentParser],
) -> str | None:
    """Merge the mgf.fed.cli registry-registered subcommands into the parser.

    Degrades to the core fleet commands when the ``[cli]`` extra isn't installed
    — the zero-dep base install stays runnable, which is deliberate.

    Returns ``None`` on success, or the reason the extended CLI could not load.
    **The caller must surface that reason.** It used to be swallowed with a bare
    ``return``, and the docstring said "silently" as if that were a feature: a
    bench missing ``mgf-common`` exposed **17 of 43 verbs** and answered
    ``mgf-fed app-doctor`` with ``invalid choice: 'app-doctor'`` — which reads as
    *"that verb does not exist"*, not *"half this tool failed to load"*. Measured
    on this bench after a reinstall dropped the dependency; the operator had no
    way to tell a missing feature from a broken install. That is the house's
    oldest defect wearing a new costume — "I could not look" rendered as "there
    was nothing to see".
    """
    try:
        from mgf.common.cli._subcommand_registry import _iter_registered

        import mgf.fed.cli  # noqa: F401  (registration side-effect; needs [cli])
    except ImportError as exc:
        return str(exc)
    native = _native_commands()
    for entry in _iter_registered():
        if entry.name in native:  # defensive: never shadow a fleet verb
            continue
        sp = sub.add_parser(entry.name, help=entry.help)
        if entry.parser_setup is not None:
            entry.parser_setup(sp)
    return None


def _harden_streams() -> None:
    """Never let an em dash kill a gate (board item 39d).

    Every message this CLI prints is written by us and carries em dashes, arrows
    and severity emoji. Under ``PYTHONUTF8=0`` with an ASCII locale — the default
    in a bare container — ``print`` raises ``UnicodeEncodeError`` and the command
    exits non-zero for a reason that has nothing to do with what it checked.

    **No `capsys` test can ever catch this**: pytest's capture holds text and
    never encodes it, so the failing path does not exist under test. It is pinned
    by a real subprocess instead.

    ``backslashreplace`` rather than ``replace``: a mangled character should stay
    legible as an escape, not become an anonymous ``?``.
    """
    for stream in (sys.stdout, sys.stderr):
        reconfigure = getattr(stream, "reconfigure", None)
        if reconfigure is None:
            continue
        try:
            reconfigure(errors="backslashreplace")
        except (OSError, ValueError):  # a stream that refuses is not fatal
            pass


def main(argv: list[str] | None = None) -> int:
    _harden_streams()
    # Logging L2 (e): ship this invocation's logs to the federation log host
    # when the env configures it (MGF_LOGSHIP_URL + MGF_LOGSHIP_INGEST_TOKEN).
    # No-op otherwise; never raises. Lazy import keeps the zero-dep core
    # importable without mgf-logship installed.
    from mgf.fed._logging import bootstrap_logging

    bootstrap_logging()
    parser = build_parser()
    args = parser.parse_args(argv)

    # `hooks` dispatches BEFORE the command-module imports below — deliberately.
    # Those imports pull modules that reach ``mgf.common`` (the ``[cli]`` extra), so
    # running them first would reintroduce the very dependency this verb exists
    # without: the shim runs at every session start in every adopting repo, base
    # install included. Kept above the shared try/except for the same reason — a
    # hook entrypoint owns its own exit code and returns 0 on every path.
    if args.command == "hooks":
        from mgf.fed.sessionhooks._cli import run_hooks

        return run_hooks(args)

    # Lazy-import the command modules AFTER parse_args so ``--version`` / ``--help`` fire
    # on the zero-dep base wheel (``[project].dependencies`` is empty). Several command
    # modules pull ``mgf.common`` (the ``[cli]`` extra); importing them at module top
    # broke the bare console script — and the release-smoke `mgf-fed --version` — with
    # ``ModuleNotFoundError: mgf.common``. A command that needs ``[cli]`` still surfaces
    # that import error itself, with mgf.common's own message.
    from mgf.fed._audit import CHECKS, run_audit
    from mgf.fed._conformance import run_conformance
    from mgf.fed._doctor import run_doctor
    from mgf.fed._drift import run_drift_check
    from mgf.fed._feedback import run_feedback
    from mgf.fed._gates import run_gates
    from mgf.fed._index import run_local_index
    from mgf.fed._localci import run_local_ci
    from mgf.fed._provision import provision_ci_host
    from mgf.fed._registry import run_registry
    from mgf.fed._release import release_local, release_remote
    from mgf.fed._scaffold import run_scaffold
    from mgf.fed._status import run_status

    try:
        if args.command == "status":
            return run_status(shape=args.shape, as_json=args.json, root=args.root)
        if args.command == "drift-check":
            return run_drift_check(shape=args.shape, root=args.root)
        if args.command == "local-index":
            return run_local_index()
        if args.command == "gates":
            return run_gates(shape=args.shape, fix=args.fix, root=args.root)
        if args.command == "local-ci":
            return run_local_ci(args.repos, root=args.root)
        if args.command == "release-local":
            return release_local(args.repos, root=args.root)
        if args.command == "release-remote":
            return release_remote(
                args.repos,
                dry_run=args.dry_run,
                root=args.root,
                skip_federation_gate=args.skip_federation_gate,
            )
        if args.command == "scaffold":
            return run_scaffold(args.repos, dry_run=args.dry_run, root=args.root)
        if args.command == "registry":
            return run_registry(
                check=args.check, root=args.root, source=args.source, fetch=args.fetch
            )
        if args.command == "conformance":
            return run_conformance(
                json_only=args.json_only,
                generated=args.generated,
                extra_roots=tuple(args.extra_root),
                root=args.root,
            )
        if args.command == "court":
            from mgf.fed._court import run_court, write_round_template
            from mgf.fed._roster import federation_root

            if args.write:
                root = args.root or federation_root()
                p = write_round_template(
                    root, args.write, trigger=args.trigger, act=args.act or args.write
                )
                print(f"wrote round skeleton: {p}")
                print("fill in every docket answer — blanks fail `mgf-fed court --check`")
                return 0
            return run_court(since=args.since, root=args.root, check=args.check)
        if args.command == "audit":
            if args.list_checks:
                for name, (_fn, needs_net, layers) in CHECKS.items():
                    net = " (network)" if needs_net else ""
                    print(f"  {name:<20} {layers[0]} ↔ {layers[1]}{net}")
                return 0
            if args.check_artifact:
                from mgf.fed._audit import MAX_AUDIT_AGE_DAYS, check_artifact

                return check_artifact(
                    None if args.root is None else args.root / "mgf-standard" / "AUDIT.json",
                    args.max_age_days if args.max_age_days is not None else MAX_AUDIT_AGE_DAYS,
                )
            unknown = [c for c in args.check if c not in CHECKS]
            if unknown:
                print(f"unknown check(s): {', '.join(unknown)} — try --list-checks")
                return 2
            from mgf.fed._audit import AUDIT_ARTIFACT
            from mgf.fed._roster import federation_root

            write = None
            if args.write is not None:
                root = args.root or federation_root()
                write = Path(args.write) if args.write else root / AUDIT_ARTIFACT
            return run_audit(
                args.check or None,
                offline=args.offline,
                json_only=args.json_only,
                root=args.root,
                write=write,
            )
        if args.command == "doctor":
            return run_doctor(root=args.root)
        if args.command == "pr-health":
            from mgf.fed._prhealth import run_pr_health

            return run_pr_health(args)
        if args.command == "feedback":
            if args.papers:
                from mgf.fed._papers import run_papers

                return run_papers(
                    Path(args.in_repo) if args.in_repo else None,
                    all_repos=args.all,
                    root=args.root,
                )
            return run_feedback(
                relay=args.relay,
                target=args.target,
                dry_run=args.dry_run,
                root=args.root,
                in_repo=args.in_repo,
                fresh=args.fresh,
            )
        if args.command == "provision-ci-host":
            return provision_ci_host(
                hostname=args.hostname,
                env=args.env,
                flavor=args.flavor,
                image=args.image,
                network=args.network,
                volume_size_gb=(args.volume_size or None),
                floating_ip=args.floating_ip,
                key_name=args.key_name,
                bundle_dir=args.bundle_dir,
                provider_name=args.provider,
                clouds_yaml=args.clouds_yaml,
                cloud=args.cloud,
                dry_run=args.dry_run,
                yes=args.yes,
            )
        # Not a native fleet verb → a moved consumer/governance CLI command
        # registered by mgf.fed.cli. Dispatch through mgf-common's registry
        # (reuses its exit-code translation + no-subcommand help fallback).
        from mgf.common.cli import dispatch

        return dispatch(args, parser=parser)
    except Exception as exc:
        # LG-05: ship the traceback to the central log (exc_info) instead of
        # swallowing it; keep a concise human-facing line on stderr.
        from mgf.fed._logging import LOG

        LOG.error("unexpected error in '%s'", args.command, exc_info=True)
        print(f"mgf-fed: unexpected error in '{args.command}': {exc!r}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
