defmodule Tarakan.Credits.MintWorker do
  @moduledoc """
  Mints credits when a canonical finding's status settles.

  Enqueued by `Tarakan.FindingMemory` whenever `refresh_canonical_checks/1`
  changes a canonical's status. Every credit rides the ledger's unique index,
  so re-running a job (or a status oscillating back to a settled state) never
  double-mints.
  """

  use Oban.Worker, queue: :credits, max_attempts: 5

  import Ecto.Query, warn: false

  alias Tarakan.Accounts.Account
  alias Tarakan.Credits
  alias Tarakan.Repo
  alias Tarakan.Scans.{CanonicalFinding, FindingCheck}

  @doc "Enqueues a mint job for a settled status transition."
  def enqueue(canonical_finding_id, previous_status, new_status) do
    %{
      canonical_finding_id: canonical_finding_id,
      event: "status_settled",
      previous_status: previous_status,
      new_status: new_status
    }
    |> new()
    |> Oban.insert()
  end

  @impl Oban.Worker
  def perform(%Oban.Job{
        args: %{
          "canonical_finding_id" => canonical_finding_id,
          "previous_status" => previous_status,
          "new_status" => new_status
        }
      }) do
    case Repo.get(CanonicalFinding, canonical_finding_id) do
      %CanonicalFinding{} = canonical ->
        mint_for_transition(canonical, previous_status, new_status)

      nil ->
        :ok
    end
  end

  def perform(_job), do: :ok

  defp mint_for_transition(canonical, previous_status, new_status) do
    with :ok <- mint_finder(canonical, previous_status, new_status),
         :ok <- mint_matching_checks(canonical, new_status),
         :ok <- mint_fix_settlements(canonical, new_status) do
      :ok
    end
  end

  # The finder earns once, the first time the finding reaches verified.
  defp mint_finder(canonical, previous_status, "verified") when previous_status != "verified" do
    case finder_account(canonical.id) do
      %Account{} = account ->
        mint(account, :mint_finding_verified, {"canonical_finding", canonical.id})

      nil ->
        :ok
    end
  end

  defp mint_finder(_canonical, _previous_status, _new_status), do: :ok

  # Checkers whose verdict matches the settled outcome earn for being right.
  defp mint_matching_checks(canonical, new_status)
       when new_status in ~w(verified fixed disputed) do
    verdict =
      case new_status do
        "verified" -> "confirmed"
        "fixed" -> "fixed"
        "disputed" -> "disputed"
      end

    canonical.id
    |> checks_with_accounts(verdict)
    |> mint_all(fn {check, account} ->
      mint(account, :mint_check_correct, {"finding_check", check.id})
    end)
  end

  defp mint_matching_checks(_canonical, _new_status), do: :ok

  # Settling a fix pays an extra reward to the accounts that attested the fix.
  defp mint_fix_settlements(canonical, "fixed") do
    canonical.id
    |> checks_with_accounts("fixed")
    |> mint_all(fn {check, account} ->
      mint(account, :mint_fix_settled, {"finding_check", check.id})
    end)
  end

  defp mint_fix_settlements(_canonical, _new_status), do: :ok

  # Shared with the public record so the account credited and the account
  # displayed as first finder can never diverge.
  defp finder_account(canonical_id) do
    case Tarakan.FindingMemory.first_finder(canonical_id) do
      %{account_id: account_id} -> Repo.get(Account, account_id)
      nil -> nil
    end
  end

  defp checks_with_accounts(canonical_id, verdict) do
    Repo.all(
      from check in FindingCheck,
        join: account in Account,
        on: account.id == check.account_id,
        where: check.canonical_finding_id == ^canonical_id and check.verdict == ^verdict,
        select: {check, account}
    )
  end

  defp mint_all(pairs, fun) do
    Enum.reduce_while(pairs, :ok, fn pair, :ok ->
      case fun.(pair) do
        :ok -> {:cont, :ok}
        {:error, reason} -> {:halt, {:error, reason}}
      end
    end)
  end

  defp mint(account, kind, subject) do
    case Credits.credit(account, kind, subject) do
      {:ok, _entry} -> :ok
      {:error, :already_minted} -> :ok
      {:error, reason} -> {:error, reason}
    end
  end
end
