"""Zsh completion script generator.

Generates static zsh completion scripts using the compsys framework.
No runtime Python dependency.
"""

import re
from textwrap import dedent
from textwrap import indent as textwrap_indent
from typing import TYPE_CHECKING

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

if TYPE_CHECKING:
    from cyclopts import App
    from cyclopts.argument import Argument, ArgumentCollection


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

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

    Returns
    -------
    str
        Complete zsh 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"#compdef {prog_name}",
        "",
        f"_{prog_name}() {{",
        "  local line state",
        "",
    ]

    lines.extend(
        _generate_completion_for_path(
            completion_data,
            (),
            prog_name=prog_name,
            help_flags=tuple(app.help_flags) if app.help_flags else (),
            version_flags=tuple(app.version_flags) if app.version_flags else (),
        )
    )

    lines.extend(
        [
            "}",
            "",
        ]
    )

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


def _generate_run_command_completion(
    arguments: "ArgumentCollection",
    indent_str: str,
    prog_name: str,
) -> list[str]:
    """Generate dynamic completion for the 'run' command.

    Parameters
    ----------
    arguments : ArgumentCollection
        Arguments for run command.
    indent_str : str
        Indentation string.
    prog_name : str
        Program name.

    Returns
    -------
    list[str]
        Zsh completion code lines.
    """
    template = dedent(f"""\
        local script_path
        local -a completions
        local -a remaining_words

        # If completing first argument (the script path), suggest files
        if [[ $CURRENT -eq 2 ]]; then
          _files
          return
        fi

        # Get absolute path to the script file
        script_path=${{words[2]}}
        script_path=${{script_path:a}}
        if [[ -f $script_path ]]; then
          remaining_words=(${{words[3,-1]}})
          local result
          local cmd

          if command -v {prog_name} &>/dev/null; then
            cmd="{prog_name}"
          else
            return
          fi
          # Call back into cyclopts to get dynamic completions from the script
          result=$($cmd _complete run "$script_path" "${{remaining_words[@]}}" 2>/dev/null)
          if [[ -n $result ]]; then
            # Parse and display completion results
            completions=()
            while IFS= read -r line; do
              completions+=($line)
            done <<< $result
            _describe 'command' completions
          fi
        fi""")

    indented = textwrap_indent(template, indent_str)
    return [line.rstrip() for line in indented.split("\n")]


def _generate_nested_positional_specs(
    positional_args: list["Argument"],
    help_format: str,
) -> list[str]:
    """Generate positional argument specs for nested command context.

    In nested contexts (after *::arg:->args), word indexing is shifted:
    - words[1] = subcommand name
    - words[2] = first positional argument
    - words[3] = second positional argument, etc.

    Parameters
    ----------
    positional_args : list[Argument]
        Positional arguments to generate specs for.
    help_format : str
        Help text format.

    Returns
    -------
    list[str]
        List of zsh positional argument specs.
    """
    specs = []

    # Check if we have variadic positionals
    variadic_args = [arg for arg in positional_args if arg.is_var_positional()]
    non_variadic_args = [arg for arg in positional_args if not arg.is_var_positional()]

    # Generate specs for non-variadic positionals
    for arg in non_variadic_args:
        # Position in nested context: After *::arg:->args, $words[1] is the subcommand
        # So positionals start at position 1 (not 2)
        # Use 1-based indexing: first positional is '1:', second is '2:', etc.
        pos = 1 + (arg.index or 0)
        desc = _get_description_from_argument(arg, help_format)

        # Check for choices first (Literal/Enum types)
        choices = arg.get_choices(force=True)
        if choices:
            escaped_choices = [_escape_completion_choice(clean_choice_text(c)) for c in choices]
            choices_str = " ".join(escaped_choices)
            action = f"({choices_str})"
        else:
            action = _map_completion_action_to_zsh(get_completion_action(arg.hint))

        spec = f"'{pos}:{desc}:{action}'" if action else f"'{pos}:{desc}'"
        specs.append(spec)

    # Generate specs for variadic positionals
    for arg in variadic_args:
        desc = _get_description_from_argument(arg, help_format)

        choices = arg.get_choices(force=True)
        if choices:
            escaped_choices = [_escape_completion_choice(clean_choice_text(c)) for c in choices]
            choices_str = " ".join(escaped_choices)
            action = f"({choices_str})"
        else:
            action = _map_completion_action_to_zsh(get_completion_action(arg.hint))

        spec = f"'*:{desc}:{action}'" if action else f"'*:{desc}'"
        specs.append(spec)

    return specs


