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

defmodule Pleroma.Migrators.Support.BaseMigratorState do
  @moduledoc """
  Base background migrator state functionality.
  """

  @callback data_migration() :: Pleroma.DataMigration.t()

  defmacro __using__(_opts) do
    quote do
      use Agent

      alias Pleroma.DataMigration

      @behaviour Pleroma.Migrators.Support.BaseMigratorState
      @reg_name {:global, __MODULE__}

      def start_link(_) do
        Agent.start_link(fn -> load_state_from_db() end, name: @reg_name)
      end

      def data_migration, do: raise("data_migration/0 is not implemented")
      defoverridable data_migration: 0

      defp load_state_from_db do
        data_migration = data_migration()

        data =
          if data_migration do
            Map.new(data_migration.data, fn {k, v} -> {String.to_atom(k), v} end)
          else
            %{}
          end

        %{
          data_migration_id: data_migration && data_migration.id,
          data: data
        }
      end

      def persist_to_db do
        %{data_migration_id: data_migration_id, data: data} = state()

        if data_migration_id do
          DataMigration.update_one_by_id(data_migration_id, data: data)
        else
          {:error, :nil_data_migration_id}
        end
      end

      def reset do
        %{data_migration_id: data_migration_id} = state()

        with false <- is_nil(data_migration_id),
             :ok <-
               DataMigration.update_one_by_id(data_migration_id,
                 state: :pending,
                 data: %{}
               ) do
          reinit()
        else
          true -> {:error, :nil_data_migration_id}
          e -> e
        end
      end

      def reinit do
        Agent.update(@reg_name, fn _state -> load_state_from_db() end)
      end

      def state do
        Agent.get(@reg_name, & &1)
      end

      def get_data_key(key, default \\ nil) do
        get_in(state(), [:data, key]) || default
      end

      def put_data_key(key, value) do
        _ = persist_non_data_change(key, value)

        Agent.update(@reg_name, fn state ->
          put_in(state, [:data, key], value)
        end)
      end

      def increment_data_key(key, increment \\ 1) do
        Agent.update(@reg_name, fn state ->
          initial_value = get_in(state, [:data, key]) || 0
          updated_value = initial_value + increment
          put_in(state, [:data, key], updated_value)
        end)
      end

      defp persist_non_data_change(:state, value) do
        with true <- get_data_key(:state) != value,
             true <- value in Pleroma.DataMigration.State.__valid_values__(),
             %{data_migration_id: data_migration_id} when not is_nil(data_migration_id) <-
               state() do
          DataMigration.update_one_by_id(data_migration_id, state: value)
        else
          false -> :ok
          _ -> {:error, :nil_data_migration_id}
        end
      end

      defp persist_non_data_change(_, _) do
        nil
      end

      def data_migration_id, do: Map.get(state(), :data_migration_id)
    end
  end
end
