"""Subcommand registry — closes the v0.3 closed-box leak around CLI extension.

v0.4 Phase E. Before this, consumers building CLIs had two
unsatisfactory options:

  1. Reach past :mod:`mgf.common.cli` to :mod:`argparse` directly,
     re-implementing exit-code translation + crash routing on
     every consumer (defeats the closed-box principle).
  2. Live without ``mgf-common`` integration entirely.

Phase E gives consumers a third, paved option: register a
subcommand against the same primitive the four built-ins
(``explain`` / ``validate`` / ``doctor`` / ``migrate``) consume.
The registry is the single source of truth for "what subcommands
does the CLI know about?"; both the diagnostic ``mgf-common`` CLI
AND consumer-built top-level CLIs (e.g. ``vmanager``) read from
it via :func:`build_cli`.

The pattern matches the v0.3 registries (crash sinks, log handlers,
redaction patterns, span processors, vault backends): ``register_*``
decorator + ``unregister_*`` no-op-if-unknown +
``_registered_*`` internal snapshot for tests.

Usage — register from a consumer module::

    from mgf.common.cli import register_cli_subcommand

    def _setup(p):
        p.add_argument("--name", required=True)

    @register_cli_subcommand(
        "greet",
        help="Print a friendly greeting.",
        parser_setup=_setup,
    )
    def run_greet(args) -> int:
        print(f"Hello, {args.name}!")
        return 0

Usage — build a consumer CLI on top of the registry::

    from mgf.common.cli import build_cli

    parser = build_cli(prog="myapp", description="my app's CLI")
    args = parser.parse_args()
    # Dispatch via the registry's main() helper, OR directly:
    from mgf.common.cli._subcommand_registry import dispatch
    return dispatch(args, parser=parser)

Threading: register at import / bootstrap time, not from a worker.
The registry is read-only on the hot path.
"""

from __future__ import annotations

import argparse
import sys
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, overload

from mgf.common.cli._exit_codes import EXIT_UNKNOWN

if TYPE_CHECKING:
    from collections.abc import Iterator

# Type aliases — match the existing four built-ins' shape (which use
# ``Any`` for the args / parser parameters per the dispatch convention
# in ``_mgfcli.py``). Future minor version may tighten these to
# ``argparse.Namespace`` and ``argparse.ArgumentParser`` once the
# built-ins are also tightened.
SubcommandHandler = Callable[[Any], int]
"""``def handler(args) -> int``: receives the parsed Namespace,
returns an exit code (typically one of the ``EXIT_*`` constants
from :mod:`mgf.common.cli`)."""

SubcommandParserSetup = Callable[[Any], None]
"""``def parser_setup(subparser) -> None``: optional. Called once
when the parser is built; the registered function uses the standard
``subparser.add_argument(...)`` API to wire flags. ``None`` means
the subcommand takes no flags beyond ``--help``."""


@dataclass(frozen=True)
class CliSubcommand:
    """Frozen registry record. Public so consumers can introspect.

    Tests + the future ``mgf-common doctor`` snapshot the registry
    via :func:`_registered_cli_subcommands` and inspect these
    records.
    """

    name: str
    """Subcommand name as it appears on the CLI (``mgf-common <name>``)."""

    help: str
    """One-line help text shown by argparse's ``-h``."""

    handler: SubcommandHandler
    """The function executed when the subcommand is dispatched."""

    parser_setup: SubcommandParserSetup | None = None
    """Optional flag-wiring callback. ``None`` = no flags."""


_SUBCOMMANDS: dict[str, CliSubcommand] = {}


@overload
def register_cli_subcommand(
    name: str,
    *,
    handler: SubcommandHandler,
    help: str = "",
    parser_setup: SubcommandParserSetup | None = None,
) -> SubcommandHandler: ...


@overload
def register_cli_subcommand(
    name: str,
    *,
    handler: None = None,
    help: str = "",
    parser_setup: SubcommandParserSetup | None = None,
) -> Callable[[SubcommandHandler], SubcommandHandler]: ...


def register_cli_subcommand(
    name: str,
    *,
    handler: SubcommandHandler | None = None,
    help: str = "",  # noqa: A002 — argparse uses 'help'; mirroring is intentional
    parser_setup: SubcommandParserSetup | None = None,
) -> SubcommandHandler | Callable[[SubcommandHandler], SubcommandHandler]:
    """Register a subcommand under ``name``.

    Two call shapes are supported:

    1. **Plain call** — pass the handler directly::

           register_cli_subcommand(
               "greet",
               handler=run_greet,
               help="Print a friendly greeting.",
               parser_setup=_setup,
           )

    2. **Decorator** — omit the handler kwarg; the decorated function
       becomes the handler::

           @register_cli_subcommand(
               "greet",
               help="Print a friendly greeting.",
               parser_setup=_setup,
           )
           def run_greet(args) -> int:
               ...

    Raises :class:`ValueError` if ``name`` is already registered;
    callers MUST :func:`unregister_cli_subcommand` before re-registering.
    This catches accidental double-registration in plug-in chains.

    Threading: register at import / bootstrap time, not from a worker.
    """
    if handler is not None:
        # Plain call — register immediately and return the handler.
        _store(name, handler, help, parser_setup)
        return handler

    # Decorator form — return a function that registers + returns the
    # wrapped function unchanged.
    def _decorator(fn: SubcommandHandler) -> SubcommandHandler:
        _store(name, fn, help, parser_setup)
        return fn

    return _decorator


