"""
Command discovery and argparse introspection for Django management commands.
"""

import logging
import re
from argparse import ArgumentParser
from dataclasses import dataclass
from functools import lru_cache
from typing import Any

from django.core.management import get_commands, load_command_class
from django.core.management.base import BaseCommand

logger = logging.getLogger(__name__)

# Actions that exit the process and cannot be used as form fields
NON_FIELD_ACTIONS = frozenset(['help', 'version'])



@dataclass
class CommandInfo:
    """Metadata about a Django management command."""
    name: str                    # Command name (e.g., 'migrate')
    app_label: str              # Source app (e.g., 'django.core')
    help_text: str              # Command help/description


@dataclass
class ArgumentInfo:
    """Metadata about a single command argument."""
    name: str                           # Argument name (e.g., '--verbosity')
    dest: str                           # Destination variable name
    type: type | None                   # Python type (str, int, float)
    required: bool                      # Is argument required?
    default: Any                        # Default value
    help_text: str                      # Help text for form label
    choices: list[str] | None          # Valid choices (if any)
    action: str                         # argparse action (store, store_true, etc.)
    nargs: str | int | None            # Number of arguments expected
    is_positional: bool                 # True if positional argument
    position: int | None                # Position in _actions list (for positional args)
    is_base_command_argument: bool      # True if BaseCommand arg, False if command-specific


@dataclass
class CommandSchema:
    """Schema of a management command: help text and argument definitions."""
    help_text: str                      # Command description
    arguments: list[ArgumentInfo]       # List of arguments
    app_label: str = ''                 # Source app (e.g., 'django.core')


@lru_cache(maxsize=1)
def discover_commands() -> dict[str, list[CommandInfo]]:
    """
    Discover all available Django management commands grouped by app.

    Uses Django's management API to find all registered commands,
    loads their classes to extract help text, and groups by source app.

    Returns:
        Dict mapping app_label to list of CommandInfo objects.
        Commands are not sorted; caller should sort if needed.

    Example:
        {
            'django.core': [
                CommandInfo(name='migrate', app_label='django.core', help_text='...'),
                CommandInfo(name='makemigrations', app_label='django.core', help_text='...'),
            ],
            'myapp': [
                CommandInfo(name='my_command', app_label='myapp', help_text='...'),
            ],
        }
    """
    commands_by_app: dict[str, list[CommandInfo]] = {}
    command_names = get_commands()

    for command_name, app_label in command_names.items():
        try:
            command_class = load_command_class(app_label, command_name)
            help_text = command_class.help or ''

            command_info = CommandInfo(
                name=command_name,
                app_label=app_label,
                help_text=help_text,
            )

            if app_label not in commands_by_app:
                commands_by_app[app_label] = []

            commands_by_app[app_label].append(command_info)

        except Exception as e:
            logger.warning(
                'Failed to load command %s from app %s: %s',
                command_name,
                app_label,
                str(e),
            )
            continue

    return commands_by_app


@lru_cache(maxsize=128)
def introspect_command(command_name: str) -> CommandSchema:
    """
    Extract argparse arguments from a Django management command.

    Creates a temporary ArgumentParser, calls command.add_arguments(),
    and introspects the parser's internal structure to extract argument metadata.
    Filters out standard Django arguments (verbosity, settings, etc.).

    Args:
        command_name: Name of the management command to introspect

    Returns:
        CommandSchema with help text and list of ArgumentInfo objects

    Raises:
        KeyError: If command not found in registered commands
        ValueError: If command class fails to load or introspect
    """
    command_names = get_commands()

    # FAIL FAST: Validate command exists
    if command_name not in command_names:
        raise KeyError(f'Command "{command_name}" not found in registered commands')

    app_label = command_names[command_name]

    try:
        # load_command_class returns a class (<Django 6.0) or instance (Django 6.0+)
        command_or_class = load_command_class(app_label, command_name)
        command = command_or_class if isinstance(command_or_class, BaseCommand) else command_or_class()
        parser = command.create_parser('django-admin', command_name)

        # Extract help text
        help_text = command.help or parser.description or ''

        # Introspect parser actions
        arguments = _extract_arguments_from_parser(parser)

        return CommandSchema(
            help_text=help_text,
            arguments=arguments,
            app_label=app_label,
        )

    except Exception as e:
        logger.error(
            'Failed to introspect command %s from app %s: %s',
            command_name,
            app_label,
            str(e),
            exc_info=True,  # Include full traceback in logs
        )
        raise ValueError(
            f'Failed to introspect command "{command_name}": {str(e)}'
        ) from e


def _camel_to_snake(name: str) -> str:
    """
    Convert CamelCase to snake_case.

    Examples:
        StoreTrue → store_true
        StoreFalse → store_false
        StoreConst → store_const
    """
    # Insert underscore before uppercase letters (except first)
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    # Insert underscore before uppercase followed by lowercase
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()


@lru_cache(maxsize=1)
def _get_base_command_arguments() -> frozenset[str]:
    """
    Extract argument dest names from a clean BaseCommand instance.

    Creates a minimal BaseCommand parser and introspects its actions
    to identify all standard Django BaseCommand arguments. This is the
    authoritative source for classifying arguments.

    Returns:
        Frozenset of argument dest names (e.g., {'verbosity', 'traceback', 'help', 'version', ...})

    Implementation notes:
        - Cached for performance (BaseCommand args are static)
        - Includes help and version (even though we won't create form fields for them)
        - Used by _extract_arguments_from_parser() to classify arguments
    """
    # Create clean BaseCommand instance and parser
    base_command = BaseCommand()
    parser = base_command.create_parser('django-admin', 'basecommand')

    # Extract all dest names from parser actions
    base_command_dests = {action.dest for action in parser._actions}

    return frozenset(base_command_dests)


def _extract_arguments_from_parser(parser: ArgumentParser) -> list[ArgumentInfo]:
    """
    Extract ArgumentInfo objects from ArgumentParser._actions.

    Classifies arguments as BaseCommand or command-specific.
    Includes all arguments (help, version, etc.) with classification metadata.

    Args:
        parser: ArgumentParser instance with added arguments

    Returns:
        List of ArgumentInfo objects for all arguments
    """
    arguments = []
    base_command_dests = _get_base_command_arguments()

    for position, action in enumerate(parser._actions):
        # Determine if positional argument
        is_positional = not action.option_strings

        # Classify as BaseCommand argument or command-specific
        is_base_command_argument = action.dest in base_command_dests

        # Get primary name (first option string or dest for positional)
        if action.option_strings:
            name = action.option_strings[0]
        else:
            name = action.dest

        # Extract action name (convert _StoreTrueAction → store_true)
        action_class_name = action.__class__.__name__
        action_name = action_class_name.lstrip('_').replace('Action', '')
        action_name_snake = _camel_to_snake(action_name)

        # Extract argument metadata
        arg_info = ArgumentInfo(
            name=name,
            dest=action.dest,
            type=action.type,
            required=action.required if hasattr(action, 'required') else is_positional,
            default=action.default,
            help_text=action.help or '',
            choices=list(action.choices) if action.choices else None,
            action=action_name_snake,
            nargs=action.nargs,
            is_positional=is_positional,
            position=position if is_positional else None,
            is_base_command_argument=is_base_command_argument,
        )

        arguments.append(arg_info)

    return arguments
