"""Bash completion script generator.

Generates static bash completion scripts using COMPREPLY and compgen.
Targets bash 3.2+ with no external dependencies.
"""

import re
from typing import TYPE_CHECKING

from cyclopts.completion._base import (
    CompletionAction,
    CompletionData,
    clean_choice_text,
    extract_completion_data,
    get_completion_action,
)

if TYPE_CHECKING:
    from cyclopts import App


def generate_completion_script(app: "App", prog_name: str) -> str:
    """Generate bash completion script.

    Parameters
    ----------
    app : App
        The Cyclopts application to generate completion for.
    prog_name : str
        Program name (alphanumeric with hyphens/underscores).

    Returns
    -------
    str
        Complete bash completion script.

    Raises
    ------
    ValueError
        If prog_name contains invalid characters.
    """
    if not prog_name or not re.match(r"^[a-zA-Z0-9_-]+$", prog_name):
        raise ValueError(f"Invalid prog_name: {prog_name!r}. Must be alphanumeric with hyphens/underscores.")

    func_name = prog_name.replace("-", "_")
    completion_data = extract_completion_data(app)

    lines = [
        f"# Bash completion for {prog_name}",
        "# Generated by Cyclopts",
        "",
        f"_{func_name}() {{",
        "  local cur prev",
        "",
    ]

    lines.extend(_generate_completion_function_body(completion_data, prog_name, app))

    lines.extend(["}"])
    lines.append("")
    lines.append(f"complete -F _{func_name} {prog_name}")
    lines.append("")

    return "\n".join(lines)


def _escape_bash_choice(choice: str) -> str:
    r"""Escape single quotes for bash strings."""
    return choice.replace("'", "'\\''")


def _escape_bash_description(text: str) -> str:
    r"""Escape description text for bash comments."""
    text = text.replace("\n", " ")
    text = text.replace("\r", " ")
    return text


def _map_completion_action_to_bash(action: CompletionAction) -> str:
    """Map completion action to bash compgen flags.

    Parameters
    ----------
    action : CompletionAction
        Completion action type.

    Returns
    -------
    str
        Compgen flags ("-f", "-d", or "").
    """
    if action == CompletionAction.FILES:
        return "-f"
    elif action == CompletionAction.DIRECTORIES:
        return "-d"
    return ""


def _generate_completion_function_body(
    completion_data: dict[tuple[str, ...], CompletionData],
    prog_name: str,
    app: "App",
) -> list[str]:
    """Generate the body of the bash completion function.

    Parameters
    ----------
    completion_data : dict
        All extracted completion data.
    prog_name : str
        Program name.
    app : App
        Application instance.

    Returns
    -------
    list[str]
        Lines of bash code for the completion function body.
    """
    lines = []
    lines.append('  cur="${COMP_WORDS[COMP_CWORD]}"')
    lines.append('  prev="${COMP_WORDS[COMP_CWORD-1]}"')
    lines.append("")

    lines.extend(_generate_command_path_detection(completion_data))
    lines.append("")

    lines.extend(_generate_completion_logic(completion_data, prog_name, app))

    return lines


