defmodule WhoNeedHelp.Workers.ExpireRequests do
  use Oban.Worker, queue: :maintenance, unique: [period: 55]

  import Ecto.Query
  alias WhoNeedHelp.Help
  alias WhoNeedHelp.Help.HelpRequest
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Tracking
  alias WhoNeedHelp.Trust
  alias WhoNeedHelp.Trust.RateLimiter

  @impl Oban.Worker
  def perform(_job) do
    now = DateTime.utc_now(:second)

    with {:ok, expired} <- expire_available(now, 0),
         {:ok, cleanup} <- Tracking.cleanup_finished_sessions() do
      {pruned_buckets, _} = RateLimiter.prune_expired()
      {pruned_user_tokens, _} = WhoNeedHelp.Accounts.delete_expired_user_tokens(now)

      {:ok,
       %{
         expired: expired,
         tracking_positions_deleted: cleanup.positions_deleted,
         tracking_sessions_ended: cleanup.sessions_ended,
         rate_limit_buckets_pruned: pruned_buckets,
         user_tokens_pruned: pruned_user_tokens
       }}
    end
  end

  # Claim a single row per short transaction. This keeps memory bounded and
  # allows another worker to make progress without waiting for a large backlog
  # lock. The recursion is tail-recursive and stops on the first empty claim.
  defp expire_available(now, expired) do
    result =
      Repo.transact(fn ->
        request =
          HelpRequest
          |> where([request], request.status == :open and request.expires_at <= ^now)
          |> order_by([request], asc: request.expires_at, asc: request.id)
          |> limit(1)
          |> lock("FOR UPDATE SKIP LOCKED")
          |> Repo.one()

        case request do
          nil ->
            {:ok, :empty}

          request ->
            with {:ok, request} <-
                   request
                   |> Ecto.Changeset.change(status: :expired)
                   |> Repo.update(),
                 {:ok, _audit} <-
                   Trust.audit(nil, "request.expired", "request", request.id) do
              {:ok, request}
            end
        end
      end)

    case result do
      {:ok, :empty} ->
        {:ok, expired}

      {:ok, %HelpRequest{} = request} ->
        :ok = Help.notify_request_updated(request.id)
        expire_available(now, expired + 1)

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