defmodule DoIt.Completion do
  @moduledoc """
  Provides shell auto-completion functionality for DoIt CLI applications.

  This module generates completion scripts for popular shells (bash, fish, zsh)
  and provides the completion logic for commands, subcommands, options, and arguments.
  """

  alias DoIt.Introspection

  @doc """
  Generates a bash completion script for the given main module and app name.
  """
  def generate_bash_completion(_main_module, app_name) do
    """
    #!/bin/bash
    # Bash completion script for #{app_name}
    # Generated by DoIt library

    _#{app_name}_completions() {
        local cur prev opts cmd_line
        local -a cmd_args
        COMPREPLY=()
        cur="${COMP_WORDS[COMP_CWORD]}"
        prev="${COMP_WORDS[COMP_CWORD-1]}"

        # Build command arguments array, excluding the command name
        cmd_args=("${COMP_WORDS[@]:1}")

        # Handle the case where we're completing an empty position after a space
        if [[ ${COMP_CWORD} -gt ${#COMP_WORDS[@]} ]] || [[ -z "${cur}" && ${COMP_CWORD} -eq ${#COMP_WORDS[@]} ]]; then
            cmd_args+=("")
        fi

        # Get completions from the CLI tool
        local completions
        completions=$(#{app_name} completion complete "${cmd_args[@]}" 2>/dev/null)

        if [[ $? -eq 0 ]] && [[ -n "$completions" ]]; then
            # Convert newlines to spaces for compgen
            local space_separated_completions
            space_separated_completions=$(echo "$completions" | tr '\n' ' ')
            COMPREPLY=( $(compgen -W "${space_separated_completions}" -- ${cur}) )
        fi

        return 0
    }

    complete -F _#{app_name}_completions #{app_name}
    """
  end

  @doc """
  Generates a fish completion script for the given main module and app name.
  """
  def generate_fish_completion(_main_module, app_name) do
    """
    # Fish completion script for #{app_name}
    # Generated by DoIt library

    function __#{app_name}_complete
        set -l cmd_line (commandline -poc)
        # Remove the first element (app name) from cmd_line
        set -l args $cmd_line[2..-1]
        
        # Handle the case where we're completing after a space
        set -l current_token (commandline -ct)
        if test -z "$current_token"
            set args $args ""
        end
        
        set -l completions (#{app_name} completion complete $args 2>/dev/null)

        if test $status -eq 0
            for completion in $completions
                echo $completion
            end
        end
    end

    complete -c #{app_name} -f -a "(__#{app_name}_complete)"
    """
  end

  @doc """
  Generates a zsh completion script for the given main module and app name.
  """
  def generate_zsh_completion(_main_module, app_name) do
    """
    #compdef #{app_name}
    # Zsh completion script for #{app_name}
    # Generated by DoIt library

    _#{app_name}() {
        local context state state_descr line
        local -a completions cmd_args
        local completion_output

        # Prevent multiple executions for invalid states
        [[ $CURRENT -eq 0 ]] && return 1

        # Build command arguments, excluding the command name
        cmd_args=("${words[@]:1}")

        # If we're completing at a position beyond the current words,
        # add an empty string to indicate we're completing a new argument
        if [[ $CURRENT -gt ${#words[@]} ]]; then
            cmd_args+=("")
        fi

        # Get completions from the CLI tool
        completion_output=$(#{app_name} completion complete "${cmd_args[@]}" 2>/dev/null)

        if [[ $? -eq 0 ]] && [[ -n "$completion_output" ]]; then
            # Split output into array
            completions=("${(@f)completion_output}")

            # Only show completions if we have any
            if [[ ${#completions[@]} -gt 0 ]]; then
                _describe 'commands' completions
                return 0
            fi
        fi

        return 1
    }

    compdef _#{app_name} #{app_name}
    """
  end

  @doc """
  Provides completions for the given command line arguments.

  This is the main entry point called by shell completion scripts.
  """
  def complete_command(main_module, args) do
    context = parse_completion_context(main_module, args)

    case context do
      {:command, command_path} ->
        case command_path do
          [] ->
            # Completing top-level commands
            Introspection.get_all_commands(main_module) ++ ["help", "version", "completion"]

          _ ->
            # Check for built-in commands first
            case handle_builtin_command(command_path) do
              {:builtin, completions} ->
                completions

              :not_builtin ->
                case Introspection.resolve_command_path(main_module, command_path) do
                  nil ->
                    # Command path is invalid, try completing it as a partial command
                    all_commands =
                      Introspection.get_all_commands(main_module) ++
                        ["help", "version", "completion"]

                    # Filter commands that start with the last element in the path
                    if length(command_path) == 1 do
                      last_part = List.last(command_path)
                      Enum.filter(all_commands, &String.starts_with?(&1, last_part))
                    else
                      []
                    end

                  ^main_module ->
                    # Still at main level, complete commands
                    Introspection.get_all_commands(main_module) ++
                      ["help", "version", "completion"]

                  command_module ->
                    # Complete subcommands and options for this command
                    subcommands = Introspection.get_subcommands(command_module)
                    options = Introspection.get_command_option_flags(command_module)
                    subcommands ++ options
                end
            end
        end

      {:partial_command, partial, command_path} ->
        case command_path do
          [] ->
            # Partial top-level command
            all_commands =
              Introspection.get_all_commands(main_module) ++ ["help", "version", "completion"]

            Enum.filter(all_commands, &String.starts_with?(&1, partial))

          _ ->
            # Check for built-in commands first
            case handle_builtin_command(command_path) do
              {:builtin, completions} ->
                Enum.filter(completions, &String.starts_with?(&1, partial))

              :not_builtin ->
                case Introspection.resolve_command_path(main_module, command_path) do
                  nil ->
                    # Invalid command path
                    []

                  ^main_module ->
                    # Still at main level, filter commands
                    all_commands =
                      Introspection.get_all_commands(main_module) ++
                        ["help", "version", "completion"]

                    Enum.filter(all_commands, &String.starts_with?(&1, partial))

                  command_module ->
                    # Complete partial subcommands
                    subcommands = Introspection.get_subcommands(command_module)
                    Enum.filter(subcommands, &String.starts_with?(&1, partial))
                end
            end
        end

      {:option_value, option_name, command_path} ->
        # Complete values for specific options
        complete_option_value(main_module, option_name, command_path)

      {:argument, arg_index, command_path} ->
        # Complete argument values
        complete_argument_value(main_module, arg_index, command_path)

      {:partial_option, partial, command_path} ->
        # Complete partial option names
        case Introspection.resolve_command_path(main_module, command_path) do
          nil ->
            []

          command_module ->
            Introspection.get_command_option_flags(command_module)
            |> Enum.filter(&String.starts_with?(&1, partial))
        end
    end
  end

  # Handles completion for built-in commands that have subcommands.
  defp handle_builtin_command(command_path) do
    case command_path do
      ["completion"] ->
        {:builtin, ["bash", "fish", "zsh", "complete", "install", "debug"]}

      ["help"] ->
        {:builtin, []}

      ["version"] ->
        {:builtin, []}

      _ ->
        :not_builtin
    end
  end

  @doc """
  Parses the completion context from command line arguments.

  Returns one of:
  - `{:command, command_path}` - completing a command or subcommand
  - `{:partial_command, partial, command_path}` - completing a partial command
  - `{:option_value, option_name, command_path}` - completing an option value
  - `{:argument, arg_index, command_path}` - completing an argument
  - `{:partial_option, partial, command_path}` - completing a partial option
  """
  def parse_completion_context(main_module, args) do
    # Split args into completed args and the current (partial) token being completed
    {completed_args, current_token} =
      case args do
        [] -> {[], ""}
        list -> {Enum.drop(list, -1), List.last(list)}
      end

    parse_completion_context(main_module, completed_args, current_token, [], 0, nil)
  end

  defp parse_completion_context(main_module, [], current_token, command_path, arg_index, nil) do
    cond do
      String.starts_with?(current_token, "--") ->
        # Completing a long option
        {:partial_option, current_token, Enum.reverse(command_path)}

      String.starts_with?(current_token, "-") and String.length(current_token) > 1 ->
        # Completing a short option
        {:partial_option, current_token, Enum.reverse(command_path)}

      current_token == "-" ->
        # Single dash, treat as partial option
        {:partial_option, current_token, Enum.reverse(command_path)}

      current_token == "" or not String.starts_with?(current_token, "-") ->
        # Completing a command or argument
        reversed_path = Enum.reverse(command_path)

        case reversed_path do
          [] ->
            # No command path yet
            if current_token == "" do
              # Empty current token, completing top-level commands
              {:command, []}
            else
              # Partial command
              {:partial_command, current_token, []}
            end

          path ->
            # Check if path represents a complete command
            case Introspection.resolve_command_path(main_module, path) do
              nil ->
                # Invalid command path, treat as partial command completion
                if current_token == "" do
                  {:command, path}
                else
                  {:partial_command, current_token, path}
                end

              ^main_module ->
                # Still at main level, completing commands
                if current_token == "" do
                  {:command, path}
                else
                  {:partial_command, current_token, path}
                end

              command_module ->
                # Have a complete command
                if current_token == "" do
                  # Empty current token means we're completing after a space
                  # Always show subcommands and options first
                  subcommands = Introspection.get_subcommands(command_module)
                  options = Introspection.get_command_option_flags(command_module)

                  if length(subcommands) > 0 || length(options) > 0 do
                    # Has subcommands or options, complete them
                    {:command, path}
                  else
                    # No subcommands or options, complete arguments
                    {:argument, arg_index, path}
                  end
                else
                  # Non-empty current token, could be partial subcommand or argument
                  {:partial_command, current_token, path}
                end
            end
        end
    end
  end

  defp parse_completion_context(
         _main_module,
         [],
         _current_token,
         command_path,
         _arg_index,
         waiting_for_option
       ) do
    # We have a waiting option and current_token is its value
    {:option_value, waiting_for_option, Enum.reverse(command_path)}
  end

  defp parse_completion_context(
         main_module,
         [current | rest],
         current_token,
         command_path,
         arg_index,
         waiting_for_option
       ) do
    cond do
      waiting_for_option != nil ->
        # Previous token was an option expecting a value
        if String.starts_with?(current, "-") do
          # Current token is another option, previous option has no value
          parse_completion_context(
            main_module,
            [current | rest],
            current_token,
            command_path,
            arg_index,
            nil
          )
        else
          # Current token is the value for the previous option
          parse_completion_context(main_module, rest, current_token, command_path, arg_index, nil)
        end

      String.starts_with?(current, "--") ->
        option_name = String.slice(current, 2..-1//1)

        if String.contains?(option_name, "=") do
          # Option with value: --option=value
          parse_completion_context(main_module, rest, current_token, command_path, arg_index, nil)
        else
          # Option expecting separate value
          parse_completion_context(
            main_module,
            rest,
            current_token,
            command_path,
            arg_index,
            option_name
          )
        end

      String.starts_with?(current, "-") and String.length(current) > 0 ->
        # Short option
        option_name = String.slice(current, 1..-1//1)

        parse_completion_context(
          main_module,
          rest,
          current_token,
          command_path,
          arg_index,
          option_name
        )

      true ->
        # Regular argument - could be command, subcommand, or argument
        new_command_path = [current | command_path]
        reversed_new_path = Enum.reverse(new_command_path)

        # Increment arg_index only if we're past the command path
        new_arg_index =
          case Introspection.resolve_command_path(main_module, reversed_new_path) do
            # Invalid command path, don't increment
            nil -> arg_index
            # Still at main level
            ^main_module -> arg_index
            # Complete command, increment for arguments
            _command_module -> arg_index + 1
          end

        parse_completion_context(
          main_module,
          rest,
          current_token,
          new_command_path,
          new_arg_index,
          nil
        )
    end
  end

  @doc """
  Completes values for a specific option.
  """
  def complete_option_value(main_module, option_name, command_path) do
    case Introspection.resolve_command_path(main_module, command_path) do
      nil ->
        []

      command_module ->
        options = Introspection.get_command_options(command_module)

        case Enum.find(options, &(&1.name == String.to_atom(option_name))) do
          nil -> []
          option -> complete_option_values_from_spec(option)
        end
    end
  end

  defp complete_option_values_from_spec(%DoIt.Option{allowed_values: values})
       when is_list(values) do
    Enum.map(values, &to_string/1)
  end

  defp complete_option_values_from_spec(%DoIt.Option{type: :boolean}) do
    ["true", "false"]
  end

  defp complete_option_values_from_spec(_option) do
    # No specific completions available
    []
  end

  @doc """
  Completes values for command arguments.
  """
  def complete_argument_value(main_module, arg_index, command_path) do
    case Introspection.resolve_command_path(main_module, command_path) do
      nil ->
        []

      command_module ->
        arguments = Introspection.get_command_arguments(command_module)

        # Get the argument at the specified index (0-based)
        case Enum.at(arguments, arg_index) do
          nil -> []
          argument -> complete_argument_values_from_spec(argument)
        end
    end
  end

  defp complete_argument_values_from_spec(_argument) do
    # For now, no specific argument completions
    # This could be extended to support file completions, etc.
    []
  end

  @doc """
  Gets installation instructions for shell completion.
  """
  def get_installation_instructions(app_name, shell) do
    case shell do
      "bash" ->
        """
        To enable bash completion, add the following to your ~/.bashrc:

            eval "$(#{app_name} completion bash)"

        Or to install system-wide, save the completion script:

            #{app_name} completion bash | sudo tee /etc/bash_completion.d/#{app_name}
        """

      "fish" ->
        """
        To enable fish completion, run:

            #{app_name} completion fish > ~/.config/fish/completions/#{app_name}.fish
        """

      "zsh" ->
        """
        To enable zsh completion, add the following to your ~/.zshrc:

            eval "$(#{app_name} completion zsh)"

        Make sure you have completion system initialized before this line:

            autoload -U compinit
            compinit
        """

      _ ->
        """
        Unsupported shell: #{shell}
        Supported shells: bash, fish, zsh
        """
    end
  end

  @doc """
  Lists all available completions for debugging purposes.
  """
  def debug_completions(main_module) do
    structure = Introspection.get_command_structure(main_module)

    %{
      app: structure.name,
      description: structure.description,
      commands: Enum.map(structure.commands, &debug_command_info/1),
      built_in_commands: ["help", "version", "completion"]
    }
  end

  defp debug_command_info(command) do
    %{
      name: command.name,
      description: command.description,
      subcommands: Enum.map(command.subcommands, &debug_command_info/1),
      options:
        Enum.map(command.options, fn opt ->
          %{
            name: "--#{opt.name}",
            alias: if(opt.alias, do: "-#{opt.alias}"),
            type: opt.type,
            description: opt.description,
            allowed_values: opt.allowed_values
          }
        end),
      arguments:
        Enum.map(command.arguments, fn arg ->
          %{
            name: arg.name,
            type: arg.type,
            description: arg.description
          }
        end)
    }
  end
end