def _generate_command_path_detection(completion_data: dict[tuple[str, ...], CompletionData]) -> list[str]:
    """Generate bash code to detect the current command path.

    This function generates two passes through COMP_WORDS:
    1. First pass builds cmd_path by identifying valid command names
    2. Second pass counts positionals (non-option words after the command path)

    The two-pass approach is necessary because we need to know the full command
    path length before we can correctly identify which words are positionals.

    Note: all_commands is built globally across all command levels. If a positional
    argument value happens to match a command name from a different level, it could
    be incorrectly classified (though this represents poor CLI design).

    Parameters
    ----------
    completion_data : dict
        All extracted completion data.

    Returns
    -------
    list[str]
        Lines of bash code for command path detection.
    """
    options_with_values = set()
    all_commands = set()

    for data in completion_data.values():
        for argument in data.arguments:
            if not argument.is_flag() and argument.parameter.name:
                for name in argument.parameter.name:
                    if name.startswith("-"):
                        options_with_values.add(name)

        for registered_command in data.commands:
            for cmd_name in registered_command.names:
                if not cmd_name.startswith("-"):
                    all_commands.add(cmd_name)

    lines = []
    lines.append("  # Build list of options that take values (to skip their arguments)")
    if options_with_values:
        escaped_opts = [_escape_bash_choice(opt) for opt in sorted(options_with_values)]
        opts_str = " ".join(escaped_opts)
        lines.append(f"  local options_with_values='{opts_str}'")
    else:
        lines.append("  local options_with_values=''")

    lines.append("")
    lines.append("  # Build list of all valid command names (to distinguish from positionals)")
    if all_commands:
        escaped_cmds = [_escape_bash_choice(cmd) for cmd in sorted(all_commands)]
        cmds_str = " ".join(escaped_cmds)
        lines.append(f"  local all_commands='{cmds_str}'")
    else:
        lines.append("  local all_commands=''")

    lines.append("")
    lines.append("  # Detect command path by collecting valid command words only")
    lines.append("  local -a cmd_path=()")
    lines.append("  local i skip_next=0")
    lines.append("  for ((i=1; i<COMP_CWORD; i++)); do")
    lines.append('    local word="${COMP_WORDS[i]}"')
    lines.append("    if [[ $skip_next -eq 1 ]]; then")
    lines.append("      skip_next=0")
    lines.append("      continue")
    lines.append("    fi")
    lines.append("    if [[ $word =~ ^- ]]; then")
    lines.append("      # Check if this option takes a value")
    lines.append('      if [[ " $options_with_values " =~ " $word " ]]; then')
    lines.append("        skip_next=1")
    lines.append("      fi")
    lines.append("    else")
    lines.append("      # Non-option word - only add to cmd_path if it's a valid command")
    lines.append('      if [[ " $all_commands " =~ " $word " ]]; then')
    lines.append('        cmd_path+=("$word")')
    lines.append("      fi")
    lines.append("    fi")
    lines.append("  done")
    lines.append("")
    lines.append("  # Count positionals (non-option words after command path)")
    lines.append("  local positional_count=0")
    lines.append("  local cmd_path_len=${#cmd_path[@]}")
    lines.append("  skip_next=0")
    lines.append("  local cmd_depth=0")
    lines.append("  for ((i=1; i<COMP_CWORD; i++)); do")
    lines.append('    local word="${COMP_WORDS[i]}"')
    lines.append("    if [[ $skip_next -eq 1 ]]; then")
    lines.append("      skip_next=0")
    lines.append("      continue")
    lines.append("    fi")
    lines.append("    if [[ $word =~ ^- ]]; then")
    lines.append('      if [[ " $options_with_values " =~ " $word " ]]; then')
    lines.append("        skip_next=1")
    lines.append("      fi")
    lines.append("    else")
    lines.append("      # Non-option word")
    lines.append("      if [[ $cmd_depth -lt $cmd_path_len ]]; then")
    lines.append("        # Still in command path")
    lines.append("        ((cmd_depth++))")
    lines.append("      else")
    lines.append("        # Past command path, this is a positional")
    lines.append("        ((positional_count++))")
    lines.append("      fi")
    lines.append("    fi")
    lines.append("  done")
    return lines


def _generate_completion_logic(
    completion_data: dict[tuple[str, ...], CompletionData],
    prog_name: str,
    app: "App",
) -> list[str]:
    """Generate the main completion logic using case statements.

    Parameters
    ----------
    completion_data : dict
        All extracted completion data.
    prog_name : str
        Program name.
    app : App
        Application instance.

    Returns
    -------
    list[str]
        Lines of bash code for completion logic.
    """
    lines = []

    help_flags = tuple(app.help_flags) if app.help_flags else ()
    version_flags = tuple(app.version_flags) if app.version_flags else ()

    lines.append("  # Determine command level and generate completions")
    lines.append('  case "${#cmd_path[@]}" in')

    max_depth = max(len(path) for path in completion_data.keys())
    for depth in range(max_depth + 1):
        relevant_paths = [path for path in completion_data.keys() if len(path) == depth]
        if not relevant_paths:
            continue

        lines.append(f"    {depth})")

        if depth == 0:
            lines.extend(_generate_completions_for_path(completion_data, (), "      ", help_flags, version_flags))
        else:
            lines.append('      case "${cmd_path[@]}" in')
            for path in sorted(relevant_paths):
                path_str = " ".join(path)
                lines.append(f'        "{path_str}")')
                lines.extend(
                    _generate_completions_for_path(completion_data, path, "          ", help_flags, version_flags)
                )
                lines.append("          ;;")
            lines.append("        *)")
            lines.append("          ;;")
            lines.append("      esac")

        lines.append("      ;;")

    lines.append("    *)")
    lines.append("      ;;")
    lines.append("  esac")

    return lines


