"""Public CLI API for Vidhi configuration management.

This module provides the user-facing API for CLI-based configuration:
- field(): Enhanced dataclass field with CLI options
- parse_cli_args(): Parse CLI arguments into a single config instance
- parse_cli_sweep(): Parse CLI arguments into multiple configs (for sweeps)
- with_cli_overrides(): Override a manually constructed config from CLI
"""

from __future__ import annotations

import os
import sys
from dataclasses import MISSING
from dataclasses import field as dataclass_field
from typing import Any, List, Type, TypeVar, cast

from vidhi.base_command import BaseCommand
from vidhi.nested_config import (
    ConfigWalker,
    _collect_dict_paths,
    _extract_config_file_arg,
    _merge_with_provided,
    parse_cli_sweep_nested,
)
from vidhi.utils import create_class_from_dict, dataclass_to_dict, load_yaml_config

T = TypeVar("T")


def field(
    default: Any = MISSING,
    *,
    default_factory: Any = MISSING,
    help: str | None = None,
    name: str | None = None,
    aliases: list[str] | None = None,
    choices: list | None = None,
    shortcuts: dict[str, str] | None = None,
    positional: bool = False,
    # Standard dataclass field parameters
    repr: bool = True,
    hash: bool | None = None,
    init: bool = True,
    compare: bool = True,
    metadata: dict | None = None,
    kw_only: bool = False,
) -> Any:
    """Define a dataclass field with optional CLI configuration.

    This is a drop-in replacement for dataclasses.field() that adds
    convenient CLI argument configuration.

    Args:
        default: Default value for the field
        default_factory: Factory function for default value
        help: Help text shown in --help output
        name: Custom CLI argument name (e.g., "lr" instead of "learning_rate")
        aliases: Alternative CLI names for backwards compatibility (e.g., ["learning-rate"])
        choices: List of valid values for this argument
        shortcuts: Map of Enum members to short flag strings. Each entry creates a
            single-dash flag that sets this field to that Enum value.
            E.g., ``{Action.REATTACH: "r"}`` creates ``-r`` which sets the field
            to ``Action.REATTACH``. Multi-character flags are supported
            (e.g., ``{Action.LIST_DIR: "ld"}`` creates ``-ld``).
        positional: If True, this field accepts positional (non-flag) arguments.
            Only one field per config can be positional.
        repr: Include in repr output
        hash: Include in hash
        init: Include in __init__
        compare: Include in comparison
        metadata: Additional metadata dict
        kw_only: Keyword-only argument

    Returns:
        A dataclass field descriptor

    Example:
        >>> @frozen_dataclass
        >>> class Config:
        >>>     learning_rate: float = field(
        >>>         0.001,
        >>>         help="Learning rate",
        >>>         name="lr",
        >>>         aliases=["learning-rate"]  # For backwards compatibility
        >>>     )
        >>>     batch_size: int = field(32, help="Training batch size")
        >>>     debug: bool = False  # Regular default also works
        >>>     optimizer: str = field("adam", choices=["adam", "sgd", "rmsprop"])
        >>>     action: Action = field(
        >>>         Action.DEFAULT,
        >>>         shortcuts={Action.RUN: "r", Action.LIST: "l"},
        >>>     )
        >>>     target: str = field(None, positional=True)
    """
    # Build CLI metadata
    cli_metadata = {}
    if help is not None:
        cli_metadata["help"] = help
    if name is not None:
        cli_metadata["argname"] = name
    if aliases is not None:
        cli_metadata["aliases"] = aliases
    if choices is not None:
        cli_metadata["choices"] = choices
    if shortcuts is not None:
        from enum import Enum

        for enum_val, flag in shortcuts.items():
            if not isinstance(enum_val, Enum):
                raise TypeError(
                    f"Shortcut key must be an Enum member, got {type(enum_val).__name__}: {enum_val!r}"
                )
            if not isinstance(flag, str):
                raise TypeError(
                    f"Shortcut flag for {enum_val} must be a string, got {type(flag).__name__}: {flag!r}"
                )
            if flag in ("h", "help"):
                raise ValueError(
                    f"Shortcut flag '-{flag}' is reserved for --help and cannot be overridden"
                )
        cli_metadata["shortcuts"] = shortcuts
    if positional:
        cli_metadata["positional"] = positional

    # Merge with user-provided metadata
    final_metadata = {**(metadata or {}), **cli_metadata}

    # Handle default vs default_factory
    field_kwargs = {
        "repr": repr,
        "hash": hash,
        "init": init,
        "compare": compare,
        "metadata": final_metadata if final_metadata else None,
        "kw_only": kw_only,
    }

    if default is not MISSING:
        field_kwargs["default"] = default
    if default_factory is not MISSING:
        field_kwargs["default_factory"] = default_factory

    return dataclass_field(**field_kwargs)


