## This Source Code Form is subject to the terms of the Mozilla Public
## License, v. 2.0. If a copy of the MPL was not distributed with this
## file, You can obtain one at https://mozilla.org/MPL/2.0/.
##
## Copyright (c) 2007-2023 VMware, Inc. or its affiliates.  All rights reserved.

alias RabbitMQ.CLI.CommandBehaviour

defmodule RabbitMQ.CLI.Ctl.Commands.HelpCommand do
  alias RabbitMQ.CLI.Core.{CommandModules, Config, ExitCodes}
  alias RabbitMQ.CLI.Core.CommandModules

  import RabbitMQ.CLI.Core.ANSI

  @behaviour RabbitMQ.CLI.CommandBehaviour

  def scopes(), do: [:ctl, :diagnostics, :plugins, :queues, :streams, :vmware, :upgrade]
  def switches(), do: [list_commands: :boolean]

  def distribution(_), do: :none
  use RabbitMQ.CLI.Core.MergesNoDefaults

  def validate([], _), do: :ok
  def validate([_command], _), do: :ok

  def validate(args, _) when length(args) > 1 do
    {:validation_failure, :too_many_args}
  end

  def run([command_name | _], opts) do
    _ = CommandModules.load(opts)

    module_map = CommandModules.module_map(opts)

    case module_map[command_name] do
      nil ->
        # command not found
        # {:error, all_usage(opts)}
        case RabbitMQ.CLI.AutoComplete.suggest_command(command_name, module_map) do
          {:suggest, suggested} ->
            suggest_message =
              "\nCommand '#{command_name}' not found. \n" <>
                "Did you mean '#{suggested}'? \n"

            {:error, ExitCodes.exit_usage(), suggest_message}

          nil ->
            {:error, ExitCodes.exit_usage(), "\nCommand '#{command_name}' not found."}
        end

      command ->
        {:ok, command_usage(command, opts)}
    end
  end

  def run([], opts) do
    _ = CommandModules.load(opts)

    case opts[:list_commands] do
      true ->
        {:ok, commands_description(opts)}

      _ ->
        {:ok, all_usage(opts)}
    end
  end

  def output({:ok, result}, _) do
    {:ok, result}
  end

  def output({:error, result}, _) do
    {:error, ExitCodes.exit_usage(), result}
  end

  use RabbitMQ.CLI.DefaultOutput

  def banner(_, _), do: nil

  def help_section(), do: :help

  def description(), do: "Displays usage information for a command"

  def usage(), do: "help (<command> | [--list-commands])"

  def usage_additional() do
    [
      ["--list-commands", "only output a list of discovered commands"]
    ]
  end

  #
  # Implementation
  #

  def all_usage(opts) do
    tool_name = program_name(opts)

    tool_usage(tool_name) ++
      ["\n\nAvailable commands:\n"] ++
      commands_description(opts) ++
      help_footer(tool_name)
  end

  def command_usage(command, opts) do
    Enum.join(
      [base_usage(command, opts)] ++
        command_description(command) ++
        additional_usage(command) ++
        relevant_doc_guides(command) ++
        general_options_usage(),
      "\n\n"
    ) <> "\n"
  end

  defp tool_usage(tool_name) do
    [
      "\n#{bright("Usage")}\n\n" <>
        "#{tool_name} [--node <node>] [--timeout <timeout>] [--longnames] [--quiet] <command> [<command options>]"
    ]
  end

  def base_usage(command, opts) do
    tool_name = program_name(opts)

    maybe_timeout =
      case command_supports_timeout(command) do
        true -> " [--timeout <timeout>]"
        false -> ""
      end

    Enum.join([
      "\n#{bright("Usage")}\n\n",
      "#{tool_name} [--node <node>] [--longnames] [--quiet] " <>
        flatten_string(command.usage(), maybe_timeout)
    ])
  end

  defp flatten_string(list, additional) when is_list(list) do
    list
    |> Enum.map(fn line -> line <> additional end)
    |> Enum.join("\n")
  end

  defp flatten_string(str, additional) when is_binary(str) do
    str <> additional
  end

  defp general_options_usage() do
    [
      "#{bright("General Options")}

The following options are accepted by most or all commands.

short            | long          | description
-----------------|---------------|--------------------------------
-?               | --help        | displays command help
-n <node>        | --node <node> | connect to node <node>
-l               | --longnames   | use long host names
-t               | --timeout <n> | for commands that support it, operation timeout in seconds
-q               | --quiet       | suppress informational messages
-s               | --silent      | suppress informational messages
                                 | and table header row
-p               | --vhost       | for commands that are scoped to a virtual host,
                 |               | virtual host to use
                 | --formatter   | alternative result formatter to use
                                 | if supported: json, pretty_table, table, csv, erlang
                                   not all commands support all (or any) alternative formatters."
    ]
  end

  defp command_description(command) do
    case CommandBehaviour.description(command) do
      "" -> []
      other -> [other <> ".\n"]
    end
  end

  defp list_item_formatter([option, description]) do
    "#{option}\n\t#{description}\n"
  end

  defp list_item_formatter({option, description}) do
    "#{option}\n\t#{description}\n"
  end

  defp list_item_formatter(line) do
    "#{line}\n"
  end

  defp additional_usage(command) do
    command_usage =
      case CommandBehaviour.usage_additional(command) do
        list when is_list(list) -> list |> Enum.map(&list_item_formatter/1)
        bin when is_binary(bin) -> ["#{bin}\n"]
      end

    case command_usage do
      [] ->
        []

      usage ->
        [flatten_string(["#{bright("Arguments and Options")}\n" | usage], "")]
    end
  end

  defp relevant_doc_guides(command) do
    guide_list =
      case CommandBehaviour.usage_doc_guides(command) do
        list when is_list(list) -> list |> Enum.map(fn ln -> " * #{ln}\n" end)
        bin when is_binary(bin) -> [" * #{bin}\n"]
      end

    case guide_list do
      [] ->
        []

      usage ->
        [flatten_string(["#{bright("Relevant Doc Guides")}\n" | usage], "")]
    end
  end

  defp help_footer(tool_name) do
    ["Use '#{tool_name} help <command>' to learn more about a specific command"]
  end

  defp command_supports_timeout(command) do
    nil != CommandBehaviour.switches(command)[:timeout]
  end

  defp commands_description(opts) do
    module_map = CommandModules.module_map(opts)

    pad_commands_to =
      Enum.reduce(module_map, 0, fn {name, _}, longest ->
        name_length = String.length(name)

        case name_length > longest do
          true -> name_length
          false -> longest
        end
      end)

    lines =
      module_map
      |> Enum.map(fn {name, cmd} ->
        description = CommandBehaviour.description(cmd)
        help_section = CommandBehaviour.help_section(cmd)
        {name, {description, help_section}}
      end)
      |> Enum.group_by(fn {_, {_, help_section}} -> help_section end)
      |> Enum.sort_by(fn {help_section, _} ->
        case help_section do
          :deprecated -> 999
          :other -> 100
          {:plugin, _} -> 99
          :help -> 1
          :node_management -> 2
          :cluster_management -> 3
          :replication -> 3
          :user_management -> 4
          :access_control -> 5
          :observability_and_health_checks -> 6
          :parameters -> 7
          :policies -> 8
          :virtual_hosts -> 9
          _ -> 98
        end
      end)
      |> Enum.map(fn {help_section, section_helps} ->
        [
          "\n" <> bright(section_head(help_section)) <> ":\n\n"
          | Enum.sort(section_helps)
            |> Enum.map(fn {name, {description, _}} ->
              "   #{String.pad_trailing(name, pad_commands_to)}  #{description}\n"
            end)
        ]
      end)
      |> Enum.concat()

    lines ++ ["\n"]
  end

  defp section_head(help_section) do
    case help_section do
      :help ->
        "Help"

      :user_management ->
        "Users"

      :cluster_management ->
        "Cluster"

      :replication ->
        "Replication"

      :node_management ->
        "Nodes"

      :queues ->
        "Queues"

      :observability_and_health_checks ->
        "Monitoring, observability and health checks"

      :virtual_hosts ->
        "Virtual hosts"

      :access_control ->
        "Access Control"

      :parameters ->
        "Parameters"

      :policies ->
        "Policies"

      :configuration ->
        "Configuration and Environment"

      :feature_flags ->
        "Feature flags"

      :other ->
        "Other"

      {:plugin, plugin} ->
        plugin_section(plugin) <> " plugin"

      custom ->
        snake_case_to_capitalized_string(custom)
    end
  end

  defp strip_rabbitmq_prefix(value, regex) do
    Regex.replace(regex, value, "")
  end

  defp format_known_plugin_name_fragments(value) do
    case value do
      ["amqp1.0"] -> "AMQP 1.0"
      ["amqp1", "0"] -> "AMQP 1.0"
      ["management"] -> "Management"
      ["management", "agent"] -> "Management"
      ["mqtt"] -> "MQTT"
      ["stomp"] -> "STOMP"
      ["web", "mqtt"] -> "Web MQTT"
      ["web", "stomp"] -> "Web STOMP"
      [other] -> snake_case_to_capitalized_string(other)
      fragments -> snake_case_to_capitalized_string(Enum.join(fragments, "_"))
    end
  end

  defp plugin_section(plugin) do
    regex = Regex.recompile!(~r/^rabbitmq_/)

    to_string(plugin)
    # drop rabbitmq_
    |> strip_rabbitmq_prefix(regex)
    |> String.split("_")
    |> format_known_plugin_name_fragments()
  end

  defp snake_case_to_capitalized_string(value) do
    to_string(value)
    |> String.split("_")
    |> Enum.map(&String.capitalize/1)
    |> Enum.join(" ")
  end

  defp program_name(opts) do
    Config.get_option(:script_name, opts)
  end
end