def _generate_completions_for_path(
    completion_data: dict[tuple[str, ...], CompletionData],
    command_path: tuple[str, ...],
    indent: str,
    help_flags: tuple[str, ...],
    version_flags: tuple[str, ...],
) -> list[str]:
    """Generate completions for a specific command path.

    Parameters
    ----------
    completion_data : dict
        All extracted completion data.
    command_path : tuple[str, ...]
        Current command path.
    indent : str
        Indentation string.
    help_flags : tuple[str, ...]
        Help flag names.
    version_flags : tuple[str, ...]
        Version flag names.

    Returns
    -------
    list[str]
        Lines of bash code for completions at this command path.
    """
    if command_path not in completion_data:
        return [f"{indent}COMPREPLY=()"]

    data = completion_data[command_path]
    lines = []

    options = []
    keyword_args = [arg for arg in data.arguments if not arg.is_positional_only() and arg.show]

    for argument in keyword_args:
        for name in argument.parameter.name or []:
            if name.startswith("-"):
                options.append(name)

        for name in argument.negatives:
            if name.startswith("-"):
                options.append(name)

    flag_commands = []
    for registered_command in data.commands:
        for name in registered_command.names:
            if name.startswith("-"):
                flag_commands.append(name)

    for flag in help_flags:
        if flag.startswith("-") and flag not in options and flag not in flag_commands:
            options.append(flag)

    for flag in version_flags:
        if flag.startswith("-") and flag not in options and flag not in flag_commands:
            options.append(flag)

    options.extend(flag_commands)

    commands = []
    for registered_command in data.commands:
        for cmd_name in registered_command.names:
            if not cmd_name.startswith("-"):
                commands.append(cmd_name)

    positional_args = [arg for arg in data.arguments if arg.index is not None and arg.show]
    positional_args.sort(key=lambda a: a.index if a.index is not None else 0)

    lines.append(f"{indent}if [[ ${{cur}} == -* ]]; then")

    if options:
        escaped_options = [_escape_bash_choice(opt) for opt in options]
        options_str = " ".join(escaped_options)
        lines.append(f"{indent}  COMPREPLY=( $(compgen -W '{options_str}' -- \"${{cur}}\") )")
    else:
        lines.append(f"{indent}  COMPREPLY=()")

    lines.append(f"{indent}else")

    needs_value_completion = _check_if_prev_needs_value(data.arguments)

    if needs_value_completion:
        value_completion_lines = _generate_value_completion_for_prev(
            data.arguments, commands, positional_args, f"{indent}  "
        )
        lines.extend(value_completion_lines)
    elif commands:
        escaped_commands = [_escape_bash_choice(cmd) for cmd in commands]
        commands_str = " ".join(escaped_commands)
        lines.append(f"{indent}  COMPREPLY=( $(compgen -W '{commands_str}' -- \"${{cur}}\") )")
    elif positional_args:
        lines.extend(_generate_positional_completion(positional_args, f"{indent}  "))
    else:
        lines.append(f"{indent}  COMPREPLY=()")

    lines.append(f"{indent}fi")

    return lines