def _store(
    name: str,
    handler: SubcommandHandler,
    help_text: str,
    parser_setup: SubcommandParserSetup | None,
) -> None:
    """Inner: validate uniqueness + write the registry entry."""
    if name in _SUBCOMMANDS:
        raise ValueError(
            f"CLI subcommand {name!r} is already registered "
            f"(unregister first if you intend to replace it)"
        )
    _SUBCOMMANDS[name] = CliSubcommand(
        name=name,
        help=help_text,
        handler=handler,
        parser_setup=parser_setup,
    )


#: Short alias for :func:`register_cli_subcommand`. Closes
#: FEEDBACK V04-02 — the long form is canonical (used in error
#: messages + diagnostic output); the short alias matches click's
#: ``@click.command`` idiom for ergonomic call sites.
#:
#: Both names refer to the SAME function — ``subcommand is
#: register_cli_subcommand`` is True. Pin tests verify identity.
subcommand = register_cli_subcommand


def unregister_cli_subcommand(name: str) -> None:
    """Remove a registered subcommand. No-op if ``name`` is unknown.

    Useful for tests + for consumers swapping a subcommand at runtime
    (e.g., overriding the built-in ``doctor`` with a project-specific
    extended version: unregister, then re-register).
    """
    _SUBCOMMANDS.pop(name, None)


def _registered_cli_subcommands() -> dict[str, CliSubcommand]:
    """Internal: snapshot of the registry. Used by tests."""
    return dict(_SUBCOMMANDS)


def _iter_registered() -> Iterator[CliSubcommand]:
    """Iterate registered subcommands in insertion order. Used by
    :func:`build_cli`."""
    return iter(_SUBCOMMANDS.values())


# ---------------------------------------------------------------------------
# build_cli + dispatch — the public CLI-construction primitives
# ---------------------------------------------------------------------------


def build_cli(
    prog: str = "mgf-common",
    *,
    description: str | None = None,
    include_builtins: bool = True,
) -> argparse.ArgumentParser:
    """Construct an :class:`argparse.ArgumentParser` populated from
    the registry.

    Consumers building their own top-level CLIs use this to share
    the diagnostic surface that ``mgf-common`` ships — for example,
    a consumer's ``vmanager`` CLI can include ``vmanager app-doctor``
    by calling ``build_cli(prog="vmanager")``. The four built-in
    subcommands (registered by ``mgf-fed[cli]`` at import time since the CLI extraction: ``app-doctor`` / ``explain`` / ``validate`` /
    ``migrate``) are pre-registered at import of
    :mod:`mgf.common.cli._mgfcli`; consumer-registered subcommands
    are added on top.

    Parameters
    ----------
    prog
        The program name shown in help / usage. Default
        ``"mgf-common"`` for the diagnostic CLI; pass ``"myapp"``
        for your own top-level CLI.
    description
        Optional CLI description. ``None`` uses a sensible default.
    include_builtins
        When ``True`` (default), every subcommand currently in the
        registry — including the four built-ins — appears in the
        parser. When ``False``, the four built-ins (``explain`` /
        ``validate`` / ``doctor`` / ``migrate``) are filtered out
        and only consumer-registered subcommands ship; useful for
        consumers who want to expose a subset.

    Returns
    -------
    argparse.ArgumentParser
        Ready to ``parser.parse_args()``. The dispatch step is
        :func:`dispatch` — pass the result of ``parse_args`` plus
        the parser itself (for the no-subcommand help fallback).
    """
    if description is None:
        description = (
            "mgf-common diagnostic CLI — explain settings, validate "
            "config files, run install-state checks, run migration "
            "codemods."
        )

    parser = argparse.ArgumentParser(prog=prog, description=description)
    sub = parser.add_subparsers(
        title="subcommands",
        dest="command",
        # Allow `<prog>` (no subcommand) to print help-on-stderr +
        # return EXIT_UNKNOWN. Mirrors the v0.3 _mgfcli behaviour.
        required=False,
    )

    builtins = {"explain", "validate", "doctor", "migrate"}
    for entry in _iter_registered():
        if not include_builtins and entry.name in builtins:
            continue
        sp = sub.add_parser(entry.name, help=entry.help)
        if entry.parser_setup is not None:
            entry.parser_setup(sp)

    return parser


def dispatch(
    args: argparse.Namespace,
    *,
    parser: argparse.ArgumentParser | None = None,
) -> int:
    """Look up ``args.command`` in the registry and run its handler.

    Returns the handler's exit code. When ``args.command`` is empty
    (the user ran the CLI with no subcommand) OR unknown (typo'd
    name; shouldn't happen via argparse but defensive), prints the
    help text to stderr (when ``parser`` is supplied) and returns
    :data:`mgf.common.cli.EXIT_UNKNOWN`.

    Use as the bottom half of a consumer top-level CLI::

        parser = build_cli(prog="myapp")
        args = parser.parse_args()
        sys.exit(dispatch(args, parser=parser))
    """
    name = getattr(args, "command", None)
    entry = _SUBCOMMANDS.get(name) if name else None
    if entry is None:
        if parser is not None:
            parser.print_help(sys.stderr)
        return EXIT_UNKNOWN
    return entry.handler(args)


__all__ = [
    "CliSubcommand",
    "SubcommandHandler",
    "SubcommandParserSetup",
    "build_cli",
    "dispatch",
    "register_cli_subcommand",
    "subcommand",
    "unregister_cli_subcommand",
]
