lib/do_it/introspection.ex

defmodule DoIt.Introspection do
  @moduledoc """
  Provides introspection capabilities for DoIt commands and their structure.

  This module enables analysis of command hierarchies, options, arguments,
  and other metadata needed for features like auto-completion.
  """

  @doc """
  Gets all top-level commands from a MainCommand module.

  Returns a list of command names as strings.
  """
  def get_all_commands(main_module) do
    case main_module.__info__(:attributes) do
      attributes when is_list(attributes) ->
        attributes
        |> Keyword.get_values(:commands)
        |> List.flatten()
        |> Enum.map(fn module ->
          {command_name, _} = module.command()
          command_name
        end)

      _ ->
        []
    end
  end

  @doc """
  Gets all subcommands from a Command module.

  Returns a list of subcommand names as strings.
  """
  def get_subcommands(command_module) do
    if function_exported?(command_module, :__info__, 1) do
      case command_module.__info__(:attributes) do
        attributes when is_list(attributes) ->
          attributes
          |> Keyword.get_values(:subcommands)
          |> List.flatten()
          |> Enum.map(fn module ->
            {subcommand_name, _} = module.command()
            subcommand_name
          end)

        _ ->
          []
      end
    else
      []
    end
  end

  @doc """
  Gets all options from a Command module.

  Returns a list of DoIt.Option structs.
  """
  def get_command_options(command_module) do
    if function_exported?(command_module, :__info__, 1) do
      case command_module.__info__(:attributes) do
        attributes when is_list(attributes) ->
          attributes
          |> Keyword.get_values(:options)
          |> List.flatten()

        _ ->
          []
      end
    else
      []
    end
  end

  @doc """
  Gets all option flags (--name and -alias) from a Command module.

  Returns a list of option flags as strings.
  """
  def get_command_option_flags(command_module) do
    get_command_options(command_module)
    |> Enum.flat_map(fn option ->
      flags = ["--#{option.name}"]

      if option.alias do
        flags ++ ["-#{option.alias}"]
      else
        flags
      end
    end)
  end

  @doc """
  Gets all arguments from a Command module.

  Returns a list of DoIt.Argument structs.
  """
  def get_command_arguments(command_module) do
    if function_exported?(command_module, :__info__, 1) do
      case command_module.__info__(:attributes) do
        attributes when is_list(attributes) ->
          attributes
          |> Keyword.get_values(:arguments)
          |> List.flatten()

        _ ->
          []
      end
    else
      []
    end
  end

  @doc """
  Finds a command module by name from a MainCommand module.

  Returns the module atom or nil if not found.
  """
  def find_command_module(main_module, command_name) do
    case main_module.__info__(:attributes) do
      attributes when is_list(attributes) ->
        attributes
        |> Keyword.get_values(:commands)
        |> List.flatten()
        |> Enum.find(fn module ->
          {name, _} = module.command()
          name == command_name
        end)

      _ ->
        nil
    end
  end

  @doc """
  Finds a subcommand module by name from a Command module.

  Returns the module atom or nil if not found.
  """
  def find_subcommand_module(command_module, subcommand_name) do
    if function_exported?(command_module, :__info__, 1) do
      case command_module.__info__(:attributes) do
        attributes when is_list(attributes) ->
          attributes
          |> Keyword.get_values(:subcommands)
          |> List.flatten()
          |> Enum.find(fn module ->
            {name, _} = module.command()
            name == subcommand_name
          end)

        _ ->
          nil
      end
    else
      nil
    end
  end

  @doc """
  Resolves a command path to its final command module.

  Takes a main module and a list of command/subcommand names,
  returns the final command module or nil if path is invalid.

  ## Examples

      iex> resolve_command_path(MyApp, ["user", "create"])
      MyApp.User.Create
      
      iex> resolve_command_path(MyApp, ["invalid"])
      nil
  """
  def resolve_command_path(main_module, []) do
    main_module
  end

  def resolve_command_path(main_module, [command_name | rest]) do
    case find_command_module(main_module, command_name) do
      nil -> nil
      command_module -> resolve_subcommand_path(command_module, rest)
    end
  end

  defp resolve_subcommand_path(command_module, []) do
    command_module
  end

  defp resolve_subcommand_path(command_module, [subcommand_name | rest]) do
    case find_subcommand_module(command_module, subcommand_name) do
      nil -> nil
      subcommand_module -> resolve_subcommand_path(subcommand_module, rest)
    end
  end

  @doc """
  Gets the command structure as a nested map for introspection.

  Returns a map containing the full command hierarchy with metadata.
  """
  def get_command_structure(main_module) do
    {app_name, description} = main_module.command()

    commands =
      get_all_commands(main_module)
      |> Enum.map(fn command_name ->
        command_module = find_command_module(main_module, command_name)
        {_, command_description} = command_module.command()

        %{
          name: command_name,
          description: command_description,
          module: command_module,
          subcommands: get_subcommand_structure(command_module),
          options: get_command_options(command_module),
          arguments: get_command_arguments(command_module)
        }
      end)

    %{
      name: app_name,
      description: description,
      module: main_module,
      commands: commands
    }
  end

  defp get_subcommand_structure(command_module) do
    get_subcommands(command_module)
    |> Enum.map(fn subcommand_name ->
      subcommand_module = find_subcommand_module(command_module, subcommand_name)
      {_, subcommand_description} = subcommand_module.command()

      %{
        name: subcommand_name,
        description: subcommand_description,
        module: subcommand_module,
        subcommands: get_subcommand_structure(subcommand_module),
        options: get_command_options(subcommand_module),
        arguments: get_command_arguments(subcommand_module)
      }
    end)
  end

  @doc """
  Checks if a command has subcommands.
  """
  def has_subcommands?(command_module) do
    get_subcommands(command_module) != []
  end

  @doc """
  Gets all possible command paths as flat list.

  Returns a list of command paths where each path is a list of strings.
  """
  def get_all_command_paths(main_module) do
    get_command_structure(main_module).commands
    |> Enum.flat_map(&extract_command_paths(&1, []))
  end

  defp extract_command_paths(%{name: name, subcommands: []}, prefix) do
    [prefix ++ [name]]
  end

  defp extract_command_paths(%{name: name, subcommands: subcommands}, prefix) do
    current_path = [prefix ++ [name]]

    subcommand_paths =
      subcommands
      |> Enum.flat_map(&extract_command_paths(&1, prefix ++ [name]))

    current_path ++ subcommand_paths
  end
end