# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.AdminAPI.ConfigController do
  use Pleroma.Web, :controller

  alias Pleroma.Config
  alias Pleroma.ConfigDB
  alias Pleroma.Web.Plugs.OAuthScopesPlug

  plug(Pleroma.Web.ApiSpec.CastAndValidate)
  plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update)

  plug(
    OAuthScopesPlug,
    %{scopes: ["admin:read"]}
    when action in [:show, :descriptions]
  )

  action_fallback(Pleroma.Web.AdminAPI.FallbackController)

  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation

  defp translate_descriptions(descriptions, path \\ []) do
    Enum.map(descriptions, fn desc -> translate_item(desc, path) end)
  end

  defp translate_string(str, path, type) do
    Gettext.dpgettext(
      Pleroma.Web.Gettext,
      "config_descriptions",
      Pleroma.Docs.Translator.Compiler.msgctxt_for(path, type),
      str
    )
  end

  defp maybe_put_translated(item, key, path) do
    if item[key] do
      Map.put(
        item,
        key,
        translate_string(
          item[key],
          path ++ [Pleroma.Docs.Translator.Compiler.key_for(item)],
          to_string(key)
        )
      )
    else
      item
    end
  end

  defp translate_item(item, path) do
    item
    |> maybe_put_translated(:label, path)
    |> maybe_put_translated(:description, path)
    |> translate_children(path)
  end

  defp translate_children(%{children: children} = item, path) when is_list(children) do
    item
    |> Map.put(
      :children,
      translate_descriptions(children, path ++ [Pleroma.Docs.Translator.Compiler.key_for(item)])
    )
  end

  defp translate_children(item, _path) do
    item
  end

  def descriptions(conn, _params) do
    descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)

    json(conn, translate_descriptions(descriptions))
  end

  def show(conn, %{only_db: true}) do
    with :ok <- configurable_from_database() do
      configs = Pleroma.Repo.all(ConfigDB)

      render(conn, "index.json", %{
        configs: configs,
        need_reboot: Restarter.Pleroma.need_reboot?()
      })
    end
  end

  def show(conn, _params) do
    with :ok <- configurable_from_database() do
      configs = ConfigDB.get_all_as_keyword()

      merged =
        Config.Holder.default_config()
        |> ConfigDB.merge(configs)
        |> Enum.map(fn {group, values} ->
          Enum.map(values, fn {key, value} ->
            db =
              if configs[group][key] do
                ConfigDB.get_db_keys(configs[group][key], key)
              end

            db_value = configs[group][key]

            merged_value =
              if not is_nil(db_value) and Keyword.keyword?(db_value) and
                   ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
                ConfigDB.merge_group(group, key, value, db_value)
              else
                value
              end

            %ConfigDB{
              group: group,
              key: key,
              value: merged_value
            }
            |> Pleroma.Maps.put_if_present(:db, db)
          end)
        end)
        |> List.flatten()

      render(conn, "index.json", %{
        configs: merged,
        need_reboot: Restarter.Pleroma.need_reboot?()
      })
    end
  end

  def update(%{body_params: %{configs: configs}} = conn, _) do
    with :ok <- configurable_from_database() do
      results =
        configs
        |> Enum.filter(&whitelisted_config?/1)
        |> Enum.map(fn
          %{group: group, key: key, delete: true} = params ->
            ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]})

          %{group: group, key: key, value: value} ->
            ConfigDB.update_or_create(%{group: group, key: key, value: value})
        end)
        |> Enum.reject(fn {result, _} -> result == :error end)

      {deleted, updated} =
        results
        |> Enum.map(fn {:ok, %{key: key, value: value} = config} ->
          Map.put(config, :db, ConfigDB.get_db_keys(value, key))
        end)
        |> Enum.split_with(&(Ecto.get_meta(&1, :state) == :deleted))

      Config.TransferTask.load_and_update_env(deleted, false)

      if not Restarter.Pleroma.need_reboot?() do
        changed_reboot_settings? =
          (updated ++ deleted)
          |> Enum.any?(&Config.TransferTask.pleroma_need_restart?(&1.group, &1.key, &1.value))

        if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
      end

      render(conn, "index.json", %{
        configs: updated,
        need_reboot: Restarter.Pleroma.need_reboot?()
      })
    end
  end

  defp configurable_from_database do
    if Config.get(:configurable_from_database) do
      :ok
    else
      {:error, "You must enable configurable_from_database in your config file."}
    end
  end

  defp whitelisted_config?(group, key) do
    if whitelisted_configs = Config.get(:database_config_whitelist) do
      Enum.any?(whitelisted_configs, fn
        {whitelisted_group} ->
          group == inspect(whitelisted_group)

        {whitelisted_group, whitelisted_key} ->
          group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
      end)
    else
      true
    end
  end

  defp whitelisted_config?(%{group: group, key: key}) do
    whitelisted_config?(group, key)
  end

  defp whitelisted_config?(%{group: group} = config) do
    whitelisted_config?(group, config[:key])
  end
end
