"""Argument preprocessor for flexible option ordering and positional parameter support."""

import inspect
from dataclasses import dataclass
from typing import Any

from freyja.command.command_tree import CommandTree
from freyja.parser.docstring_parser import DocStringParser
from freyja.utils.text_util import TextUtil


@dataclass
class PositionalInfo:
  """Information about a positional parameter."""
  param_name: str
  param_type: type
  is_required: bool
  help_text: str | None = None


class ArgumentPreprocessor:
  """Preprocesses command line arguments to enable flexible option ordering AND positional parameter support."""

  def __init__(self, command_tree: CommandTree, target_class: type | None = None):
    """Initialize the preprocessor with command tree and target class info."""
    self.command_tree = command_tree
    self.target_class = target_class
    self._global_options: set[str] = set()
    self._subglobal_options: dict[str, set[str]] = {}  # group_name -> set of options
    self._command_options: dict[str, set[str]] = {}    # command_name -> set of options
    self._positional_params: dict[str, PositionalInfo] = {}  # command_name -> positional parameter info
    self._build_option_maps()
    self._build_positional_maps()

  def validate_arguments(self, args: list[str]) -> tuple[bool, list[str]]:
    """Validate arguments before preprocessing."""
    errors = []

    # Basic validation - check for obviously malformed arguments
    for i, arg in enumerate(args):
      if arg.startswith('--') and '=' in arg:
        # Handle --flag=value format
        flag_part = arg.split('=')[0]
        if not self._is_known_option(flag_part):
          errors.append(f"Unknown option: {flag_part}")
      elif arg.startswith('--') and not self._is_known_option(arg):
        # Check if next arg exists and isn't a flag (for --flag value format)
        if i + 1 < len(args) and not args[i + 1].startswith('-'):
          continue  # This is likely --flag value format, will be validated later
        errors.append(f"Unknown option: {arg}")

    return len(errors) == 0, errors

  def preprocess_args(self, args: list[str]) -> list[str]:
    """Reorder arguments to match argparse hierarchical expectations."""
    # Parse command structure from positional arguments
    command_path = self._extract_command_path(args)

    # Convert flag-style positional parameters to positional arguments for backward compatibility
    args_with_converted_positionals = self._convert_positional_flags_to_positional(args, command_path)

    # Categorize options by scope
    categorized_args = self._categorize_arguments(args_with_converted_positionals, command_path)

    # Reorder arguments: global -> group -> command -> remaining positionals
    return self._reorder_arguments(categorized_args, command_path)

  def _build_option_maps(self) -> None:
    """Build maps of which options belong to which scope."""
    # Build global options from target class constructor
    if self.target_class:
      self._global_options = self._extract_constructor_options(self.target_class)
    else:
      # Fallback global options
      self._global_options = {'-n', '--no-color', '-h', '--help'}

    # Build sub-global options from inner classes and command options
    if self.command_tree:
      for group_name, group_info in self.command_tree.tree.items():
        if group_info.get('type') == 'group':
          # This is a group, check for inner_class
          if group_info.get('inner_class'):
            self._subglobal_options[group_name] = self._extract_constructor_options(group_info['inner_class'], skip_params=1)

          # Process commands within the group
          if 'cmd_tree' in group_info:
            for cmd_name, cmd_info in group_info['cmd_tree'].items():
              if cmd_info.get('type') == 'command' and cmd_info.get('function'):
                full_cmd_name = f"{group_name}--{cmd_name}"  # Use double-dash format
                self._command_options[full_cmd_name] = self._extract_function_options(cmd_info['function'])
        elif group_info.get('type') == 'command':
          # This is a flat command
          if group_info.get('function'):
            self._command_options[group_name] = self._extract_function_options(group_info['function'])

  def _build_positional_maps(self) -> None:
    """Build maps of positional parameters for each command."""
    if not self.command_tree:
      return

    for group_name, group_info in self.command_tree.tree.items():
      if group_info.get('type') == 'group':
        # Process commands within the group
        if 'cmd_tree' in group_info:
          for cmd_name, cmd_info in group_info['cmd_tree'].items():
            if cmd_info.get('type') == 'command' and cmd_info.get('function'):
              full_cmd_name = f"{group_name}--{cmd_name}"  # Use double-dash format
              positional_info = self._extract_positional_parameter(cmd_info['function'])
              if positional_info:
                self._positional_params[full_cmd_name] = positional_info
      elif group_info.get('type') == 'command':
        # This is a flat command
        if group_info.get('function'):
          positional_info = self._extract_positional_parameter(group_info['function'])
          if positional_info:
            self._positional_params[group_name] = positional_info

  def _extract_constructor_options(self, cls: type, skip_params: int = 1) -> set[str]:
    """Extract options from class constructor."""
    options = set()
    if not hasattr(cls, '__init__'):
      return options

    sig = inspect.signature(cls.__init__)
    params = list(sig.parameters.items())

    # Skip 'self' and potentially other params (like 'main' in inner classes)
    for param_name, param in params[skip_params:]:
      if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
        continue

      flag_name = f"--{TextUtil.kebab_case(param_name)}"
      options.add(flag_name)

    return options

  def _extract_function_options(self, func: Any) -> set[str]:
    """Extract options from function signature."""
    options = set()
    sig = inspect.signature(func)

    for param_name, param in sig.parameters.items():
      if param_name == 'self' or param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
        continue

      # Skip positional parameters (first param without default)
      if self._is_first_non_default_param(func, param_name):
        continue

      flag_name = f"--{TextUtil.kebab_case(param_name)}"
      options.add(flag_name)

    return options

  def _extract_positional_parameter(self, func: Any) -> PositionalInfo | None:
    """Extract positional parameter info from function signature."""
    sig = inspect.signature(func)
    _, param_help = DocStringParser.extract_function_help(func)

    for param_name, param in sig.parameters.items():
      if param_name == 'self' or param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
        continue

      # First parameter without default value becomes positional
      if param.default == param.empty:
        return PositionalInfo(
          param_name=param_name,
          param_type=param.annotation if param.annotation != param.empty else str,
          is_required=True,
          help_text=param_help.get(param_name)
        )

    return None

  def _is_first_non_default_param(self, func: Any, param_name: str) -> bool:
    """Check if this is the first parameter without a default value."""
    sig = inspect.signature(func)

    for name, param in sig.parameters.items():
      if name == 'self' or param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
        continue

      if param.default == param.empty:
        return name == param_name  # This is the first non-default param

    return False

  def _extract_command_path(self, args: list[str]) -> list[str]:
    """Extract hierarchical command path from arguments."""
    command_path = []

    for arg in args:
      if arg.startswith('-'):
        break  # Stop at first option

      # Check if this arg is a valid command/group name
      if self._is_command_or_group(command_path + [arg]):
        command_path.append(arg)
      else:
        # This might be a positional parameter, stop building command path
        break

    return command_path

  def _is_command_or_group(self, path: list[str]) -> bool:
    """Check if the given path represents a valid command or group."""
    if not self.command_tree or not path:
      return False

    # Check if it's a group
    if len(path) == 1 and path[0] in self.command_tree.tree:
      group_info = self.command_tree.tree[path[0]]
      if group_info.get('type') == 'group':
        return True

    # Check if it's a command (full path)
    if len(path) >= 2:
      command_name = '--'.join(path)  # Inner class commands use double-dash
      # Check in the command tree structure
      group_name = path[0]
      if group_name in self.command_tree.tree:
        group_info = self.command_tree.tree[group_name]
        if group_info.get('type') == 'group' and 'cmd_tree' in group_info:
          cmd_name = path[1]
          if cmd_name in group_info['cmd_tree']:
            return True

    # Check if it's a flat command
    if len(path) == 1 and path[0] in self.command_tree.tree:
      cmd_info = self.command_tree.tree[path[0]]
      if cmd_info.get('type') == 'command':
        return True

    return False

  def _convert_positional_flags_to_positional(self, args: list[str], command_path: list[str]) -> list[str]:
    """Convert flag-style positional parameters back to positional for backward compatibility.

    Example: ['function-with-types', '--text', 'hello'] -> ['function-with-types', 'hello']
    """
    if not command_path:
      return args

    # Get the appropriate command name
    command_name = '--'.join(command_path) if len(command_path) > 1 else command_path[0] if command_path else None
    if not command_name:
      return args

    # Check if this command has a positional parameter
    positional_info = self._positional_params.get(command_name)
    if not positional_info:
      return args

    # Look for the flag version of the positional parameter
    flag_name = f"--{TextUtil.kebab_case(positional_info.param_name)}"
    result_args = args.copy()

    # Find flag and its value, convert to positional
    i = 0
    while i < len(result_args) - 1:  # -1 because we need to check next element
      if result_args[i] == flag_name:
        # Found the flag, next element should be the value
        if i + 1 < len(result_args) and not result_args[i + 1].startswith('-'):
          flag_value = result_args[i + 1]
          # Remove the flag and its value
          result_args.pop(i)  # Remove flag
          result_args.pop(i)  # Remove value (index shifts after first pop)
          # Find where to insert the positional value (after command path)
          insert_position = len(command_path)
          result_args.insert(insert_position, flag_value)
          break
      i += 1

    return result_args

  def _handle_positional_parameters(self, args: list[str], command_path: list[str]) -> list[str]:
    """Convert positional parameters to flag format for argparse compatibility."""
    if not command_path:
      return args

    # Get command name (for flat commands or hierarchical commands)
    if len(command_path) == 1:
      command_name = command_path[0]
    else:
      command_name = '--'.join(command_path)

    # Check if this command has a positional parameter
    positional_info = self._positional_params.get(command_name)
    if not positional_info:
      return args

    # Find the positional value in the args
    result_args = args.copy()
    command_path_end = len(command_path)
    flag_name = f"--{TextUtil.kebab_case(positional_info.param_name)}"

    # Look for positional value after command path
    i = command_path_end
    while i < len(args):
      arg = args[i]

      # Skip options and their values
      if arg.startswith('-'):
        # Check if this is the flag we're looking for
        if arg == flag_name:
          # The flag already exists, no need to convert positional
          return args

        # Skip this option and its value if it exists
        i += 1
        if i < len(args) and not args[i].startswith('-'):
          i += 1  # Skip the option value
        continue

      # This is a potential positional value
      # Replace positional value with flag format
      result_args[i:i+1] = [flag_name, arg]
      break

    return result_args

  def _categorize_arguments(self, args: list[str], command_path: list[str]) -> dict[str, list[str]]:
    """Categorize arguments by scope (global, sub-global, command)."""
    categorized = {
      'global': [],
      'subglobal': [],
      'command': [],
      'positional': command_path.copy()
    }

    i = 0
    # Skip command path
    while i < len(args) and i < len(command_path):
      if args[i] == command_path[i]:
        i += 1
      else:
        break

    # Categorize remaining arguments
    while i < len(args):
      arg = args[i]

      if not arg.startswith('-'):
        # Non-option argument, probably a value
        categorized['command'].append(arg)
        i += 1
        continue

      # This is an option, determine its scope
      if arg in self._global_options:
        categorized['global'].append(arg)
        # Check if it has a value
        if i + 1 < len(args) and not args[i + 1].startswith('-'):
          categorized['global'].append(args[i + 1])
          i += 2
        else:
          i += 1
      elif self._is_subglobal_option(arg, command_path):
        categorized['subglobal'].append(arg)
        # Check if it has a value
        if i + 1 < len(args) and not args[i + 1].startswith('-'):
          categorized['subglobal'].append(args[i + 1])
          i += 2
        else:
          i += 1
      else:
        # Assume it's a command option
        categorized['command'].append(arg)
        # Check if it has a value
        if i + 1 < len(args) and not args[i + 1].startswith('-'):
          categorized['command'].append(args[i + 1])
          i += 2
        else:
          i += 1

    return categorized

  def _is_subglobal_option(self, option: str, command_path: list[str]) -> bool:
    """Check if an option is a sub-global option for the current command path."""
    if not command_path:
      return False

    group_name = command_path[0]  # First element is the group
    return group_name in self._subglobal_options and option in self._subglobal_options[group_name]

  def _reorder_arguments(self, categorized: dict[str, list[str]], command_path: list[str]) -> list[str]:
    """Reorder arguments into hierarchical format."""
    reordered = []

    # Add global options first
    reordered.extend(categorized['global'])

    # Add command path
    reordered.extend(categorized['positional'])

    # Add sub-global options
    reordered.extend(categorized['subglobal'])

    # Add command-specific options
    reordered.extend(categorized['command'])

    return reordered

  def _is_known_option(self, option: str) -> bool:
    """Check if an option is known in any scope."""
    if option in self._global_options:
      return True

    for group_options in self._subglobal_options.values():
      if option in group_options:
        return True

    for command_options in self._command_options.values():
      if option in command_options:
        return True

    # Check for built-in options
    builtin_options = {'--help', '-h', '--no-color', '-n'}
    if option in builtin_options:
      return True

    return False
