defmodule Aprsme.MigrationLock do
  @moduledoc """
  Provides distributed locking for database migrations using PostgreSQL advisory locks.
  This ensures only one node runs migrations at a time in a clustered environment.
  """

  require Logger

  # Use a consistent lock ID for migrations
  # This is a 64-bit integer that's unlikely to conflict with other locks
  @migration_lock_id 8_764_293_847_291

  @doc """
  Attempts to acquire an exclusive advisory lock and run migrations.
  Returns :ok if migrations were run successfully or :skipped if another node holds the lock.
  """
  def with_lock(repo, fun) do
    case acquire_lock(repo) do
      :ok ->
        try do
          Logger.info("Acquired migration lock, running migrations...")
          result = fun.()
          Logger.info("Migrations completed successfully")
          result
        after
          release_lock(repo)
          Logger.info("Released migration lock")
        end

      :locked ->
        Logger.info("Another node is running migrations, waiting...")
        wait_for_migrations(repo)
        :skipped
    end
  end

  defp acquire_lock(repo) do
    # Try to acquire an exclusive advisory lock (non-blocking)
    query = "SELECT pg_try_advisory_lock($1)"

    case repo.query(query, [@migration_lock_id]) do
      {:ok, %{rows: [[true]]}} ->
        :ok

      {:ok, %{rows: [[false]]}} ->
        :locked

      error ->
        Logger.error("Failed to acquire migration lock: #{inspect(error)}")
        :locked
    end
  end

  defp release_lock(repo) do
    query = "SELECT pg_advisory_unlock($1)"

    case repo.query(query, [@migration_lock_id]) do
      {:ok, %{rows: [[true]]}} ->
        :ok

      error ->
        Logger.error("Failed to release migration lock: #{inspect(error)}")
        error
    end
  end

  defp wait_for_migrations(repo) do
    # Wait for up to 60 seconds for migrations to complete
    wait_for_migrations(repo, 60)
  end

  defp wait_for_migrations(_repo, 0) do
    Logger.warning("Timeout waiting for migrations to complete")
    :timeout
  end

  defp wait_for_migrations(repo, retries) do
    # Check if lock is still held
    query = "SELECT pg_try_advisory_lock($1)"

    case repo.query(query, [@migration_lock_id]) do
      {:ok, %{rows: [[true]]}} ->
        # We got the lock, which means migrations are done
        release_lock(repo)
        Logger.info("Migrations completed on another node")
        :ok

      {:ok, %{rows: [[false]]}} ->
        # Lock is still held, wait and retry
        Process.sleep(1_000)
        wait_for_migrations(repo, retries - 1)

      error ->
        Logger.error("Error checking migration lock: #{inspect(error)}")
        :error
    end
  end
end