def parse_cli_args(
    config_class: Type[T],
    *,
    args: list[str] | None = None,
    description: str | None = None,
) -> T:
    """Parse CLI arguments and return a single configuration instance.

    This is the main entry point for CLI-based configuration. It automatically
    generates an argument parser from the config class structure and returns
    a fully instantiated config object.

    Args:
        config_class: The dataclass type to parse arguments for
        args: Optional list of arguments (defaults to sys.argv)
        description: Optional program description for --help

    Returns:
        A single config instance.

    Raises:
        ValueError: If the CLI arguments would produce multiple configs
            (e.g., from a sweep file). Use parse_cli_sweep() for sweeps.

    Example:
        >>> @frozen_dataclass
        >>> class TrainingConfig:
        >>>     learning_rate: float = field(0.001, help="Learning rate")
        >>>     batch_size: int = field(32, help="Batch size")
        >>>
        >>> config = parse_cli_args(TrainingConfig)
        >>> print(config.learning_rate)

        Command line:
        $ python train.py --learning_rate 0.01 --batch_size 64
    """
    # Subcommand dispatch: resolve early, delegate to normal parsing
    if _is_base_command(config_class):
        return cast(
            T,
            _parse_subcommand(
                cast(Type[BaseCommand], config_class),
                args=args,
                description=description,
            ),
        )

    configs = parse_cli_sweep(config_class, args=args, description=description)

    if len(configs) > 1:
        raise ValueError(
            f"CLI arguments produced {len(configs)} configs. "
            "Use parse_cli_sweep() for sweep configurations."
        )

    return configs[0]


def parse_cli_sweep(
    config_class: Type[T],
    *,
    args: list[str] | None = None,
    description: str | None = None,
) -> List[T]:
    """Parse CLI arguments and return multiple configuration instances.

    Use this for sweep/grid configurations where CLI arguments or config
    files can produce multiple config combinations.

    Args:
        config_class: The dataclass type to parse arguments for
        args: Optional list of arguments (defaults to sys.argv)
        description: Optional program description for --help

    Returns:
        List of config instances.

    Example:
        >>> configs = parse_cli_sweep(TrainingConfig)
        >>> for config in configs:
        >>>     train(config)
    """
    _ = args  # Reserved for future: custom argument list
    _ = description  # Reserved for future: argparse description

    walker = ConfigWalker(config_class)
    parser = walker.build_parser()
    return parse_cli_sweep_nested(config_class, walker, parser)


def with_cli_overrides(config: T) -> T:
    """Override a manually constructed config with CLI arguments.

    This allows you to define defaults programmatically and let users
    override specific values from the command line. Supports YAML file
    loading via --config.

    Priority: CLI args > YAML file > code defaults

    Args:
        config: A dataclass config instance with your default values

    Returns:
        A new config instance with CLI overrides applied.

    Example:
        >>> def create_config():
        >>>     return InferenceEngineConfig(
        >>>         model="gpt-4",
        >>>         temperature=0.7,
        >>>         max_tokens=1000,
        >>>     )
        >>>
        >>> config = with_cli_overrides(create_config())
        >>>
        >>> # Users can now run:
        >>> # python script.py --temperature 0.9 --max_tokens 2000
        >>> # Or with a YAML file:
        >>> # python script.py --config settings.yaml --temperature 0.9
    """
    config_class = type(config)

    base_dict = dataclass_to_dict(config)

    walker = ConfigWalker(config_class)
    parser = walker.build_parser()

    # Check if any CLI args were provided
    argv_copy = sys.argv.copy()
    config_file = _extract_config_file_arg(argv_copy)

    _, cli_provided = parser.parse_args(argv_copy[1:])

    if not cli_provided and not config_file:
        return config

    # Full parse to get CLI config
    configs = parse_cli_sweep_nested(config_class, walker, parser)

    if len(configs) > 1:
        raise ValueError(
            f"CLI arguments produced {len(configs)} configs. "
            "with_cli_overrides() only supports single configs."
        )

    cli_config = configs[0]
    cli_dict = dataclass_to_dict(cli_config)

    provided_fields = _get_provided_fields(cli_provided)

    # YAML file values also override the base config
    if config_file:
        file_config = load_yaml_config(config_file)
        _collect_dict_paths(file_config, (), provided_fields)

    merged_dict = _merge_with_provided(base_dict, cli_dict, provided_fields)

    return create_class_from_dict(config_class, merged_dict)


