"""Fish completion script generator.

Generates static fish completion scripts using `complete -c COMMAND` statements.
Completions auto-load from ~/.config/fish/completions/PROGNAME.fish.
"""

import re
from typing import TYPE_CHECKING

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

if TYPE_CHECKING:
    from cyclopts import App
    from cyclopts.command_spec import CommandSpec


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

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

    Returns
    -------
    str
        Complete fish 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.")

    completion_data = extract_completion_data(app)

    lines = [
        f"# Fish completion for {prog_name}",
        "# Generated by Cyclopts",
        "",
    ]

    has_nested_commands = any(len(path) > 0 for path in completion_data.keys())
    if has_nested_commands:
        lines.extend(_generate_helper_functions(prog_name, completion_data))
        lines.append("")

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

    lines.extend(_generate_completions(completion_data, prog_name, help_flags, version_flags))

    return "\n".join(lines) + "\n"


def _escape_fish_string(text: str) -> str:
    r"""Escape single quotes for fish strings."""
    return text.replace("'", r"'\''")


def _escape_fish_description(text: str) -> str:
    """Escape description text for fish."""
    text = text.replace("\n", " ")
    text = text.replace("\r", " ")
    return _escape_fish_string(text)


def _generate_helper_functions(
    prog_name: str,
    completion_data: dict[tuple[str, ...], CompletionData],
) -> list[str]:
    """Generate helper function for command path detection.

    Parameters
    ----------
    prog_name : str
        Program name.
    completion_data : dict
        Completion data used to identify options that take values.

    Returns
    -------
    list[str]
        Lines defining the helper function.
    """
    options_with_values = 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)

    func_name = f"__fish_{prog_name}_using_command"
    lines = [
        "# Helper function to check exact command path sequence",
        f"function {func_name}",
        "    set -l cmd (commandline -opc)",
        "    set -l subcommands",
    ]

    if options_with_values:
        escaped_opts = " ".join(_escape_fish_string(opt) for opt in sorted(options_with_values))
        lines.append(f"    set -l options_with_values '{escaped_opts}'")
    else:
        lines.append("    set -l options_with_values ''")

    lines.extend(
        [
            "    set -l skip_next 0",
            "    # Extract non-option words (commands) from command line",
            "    for i in (seq 2 (count $cmd))",
            "        set -l word $cmd[$i]",
            "        if test $skip_next -eq 1",
            "            set skip_next 0",
            "            continue",
            "        end",
            "        if string match -qr -- '^-' $word",
            "            # Check if this option takes a value (exact match)",
            '            if string match -q -- "* $word *" " $options_with_values "',
            "                set skip_next 1",
            "            end",
            "        else",
            "            # Non-option word is a command",
            "            set -a subcommands $word",
            "        end",
            "    end",
            "    # Check if subcommand sequence matches expected path",
            "    if test (count $subcommands) -ne (count $argv)",
            "        return 1",
            "    end",
            "    for i in (seq 1 (count $argv))",
            "        if test $subcommands[$i] != $argv[$i]",
            "            return 1",
            "        end",
            "    end",
            "    return 0",
            "end",
        ]
    )
    return lines


def _map_completion_action_to_fish(action: CompletionAction) -> str:
    """Map completion action to fish flags.

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

    Returns
    -------
    str
        Fish completion flags ("-r -F" for files, "-r -a '(...)'" for directories, "" otherwise).
    """
    if action == CompletionAction.FILES:
        return "-r -F"
    if action == CompletionAction.DIRECTORIES:
        return "-r -a '(__fish_complete_directories)'"
    return ""