def _generate_describe_completion(
    argument: "Argument",
    help_format: str,
    indent_str: str,
) -> list[str]:
    """Generate _describe-based completion for a single positional argument.

    Parameters
    ----------
    argument : Argument
        Argument to generate completion for.
    help_format : str
        Help text format.
    indent_str : str
        Indentation string.

    Returns
    -------
    list[str]
        Zsh completion code lines.
    """
    lines = []
    desc = _get_description_from_argument(argument, help_format)

    # Check for choices (Literal/Enum types)
    choices = argument.get_choices(force=True)
    if choices:
        # Generate choices array with descriptions
        escaped_choices = [_escape_completion_choice(clean_choice_text(c)) for c in choices]
        lines.append(f"{indent_str}local -a choices")
        lines.append(f"{indent_str}choices=(")
        for choice in escaped_choices:
            lines.append(f"{indent_str}  '{choice}:{desc}'")
        lines.append(f"{indent_str})")
        lines.append(f"{indent_str}_describe 'argument' choices")
    else:
        # Use completion action (files, directories, or nothing)
        action = get_completion_action(argument.hint)
        if action == CompletionAction.FILES:
            lines.append(f"{indent_str}_files")
        elif action == CompletionAction.DIRECTORIES:
            lines.append(f"{indent_str}_directories")
        # For other types, provide no completion

    return lines


def _generate_completion_for_path(
    completion_data: dict[tuple[str, ...], CompletionData],
    command_path: tuple[str, ...],
    indent: int = 2,
    prog_name: str = "cyclopts",
    help_flags: tuple[str, ...] = (),
    version_flags: tuple[str, ...] = (),
) -> list[str]:
    """Generate completion code for a specific command path.

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

    Returns
    -------
    list[str]
        Zsh code lines.
    """
    data = completion_data[command_path]
    commands = data.commands
    arguments = data.arguments
    indent_str = " " * indent
    lines = []

    if command_path == ("run",) and prog_name == "cyclopts":
        lines.extend(_generate_run_command_completion(arguments, indent_str, prog_name))
        return lines

    args_specs = []
    positional_specs = []

    # Separate positional from keyword arguments
    # Include all arguments with an index (both positional-only and positional-or-keyword)
    positional_args = [arg for arg in arguments if arg.index is not None and arg.show]
    keyword_args = [arg for arg in arguments if not arg.is_positional_only() and arg.show]

    # Sort positionals by index (should never be None for positional-only args)
    positional_args.sort(key=lambda a: a.index or 0)

    # Generate keyword argument specs
    for argument in keyword_args:
        specs = _generate_keyword_specs(argument, data.help_format)
        args_specs.extend(specs)

    # Check for flag commands (commands that look like options)
    flag_command_names = set()
    for registered_command in commands:
        if any(name.startswith("-") for name in registered_command.names):
            specs = _generate_keyword_specs_for_command(
                registered_command.names, registered_command.app, data.help_format
            )
            args_specs.extend(specs)
            flag_command_names.update(registered_command.names)

    # Add help and version flags to all command paths (if not already added as flag commands)
    for flag in help_flags:
        if flag.startswith("-") and flag not in flag_command_names:
            spec = f"'{flag}[Display this message and exit.]'"
            args_specs.append(spec)

    for flag in version_flags:
        if flag.startswith("-") and flag not in flag_command_names:
            spec = f"'{flag}[Display application version.]'"
            args_specs.append(spec)

    has_non_flag_commands = any(
        not cmd_name.startswith("-") for registered_command in commands for cmd_name in registered_command.names
    )

    # Generate positional argument specs
    # Only add positionals if there are no subcommands (they conflict in zsh)
    if positional_args and not has_non_flag_commands:
        if command_path:
            # Nested context: use shifted positional indexing (words[1] is subcommand)
            positional_specs = _generate_nested_positional_specs(positional_args, data.help_format)
        else:
            # Root context: standard _arguments works fine
            for argument in positional_args:
                spec = _generate_positional_spec(argument, data.help_format)
                positional_specs.append(spec)

        # Add positionals BEFORE options to prioritize them in completion
        args_specs = positional_specs + args_specs

    if has_non_flag_commands:
        args_specs.append("'1: :->cmds'")
        args_specs.append("'*::arg:->args'")

    if args_specs:
        c_flag = "-C " if has_non_flag_commands else ""
        lines.append(f"{indent_str}_arguments {c_flag}\\")
        for spec in args_specs[:-1]:
            lines.append(f"{indent_str}  {spec} \\")
        lines.append(f"{indent_str}  {args_specs[-1]}")
        lines.append("")

    if has_non_flag_commands:
        lines.append(f"{indent_str}case $state in")
        lines.append(f"{indent_str}  cmds)")

        cmd_list = []
        for registered_command in commands:
            for cmd_name in registered_command.names:
                if not cmd_name.startswith("-"):
                    desc = _safe_get_description_from_app(registered_command.app, data.help_format)
                    cmd_list.append(f"'{cmd_name}:{desc}'")

        lines.append(f"{indent_str}    local -a commands")
        lines.append(f"{indent_str}    commands=(")
        for cmd in cmd_list:
            lines.append(f"{indent_str}      {cmd}")
        lines.append(f"{indent_str}    )")
        lines.append(f"{indent_str}    _describe -t commands 'command' commands")
        lines.append(f"{indent_str}    ;;")

        lines.append(f"{indent_str}  args)")
        lines.append(f"{indent_str}    case $words[1] in")

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

                sub_path = command_path + (cmd_name,)
                if sub_path in completion_data:
                    lines.append(f"{indent_str}      {cmd_name})")
                    sub_lines = _generate_completion_for_path(
                        completion_data, sub_path, indent + 8, prog_name, help_flags, version_flags
                    )
                    lines.extend(sub_lines)
                    lines.append(f"{indent_str}        ;;")

        lines.append(f"{indent_str}    esac")
        lines.append(f"{indent_str}    ;;")
        lines.append(f"{indent_str}esac")

    return lines


