"""
Core module providing the autoarg decorator/wrapper functionality.
"""

import argparse
import sys
from typing import Any, Callable, Dict, List, Optional, Set, Union

from .parser import (
    create_argument_name,
    extract_parameter_info,
    get_argparse_type,
    parse_docstring,
)


def autoarg(
    func_or_funcs: Union[Callable, List[Callable], Set[Callable]],
    return_type: str = "unix"
) -> Callable:
    """
    Wrap a callable or collection of callables with automatic argparse CLI generation.
    
    For a single callable, creates a CLI that parses arguments for that function.
    For a list of callables (sequence), creates a CLI that merges all parameters
    and calls each function in order. Conflicting parameter names are prefixed
    with the function name.
    For a set of callables (commands), creates subcommands for each function.
    
    Args:
        func_or_funcs: A single callable, list of callables (sequence), or set of callables (commands)
        return_type: For sequences only - "unix" (last result), "list" (all results), or "dict" (name->result)
        
    Returns:
        A callable that when invoked will parse command-line arguments and
        execute the appropriate function(s)
        
    Raises:
        ValueError: If return_type is not one of "unix", "list", or "dict"
    """
    # Validate return_type
    if return_type not in ("unix", "list", "dict"):
        raise ValueError(f"return_type must be 'unix', 'list', or 'dict', got '{return_type}'")
    
    # Determine if single function, sequence, or commands
    if isinstance(func_or_funcs, list):
        return _create_sequence_wrapper(func_or_funcs, return_type)
    elif isinstance(func_or_funcs, set):
        return _create_multi_command_wrapper(list(func_or_funcs))
    else:
        return _create_single_command_wrapper(func_or_funcs)


def _create_single_command_wrapper(func: Callable) -> Callable:
    """
    Create a wrapper for a single callable with argparse.
    
    Args:
        func: The callable to wrap
        
    Returns:
        Wrapped callable that parses CLI arguments
    """
    # Parse function metadata outside wrapper to trigger warnings at wrap time
    description, param_help = parse_docstring(func)
    params_info = extract_parameter_info(func)
    
    def wrapper(argv: Optional[List[str]] = None) -> Any:
        
        # Create argument parser
        parser = argparse.ArgumentParser(
            description=description or f"CLI for {func.__name__}",
            prog=func.__name__
        )
        
        # Add arguments
        for param_name, info in params_info.items():
            _add_argument_to_parser(parser, param_name, info, param_help)
        
        # Parse arguments
        args = parser.parse_args(argv)
        
        # Call function with parsed arguments
        kwargs = vars(args)
        return func(**kwargs)
    
    return wrapper


def _create_sequence_wrapper(funcs: List[Callable], return_type: str) -> Callable:
    """
    Create a wrapper for a sequence of callables with merged parameters.
    
    All parameters from all callables are exposed in a single flat CLI.
    Conflicting parameter names are prefixed with the function name.
    
    Args:
        funcs: List of callables to execute in sequence
        return_type: How to return results - "unix", "list", or "dict"
        
    Returns:
        Wrapped callable that parses CLI arguments and executes all functions in order
    """
    # Parse all function metadata and detect conflicts
    funcs_metadata = []
    all_params: Dict[str, List[tuple]] = {}  # param_name -> [(func, description, param_help, params_info)]
    
    for func in funcs:
        description, param_help = parse_docstring(func)
        params_info = extract_parameter_info(func)
        funcs_metadata.append((func, description, param_help, params_info))
        
        # Track which functions use which parameter names
        for param_name in params_info.keys():
            if param_name not in all_params:
                all_params[param_name] = []
            all_params[param_name].append((func, description, param_help, params_info))
    
    # Identify conflicts (param names used by multiple functions)
    conflicts = {name for name, funcs_list in all_params.items() if len(funcs_list) > 1}
    
    def wrapper(argv: Optional[List[str]] = None) -> Any:
        # Create main parser
        parser = argparse.ArgumentParser(
            description="Execute a sequence of operations",
            prog="cli"
        )
        
        # Build mapping of CLI arg names to (func, param_name)
        arg_mapping: Dict[str, tuple] = {}
        
        # Add arguments for each function
        for func, description, param_help, params_info in funcs_metadata:
            for param_name, info in params_info.items():
                # Determine if we need to prefix this parameter
                if param_name in conflicts:
                    # Prefix with function name
                    prefixed_name = f"{func.__name__}_{param_name}"
                    cli_arg_name = create_argument_name(prefixed_name)
                    dest_name = prefixed_name
                else:
                    cli_arg_name = create_argument_name(param_name)
                    dest_name = param_name
                
                # Add argument to parser
                _add_argument_to_parser(parser, dest_name, info, param_help)
                
                # Track mapping for later
                arg_mapping[dest_name] = (func, param_name)
        
        # Parse arguments
        args = parser.parse_args(argv)
        parsed_args = vars(args)
        
        # Build kwargs for each function
        func_kwargs: Dict[Callable, Dict[str, Any]] = {func: {} for func, _, _, _ in funcs_metadata}
        
        for dest_name, value in parsed_args.items():
            func, original_param_name = arg_mapping[dest_name]
            func_kwargs[func][original_param_name] = value
        
        # Execute functions in order
        results = []
        for func, _, _, _ in funcs_metadata:
            result = func(**func_kwargs[func])
            results.append(result)
        
        # Return based on return_type
        if return_type == "unix":
            return results[-1] if results else None
        elif return_type == "list":
            return results
        elif return_type == "dict":
            return {func.__name__: result for (func, _, _, _), result in zip(funcs_metadata, results)}
    
    return wrapper


