"""CLI commands for valn8n."""

import sys
from collections.abc import Sequence
from pathlib import Path

from valn8n.cli.args import build_parser
from valn8n_lib.output import Formatter, get_formatter
from valn8n_lib.rules import IO_RULE_GROUPS, IO_RULE_REGISTRY, RULE_GROUPS, RULE_REGISTRY
from valn8n_lib.types import SuggestionLevel, parse_tolerate_node_keys
from valn8n_lib.validator import validate_all

EXIT_CLEAN = 0
EXIT_VIOLATIONS = 1
EXIT_ERROR = 2


def cmd_check(args: object) -> int:
    """Run the check subcommand."""
    paths: list[str] = getattr(args, "paths", [])
    output_format: str = getattr(args, "output_format", "rich")
    select: set[str] | None = getattr(args, "select", None)
    ignore: set[str] | None = getattr(args, "ignore", None)
    extend_select: set[str] | None = getattr(args, "extend_select", None)
    strict_only: bool = getattr(args, "strict_only", False)
    quiet: bool = getattr(args, "quiet", False)
    suggestions_str: str = getattr(args, "suggestions", "full")
    suggestions = SuggestionLevel(suggestions_str)
    tolerate_raw: str = getattr(args, "tolerate_node_keys", "")
    tolerate_node_keys = parse_tolerate_node_keys(tolerate_raw)

    formatter = get_formatter(output_format)
    resolved = [Path(p) for p in paths]

    return cmd_check_provisioned(
        resolved,
        formatter=formatter,
        select=select,
        ignore=ignore,
        extend_select=extend_select,
        quiet=quiet,
        suggestions=suggestions,
        tolerate_node_keys=tolerate_node_keys,
        strict_only=strict_only,
    )


def cmd_check_provisioned(
    paths: list[Path],
    *,
    formatter: Formatter,
    select: set[str] | None = None,
    ignore: set[str] | None = None,
    extend_select: set[str] | None = None,
    quiet: bool = False,
    suggestions: SuggestionLevel = SuggestionLevel.FULL,
    tolerate_node_keys: frozenset[str] | None = frozenset(),
    strict_only: bool = False,
) -> int:
    """Run check with injected dependencies (for testability)."""
    missing = [p for p in paths if not p.exists()]
    if missing:
        for m in missing:
            print(f"valn8n: error: path not found: {m}", file=sys.stderr)
        return EXIT_ERROR

    results, _ = validate_all(
        paths,
        select=select,
        ignore=ignore,
        extend_select=extend_select,
        suggestions=suggestions,
        tolerate_node_keys=tolerate_node_keys,
        strict_only=strict_only,
    )

    formatter.render(results, quiet=quiet)

    has_violations = any(len(d) > 0 for d in results.values())
    return EXIT_VIOLATIONS if has_violations else EXIT_CLEAN


def _print_rule_groups(
    rules_by_code: list[tuple[str, str, str, str]],
    groups: dict[str, str],
) -> None:
    """Print rules organized by group prefix."""
    grouped: dict[str, list[tuple[str, str, str, str]]] = {prefix: [] for prefix in groups}
    other: list[tuple[str, str, str, str]] = []

    for code, severity, strict_label, description in rules_by_code:
        matched = False
        for prefix in groups:
            if code.startswith(prefix):
                grouped[prefix].append((code, severity, strict_label, description))
                matched = True
                break
        if not matched:
            other.append((code, severity, strict_label, description))

    for prefix, group_name in groups.items():
        entries = grouped[prefix]
        if not entries:
            continue
        print(f"{group_name} ({prefix}xx):")
        for code, severity, strict_label, description in entries:
            print(f"  {code}  {severity:<7}  {strict_label:<6}  {description}")
        print()

    if other:
        print("Other:")
        for code, severity, strict_label, description in other:
            print(f"  {code}  {severity:<7}  {strict_label:<6}  {description}")
        print()


def cmd_rule() -> int:
    """List all registered rules grouped by topic."""
    # IO rules (metadata-only)
    io_entries = [
        (code, rc.severity.value, "", rc.description)
        for code, rc in sorted(IO_RULE_REGISTRY.items())
    ]
    _print_rule_groups(io_entries, IO_RULE_GROUPS)

    # ND rules
    nd_entries = [
        (code, rc.severity.value, "strict" if rc.strict else "", rc.description)
        for code, (rc, _) in sorted(RULE_REGISTRY.items())
    ]
    _print_rule_groups(nd_entries, RULE_GROUPS)

    return EXIT_CLEAN


def run(argv: Sequence[str] | None = None) -> int:
    """CLI entry point. Parse args, dispatch to subcommand, return exit code."""
    parser = build_parser()
    args = parser.parse_args(argv)

    if args.command is None:
        parser.print_help()
        return EXIT_CLEAN

    try:
        if args.command == "check":
            return cmd_check(args)
        if args.command == "rule":
            return cmd_rule()
    except Exception as exc:
        print(f"valn8n: internal error: {exc}", file=sys.stderr)
        return EXIT_ERROR

    return EXIT_CLEAN