def _generate_completions(
    completion_data: dict[tuple[str, ...], CompletionData],
    prog_name: str,
    help_flags: tuple[str, ...],
    version_flags: tuple[str, ...],
) -> list[str]:
    """Generate all fish completion commands.

    Parameters
    ----------
    completion_data : dict
        Extracted completion data.
    prog_name : str
        Program name.
    help_flags : tuple[str, ...]
        Help flags.
    version_flags : tuple[str, ...]
        Version flags.

    Returns
    -------
    list[str]
        Completion command lines.
    """
    lines = []

    for command_path, _data in sorted(completion_data.items()):
        lines.extend(
            _generate_completions_for_path(
                completion_data,
                command_path,
                prog_name,
                help_flags,
                version_flags,
            )
        )
        if command_path != max(completion_data.keys(), key=len):
            lines.append("")

    return lines


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

    Parameters
    ----------
    completion_data : dict
        Extracted completion data.
    command_path : tuple[str, ...]
        Command path.
    prog_name : str
        Program name.
    help_flags : tuple[str, ...]
        Help flags.
    version_flags : tuple[str, ...]
        Version flags.

    Returns
    -------
    list[str]
        Completion command lines.
    """
    if command_path not in completion_data:
        return []

    data = completion_data[command_path]
    lines = []
    condition = _get_condition_for_path(command_path, prog_name)

    lines.extend(_generate_subcommand_completions(data, command_path, prog_name, condition))

    keyword_args = [arg for arg in data.arguments if not arg.is_positional_only() and arg.show]
    if keyword_args or help_flags or version_flags:
        lines.extend(_generate_option_section_header(command_path))
        lines.extend(_generate_help_version_completions(prog_name, condition, help_flags, version_flags))
        lines.extend(_generate_keyword_arg_completions(keyword_args, prog_name, condition, data.help_format))
        lines.extend(_generate_command_option_completions(data.commands, prog_name, condition, data.help_format))

    return lines


def _generate_subcommand_completions(
    data: CompletionData,
    command_path: tuple[str, ...],
    prog_name: str,
    condition: str,
) -> list[str]:
    """Generate completions for subcommands.

    Parameters
    ----------
    data : CompletionData
        Completion data.
    command_path : tuple[str, ...]
        Command path.
    prog_name : str
        Program name.
    condition : str
        Fish condition.

    Returns
    -------
    list[str]
        Completion command lines.
    """
    commands = [
        name for registered_command in data.commands for name in registered_command.names if not name.startswith("-")
    ]
    if not commands:
        return []

    lines = []
    if command_path:
        lines.append(f"# Subcommands for: {' '.join(command_path)}")
    else:
        lines.append("# Root-level commands")

    for registered_command in data.commands:
        for cmd_name in registered_command.names:
            if cmd_name.startswith("-"):
                continue

            desc = _get_description_from_app(registered_command.app, data.help_format)
            escaped_desc = _escape_fish_description(desc)
            escaped_cmd = _escape_fish_string(cmd_name)

            lines.append(f"complete -c {prog_name} {condition} -a '{escaped_cmd}' -d '{escaped_desc}'")

    return lines


def _generate_option_section_header(command_path: tuple[str, ...]) -> list[str]:
    """Generate section header comment for options.

    Parameters
    ----------
    command_path : tuple[str, ...]
        Command path.

    Returns
    -------
    list[str]
        Comment line.
    """
    if command_path:
        return [f"# Options for: {' '.join(command_path)}"]
    return ["# Root-level options"]


def _generate_help_version_completions(
    prog_name: str,
    condition: str,
    help_flags: tuple[str, ...],
    version_flags: tuple[str, ...],
) -> list[str]:
    """Generate completions for help and version flags.

    Parameters
    ----------
    prog_name : str
        Program name.
    condition : str
        Fish condition.
    help_flags : tuple[str, ...]
        Help flags.
    version_flags : tuple[str, ...]
        Version flags.

    Returns
    -------
    list[str]
        Completion command lines.
    """
    lines = []

    for flag in help_flags:
        if flag.startswith("--"):
            long_name = flag[2:]
            lines.append(f"complete -c {prog_name} {condition} -l {long_name} -d 'Display this message and exit.'")
        elif flag.startswith("-") and len(flag) == 2:
            short_name = flag[1]
            lines.append(f"complete -c {prog_name} {condition} -s {short_name} -d 'Display this message and exit.'")

    for flag in version_flags:
        if flag.startswith("--"):
            long_name = flag[2:]
            lines.append(f"complete -c {prog_name} {condition} -l {long_name} -d 'Display application version.'")
        elif flag.startswith("-") and len(flag) == 2:
            short_name = flag[1]
            lines.append(f"complete -c {prog_name} {condition} -s {short_name} -d 'Display application version.'")

    return lines


def _generate_keyword_arg_completions(
    keyword_args: list,
    prog_name: str,
    condition: str,
    help_format: str,
) -> list[str]:
    """Generate completions for keyword arguments.

    Parameters
    ----------
    keyword_args : list
        Keyword arguments.
    prog_name : str
        Program name.
    condition : str
        Fish condition.
    help_format : str
        Help text format.

    Returns
    -------
    list[str]
        Completion command lines.
    """
    lines = []

    for argument in keyword_args:
        desc = strip_markup(argument.parameter.help or "", format=help_format)
        escaped_desc = _escape_fish_description(desc)

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

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

            if name.startswith("--"):
                long_name = name[2:]
                line_parts = [f"complete -c {prog_name} {condition} -l {long_name}"]
            elif len(name) == 2:
                short_name = name[1]
                line_parts = [f"complete -c {prog_name} {condition} -s {short_name}"]
            else:
                continue

            if is_flag:
                line_parts.append(f"-d '{escaped_desc}'")
            elif choices:
                escaped_choices = [_escape_fish_string(clean_choice_text(c)) for c in choices]
                choices_str = " ".join(escaped_choices)
                line_parts.append(f"-x -a '{choices_str}' -d '{escaped_desc}'")
            else:
                action_flags = _map_completion_action_to_fish(action)
                if action_flags:
                    line_parts.append(f"{action_flags} -d '{escaped_desc}'")
                else:
                    line_parts.append(f"-r -d '{escaped_desc}'")

            lines.append(" ".join(line_parts))

        for name in argument.negatives:
            if not name.startswith("-"):
                continue

            if name.startswith("--"):
                long_name = name[2:]
                lines.append(f"complete -c {prog_name} {condition} -l {long_name} -d '{escaped_desc}'")
            elif len(name) == 2:
                short_name = name[1]
                lines.append(f"complete -c {prog_name} {condition} -s {short_name} -d '{escaped_desc}'")

    return lines


def _generate_command_option_completions(
    commands: list,
    prog_name: str,
    condition: str,
    help_format: str,
) -> list[str]:
    """Generate completions for commands that look like options.

    Parameters
    ----------
    commands : list
        List of RegisteredCommand tuples.
    prog_name : str
        Program name.
    condition : str
        Fish condition.
    help_format : str
        Help text format.

    Returns
    -------
    list[str]
        Completion command lines.
    """
    lines = []

    for registered_command in commands:
        for cmd_name in registered_command.names:
            if not cmd_name.startswith("-"):
                continue

            desc = _get_description_from_app(registered_command.app, help_format)
            escaped_desc = _escape_fish_description(desc)

            if cmd_name.startswith("--"):
                long_name = cmd_name[2:]
                lines.append(f"complete -c {prog_name} {condition} -l {long_name} -d '{escaped_desc}'")
            elif len(cmd_name) == 2:
                short_name = cmd_name[1]
                lines.append(f"complete -c {prog_name} {condition} -s {short_name} -d '{escaped_desc}'")

    return lines


def _get_condition_for_path(command_path: tuple[str, ...], prog_name: str) -> str:
    """Generate fish condition string for a command path.

    Parameters
    ----------
    command_path : tuple[str, ...]
        Command path (empty for root).
    prog_name : str
        Program name.

    Returns
    -------
    str
        Fish condition flag.
    """
    if not command_path:
        return "-n __fish_use_subcommand"

    func_name = f"__fish_{prog_name}_using_command"
    escaped_commands = " ".join(_escape_fish_string(cmd) for cmd in command_path)
    return f"-n '{func_name} {escaped_commands}'"


def _get_description_from_app(cmd_app: "App | CommandSpec", help_format: str) -> str:
    """Extract description from App.

    Parameters
    ----------
    cmd_app : App | CommandSpec
        Command app or spec.
    help_format : str
        Help text format.

    Returns
    -------
    str
        Description text.
    """
    from cyclopts.help.help import docstring_parse

    try:
        parsed = docstring_parse(cmd_app.help, "plaintext")
        text = parsed.short_description or ""
    except Exception:
        text = str(cmd_app.help or "")

    return strip_markup(text, format=help_format)
