## 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 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.  All rights reserved.

defmodule RabbitMQ.CLI.Plugins.Commands.EnableCommand do
  alias RabbitMQ.CLI.Plugins.Helpers, as: PluginHelpers
  alias RabbitMQ.CLI.Core.{DocGuide, Validators}
  import RabbitMQ.CLI.Core.{CodePath, Paths}

  @behaviour RabbitMQ.CLI.CommandBehaviour

  def formatter(), do: RabbitMQ.CLI.Formatters.Plugins

  def merge_defaults(args, opts) do
    {args, Map.merge(%{online: false, offline: false, all: false}, opts)}
  end

  def distribution(%{offline: true}), do: :none
  def distribution(%{offline: false}), do: :cli

  def switches(), do: [online: :boolean, offline: :boolean, all: :boolean]

  def validate([], %{all: false}) do
    {:validation_failure, :not_enough_args}
  end

  def validate([_ | _], %{all: true}) do
    {:validation_failure, {:bad_argument, "Cannot set both --all and a list of plugins"}}
  end

  def validate(_, %{online: true, offline: true}) do
    {:validation_failure, {:bad_argument, "Cannot set both online and offline"}}
  end

  def validate(_, _) do
    :ok
  end

  def validate_execution_environment(args, opts) do
    validators =
      if PluginHelpers.node_is_local?(opts) do
        [
          &require_rabbit_and_plugins/2,
          &PluginHelpers.validate_node_and_mode/2,
          &PluginHelpers.enabled_plugins_file/2,
          &plugins_dir/2
        ]
      else
        [
          &PluginHelpers.validate_node_and_mode/2
        ]
      end

    Validators.chain(validators, [args, opts])
  end

  def run(plugin_names, %{all: all_flag, node: node_name} = opts) do
    all = PluginHelpers.list(opts)

    plugins =
      case all_flag do
        false -> for s <- plugin_names, do: String.to_atom(s)
        true -> PluginHelpers.plugin_names(all)
      end

    case PluginHelpers.validate_plugins(plugins, opts) do
      :ok ->
        enabled = PluginHelpers.read_enabled(opts)
        implicit = :rabbit_plugins.dependencies(false, enabled, all)
        enabled_implicitly = MapSet.difference(MapSet.new(implicit), MapSet.new(enabled))

        plugins_to_set =
          MapSet.union(
            MapSet.new(enabled),
            MapSet.difference(MapSet.new(plugins), enabled_implicitly)
          )

        mode = PluginHelpers.mode(opts)

        case PluginHelpers.set_enabled_plugins(
               MapSet.to_list(plugins_to_set),
               Map.put(opts, :keep_missing_plugins, true)
             ) do
          {:ok, enabled_plugins} ->
            {:stream,
             Stream.concat([
               [:rabbit_plugins.strictly_plugins(enabled_plugins, all)],
               RabbitMQ.CLI.Core.Helpers.defer(fn ->
                 case PluginHelpers.update_enabled_plugins(
                        enabled_plugins,
                        mode,
                        node_name,
                        opts
                      ) do
                   %{set: new_enabled} = result ->
                     newly_enabled = new_enabled -- implicit

                     filter_strictly_plugins(
                       Map.put(
                         result,
                         :enabled,
                         :rabbit_plugins.strictly_plugins(newly_enabled, all)
                       ),
                       all,
                       [:set, :started, :stopped]
                     )

                   other ->
                     other
                 end
               end)
             ])}

          {:error, _} = err ->
            err
        end

      other ->
        other
    end
  end

  use RabbitMQ.CLI.Plugins.ErrorOutput

  def banner([], %{all: true, node: node_name}) do
    "Enabling ALL plugins on node #{node_name}"
  end

  def banner(plugins, %{node: node_name}) do
    ["Enabling plugins on node #{node_name}:" | plugins]
  end

  def usage, do: "enable <plugin1> [ <plugin2>] | --all [--offline] [--online]"

  def usage_additional() do
    [
      ["<plugin1> [ <plugin2>]", "names of plugins to enable separated by a space"],
      ["--online", "contact target node to enable the plugins. Changes are applied immediately."],
      [
        "--offline",
        "update enabled plugins file directly without contacting target node. Changes will be delayed until the node is restarted."
      ],
      [
        "--all",
        "enable all available plugins. Not recommended as some plugins may conflict or otherwise be incompatible!"
      ]
    ]
  end

  def usage_doc_guides() do
    [
      DocGuide.plugins()
    ]
  end

  def help_section(), do: :plugin_management

  def description(), do: "Enables one or more plugins"

  #
  # Implementation
  #

  defp filter_strictly_plugins(map, _all, []) do
    map
  end

  defp filter_strictly_plugins(map, all, [head | tail]) do
    case map[head] do
      nil ->
        filter_strictly_plugins(map, all, tail)

      other ->
        value = :rabbit_plugins.strictly_plugins(other, all)
        filter_strictly_plugins(Map.put(map, head, value), all, tail)
    end
  end
end
