"""Unified CLI entry point for culture.

Commands are organized into noun-based groups:
    culture agent    {create,join,start,stop,status,rename,assign,sleep,wake,learn,message,read,archive,unarchive,delete}
    culture server   {start,stop,status,default,rename,archive,unarchive,restart,link,logs,version,serve}
    culture console  {...irc-lens verbs and flags...}    # passthrough; reactive web console
    culture mesh     {overview,setup,update,console}     # `console` here is deprecated; use `culture console`
    culture channel  {list,read,message,who}
    culture bot      {create,start,stop,list,inspect,archive,unarchive}
    culture skills   {install}
    culture devex    {...developer-experience passthrough (powered by agex-cli)...}
    culture afi      {...agent-first interface passthrough (powered by afi-cli)...}

Universal verbs (available at the root):
    culture explain [topic]    full description of topic (default: culture)
    culture overview [topic]   shallow summary
    culture learn [topic]      agent-facing onboarding prompt
"""

from __future__ import annotations

import argparse
import logging
import sys
from typing import NoReturn

from culture import __version__
from culture.cli import (
    afi,
    agent,
    bot,
    channel,
    console,
    devex,
    introspect,
    mesh,
    server,
    skills,
)
from culture.cli._errors import EXIT_USER_ERROR, CultureError
from culture.cli._output import emit_error

GROUPS = [agent, server, mesh, channel, bot, skills, devex, afi, console, introspect]


def _names_of(group) -> set[str]:
    names = getattr(group, "NAMES", None)
    if names is not None:
        return set(names)
    return {group.NAME}


def _json_mode_active(argv: list[str]) -> bool:
    """``--json`` is meaningful only on the three universal verbs.

    Other groups (`agent`, `server`, …) reject ``--json`` themselves; we
    only honor the AgentCulture JSON-error contract when the user is
    addressing an introspection verb. This matches what katvan's
    reference-sync actually invokes.
    """
    if "--json" not in argv:
        return False
    return any(v in argv for v in introspect.NAMES)


class _JsonAwareParser(argparse.ArgumentParser):
    """``argparse.ArgumentParser`` that honors the ``--json`` error contract.

    When the user runs e.g. ``culture explain --bogus --json``, argparse's
    default ``error()`` writes plain text to stderr and exits 2. That
    violates the AgentCulture sibling contract that *any* failure under
    ``--json`` emit a parseable ``{code, message, remediation}`` object on
    stderr. We override ``error()`` so parse-time failures route through
    :func:`culture.cli._output.emit_error` whenever ``--json`` is active.

    Applied at the top parser and (via ``parser_class``) at every
    subparser, so any subcommand that reaches ``parse_args()`` honors the
    contract.
    """

    def error(self, message: str) -> NoReturn:  # type: ignore[override]
        if _json_mode_active(sys.argv[1:]):
            err = CultureError(
                code=EXIT_USER_ERROR,
                message=message,
                remediation="run 'culture --help' or 'culture explain' for usage",
            )
            emit_error(err, json_mode=True)
            self.exit(EXIT_USER_ERROR)
        super().error(message)


def _build_parser() -> argparse.ArgumentParser:
    parser = _JsonAwareParser(
        prog="culture",
        description="culture — AI agent IRC mesh",
    )
    parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
    sub = parser.add_subparsers(dest="command", parser_class=_JsonAwareParser)
    for group in GROUPS:
        group.register(sub)
    return parser


def _maybe_forward_to_agentirc(argv: list[str]) -> int | None:
    """Bypass argparse for ``culture server <forwarded-verb> ...`` calls.

    Returns the exit code to propagate, or ``None`` if argparse should
    handle the invocation. argparse's ``REMAINDER`` parser cannot capture
    ``--help`` reliably (it leaks to the root parser as an unrecognized
    argument), so the forwarded surface is short-circuited here before
    argparse runs.
    """
    if len(argv) < 2 or argv[0] != "server":
        return None
    if argv[1] not in server._AGENTIRC_FORWARDED_VERBS:
        return None
    from agentirc.cli import dispatch as _agentirc_dispatch

    return _agentirc_dispatch(argv[1:])


def main() -> None:
    # Logging must be configured before any dispatch path runs — including
    # the agentirc forwarder bypass below — so any logs emitted by
    # agentirc.cli.dispatch land in culture's standard format rather than
    # whatever default the importing process happens to have.
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(name)s %(levelname)s %(message)s",
    )

    try:
        forwarded = _maybe_forward_to_agentirc(sys.argv[1:])
        if forwarded is not None:
            sys.exit(forwarded)

        parser = _build_parser()
        args = parser.parse_args()

        if args.command is None:
            parser.print_help()
            sys.exit(1)

        for group in GROUPS:
            if args.command in _names_of(group):
                group.dispatch(args)
                return
        parser.print_help()
        sys.exit(1)
    except KeyboardInterrupt:
        pass
    except Exception as exc:
        # Honor the --json contract on unexpected exceptions too: an agent
        # consumer that asked for JSON should never see a plain-text
        # "Error: ..." trailer on stderr instead of the structured shape.
        if _json_mode_active(sys.argv[1:]):
            emit_error(
                CultureError(
                    code=1,
                    message=str(exc),
                    remediation="check the command and try again",
                ),
                json_mode=True,
            )
        else:
            print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)