def _create_multi_command_wrapper(funcs: List[Callable]) -> Callable:
    """
    Create a wrapper for multiple callables with subcommands.
    
    Args:
        funcs: List of callables to create subcommands for
        
    Returns:
        Wrapped callable that parses CLI arguments and routes to subcommands
    """
    # Parse all function metadata outside wrapper to trigger warnings at wrap time
    funcs_metadata = []
    for func in funcs:
        description, param_help = parse_docstring(func)
        params_info = extract_parameter_info(func)
        funcs_metadata.append((func, description, param_help, params_info))
    
    def wrapper(argv: Optional[List[str]] = None) -> Any:
        # Create main parser
        parser = argparse.ArgumentParser(
            description="Multi-command CLI",
            prog="cli"
        )
        subparsers = parser.add_subparsers(dest='command', required=True)
        
        # Create subparser for each function
        func_map = {}
        for func, description, param_help, params_info in funcs_metadata:
            
            # Create subparser
            subparser = subparsers.add_parser(
                func.__name__,
                help=description or f"Execute {func.__name__}",
                description=description or f"CLI for {func.__name__}"
            )
            
            # Add arguments to subparser
            for param_name, info in params_info.items():
                _add_argument_to_parser(subparser, param_name, info, param_help)
            
            func_map[func.__name__] = func
        
        # Parse arguments
        args = parser.parse_args(argv)
        
        # Get the selected function
        selected_func = func_map[args.command]
        
        # Remove 'command' from kwargs and call function
        kwargs = vars(args)
        del kwargs['command']
        
        return selected_func(**kwargs)
    
    return wrapper


def _add_argument_to_parser(
    parser: argparse.ArgumentParser,
    param_name: str,
    param_info: dict,
    param_help: dict
) -> None:
    """
    Add a single argument to an argparse parser.
    
    Args:
        parser: The ArgumentParser to add the argument to
        param_name: Name of the parameter
        param_info: Dictionary with parameter metadata (type, default, etc.)
        param_help: Dictionary mapping parameter names to help text
    """
    arg_name = create_argument_name(param_name)
    param_type = param_info['type']
    has_default = param_info['has_default']
    default_value = param_info['default']
    help_text = param_help.get(param_name, '')
    
    # Get argparse type and additional kwargs
    argparse_type, type_kwargs = get_argparse_type(param_type)
    
    # Build argument kwargs
    arg_kwargs = {
        'help': help_text,
        **type_kwargs
    }
    
    # Handle boolean types specially
    if argparse_type == bool:
        if has_default:
            if default_value:
                # Default is True, create --no-param flag to set False
                arg_name = '--no-' + param_name.replace('_', '-')
                arg_kwargs['action'] = 'store_false'
                arg_kwargs['dest'] = param_name
            else:
                # Default is False, so flag should set to True
                arg_kwargs['action'] = 'store_true'
                arg_kwargs['dest'] = param_name
        else:
            # No default, use store_true
            arg_kwargs['action'] = 'store_true'
            arg_kwargs['dest'] = param_name
        
        # Remove type from kwargs for boolean actions
        if 'type' in arg_kwargs:
            del arg_kwargs['type']
    else:
        # Non-boolean type
        arg_kwargs['type'] = argparse_type
        
        if has_default:
            arg_kwargs['default'] = default_value
            if default_value is not None:
                arg_kwargs['help'] += f' (default: {default_value})'
        else:
            # Required argument
            arg_kwargs['required'] = True
    
    parser.add_argument(arg_name, **arg_kwargs)
