## 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.Plugins.Helpers do
  import RabbitMQ.CLI.Core.DataCoercion
  import RabbitCommon.Records
  import RabbitMQ.CLI.Core.Platform, only: [path_separator: 0]
  import RabbitMQ.CLI.Core.{CodePath, Paths}
  alias RabbitMQ.CLI.Core.{Config, Validators}

  def mode(opts) do
    %{online: online, offline: offline} = opts

    case {online, offline} do
      {true, false} -> :online
      {false, true} -> :offline
      {false, false} -> :best_effort
    end
  end

  def can_set_plugins_with_mode(args, opts) do
    case mode(opts) do
      # can always set offline plugins list
      :offline ->
        :ok

      # assume online mode, fall back to offline mode in case of errors
      :best_effort ->
        :ok

      # a running node is required
      :online ->
        Validators.chain(
          [&Validators.node_is_running/2, &Validators.rabbit_is_running/2],
          [args, opts]
        )
    end
  end

  def list(opts) do
    case plugins_dir(opts) do
      {:ok, dir} ->
        add_all_to_path(dir)
        :lists.usort(:rabbit_plugins.list(to_charlist(dir)))

      {:error, _} ->
        []
    end
  end

  def list_names(opts) do
    list(opts) |> plugin_names
  end

  def read_enabled(opts) do
    case enabled_plugins_file(opts) do
      {:ok, enabled} ->
        :rabbit_plugins.read_enabled(to_charlist(enabled))

      # Existence of enabled_plugins_file should be validated separately
      {:error, :no_plugins_file} ->
        []
    end
  end

  def enabled_plugins_file(opts) do
    case Config.get_option(:enabled_plugins_file, opts) do
      nil -> {:error, :no_plugins_file}
      file -> {:ok, file}
    end
  end

  def enabled_plugins_file(_, opts) do
    enabled_plugins_file(opts)
  end

  def set_enabled_plugins(plugins, opts) do
    plugin_atoms = :lists.usort(for plugin <- plugins, do: to_atom(plugin))
    _ = require_rabbit_and_plugins(opts)
    {:ok, plugins_file} = enabled_plugins_file(opts)
    write_enabled_plugins(plugin_atoms, plugins_file, opts)
  end

  @spec update_enabled_plugins(
          [atom()],
          :online | :offline | :best_effort,
          node(),
          map()
        ) :: map() | {:error, any()}
  def update_enabled_plugins(enabled_plugins, mode, node_name, opts) do
    {:ok, plugins_file} = enabled_plugins_file(opts)

    case mode do
      :online ->
        case update_enabled_plugins(node_name, plugins_file) do
          {:ok, started, stopped} ->
            %{
              mode: :online,
              started: Enum.sort(started),
              stopped: Enum.sort(stopped),
              set: Enum.sort(enabled_plugins)
            }

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

      :best_effort ->
        case update_enabled_plugins(node_name, plugins_file) do
          {:ok, started, stopped} ->
            %{
              mode: :online,
              started: Enum.sort(started),
              stopped: Enum.sort(stopped),
              set: Enum.sort(enabled_plugins)
            }

          {:error, :offline} ->
            %{mode: :offline, set: Enum.sort(enabled_plugins)}

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

      :offline ->
        %{mode: :offline, set: Enum.sort(enabled_plugins)}
    end
  end

  def validate_plugins(requested_plugins, opts) do
    ## Maybe check all plugins
    plugins =
      case opts do
        %{all: true} -> plugin_names(list(opts))
        _ -> requested_plugins
      end

    all = list(opts)
    deps = :rabbit_plugins.dependencies(false, plugins, all)

    deps_plugins =
      Enum.filter(all, fn plugin ->
        name = plugin_name(plugin)
        Enum.member?(deps, name)
      end)

    case :rabbit_plugins.validate_plugins(deps_plugins) do
      {_, []} -> :ok
      {_, invalid} -> {:error, :rabbit_plugins.format_invalid_plugins(invalid)}
    end
  end

  def plugin_name(plugin) when is_binary(plugin) do
    plugin
  end

  def plugin_name(plugin) when is_atom(plugin) do
    Atom.to_string(plugin)
  end

  def plugin_name(plugin) do
    plugin(name: name) = plugin
    name
  end

  def plugin_names(plugins) do
    for plugin <- plugins, do: plugin_name(plugin)
  end

  def comma_separated_names(plugins) do
    Enum.join(plugin_names(plugins), ", ")
  end

  #
  # Implementation
  #

  defp to_list(str) when is_binary(str) do
    :erlang.binary_to_list(str)
  end

  defp to_list(lst) when is_list(lst) do
    lst
  end

  defp to_list(atm) when is_atom(atm) do
    to_list(Atom.to_string(atm))
  end

  defp write_enabled_plugins(plugins, plugins_file, opts) do
    all = list(opts)
    all_plugin_names = Enum.map(all, &plugin_name/1)
    missing = MapSet.difference(MapSet.new(plugins), MapSet.new(all_plugin_names))

    case Enum.empty?(missing) do
      true ->
        case :rabbit_file.write_term_file(to_charlist(plugins_file), [plugins]) do
          :ok ->
            all_enabled = :rabbit_plugins.dependencies(false, plugins, all)
            {:ok, Enum.sort(all_enabled)}

          {:error, reason} ->
            {:error, {:cannot_write_enabled_plugins_file, plugins_file, reason}}
        end

      false ->
        {:error, {:plugins_not_found, Enum.to_list(missing)}}
    end
  end

  defp update_enabled_plugins(node_name, plugins_file) do
    case :rabbit_misc.rpc_call(node_name, :rabbit_plugins, :ensure, [to_list(plugins_file)]) do
      {:badrpc, :nodedown} -> {:error, :offline}
      {:error, :rabbit_not_running} -> {:error, :offline}
      {:ok, start, stop} -> {:ok, start, stop}
      {:error, _} = err -> err
    end
  end

  defp add_all_to_path(plugins_directories) do
    directories = String.split(to_string(plugins_directories), path_separator())

    Enum.each(directories, fn directory ->
      with {:ok, subdirs} <- File.ls(directory) do
        for subdir <- subdirs do
          Path.join([directory, subdir, "ebin"])
          |> Code.append_path()
        end
      end
    end)

    :ok
  end
end