def _escape_completion_choice(choice: str) -> str:
    """Escape special characters in a completion choice value for zsh.

    Choice should already be cleaned via clean_choice_text() before calling this function.
    This function only applies zsh-specific escaping.

    Parameters
    ----------
    choice : str
        Cleaned choice value.

    Returns
    -------
    str
        Escaped choice value safe for zsh completion.
    """
    choice = choice.replace("\\", "\\\\")
    choice = choice.replace("'", r"'\''")
    choice = choice.replace("`", "\\`")
    choice = choice.replace("$", "\\$")
    choice = choice.replace('"', '\\"')
    choice = choice.replace(" ", "\\ ")
    choice = choice.replace("(", "\\(")
    choice = choice.replace(")", "\\)")
    choice = choice.replace("[", "\\[")
    choice = choice.replace("]", "\\]")
    choice = choice.replace(";", "\\;")
    choice = choice.replace("|", "\\|")
    choice = choice.replace("&", "\\&")
    choice = choice.replace(":", "\\:")
    return choice


def _escape_zsh_description(text: str) -> str:
    """Escape special characters in description text for zsh.

    Parameters
    ----------
    text : str
        Cleaned description text.

    Returns
    -------
    str
        Escaped description safe for zsh completion.
    """
    text = text.replace("\\", "\\\\")
    text = text.replace("`", "\\`")
    text = text.replace("$", "\\$")
    text = text.replace('"', '\\"')
    text = text.replace("'", r"'\''")
    text = text.replace(":", r"\:")
    text = text.replace("[", r"\[")
    text = text.replace("]", r"\]")
    return text


