"""AppRunner: app-level command routing through painted.

Mirrors the CliRunner + run_cli pattern one level up. CliRunner handles
a single command with zoom/mode/format; AppRunner routes between multiple
commands and renders top-level help through painted.

Usage:
    from painted.cli import run_app, AppCommand

    commands = [
        AppCommand("status", "Show store status", _run_status),
        AppCommand("log", "Show recent facts", _run_log),
    ]
    run_app(sys.argv[1:], commands, prog="myapp")
"""

from __future__ import annotations

import json
import shutil
import sys
from collections.abc import Callable
from dataclasses import dataclass

from .help import (
    HelpArg,
    HelpData,
    HelpFlag,
    HelpGroup,
    help_args_to_flags,
    render_help,
    scan_help_args,
)
from .types import Format, Zoom


@dataclass(frozen=True)
class AppCommand:
    """A routable command with name, description, and handler.

    The handler receives argv (remaining args after command name) and
    returns an exit code.
    """

    name: str  # "status"
    description: str  # "Show store status"
    handler: Callable[[list[str]], int]  # receives argv[1:], returns exit code
    detail: str | None = None  # shown at DETAILED+ zoom, e.g. usage hint
    help_args: list[HelpArg] | None = None  # when set, AppRunner intercepts -h


@dataclass(frozen=True)
class AppRunner:
    """App-level command router with painted help rendering.

    Routes argv[0] to the matching AppCommand handler. When no args
    or --help/-h is given, renders help through painted (zoom-aware).
    """

    commands: tuple[AppCommand, ...]
    prog: str | None = None
    description: str | None = None

    def run(self, argv: list[str]) -> int:
        """Route argv to command handler, or show help."""
        # No args → painted help
        if not argv:
            return self._handle_help([])

        name = argv[0]

        # Command name first → dispatch (or intercept -h for action commands)
        rest = argv[1:]
        for cmd in self.commands:
            if cmd.name == name:
                if cmd.help_args is not None and ("-h" in rest or "--help" in rest):
                    return self._handle_subcommand_help(cmd, rest)
                return cmd.handler(rest)

        # No command matched — check for --help/-h (top-level help)
        if "-h" in argv or "--help" in argv:
            return self._handle_help(argv)

        # Unknown command → error + help to stderr
        from ..core.block import Block
        from ..core.cell import Style
        from ..core.writer import print_block

        try:
            from ..palette import current_palette

            error_style = current_palette().error
        except Exception:
            error_style = Style(fg="red")

        error_block = Block.text(f"Unknown command: {name}", error_style)
        print_block(error_block, sys.stderr, use_ansi=True)

        # Show help to stderr
        help_data = self._build_help_data()
        width = shutil.get_terminal_size().columns
        help_block = render_help(help_data, Zoom.SUMMARY, width, use_ansi=True)
        print_block(help_block, sys.stderr, use_ansi=True)

        return 1

    def _handle_help(self, args: list[str]) -> int:
        """Render zoom-aware help and return 0."""
        from ..core.writer import print_block

        zoom, fmt = scan_help_args(args)
        help_data = self._build_help_data()

        if fmt == Format.JSON:
            from dataclasses import asdict

            print(json.dumps(asdict(help_data), default=str))
            return 0

        use_ansi = fmt != Format.PLAIN
        if fmt == Format.AUTO:
            use_ansi = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()

        width = shutil.get_terminal_size().columns
        block = render_help(help_data, zoom, width, use_ansi)
        print_block(block, use_ansi=use_ansi)
        return 0

    def _handle_subcommand_help(self, cmd: AppCommand, args: list[str]) -> int:
        """Render zoom-aware help for a subcommand and return 0."""
        from ..core.writer import print_block

        zoom, fmt = scan_help_args(args)
        help_data = self._build_subcommand_help_data(cmd)

        if fmt == Format.JSON:
            from dataclasses import asdict

            print(json.dumps(asdict(help_data), default=str))
            return 0

        use_ansi = fmt != Format.PLAIN
        if fmt == Format.AUTO:
            use_ansi = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()

        width = shutil.get_terminal_size().columns
        block = render_help(help_data, zoom, width, use_ansi)
        print_block(block, use_ansi=use_ansi)
        return 0

    def _build_subcommand_help_data(self, cmd: AppCommand) -> HelpData:
        """Build HelpData for a subcommand from its help_args."""
        assert cmd.help_args is not None

        groups: list[HelpGroup] = []

        # Command args as unnamed primary group
        command_flags = help_args_to_flags(cmd.help_args)
        if command_flags:
            groups.append(HelpGroup(name="", flags=command_flags))

        # Help flag — subordinate when command has its own args
        help_min_zoom = Zoom.SUMMARY if command_flags else Zoom.MINIMAL
        help_group = HelpGroup(
            name="Help",
            flags=(HelpFlag("-h", "--help", "Show this help", detail="Add -v for more detail."),),
            min_zoom=help_min_zoom,
        )
        groups.append(help_group)

        prog = f"{self.prog} {cmd.name}" if self.prog else cmd.name
        return HelpData(
            prog=prog,
            description=cmd.description,
            groups=tuple(groups),
        )

    def _build_help_data(self) -> HelpData:
        """Build HelpData from commands."""
        # Commands as primary group
        command_flags = tuple(
            HelpFlag(short=None, long=cmd.name, description=cmd.description, detail=cmd.detail)
            for cmd in self.commands
        )
        commands_group = HelpGroup(name="Commands", flags=command_flags)

        # Framework groups — subordinate when commands are the primary content
        zoom_group = HelpGroup(
            name="Zoom",
            hint="(what to show)",
            detail="Controls how much detail is rendered. Stackable: -v for detailed, -vv for full.",
            flags=(
                HelpFlag("-q", "--quiet", "Minimal output"),
                HelpFlag("-v", "--verbose", "Detailed (-v) or full (-vv)"),
            ),
            min_zoom=Zoom.SUMMARY,
        )

        format_group = HelpGroup(
            name="Format",
            hint="(serialization)",
            detail="Output serialization. ANSI is default for TTY, PLAIN for pipes.",
            flags=(
                HelpFlag(None, "--json", "JSON output", detail="Implies --static."),
                HelpFlag(None, "--plain", "Plain text, no ANSI codes"),
            ),
            min_zoom=Zoom.SUMMARY,
        )

        help_group = HelpGroup(
            name="Help",
            flags=(HelpFlag("-h", "--help", "Show this help", detail="Add -v for more detail."),),
            min_zoom=Zoom.SUMMARY,
        )

        return HelpData(
            prog=self.prog,
            description=self.description,
            groups=(commands_group, zoom_group, format_group, help_group),
        )


def run_app(
    argv: list[str],
    commands: list[AppCommand] | tuple[AppCommand, ...],
    *,
    prog: str | None = None,
    description: str | None = None,
) -> int:
    """Run an app with command routing and painted help.

    Convenience function that creates an AppRunner and runs it.

    Args:
        argv: Command-line arguments (sys.argv[1:])
        commands: Available commands
        prog: Program name for help
        description: Program description for help

    Returns:
        Exit code (0 for success)
    """
    return AppRunner(
        commands=tuple(commands),
        prog=prog,
        description=description,
    ).run(argv)