def _get_provided_fields(provided_arg_names: set) -> set:
    """Convert flat arg names to nested field paths."""
    result = set()
    for name in provided_arg_names:
        parts = tuple(name.split("."))
        result.add(parts)
    return result


# =============================================================================
# Subcommand Support
# =============================================================================


def _is_base_command(cls: type) -> bool:
    """Check if cls is a BaseCommand subclass that serves as a command group."""
    return (
        isinstance(cls, type)
        and issubclass(cls, BaseCommand)
        and getattr(cls, "_is_base_command", False)
    )


def _parse_subcommand(
    base_class: Type[BaseCommand],
    *,
    args: list[str] | None = None,
    description: str | None = None,
) -> BaseCommand:
    """Parse a subcommand-based CLI."""
    argv = list(args if args is not None else sys.argv[1:])

    # Root-level help
    if argv and argv[0] in ("-h", "--help"):
        _print_subcommand_help(base_class, description)
        sys.exit(0)

    # No args — use default command
    if not argv:
        default_cls = base_class.get_default_subcommand()
        if default_cls is not None:
            subcommand_cls = default_cls
            remaining_args = []
        else:
            _print_subcommand_help(base_class, description)
            sys.exit(0)
    else:
        first_arg = argv[0]

        subcommand_cls = base_class.resolve_subcommand(first_arg)

        if subcommand_cls is not None:
            remaining_args = argv[1:]
        elif first_arg.startswith("-"):
            default_cls = base_class.get_default_subcommand()
            if default_cls is not None:
                subcommand_cls = default_cls
                remaining_args = argv
            else:
                _print_subcommand_help(base_class, description)
                sys.exit(1)
        else:
            all_names = sorted(base_class._subcommands.keys())
            all_aliases = sorted(base_class._aliases.keys())
            available = ", ".join(all_names + all_aliases)
            print(
                f"Error: Unknown command '{first_arg}'. " f"Available: {available}",
                file=sys.stderr,
            )
            sys.exit(1)

    # Delegate to normal parse_cli_args with the resolved subcommand class.
    saved_argv = sys.argv
    try:
        sys.argv = [sys.argv[0]] + remaining_args
        return parse_cli_args(subcommand_cls, description=description)
    finally:
        sys.argv = saved_argv


def _print_subcommand_help(
    base_class: Type[BaseCommand], description: str | None = None
) -> None:
    """Print help listing all available subcommands."""
    from vidhi.cli_parser import ColorFormatter

    c = ColorFormatter()
    lines = []

    prog = os.path.basename(sys.argv[0])
    lines.append(f"usage: {c.bold(prog)} {c.dim('<command>')} {c.dim('[options]')}")
    lines.append("")

    desc = description or (base_class.__doc__ or "").strip().split("\n")[0]
    if desc:
        lines.append(desc)
        lines.append("")

    lines.append(c.header("Commands:"))

    entries = []
    for name in sorted(base_class._subcommands.keys()):
        cls = base_class._subcommands[name]
        alias = getattr(cls, "_command_alias", None)
        label = f"{name}, {alias}" if alias else name
        doc = (cls.__doc__ or "").strip().split("\n")[0]
        is_default = name == base_class._default_command
        entries.append((label, doc, is_default))

    max_label = max((len(e[0]) for e in entries), default=0)

    for label, doc, is_default in entries:
        default_marker = c.dim(" (default)") if is_default else ""
        padded = label.ljust(max_label + 2)
        lines.append(f"  {c.option(padded)}{doc}{default_marker}")

    lines.append("")
    lines.append(
        f"Run '{c.bold(prog)} {c.dim('<command> --help')}' for command-specific options."
    )

    print("\n".join(lines))