def _generate_keyword_specs(argument: "Argument", help_format: str) -> list[str]:
    """Generate zsh _arguments specs for a keyword argument.

    Parameters
    ----------
    argument : Argument
        Argument object from ArgumentCollection.
    help_format : str
        Help text format.

    Returns
    -------
    list[str]
        List of zsh argument specs.
    """
    specs = []
    desc = _get_description_from_argument(argument, help_format)

    flag = argument.is_flag()

    # Determine completion action
    action = ""
    choices = argument.get_choices(force=True)
    if choices:
        escaped_choices = [_escape_completion_choice(clean_choice_text(c)) for c in choices]
        choices_str = " ".join(escaped_choices)
        action = f"({choices_str})"
        flag = False
    else:
        action = _map_completion_action_to_zsh(get_completion_action(argument.hint))

    # Generate specs for positive names (from parameter.name)
    for name in argument.parameter.name:  # pyright: ignore[reportOptionalIterable]
        if not name.startswith("-"):
            continue
        if flag and not action:
            spec = f"'{name}[{desc}]'"
        elif action:
            spec = f"'{name}[{desc}]:{name.lstrip('-')}:{action}'"
        else:
            spec = f"'{name}[{desc}]:{name.lstrip('-')}'"
        specs.append(spec)

    # Generate specs for negative names (always flags, consume no tokens)
    for name in argument.negatives:
        if not name.startswith("-"):
            continue
        # Negative flags always consume zero tokens (e.g., --empty-items, --no-verbose)
        spec = f"'{name}[{desc}]'"
        specs.append(spec)

    return specs


def _generate_positional_spec(argument: "Argument", help_format: str) -> str:
    """Generate zsh _arguments spec for a positional argument.

    Parameters
    ----------
    argument : Argument
        Positional argument object.
    help_format : str
        Help text format.

    Returns
    -------
    str
        Zsh positional argument spec.
    """
    desc = _get_description_from_argument(argument, help_format)

    # Check for choices first (Literal/Enum types)
    choices = argument.get_choices(force=True)
    if choices:
        escaped_choices = [_escape_completion_choice(clean_choice_text(c)) for c in choices]
        choices_str = " ".join(escaped_choices)
        action = f"({choices_str})"
    else:
        action = _map_completion_action_to_zsh(get_completion_action(argument.hint))

    if argument.is_var_positional():
        # Variadic positional (*args)
        return f"'*:{desc}:{action}'" if action else f"'*:{desc}'"

    # Regular positional - zsh uses 1-based indexing
    if argument.index is None:
        raise ValueError(f"Positional-only argument {argument.names} missing index")
    pos = argument.index + 1
    return f"'{pos}:{desc}:{action}'" if action else f"'{pos}:{desc}'"


def _generate_keyword_specs_for_command(names: tuple[str, ...], cmd_app: "App", help_format: str) -> list[str]:
    """Generate zsh _arguments specs for a command that looks like a flag.

    Parameters
    ----------
    names : tuple[str, ...]
        Registered names for the command.
    cmd_app : App
        Command app with flag-like names.
    help_format : str
        Help text format.

    Returns
    -------
    list[str]
        List of zsh argument specs.
    """
    specs = []
    desc = _safe_get_description_from_app(cmd_app, help_format)

    for name in names:
        if name.startswith("-"):
            spec = f"'{name}[{desc}]'"
            specs.append(spec)

    return specs


def _map_completion_action_to_zsh(action: CompletionAction) -> str:
    """Map shell-agnostic completion action to zsh-specific completion command.

    Parameters
    ----------
    action : CompletionAction
        Shell-agnostic completion action.

    Returns
    -------
    str
        Zsh completion command (e.g., "_files", "_directories", or "").
    """
    if action == CompletionAction.FILES:
        return "_files"
    elif action == CompletionAction.DIRECTORIES:
        return "_directories"
    return ""


def _get_description_from_argument(argument: "Argument", help_format: str) -> str:
    """Extract plain text description from Argument, escaping zsh special chars.

    Parameters
    ----------
    argument : Argument
        Argument object with parameter help text.
    help_format : str
        Help text format.

    Returns
    -------
    str
        Escaped plain text description (truncated to 80 chars).
    """
    text = strip_markup(argument.parameter.help or "", format=help_format)
    return _escape_zsh_description(text)


def _safe_get_description_from_app(cmd_app: "App", help_format: str) -> str:
    """Extract plain text description from App, escaping zsh special chars.

    Parameters
    ----------
    cmd_app : App
        Command app with help text.
    help_format : str
        Help text format.

    Returns
    -------
    str
        Escaped plain text description (truncated to 80 chars).
    """
    if not cmd_app.help:
        return ""

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

    text = strip_markup(text, format=help_format)
    return _escape_zsh_description(text)
