# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT

defmodule AshAuthentication.TokenResource.Expunger do
  @moduledoc """
  A `GenServer` which periodically removes expired token revocations.

  Scans all token revocation resources based on their configured expunge
  interval and removes any expired records.

  ```elixir
  defmodule MyApp.Accounts.Token do
    use Ash.Resource,
      extensions: [AshAuthentication.TokenResource],
      domain: MyApp.Accounts

    token do
      expunge_interval 12
    end
  end
  ```

  This GenServer is started by the `AshAuthentication.Supervisor` which should
  be added to your app's supervision tree.
  """

  use GenServer
  alias AshAuthentication.{TokenResource, TokenResource.Actions, TokenResource.Info}

  @doc false
  @spec start_link(any) :: GenServer.on_start()
  def start_link(opts), do: GenServer.start_link(__MODULE__, opts)

  @doc false
  @impl true
  @spec init(any) :: {:ok, map}
  def init(opts) do
    otp_app = Keyword.fetch!(opts, :otp_app)

    resource_states =
      otp_app
      |> Spark.sparks(Ash.Resource)
      |> Stream.filter(&(TokenResource in Spark.extensions(&1)))
      |> Enum.reduce(%{}, fn resource, resources ->
        state =
          resources
          |> Map.get(resource, %{interval: nil, timer: nil})
          |> maybe_update_timer(resource, interval_for(resource))

        Map.put(resources, resource, state)
      end)

    {:ok, %{otp_app: otp_app, resources: resource_states}}
  end

  @doc false
  @impl true
  @spec handle_info(any, map) :: {:noreply, map}
  def handle_info({:expunge, resource}, state) do
    Actions.expunge_expired(resource)

    resource_state =
      state.resources
      |> Map.get(resource)
      |> maybe_update_timer(resource, interval_for(resource))

    {:noreply, %{state | resources: Map.put(state.resources, resource, resource_state)}}
  end

  def handle_info(_, state), do: {:noreply, state}

  defp interval_for(resource) do
    Info.token_expunge_interval!(resource) * 60 * 60 * 1000
  end

  defp maybe_update_timer(state, resource, new_interval)
       when state.interval == new_interval and is_nil(state.timer) do
    {:ok, timer} = :timer.send_interval(new_interval, {:expunge, resource})
    %{state | timer: timer}
  end

  defp maybe_update_timer(state, _resource, new_interval) when state.interval == new_interval,
    do: state

  defp maybe_update_timer(state, resource, new_interval) when is_nil(state.timer) do
    {:ok, timer} = :timer.send_interval(new_interval, {:expunge, resource})
    %{state | interval: new_interval, timer: timer}
  end

  defp maybe_update_timer(state, resource, new_interval) do
    :timer.cancel(state.timer)
    {:ok, timer} = :timer.send_interval(new_interval, {:expunge, resource})
    %{state | interval: new_interval, timer: timer}
  end
end
