## 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.

defmodule RabbitMQ.CLI.CommandBehaviour do
  alias RabbitMQ.CLI.Core.Helpers

  @type pair_of_strings :: nonempty_list(String.t())

  @callback usage() :: String.t() | [String.t()]
  # validates CLI arguments
  @callback validate(list(), map()) :: :ok | {:validation_failure, atom() | {atom(), String.t()}}
  @callback merge_defaults(list(), map()) :: {list(), map()}
  @callback banner(list(), map()) :: [String.t()] | String.t() | nil
  @callback run(list(), map()) :: any
  # Coerces run/2 return value into the standard command output form
  # that is then formatted, printed and returned as an exit code.
  # There is a default implementation for this callback in DefaultOutput module
  @callback output(any, map()) ::
              :ok
              | {:ok, any}
              | {:stream, Enum.t()}
              | {:error, RabbitMQ.CLI.Core.ExitCodes.exit_code(), [String.t()]}

  @optional_callbacks formatter: 0,
                      printer: 0,
                      scopes: 0,
                      usage_additional: 0,
                      usage_doc_guides: 0,
                      description: 0,
                      help_section: 0,
                      switches: 0,
                      aliases: 0,
                      # validates execution environment, e.g. file presence,
                      # whether RabbitMQ is in an expected state on a node, etc
                      validate_execution_environment: 2,
                      distribution: 1

  @callback validate_execution_environment(list(), map()) ::
              :ok | {:validation_failure, atom() | {atom(), any}}
  @callback switches() :: Keyword.t()
  @callback aliases() :: Keyword.t()

  @callback formatter() :: atom()
  @callback printer() :: atom()
  @callback scopes() :: [atom()] | nil
  @callback description() :: String.t()
  @callback help_section() :: String.t() | atom() | {:plugin, atom()}
  @callback usage_additional() ::
              String.t()
              | [String.t()]
              | nonempty_list(pair_of_strings())
              | [{String.t(), String.t()}]
  @callback usage_doc_guides() :: String.t() | [String.t()]
  ## Erlang distribution control
  ## :cli - default rabbitmqctl generated node name
  ## :none - disable erlang distribution
  ## {:fun, fun} - use a custom function to start distribution
  @callback distribution(map()) :: :cli | :none | {:fun, (map() -> :ok | {:error, any()})}

  defmacro defcmd(map) do
    usage_q =
      case map[:usage] do
        nil ->
          :ok

        usage ->
          quote do
            def usage(), do: unquote(usage)
          end
      end

    scopes_q =
      case map[:scopes] do
        nil ->
          :ok

        scopes ->
          quote do
            def scopes(), do: unquote(scopes)
          end
      end

    description_q =
      case map[:description] do
        nil ->
          :ok

        description ->
          quote do
            def description(), do: unquote(description)
          end
      end

    help_section_q =
      case map[:help_section] do
        nil ->
          :ok

        help_section ->
          quote do
            def help_section(), do: unquote(help_section)
          end
      end

    usage_additional_q =
      case map[:usage_additional] do
        nil ->
          :ok

        usage_additional ->
          quote do
            def usage_additional(), do: unquote(usage_additional)
          end
      end

    formatter_q =
      case map[:formatter] do
        nil ->
          :ok

        formatter ->
          quote do
            def formatter(), do: unquote(formatter)
          end
      end

    switches_q =
      case map[:switches] do
        nil ->
          :ok

        switches ->
          quote do
            def switches(), do: unquote(switches)
          end
      end

    aliases_q =
      case map[:aliases] do
        nil ->
          :ok

        aliases ->
          quote do
            def aliases(), do: unquote(aliases)
          end
      end

    quote do
      unquote(usage_q)
      unquote(scopes_q)
      unquote(description_q)
      unquote(help_section_q)
      unquote(usage_additional_q)
      unquote(formatter_q)
      unquote(switches_q)
      unquote(aliases_q)
    end
  end

  def usage(cmd) do
    cmd.usage()
  end

  def scopes(cmd) do
    Helpers.apply_if_exported(cmd, :scopes, [], nil)
  end

  def description(cmd) do
    Helpers.apply_if_exported(cmd, :description, [], "")
  end

  def help_section(cmd) do
    case Helpers.apply_if_exported(cmd, :help_section, [], :other) do
      :other ->
        case Application.get_application(cmd) do
          :rabbitmqctl -> :other
          plugin -> {:plugin, plugin}
        end

      section ->
        section
    end
  end

  def usage_additional(cmd) do
    Helpers.apply_if_exported(cmd, :usage_additional, [], [])
  end

  def usage_doc_guides(cmd) do
    Helpers.apply_if_exported(cmd, :usage_doc_guides, [], [])
  end

  def formatter(cmd, default) do
    Helpers.apply_if_exported(cmd, :formatter, [], default)
  end

  def printer(cmd, default) do
    Helpers.apply_if_exported(cmd, :printer, [], default)
  end

  def switches(cmd) do
    Helpers.apply_if_exported(cmd, :switches, [], [])
  end

  def aliases(cmd) do
    Helpers.apply_if_exported(cmd, :aliases, [], [])
  end

  def validate_execution_environment(cmd, args, options) do
    Helpers.apply_if_exported(cmd, :validate_execution_environment, [args, options], :ok)
  end

  def distribution(cmd, options) do
    Helpers.apply_if_exported(cmd, :distribution, [options], :cli)
  end
end
