defmodule Tarakan.Fixes.CaptureWorker do
  @moduledoc """
  Captures a fix template as soon as a finding settles as fixed.

  Capture is automatic because it is lossless: a template is a record of what
  was already done, and building one changes nothing on any repository.
  Propagation stays a deliberate act - it opens public jobs on codebases the
  actor does not own - so this worker never propagates.

  Only findings that belong to a cross-repository pattern with somewhere left
  to carry the fix are captured; a fix nobody else needs is not a template.
  """

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

  import Ecto.Query, warn: false

  alias Tarakan.Accounts.{Account, Scope}
  alias Tarakan.Fixes
  alias Tarakan.Repo
  alias Tarakan.Scans.CanonicalFinding

  @doc "Enqueues a capture for a finding that just settled as fixed."
  def enqueue(canonical_finding_id) do
    %{canonical_finding_id: canonical_finding_id}
    |> new()
    |> Oban.insert()
  end

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"canonical_finding_id" => canonical_finding_id}}) do
    with %CanonicalFinding{status: "fixed"} = finding <-
           Repo.get(CanonicalFinding, canonical_finding_id),
         true <- not is_nil(finding.pattern_key),
         true <- pattern_spans_other_repositories?(finding),
         %Account{} = actor <- capture_actor() do
      case Fixes.capture_fix(Scope.for_account(actor), finding) do
        {:ok, _template} -> :ok
        # Nothing to capture is a normal outcome, not a failure to retry.
        {:error, reason} when reason in [:not_fixed, :no_pattern] -> :ok
        {:error, reason} -> {:error, reason}
      end
    else
      _nothing_to_capture -> :ok
    end
  end

  def perform(_job), do: :ok

  defp pattern_spans_other_repositories?(%CanonicalFinding{} = finding) do
    Repo.exists?(
      from other in CanonicalFinding,
        where: other.pattern_key == ^finding.pattern_key,
        where: other.repository_id != ^finding.repository_id,
        where: other.status in ["open", "verified"]
    )
  end

  # Capture is a platform action, so it runs as the platform: the
  # longest-standing admin, falling back to any moderator.
  defp capture_actor do
    Repo.one(
      from account in Account,
        where: account.state == "active",
        where: account.platform_role in ["admin", "moderator"],
        order_by: [
          desc: fragment("? = 'admin'", account.platform_role),
          asc: account.id
        ],
        limit: 1
    )
  end
end