def _generate_positional_completion(positional_args, indent: str) -> list[str]:
    """Generate position-aware positional argument completion.

    Parameters
    ----------
    positional_args : list
        List of positional arguments sorted by index.
    indent : str
        Indentation string.

    Returns
    -------
    list[str]
        Lines of bash code for position-aware positional completion.
    """
    lines = []

    if len(positional_args) == 1:
        # Single positional - simple case
        choices = positional_args[0].get_choices(force=True)
        action = get_completion_action(positional_args[0].hint)
        if choices:
            escaped_choices = [_escape_bash_choice(clean_choice_text(c)) for c in choices]
            choices_str = " ".join(escaped_choices)
            lines.append(f"{indent}COMPREPLY=( $(compgen -W '{choices_str}' -- \"${{cur}}\") )")
        else:
            compgen_flag = _map_completion_action_to_bash(action)
            if compgen_flag:
                lines.append(f'{indent}COMPREPLY=( $(compgen {compgen_flag} -- "${{cur}}") )')
            else:
                lines.append(f"{indent}COMPREPLY=()")
    else:
        # Multiple positionals - use case statement for position-aware completion
        lines.append(f"{indent}case ${{positional_count}} in")

        for idx, argument in enumerate(positional_args):
            choices = argument.get_choices(force=True)
            action = get_completion_action(argument.hint)
            lines.append(f"{indent}  {idx})")
            if choices:
                escaped_choices = [_escape_bash_choice(clean_choice_text(c)) for c in choices]
                choices_str = " ".join(escaped_choices)
                lines.append(f"{indent}    COMPREPLY=( $(compgen -W '{choices_str}' -- \"${{cur}}\") )")
            else:
                compgen_flag = _map_completion_action_to_bash(action)
                if compgen_flag:
                    lines.append(f'{indent}    COMPREPLY=( $(compgen {compgen_flag} -- "${{cur}}") )')
                else:
                    lines.append(f"{indent}    COMPREPLY=()")
            lines.append(f"{indent}    ;;")

        # Default case for positions beyond defined positionals
        lines.append(f"{indent}  *)")
        lines.append(f"{indent}    COMPREPLY=()")
        lines.append(f"{indent}    ;;")
        lines.append(f"{indent}esac")

    return lines


def _check_if_prev_needs_value(arguments) -> bool:
    """Check if any options take values, requiring prev-word completion logic.

    Parameters
    ----------
    arguments : ArgumentCollection
        Arguments to check.

    Returns
    -------
    bool
        True if any option (starts with -) takes a value (is not a flag).
    """
    for argument in arguments:
        if not argument.is_flag():
            for name in argument.parameter.name or []:
                if name.startswith("-"):
                    return True
    return False


def _generate_value_completion_for_prev(arguments, commands: list[str], positional_args, indent: str) -> list[str]:
    """Generate value completion based on previous word.

    Parameters
    ----------
    arguments : ArgumentCollection
        Arguments with potential values.
    commands : list[str]
        Available commands at this level.
    positional_args : list
        List of positional arguments sorted by index.
    indent : str
        Indentation string.

    Returns
    -------
    list[str]
        Lines of bash code for value completion.
    """
    lines = []
    lines.append(f'{indent}case "${{prev}}" in')

    has_cases = False
    for argument in arguments:
        if argument.is_flag():
            continue

        names = [name for name in (argument.parameter.name or []) if name.startswith("-")]
        if not names:
            continue

        has_cases = True
        choices = argument.get_choices(force=True)
        action = get_completion_action(argument.hint)

        for name in names:
            lines.append(f"{indent}  {name})")

            if choices:
                escaped_choices = [_escape_bash_choice(clean_choice_text(c)) for c in choices]
                choices_str = " ".join(escaped_choices)
                lines.append(f"{indent}    COMPREPLY=( $(compgen -W '{choices_str}' -- \"${{cur}}\") )")
            else:
                compgen_flag = _map_completion_action_to_bash(action)
                if compgen_flag:
                    lines.append(f'{indent}    COMPREPLY=( $(compgen {compgen_flag} -- "${{cur}}") )')
                else:
                    lines.append(f"{indent}    COMPREPLY=()")

            lines.append(f"{indent}    ;;")

    if has_cases:
        lines.append(f"{indent}  *)")
        if commands:
            escaped_commands = [_escape_bash_choice(cmd) for cmd in commands]
            commands_str = " ".join(escaped_commands)
            lines.append(f"{indent}    COMPREPLY=( $(compgen -W '{commands_str}' -- \"${{cur}}\") )")
        elif positional_args:
            lines.extend(_generate_positional_completion(positional_args, f"{indent}    "))
        else:
            lines.append(f"{indent}    COMPREPLY=()")
        lines.append(f"{indent}    ;;")
        lines.append(f"{indent}esac")
    else:
        lines = []
        if commands:
            escaped_commands = [_escape_bash_choice(cmd) for cmd in commands]
            commands_str = " ".join(escaped_commands)
            lines.append(f"{indent}COMPREPLY=( $(compgen -W '{commands_str}' -- \"${{cur}}\") )")
        elif positional_args:
            lines.extend(_generate_positional_completion(positional_args, indent))
        else:
            lines.append(f"{indent}COMPREPLY=()")

    return lines
