defmodule Tarakan.Work do
  @moduledoc """
  The commit-pinned security work queue.

  Tasks and contributions are public from the moment they exist; workflow
  states (proposed, claimed, submitted, accepted) are labels and claim gates,
  never disclosure gates. Only a moderation quarantine restricts content.
  Every transition is checked again while holding database row locks so
  concurrent requests cannot bypass lifecycle invariants.
  """

  import Ecto.Query, warn: false

  alias Tarakan.Accounts
  alias Tarakan.Accounts.{Account, Scope}
  alias Tarakan.Activity
  alias Tarakan.Audit
  alias Tarakan.Credits
  alias Tarakan.GitHub
  alias Tarakan.Policy
  alias Tarakan.Repo
  alias Tarakan.Repositories
  alias Tarakan.Repositories.Repository
  alias Tarakan.Reputation.Stake
  alias Tarakan.Scans
  alias Tarakan.Scans.{FindingCheck, Scan}
  alias Tarakan.Work.{Contribution, ReviewDecision, ReviewTask}

  @claim_seconds 2 * 60 * 60
  @claim_mutation_limit 30
  @claim_mutation_window_seconds 60
  @finding_kinds ~w(code_review threat_model privacy_review business_logic)
  # Cap auto-opened check jobs so spam Reports cannot empty the agent swarm.
  @verify_jobs_per_repo_day 15
  # Queue slots that still block a duplicate of the same work item.
  @active_job_statuses ~w(proposed open claimed submitted changes_requested)
  # Statuses that mean the work landed. `rejected` and `cancelled` are absent
  # on purpose: neither produced a result, so that commit is still unchecked.
  @settled_job_statuses ~w(accepted)

  def subscribe(repository_id) do
    Phoenix.PubSub.subscribe(Tarakan.PubSub, topic(repository_id))
  end

  @doc "Broadcasts a committed task-state refresh to subscribed views."
  def broadcast_refresh(%ReviewTask{} = task) do
    broadcast(task.repository_id, {:review_task_updated, task.id})
    :ok
  end

  @doc "Refreshes every connected task view for a repository listing change."
  def broadcast_repository_refresh(repository_id) when is_integer(repository_id) do
    ReviewTask
    |> where([task], task.repository_id == ^repository_id)
    |> select([task], task.id)
    |> Repo.all()
    |> Enum.each(&broadcast(repository_id, {:review_task_updated, &1}))

    :ok
  end

  def kinds, do: ReviewTask.kinds()
  def capabilities, do: ReviewTask.capabilities()
  def provenances, do: Contribution.provenances()

  @doc "Lists tasks visible to the supplied scope, or only public tasks by default."
  def list_tasks(repository, opts \\ [])

  def list_tasks(%Repository{id: repository_id} = repository, opts) do
    scope = Keyword.get(opts, :scope)

    ReviewTask
    |> where([task], task.repository_id == ^repository_id)
    |> visible_to(scope, repository)
    |> maybe_active_only(Keyword.get(opts, :active_only, false))
    |> order_by(
      [task],
      asc:
        fragment(
          "CASE ? WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 WHEN 'accepted' THEN 2 ELSE 3 END",
          task.status
        ),
      desc: task.inserted_at,
      desc: task.id
    )
    |> maybe_limit(Keyword.get(opts, :limit))
    |> Repo.all()
    |> preload_task()
    |> Enum.map(&expose_task(&1, scope))
  end

  @doc """
  Claimable public jobs across listed repositories, newest first.

  Uses the anonymous visibility rule: listed repository, claimable status
  (`open` / `changes_requested`), public visibility. Safe for logged-out
  visitors and sitemap discovery.
  """
  def list_open_public_tasks(limit \\ 6) when is_integer(limit) and limit > 0 do
    limit = limit |> max(1) |> min(100)
    claimable = ReviewTask.claimable_statuses()

    ReviewTask
    |> join(:inner, [task], repository in assoc(task, :repository))
    |> where(
      [task, repository],
      repository.listing_status == "listed" and task.status in ^claimable and
        task.visibility in ["public_summary", "public"]
    )
    |> order_by(
      [task],
      asc:
        fragment(
          "CASE ? WHEN 'verify_findings' THEN 0 ELSE 1 END",
          task.kind
        ),
      desc: task.inserted_at,
      desc: task.id
    )
    |> limit(^limit)
    |> preload([:repository, :created_by])
    |> Repo.all()
  end

  @doc """
  Compact rows for the sitemap: public claimable jobs on listed repositories.
  """
  def list_indexable_public_tasks(limit \\ 2_000) when is_integer(limit) and limit > 0 do
    limit = min(limit, 5_000)
    claimable = ReviewTask.claimable_statuses()

    ReviewTask
    |> join(:inner, [task], repository in assoc(task, :repository))
    |> where(
      [task, repository],
      repository.listing_status == "listed" and task.status in ^claimable and
        task.visibility in ["public_summary", "public"]
    )
    |> order_by(
      [task],
      asc:
        fragment(
          "CASE ? WHEN 'verify_findings' THEN 0 ELSE 1 END",
          task.kind
        ),
      desc: task.updated_at,
      desc: task.id
    )
    |> limit(^limit)
    |> select([task], %{id: task.id, updated_at: task.updated_at, kind: task.kind})
    |> Repo.all()
  end

  @doc """
  Global Jobs queue for API clients.

  Always includes open / changes_requested public jobs on listed repositories.
  When `account_id:` is set, also includes that account's **active claims**
  (`status=claimed` with unexpired lease) so a client can keep working after
  claim without the job vanishing from the queue.
  """
  def list_open_claimable_tasks(opts \\ []) when is_list(opts) do
    limit =
      opts
      |> Keyword.get(:limit, 50)
      |> max(1)
      |> min(100)

    account_id = Keyword.get(opts, :account_id)
    now = DateTime.utc_now()
    claimable = ReviewTask.claimable_statuses()

    base =
      ReviewTask
      |> join(:inner, [task], repository in assoc(task, :repository))
      |> where(
        [task, repository],
        repository.listing_status == "listed" and
          task.visibility in ["public_summary", "public"]
      )
      |> maybe_filter_task_repository(opts)

    base =
      if is_integer(account_id) do
        where(
          base,
          [task, _repository],
          task.status in ^claimable or
            (task.status == "claimed" and task.claimed_by_id == ^account_id and
               not is_nil(task.claim_expires_at) and task.claim_expires_at > ^now)
        )
      else
        where(base, [task, _repository], task.status in ^claimable)
      end

    base
    # Active claims, then check jobs, then other open work.
    |> order_by(
      [task],
      asc:
        fragment(
          "CASE ? WHEN 'claimed' THEN 0 WHEN 'open' THEN 1 WHEN 'changes_requested' THEN 2 ELSE 3 END",
          task.status
        ),
      asc:
        fragment(
          "CASE ? WHEN 'verify_findings' THEN 0 ELSE 1 END",
          task.kind
        ),
      desc: task.inserted_at,
      desc: task.id
    )
    |> limit(^limit)
    |> Repo.all()
    |> preload_task()
  end

  defp maybe_filter_task_repository(query, opts) do
    query
    |> maybe_filter_min_stars(Keyword.get(opts, :min_stars))
    |> maybe_filter_language(Keyword.get(opts, :language))
    |> maybe_filter_kind(Keyword.get(opts, :kind))
    |> maybe_filter_kinds(Keyword.get(opts, :kinds))
  end

  # A worker declares everything it can do, so it never claims a job whose kind
  # it does not implement and then sits on the lease until it expires. Unknown
  # names are dropped rather than erroring: an old server and a new client must
  # still be able to talk.
  defp maybe_filter_kinds(query, kinds) when is_list(kinds) do
    case Enum.filter(kinds, &(&1 in ReviewTask.kinds())) do
      [] -> query
      known -> where(query, [task, _repository], task.kind in ^known)
    end
  end

  defp maybe_filter_kinds(query, _kinds), do: query

  defp maybe_filter_min_stars(query, min_stars) when is_integer(min_stars) and min_stars > 0 do
    where(query, [_task, repository], repository.stars_count >= ^min_stars)
  end

  defp maybe_filter_min_stars(query, _min_stars), do: query

  defp maybe_filter_language(query, language) when is_binary(language) do
    case String.trim(language) do
      "" ->
        query

      lang ->
        where(query, [_task, repository], ilike(repository.primary_language, ^lang))
    end
  end

  defp maybe_filter_language(query, _language), do: query

  defp maybe_filter_kind(query, kind) when is_binary(kind) do
    case String.trim(kind) do
      "" ->
        query

      value ->
        where(query, [task, _repository], task.kind == ^value)
    end
  end

  defp maybe_filter_kind(query, _kind), do: query

  def get_task!(id), do: ReviewTask |> Repo.get!(id) |> preload_task()

  def get_task(id) do
    case Repo.get(ReviewTask, id) do
      nil -> nil
      task -> preload_task(task)
    end
  end

  @doc "Returns a task only when it is public or visible to the supplied scope."
  def get_visible_task(id, scope \\ nil) do
    case get_task(id) do
      nil -> nil
      task -> if visible_to_scope?(task, scope), do: expose_task(task, scope)
    end
  end

  def change_task(%ReviewTask{} = task \\ %ReviewTask{}, attrs \\ %{}) do
    ReviewTask.creation_changeset(task, attrs)
  end

  def change_contribution(%Contribution{} = contribution \\ %Contribution{}, attrs \\ %{}) do
    Contribution.changeset(contribution, attrs)
  end

  @doc """
  Fills blank job fields with mass-path defaults so proposers need only
  confirm (or tweak) rather than invent title, description, kind, and
  capability every time.

  Defaults:
  - kind → `code_review`
  - capability → `agent`
  - title / description → kind-aware stubs
  - commit_sha → target report's pin when `verify_findings` + target set
  """
  def fill_task_defaults(%Repository{} = repository, attrs) when is_map(attrs) do
    attrs = stringify_keys(attrs)
    kind = present_or(attrs["kind"], "code_review")
    capability = present_or(attrs["capability"], "agent")

    attrs =
      attrs
      |> Map.put("kind", kind)
      |> Map.put("capability", capability)
      |> maybe_pin_commit_from_target_review(kind)

    title = present_or(attrs["title"], default_task_title(repository, kind, attrs))
    description = present_or(attrs["description"], default_task_description(kind, attrs))

    attrs
    |> Map.put("title", title)
    |> Map.put("description", description)
  end

  @doc "Default steward publish reason (≥10 chars) for auto-open and form prefill."
  def default_publish_reason(%ReviewTask{} = task) do
    short = String.slice(task.commit_sha || "", 0, 7)
    kind = task.kind || "code_review"

    "Opening #{kind_label(kind)} job at #{short} for the public queue."
  end

  def default_publish_reason(_), do: "Opening this job for the public contributor queue."

  def create_task(%Repository{} = repository, %Account{} = creator, attrs) do
    create_task(repository, Scope.for_account(creator), attrs)
  end

  def create_task(
        %Repository{} = repository,
        %Scope{account: %Account{} = creator} = scope,
        attrs
      ) do
    repository = Repo.get!(Repository, repository.id)
    attrs = fill_task_defaults(repository, attrs)

    changeset =
      %ReviewTask{}
      |> ReviewTask.creation_changeset(attrs)
      |> Ecto.Changeset.put_change(:repository_id, repository.id)
      |> Ecto.Changeset.put_change(:created_by_id, creator.id)
      |> Ecto.Changeset.put_change(:status, "proposed")

    with :ok <- authorize(scope, :propose_task, repository),
         {:ok, changeset} <- valid_changeset(changeset),
         :ok <- ensure_target_review_on_repository(changeset, repository),
         :ok <- proposal_quota_precheck(scope),
         :ok <- proposal_preflight(scope, repository),
         {:ok, commit} <- verify_commit(repository, changeset) do
      changeset =
        Ecto.Changeset.put_change(
          changeset,
          :commit_committed_at,
          normalize_precision(commit.committed_at)
        )

      result =
        Repo.transaction(fn ->
          locked_repository = lock_repository!(repository.id)
          fresh_scope = lock_scope_account(scope)
          authorize_locked!(fresh_scope, :propose_task, locked_repository)
          enforce_proposal_quota!(fresh_scope)
          # Under the repository row lock so concurrent one-clicks cannot race.
          enforce_no_duplicate_active_job!(changeset, locked_repository.id)

          case Repo.insert(changeset) do
            {:ok, task} ->
              record_audit(fresh_scope, :review_task_created, task, nil)
              preload_task(task)

            {:error, changeset} ->
              Repo.rollback(changeset)
          end
        end)

      case result do
        {:ok, task} ->
          broadcast(repository.id, {:review_task_created, task.id})
          maybe_auto_publish_task(task, scope)

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

  def create_task(%Repository{}, _actor, _attrs), do: {:error, :unauthorized}

  @doc """
  After a report with findings is published, open one agent check job so
  workers can re-dig the same commit independently.

  Skips empty reports and when an active check job already targets the report.
  Failures are logged and do not fail the original submission.
  """
  def maybe_open_agent_verification_job(%Scan{} = scan) do
    scan = Repo.preload(scan, [:repository, :submitted_by])

    cond do
      (scan.findings_count || 0) < 1 ->
        {:ok, :skipped_empty}

      is_nil(scan.submitted_by_id) or is_nil(scan.repository_id) ->
        {:ok, :skipped_incomplete}

      active_verification_job?(scan.id) ->
        {:ok, :skipped_duplicate}

      not verification_job_budget_ok?(scan.repository_id) ->
        {:ok, :skipped_budget}

      true ->
        open_agent_verification_job(scan)
    end
  rescue
    error ->
      require Logger
      Logger.warning("auto verify job failed for scan #{scan.id}: #{Exception.message(error)}")
      {:error, error}
  end

  def maybe_open_agent_verification_job(_), do: {:ok, :skipped}

  defp active_verification_job?(scan_id) when is_integer(scan_id) do
    Repo.exists?(
      from task in ReviewTask,
        where:
          task.target_review_id == ^scan_id and task.kind == "verify_findings" and
            task.status in ^@active_job_statuses
    )
  end

  defp verification_job_budget_ok?(repository_id) when is_integer(repository_id) do
    cutoff = DateTime.add(DateTime.utc_now(), -1, :day)

    count =
      Repo.aggregate(
        from(task in ReviewTask,
          where:
            task.repository_id == ^repository_id and task.kind == "verify_findings" and
              task.inserted_at >= ^cutoff
        ),
        :count
      )

    count < @verify_jobs_per_repo_day
  end

  defp verification_job_budget_ok?(_), do: false

  # A verify job that was accepted at this exact commit is work the record
  # already has. Re-opening it is the spam path: the queue-slot test above frees
  # up the moment the job closes, so without this the same instance becomes
  # eligible again every time a round of checks finishes.
  defp settled_verification_job?(scan_id, commit_sha)
       when is_integer(scan_id) and is_binary(commit_sha) do
    Repo.exists?(
      from task in ReviewTask,
        where:
          task.target_review_id == ^scan_id and task.kind == "verify_findings" and
            task.commit_sha == ^commit_sha and task.status in ^@settled_job_statuses
    )
  end

  defp settled_verification_job?(_scan_id, _commit_sha), do: false

  defp checked_at_commit?(canonical_id, commit_sha)
       when is_integer(canonical_id) and is_binary(commit_sha) do
    Repo.exists?(
      from check in FindingCheck,
        where: check.canonical_finding_id == ^canonical_id and check.commit_sha == ^commit_sha
    )
  end

  defp checked_at_commit?(_canonical_id, _commit_sha), do: false

  @doc """
  Verification history for a set of scans, keyed by scan id.

  `active?` is a job still occupying a queue slot; `settled_commits` are the
  commits a verify job was accepted at. Callers use the second one to stay
  idempotent against the record rather than against the queue - a closed slot
  is not the same thing as work that got done.
  """
  def verification_state_by_scan([]), do: %{}

  def verification_state_by_scan(scan_ids) when is_list(scan_ids) do
    from(task in ReviewTask,
      where: task.kind == "verify_findings" and task.target_review_id in ^scan_ids,
      select: %{scan_id: task.target_review_id, status: task.status, commit_sha: task.commit_sha}
    )
    |> Repo.all()
    |> Enum.group_by(& &1.scan_id)
    |> Map.new(fn {scan_id, tasks} ->
      {scan_id,
       %{
         active?: Enum.any?(tasks, &(&1.status in @active_job_statuses)),
         settled_commits:
           tasks
           |> Enum.filter(&(&1.status in @settled_job_statuses and &1.commit_sha))
           |> MapSet.new(& &1.commit_sha)
       }}
    end)
  end

  @doc """
  Opens a public verify_findings job for one infestation instance map
  (`%{repository_id, scan_id, commit_sha, title, ...}`).

  Skips when the commit already carries a check or a finished verify job, when
  a job is still in flight, or when the repo's daily check-job budget is spent.
  """
  def open_infestation_verification_job(%Scope{account: %Account{} = account} = scope, instance)
      when is_map(instance) do
    scan_id = instance[:scan_id] || instance["scan_id"]
    repository_id = instance[:repository_id] || instance["repository_id"]
    canonical_id = instance[:canonical_finding_id] || instance["canonical_finding_id"]

    with :ok <- Policy.authorize(scope, :moderate),
         true <- is_integer(scan_id),
         %Scan{} = scan <- Repo.get(Scan, scan_id),
         scan <- Repo.preload(scan, :repository),
         true <- scan.repository_id == repository_id do
      cond do
        active_verification_job?(scan.id) ->
          {:ok, :skipped_duplicate}

        settled_verification_job?(scan.id, scan.commit_sha) ->
          {:ok, :skipped_settled}

        checked_at_commit?(canonical_id, scan.commit_sha) ->
          {:ok, :skipped_checked}

        not verification_job_budget_ok?(scan.repository_id) ->
          {:ok, :skipped_budget}

        true ->
          open_infestation_job_for_scan(scan, account, instance)
      end
    else
      nil -> {:error, :not_found}
      false -> {:error, :not_found}
      {:error, reason} -> {:error, reason}
      _ -> {:error, :unauthorized}
    end
  end

  def open_infestation_verification_job(_scope, _instance), do: {:error, :unauthorized}

  defp open_infestation_job_for_scan(%Scan{} = scan, %Account{} = account, instance) do
    repository = scan.repository || Repo.get!(Repository, scan.repository_id)
    short = String.slice(scan.commit_sha || "", 0, 7)
    title = instance[:title] || instance["title"] || "pattern"

    attrs =
      fill_task_defaults(repository, %{
        "commit_sha" => scan.commit_sha,
        "kind" => "verify_findings",
        "capability" => "agent",
        "target_review_id" => scan.id,
        "title" => "Infestation check · #{repository.owner}/#{repository.name} @ #{short}",
        "description" =>
          "Infestation swarm check for pattern: #{title}. " <>
            "Independently re-check findings at the pinned commit. Do not invent new findings."
      })

    now = DateTime.utc_now() |> DateTime.truncate(:microsecond)

    changeset =
      %ReviewTask{}
      |> ReviewTask.creation_changeset(attrs)
      |> Ecto.Changeset.put_change(:repository_id, repository.id)
      |> Ecto.Changeset.put_change(:created_by_id, account.id)
      |> Ecto.Changeset.put_change(:status, "open")
      |> Ecto.Changeset.put_change(:visibility, "public")
      |> Ecto.Changeset.put_change(:published_at, now)
      |> Ecto.Changeset.put_change(:disclosed_at, now)
      |> Ecto.Changeset.put_change(:commit_committed_at, scan.commit_committed_at)

    case Repo.insert(changeset) do
      {:ok, task} ->
        broadcast(repository.id, {:review_task_created, task.id})
        broadcast(repository.id, {:review_task_published, task.id})
        {:ok, preload_task(task)}

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

  @finding_job_kinds ~w(refute_finding reproduce_finding calibrate_severity)

  @doc """
  Opens a public agent job targeting one canonical finding.

  `kind` is one of `refute_finding`, `reproduce_finding` or `calibrate_severity`.
  Refuses to stack a second open job of the same kind on the same finding: these
  are cheap to queue and would otherwise pile up every time somebody clicks.
  """
  def open_finding_job(%Scope{account: %Account{} = account} = scope, finding, kind)
      when kind in @finding_job_kinds do
    finding = Repo.preload(finding, :repository)
    repository = finding.repository || Repo.get!(Repository, finding.repository_id)

    with :ok <- Policy.authorize(scope, :propose_task, repository) do
      if active_finding_job?(finding.id, kind) do
        {:ok, :skipped_duplicate}
      else
        insert_finding_job(finding, repository, account, kind)
      end
    end
  end

  def open_finding_job(_scope, _finding, _kind), do: {:error, :unauthorized}

  defp active_finding_job?(finding_id, kind) do
    Repo.exists?(
      from task in ReviewTask,
        where:
          task.target_canonical_finding_id == ^finding_id and task.kind == ^kind and
            task.status in ^@active_job_statuses
    )
  end

  defp insert_finding_job(finding, repository, %Account{} = account, kind) do
    commit_sha = finding.last_seen_commit_sha
    short = String.slice(commit_sha || "", 0, 7)
    slug = "#{repository.owner}/#{repository.name}"

    attrs =
      fill_task_defaults(repository, %{
        "commit_sha" => commit_sha,
        "kind" => kind,
        "capability" => "agent",
        "target_canonical_finding_id" => finding.id,
        "title" => "#{finding_job_title(kind)} · #{slug} @ #{short}",
        "description" => finding_job_brief(kind)
      })

    now = DateTime.utc_now() |> DateTime.truncate(:microsecond)

    changeset =
      %ReviewTask{}
      |> ReviewTask.creation_changeset(attrs)
      |> Ecto.Changeset.put_change(:repository_id, repository.id)
      |> Ecto.Changeset.put_change(:created_by_id, account.id)
      |> Ecto.Changeset.put_change(:status, "open")
      |> Ecto.Changeset.put_change(:visibility, "public")
      |> Ecto.Changeset.put_change(:published_at, now)
      |> Ecto.Changeset.put_change(:disclosed_at, now)

    case Repo.insert(changeset) do
      {:ok, task} ->
        broadcast(repository.id, {:review_task_created, task.id})
        broadcast(repository.id, {:review_task_published, task.id})
        {:ok, preload_task(task)}

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

  @doc """
  Opens a job asking an agent to write a detector for one code cluster.

  Pinned to the seed finding's repository and commit so the worker has real code
  to write the rule against, and so the rule can be validated before submission.
  """
  def open_rule_job(%Scope{account: %Account{} = account} = scope, code_pattern_key)
      when is_binary(code_pattern_key) do
    seed =
      Repo.one(
        from finding in Tarakan.Scans.CanonicalFinding,
          where: finding.code_pattern_key == ^code_pattern_key,
          order_by: [desc: finding.updated_at],
          limit: 1,
          preload: [:repository]
      )

    cond do
      is_nil(seed) ->
        {:error, :not_found}

      active_rule_job?(code_pattern_key) ->
        {:ok, :skipped_duplicate}

      true ->
        with :ok <- Policy.authorize(scope, :propose_task, seed.repository) do
          insert_rule_job(seed, account, code_pattern_key)
        end
    end
  end

  def open_rule_job(_scope, _key), do: {:error, :unauthorized}

  defp active_rule_job?(code_pattern_key) do
    Repo.exists?(
      from task in ReviewTask,
        where:
          task.target_code_pattern_key == ^code_pattern_key and
            task.kind == "synthesize_rule" and task.status in ^@active_job_statuses
    )
  end

  defp insert_rule_job(seed, %Account{} = account, code_pattern_key) do
    repository = seed.repository
    short = String.slice(code_pattern_key, 0, 16)

    attrs =
      fill_task_defaults(repository, %{
        "commit_sha" => seed.last_seen_commit_sha,
        "kind" => "synthesize_rule",
        "capability" => "agent",
        "target_code_pattern_key" => code_pattern_key,
        "title" => "Write a detector · #{short}",
        "description" =>
          "Write a Semgrep rule that matches this code pattern across the repositories " <>
            "it appears in. Run it against the known instances before submitting and " <>
            "report which ones it matched: a rule that matches nothing is rejected, and " <>
            "one that matches everything is worse than none. Submit to " <>
            "POST /api/patterns/#{code_pattern_key}/rule."
      })

    now = DateTime.utc_now() |> DateTime.truncate(:microsecond)

    changeset =
      %ReviewTask{}
      |> ReviewTask.creation_changeset(attrs)
      |> Ecto.Changeset.put_change(:repository_id, repository.id)
      |> Ecto.Changeset.put_change(:created_by_id, account.id)
      |> Ecto.Changeset.put_change(:status, "open")
      |> Ecto.Changeset.put_change(:visibility, "public")
      |> Ecto.Changeset.put_change(:published_at, now)
      |> Ecto.Changeset.put_change(:disclosed_at, now)

    case Repo.insert(changeset) do
      {:ok, task} ->
        broadcast(repository.id, {:review_task_created, task.id})
        broadcast(repository.id, {:review_task_published, task.id})
        {:ok, preload_task(task)}

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

  defp finding_job_title("refute_finding"), do: "Refute finding"
  defp finding_job_title("reproduce_finding"), do: "Reproduce finding"
  defp finding_job_title("calibrate_severity"), do: "Re-score severity"

  # The brief is the whole point of each kind, so it is written to be
  # adversarial, evidence-bound, or rubric-bound rather than open-ended.
  defp finding_job_brief("refute_finding") do
    "Try to prove this finding wrong at the pinned commit. Look for reasons it " <>
      "does not hold: unreachable code, a guard upstream, a false assumption about " <>
      "inputs. Submit a check marked adversarial. Disputed means you broke it; " <>
      "confirmed means you tried and could not."
  end

  defp finding_job_brief("reproduce_finding") do
    "Produce a reproduction that demonstrates this finding at the pinned commit. " <>
      "Run it yourself and submit the exact artifact you ran plus its output, so " <>
      "somebody else can run the same thing. An outcome without an artifact is " <>
      "rejected."
  end

  defp finding_job_brief("calibrate_severity") do
    "Re-score this finding's severity against the rubric using the real code at " <>
      "the pinned commit. The submitter's claim stays on the record; yours is " <>
      "stored beside it. Justify the score against reachability and impact."
  end

  defp open_agent_verification_job(%Scan{} = scan) do
    repository = scan.repository || Repo.get!(Repository, scan.repository_id)
    short = String.slice(scan.commit_sha || "", 0, 7)

    attrs =
      fill_task_defaults(repository, %{
        "commit_sha" => scan.commit_sha,
        "kind" => "verify_findings",
        "capability" => "agent",
        "target_review_id" => scan.id,
        "title" => "Check report ##{scan.id} · #{repository.owner}/#{repository.name} @ #{short}",
        "description" =>
          "Independently re-check every finding in report ##{scan.id} at the pinned commit. " <>
            "Reproduce or dispute with evidence. Do not invent new findings."
      })

    now = DateTime.utc_now() |> DateTime.truncate(:microsecond)

    changeset =
      %ReviewTask{}
      |> ReviewTask.creation_changeset(attrs)
      |> Ecto.Changeset.put_change(:repository_id, repository.id)
      |> Ecto.Changeset.put_change(:created_by_id, scan.submitted_by_id)
      |> Ecto.Changeset.put_change(:status, "open")
      |> Ecto.Changeset.put_change(:visibility, "public")
      |> Ecto.Changeset.put_change(:published_at, now)
      |> Ecto.Changeset.put_change(:disclosed_at, now)
      |> Ecto.Changeset.put_change(:commit_committed_at, scan.commit_committed_at)

    case Repo.insert(changeset) do
      {:ok, task} ->
        broadcast(repository.id, {:review_task_created, task.id})
        broadcast(repository.id, {:review_task_published, task.id})
        {:ok, preload_task(task)}

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

  defp enforce_no_duplicate_active_job!(changeset, repository_id) do
    kind = Ecto.Changeset.get_field(changeset, :kind)
    commit_sha = Ecto.Changeset.get_field(changeset, :commit_sha)
    target_review_id = Ecto.Changeset.get_field(changeset, :target_review_id)

    query =
      from(task in ReviewTask,
        where:
          task.repository_id == ^repository_id and task.commit_sha == ^commit_sha and
            task.kind == ^kind and task.status in ^@active_job_statuses
      )

    query =
      if kind == "verify_findings" do
        from(task in query, where: task.target_review_id == ^target_review_id)
      else
        query
      end

    if Repo.exists?(query) do
      Repo.rollback(:duplicate_job)
    else
      :ok
    end
  end

  # Stewards/moderators who can publish skip the second approval trip: the job
  # opens immediately with an attributable default publish decision.
  @doc """
  Opens a public job by spending credits instead of holding steward authority.

  This is the cheap half of the credit loop. Credits-funded bounties already
  let someone pay for review, but a bounty is a negotiation: a title, an
  expiry, a claim, a submission, a settlement. Most people who earned credits
  by scanning strangers' code just want their own commit looked at by agents
  that are not theirs, and this is that - debit, publish, done.

  The spend is the authorization. Anyone with the credits may open a job on any
  listed repository, which is also the anti-spam control: credits are minted
  only by verified work, so queue noise costs exactly what it is worth.

  Refunds are not automatic. A job that nobody claims can be closed by a
  moderator, which is when `refund_review_job/2` returns the credits.
  """
  def open_paid_task(
        %Scope{account: %Account{} = account} = scope,
        %Repository{} = repository,
        attrs
      ) do
    cost = paid_task_cost()

    # Standing is checked as well as credits. `:submit_review` deliberately
    # admits new accounts, but this puts work into the public queue on somebody
    # else's repository, which is the bounty bar rather than the report bar.
    with :ok <- paid_task_standing(scope),
         :ok <- authorize(scope, :submit_review, repository),
         {:ok, commit_sha} <- paid_task_commit(repository, attrs) do
      attrs =
        fill_task_defaults(repository, %{
          "commit_sha" => commit_sha,
          "kind" => present_or(attrs["kind"], "code_review"),
          "capability" => "agent",
          "title" => attrs["title"],
          "description" => attrs["description"]
        })

      now = DateTime.utc_now() |> DateTime.truncate(:microsecond)

      Repo.transaction(fn ->
        task =
          %ReviewTask{}
          |> ReviewTask.creation_changeset(attrs)
          |> Ecto.Changeset.put_change(:repository_id, repository.id)
          |> Ecto.Changeset.put_change(:created_by_id, account.id)
          |> Ecto.Changeset.put_change(:status, "open")
          |> Ecto.Changeset.put_change(:visibility, "public")
          |> Ecto.Changeset.put_change(:published_at, now)
          |> Ecto.Changeset.put_change(:disclosed_at, now)
          |> Repo.insert()
          |> case do
            {:ok, task} -> task
            {:error, changeset} -> Repo.rollback(changeset)
          end

        case Credits.debit(account, :spend_review_job, {"review_task", task.id}, cost) do
          {:ok, _entry} -> :ok
          {:error, reason} -> Repo.rollback(reason)
        end

        record_audit(scope, :review_task_created, task, nil)
        preload_task(task)
      end)
      |> case do
        {:ok, task} ->
          broadcast(repository.id, {:review_task_created, task.id})
          broadcast(repository.id, {:review_task_published, task.id})
          {:ok, task}

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

  def open_paid_task(_scope, _repository, _attrs), do: {:error, :unauthorized}

  @doc "Credits required to open one paid job."
  def paid_task_cost do
    :tarakan
    |> Application.get_env(__MODULE__, [])
    |> Keyword.get(:paid_task_cost, 40)
  end

  @doc """
  Returns the credits spent on a job to its opener.

  Used when a paid job is closed without ever being worked. Rides the ledger's
  per-subject unique index, so a repeated refund is a no-op rather than a
  second payout.
  """
  def refund_review_job(%Scope{} = scope, %ReviewTask{} = task) do
    with :ok <- Policy.authorize(scope, :moderate, task) do
      # The ledger is Credits' to read and write; Work only names the spend.
      case Credits.refund(:spend_review_job, {"review_task", task.id}) do
        {:error, :no_such_spend} -> {:error, :not_a_paid_job}
        result -> result
      end
    end
  end

  defp paid_task_standing(%Scope{account_state: "active"}), do: :ok
  defp paid_task_standing(_scope), do: {:error, :unauthorized}

  defp paid_task_commit(repository, attrs) do
    case present_or(attrs["commit_sha"], nil) do
      nil ->
        case Tarakan.RepositoryCode.resolve_default_commit(repository) do
          {:ok, sha} -> {:ok, sha}
          _error -> {:error, :commit_not_found}
        end

      sha ->
        {:ok, sha}
    end
  end

  defp maybe_auto_publish_task(%ReviewTask{} = task, %Scope{} = scope) do
    if Policy.allowed?(scope, :publish_task, task) do
      case publish_task(task, scope, %{"reason" => default_publish_reason(task)}) do
        {:ok, open} -> {:ok, open}
        {:error, _reason} -> {:ok, task}
      end
    else
      {:ok, task}
    end
  end

  def publish_task(%ReviewTask{} = task, %Account{} = account, attrs) do
    publish_task(task, Scope.for_account(account), attrs)
  end

  def publish_task(%ReviewTask{} = task, %Scope{account: %Account{} = account} = scope, attrs) do
    with :ok <- authorize(scope, :publish_task, task),
         :ok <- verify_disclosure_identity(task),
         {:ok, decision_changeset} <- decision_changeset("publish", attrs) do
      transition(task, scope, :review_task_published, :publish_task, fn locked, fresh_scope ->
        # Repo stewards/owners and platform moderators may publish, including Jobs
        # they proposed. Spam is gated by :publish_task policy (not mere membership).
        cond do
          locked.status != "proposed" ->
            Repo.rollback(:invalid_state)

          true ->
            insert_decision!(decision_changeset, locked, account)

            now = DateTime.utc_now()
            repository = promote_pending_repository!(locked.repository, fresh_scope)
            locked = %{locked | repository: repository}

            locked
            |> Ecto.Changeset.change(
              status: "open",
              visibility: "public",
              published_at: now,
              disclosed_at: now,
              disclosed_by_id: account.id
            )
            |> Repo.update!()
        end
      end)
    end
  end

  def publish_task(%ReviewTask{}, _actor, _attrs), do: {:error, :unauthorized}

  def claim_task(%ReviewTask{} = task, %Account{} = account) do
    claim_task(task, Scope.for_account(account))
  end

  def claim_task(%ReviewTask{} = task, %Scope{account: %Account{} = account} = scope) do
    with :ok <- authorize(scope, :claim_task, task),
         :ok <- claim_mutation_preflight(scope) do
      transition(task, scope, :review_task_updated, :claim_task, fn locked, fresh_scope ->
        # Creators may claim and perform their own Jobs (solo/hosted workflows).
        # Independent review of *submitted* work is still enforced elsewhere.
        cond do
          ReviewTask.claim_active?(locked) and locked.claimed_by_id == account.id ->
            {:noop, locked}

          ReviewTask.claim_active?(locked) ->
            Repo.rollback(:already_claimed)

          not claimable_now?(locked) ->
            Repo.rollback(if(ReviewTask.terminal?(locked), do: :closed, else: :not_open))

          active_claim_count(account.id) >= claim_limit(fresh_scope.account) ->
            Repo.rollback(:claim_limit)

          true ->
            now = DateTime.utc_now()

            locked
            |> Ecto.Changeset.change(
              status: "claimed",
              visibility: "public",
              claimed_by_id: account.id,
              claimed_at: now,
              claim_expires_at: DateTime.add(now, @claim_seconds, :second)
            )
            |> Repo.update!()
        end
      end)
    end
  end

  def claim_task(%ReviewTask{}, _actor), do: {:error, :unauthorized}

  def release_task(%ReviewTask{} = task, %Account{} = account) do
    release_task(task, Scope.for_account(account))
  end

  def release_task(%ReviewTask{} = task, %Scope{account: %Account{} = account} = scope) do
    with :ok <- authorize(scope, :claim_task, task),
         :ok <- claim_mutation_preflight(scope) do
      transition(task, scope, :review_task_updated, :claim_task, fn locked, _fresh_scope ->
        cond do
          ReviewTask.terminal?(locked) ->
            Repo.rollback(:closed)

          locked.status == "claimed" and locked.claimed_by_id == account.id ->
            next_status = release_status(locked)

            locked
            |> Ecto.Changeset.change(
              status: next_status,
              visibility: "public",
              claimed_by_id: nil,
              claimed_at: nil,
              claim_expires_at: nil
            )
            |> Repo.update!()

          true ->
            Repo.rollback(:not_claimant)
        end
      end)
    end
  end

  def release_task(%ReviewTask{}, _actor), do: {:error, :unauthorized}

  @doc "Extends an active claim held by the caller for another lease window."
  def renew_claim(%ReviewTask{} = task, %Account{} = account) do
    renew_claim(task, Scope.for_account(account))
  end

  def renew_claim(%ReviewTask{} = task, %Scope{account: %Account{} = account} = scope) do
    with :ok <- authorize(scope, :claim_task, task),
         :ok <- claim_mutation_preflight(scope) do
      transition(task, scope, :review_task_updated, :claim_task, fn locked, _fresh_scope ->
        cond do
          locked.status != "claimed" or locked.claimed_by_id != account.id ->
            Repo.rollback(:not_claimant)

          not ReviewTask.claim_active?(locked) ->
            Repo.rollback(:claim_expired)

          true ->
            locked
            |> Ecto.Changeset.change(
              claim_expires_at: DateTime.add(DateTime.utc_now(), @claim_seconds, :second)
            )
            |> Repo.update!()
        end
      end)
    end
  end

  def renew_claim(%ReviewTask{}, _actor), do: {:error, :unauthorized}

  @doc """
  Submits evidence for independent review; it does not accept the task.

  Finding-kind Requests with a Tarakan Review/Scan Format `document` create a
  linked Review (findings). Legacy prose (`summary` + `evidence`) remains when
  `:request_completion_mode` is `:document_or_legacy_prose` and no document is
  present.
  """
  def submit_task(%ReviewTask{} = task, %Account{} = account, attrs) do
    submit_task(task, Scope.for_account(account), attrs)
  end

  def submit_task(%ReviewTask{} = task, %Scope{account: %Account{} = account} = scope, attrs)
      when is_map(attrs) do
    attrs = stringify_keys(attrs)

    with :ok <- authorize(scope, :submit_contribution, task),
         {:ok, path} <- classify_complete_path(task, attrs) do
      case path do
        :review -> submit_task_with_review(task, scope, account, attrs)
        :verdict -> submit_task_with_verdict(task, scope, account, attrs)
        :legacy_prose -> submit_task_legacy_prose(task, scope, account, attrs)
      end
    end
  end

  def submit_task(%ReviewTask{}, _actor, _attrs), do: {:error, :unauthorized}

  defp classify_complete_path(%ReviewTask{kind: kind} = task, attrs) do
    document? = document_present?(attrs)
    mode = request_completion_mode()
    verdict? = verdict_present?(attrs)

    cond do
      kind == "verify_findings" and not is_nil(task.target_review_id) and verdict? ->
        {:ok, :verdict}

      kind == "verify_findings" and not is_nil(task.target_review_id) and document? ->
        {:error, :document_not_allowed}

      kind == "verify_findings" and not is_nil(task.target_review_id) ->
        {:error, :verdict_required}

      # Grandfathered verify_findings without target_review_id: prose only.
      kind == "verify_findings" and document? ->
        {:error, :document_not_allowed}

      kind == "verify_findings" ->
        {:ok, :legacy_prose}

      kind == "write_fix" and document? ->
        {:error, :document_not_allowed}

      kind == "write_fix" ->
        {:ok, :legacy_prose}

      kind in @finding_kinds and document? ->
        {:ok, :review}

      kind in @finding_kinds and mode == :document_required ->
        {:error, :document_required}

      kind in @finding_kinds ->
        # Dual mode: attempt legacy prose so incomplete fields return changeset errors.
        {:ok, :legacy_prose}

      true ->
        {:error, :invalid_state}
    end
  end

  defp verdict_present?(attrs) do
    case attrs |> Map.get("verdict", "") |> to_string() |> String.downcase() |> String.trim() do
      v when v in ["confirmed", "disputed"] -> true
      _ -> false
    end
  end

  defp document_present?(attrs) do
    case Map.get(attrs, "document") do
      nil -> false
      "" -> false
      doc when is_map(doc) -> true
      doc when is_binary(doc) -> String.trim(doc) != ""
      _other -> false
    end
  end

  defp request_completion_mode do
    Application.get_env(:tarakan, :request_completion_mode, :document_or_legacy_prose)
  end

  defp submit_task_with_verdict(task, scope, _account, attrs) do
    with {:ok, prepared} <- prepare_verdict_attrs(attrs) do
      result =
        Repo.transaction(fn ->
          locked_repository = lock_repository!(task.repository_id)
          fresh_scope = lock_scope_account(scope)
          locked = locked_task!(task.id)

          if locked.repository_id != locked_repository.id, do: Repo.rollback(:invalid_state)
          if is_nil(locked.target_review_id), do: Repo.rollback(:target_review_required)

          locked = %{locked | repository: locked_repository}
          authorize_locked!(fresh_scope, :submit_contribution, locked)
          assert_active_claimant!(locked, fresh_scope.account)

          if not contribution_satisfies_capability?(locked.capability, prepared.provenance) do
            Repo.rollback(:capability_mismatch)
          end

          case Scans.stage_confirmation(
                 fresh_scope,
                 locked.target_review_id,
                 prepared.confirmation_attrs
               ) do
            {:ok, updated_scan, confirmation} ->
              if updated_scan.repository_id != locked.repository_id do
                Repo.rollback(:target_review_mismatch)
              end

              updated =
                locked
                |> Ecto.Changeset.change(
                  status: "submitted",
                  visibility: "public",
                  linked_review_id: updated_scan.id,
                  submitted_at: DateTime.utc_now(),
                  claim_expires_at: nil
                )
                |> Repo.update!()

              record_audit(fresh_scope, :review_task_submitted, updated, locked.status)

              {preload_task(updated), updated_scan, confirmation, fresh_scope.account}

            {:error, reason} ->
              Repo.rollback(reason)
          end
        end)

      case result do
        {:ok, {task, scan, confirmation, actor}} ->
          broadcast(task.repository_id, {:review_task_submitted, task.id})
          Scans.broadcast_verdict_recorded(scan, confirmation, actor)
          {:ok, task}

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

  defp prepare_verdict_attrs(attrs) do
    verdict =
      attrs
      |> Map.get("verdict", "")
      |> to_string()
      |> String.downcase()
      |> String.trim()

    provenance =
      attrs
      |> Map.get("provenance", "human")
      |> to_string()
      |> String.downcase()
      |> String.trim()

    notes =
      attrs
      |> Map.get("notes")
      |> case do
        nil -> Map.get(attrs, "summary")
        n -> n
      end
      |> to_string()
      |> String.trim()

    evidence =
      attrs
      |> Map.get("evidence", "")
      |> to_string()
      |> String.trim()

    cond do
      verdict not in ["confirmed", "disputed"] ->
        {:error, :verdict_required}

      String.length(notes) < 20 ->
        {:error, :verdict_notes_required}

      true ->
        {:ok,
         %{
           provenance: provenance,
           confirmation_attrs: %{
             "verdict" => verdict,
             "provenance" => provenance,
             "notes" => notes,
             "evidence" => if(evidence == "", do: nil, else: evidence)
           }
         }}
    end
  end

  defp submit_task_legacy_prose(task, scope, account, attrs) do
    contribution_changeset = Contribution.changeset(%Contribution{}, attrs)

    with {:ok, contribution_changeset} <- valid_changeset(contribution_changeset) do
      transition(
        task,
        scope,
        :review_task_submitted,
        :submit_contribution,
        fn locked, _fresh_scope ->
          assert_active_claimant!(locked, account)

          if not contribution_satisfies_capability?(
               locked.capability,
               Ecto.Changeset.get_field(contribution_changeset, :provenance)
             ) do
            Repo.rollback(:capability_mismatch)
          end

          version = next_contribution_version(locked.id)

          contribution =
            contribution_changeset
            |> Ecto.Changeset.put_change(:review_task_id, locked.id)
            |> Ecto.Changeset.put_change(:account_id, account.id)
            |> Ecto.Changeset.put_change(:version, version)
            |> Repo.insert!()

          locked
          |> Ecto.Changeset.change(
            status: "submitted",
            visibility: "public",
            submitted_at: DateTime.utc_now(),
            claim_expires_at: nil,
            latest_contribution_id: contribution.id
          )
          |> Repo.update!()
        end
      )
    end
  end

  defp submit_task_with_review(task, scope, account, attrs) do
    repository = task.repository || Repo.get!(Repository, task.repository_id)

    with :ok <- authorize(scope, :submit_review, repository),
         :ok <- Scans.enforce_submission_budget(scope, repository),
         {:ok, prepared} <- prepare_review_submission(task, repository, account, attrs) do
      result =
        Repo.transaction(fn ->
          locked_repository = lock_repository!(repository.id)
          fresh_scope = lock_scope_account(scope)
          locked = locked_task!(task.id)

          if locked.repository_id != locked_repository.id, do: Repo.rollback(:invalid_state)

          locked = %{locked | repository: locked_repository}
          authorize_locked!(fresh_scope, :submit_contribution, locked)

          case Policy.authorize(fresh_scope, :submit_review, locked_repository) do
            :ok -> :ok
            {:error, reason} -> Repo.rollback(reason)
          end

          assert_active_claimant!(locked, fresh_scope.account)

          if not contribution_satisfies_capability?(locked.capability, prepared.provenance) do
            Repo.rollback(:capability_mismatch)
          end

          case Scans.enforce_submission_budget_under_lock(Repo, fresh_scope.account) do
            :ok -> :ok
            {:error, reason} -> Repo.rollback(reason)
          end

          attempt = Scans.count_reviews_for_request(locked.id) + 1

          prompt_version =
            attempt_prompt_version(prepared.client_prompt_version, locked.id, attempt)

          insert_attrs =
            prepared.scan_attrs
            |> Map.put("repository_id", locked.repository_id)
            |> Map.put("submitted_by_id", fresh_scope.account.id)
            |> Map.put("commit_sha", locked.commit_sha)
            |> Map.put("commit_committed_at", locked.commit_committed_at)
            |> Map.put("source_request_id", locked.id)
            |> Map.put("prompt_version", prompt_version)
            |> Map.put("run_id", "request-#{locked.id}-attempt-#{attempt}")
            |> Map.put("visibility", "public")

          review =
            case Scans.stage_review_insert(insert_attrs) do
              {:ok, review} ->
                if review.repository_id != locked.repository_id do
                  Repo.rollback(:invalid_state)
                end

                review

              {:error, %Ecto.Changeset{} = changeset} ->
                Repo.rollback(changeset)
            end

          %Stake{}
          |> Stake.changeset(%{
            scan_id: review.id,
            account_id: fresh_scope.account.id,
            amount: Tarakan.Reputation.default_stake()
          })
          |> Repo.insert!()

          fresh_scope
          |> Audit.event_changeset(:review_submitted, review, %{
            from_state: nil,
            to_state: "#{review.review_status}:#{review.visibility}",
            metadata: %{source_request_id: locked.id}
          })
          |> Repo.insert!()

          case Scans.recalculate_repository_metrics(locked.repository_id) do
            {:ok, _repo} -> :ok
            {:error, reason} -> Repo.rollback(reason)
          end

          updated =
            locked
            |> Ecto.Changeset.change(
              status: "submitted",
              visibility: "public",
              linked_review_id: review.id,
              submitted_at: DateTime.utc_now(),
              claim_expires_at: nil
            )
            |> Repo.update!()

          record_audit(fresh_scope, :review_task_submitted, updated, locked.status)

          {preload_task(updated), review}
        end)

      case result do
        {:ok, {task, review}} ->
          broadcast(task.repository_id, {:review_task_submitted, task.id})
          Scans.broadcast_review_submitted(review)
          _ = maybe_open_agent_verification_job(review)

          _ =
            Tarakan.Infestations.schedule_refresh_for_scan_after_commit(review,
              reason: :assimilate
            )

          {:ok, task}

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

  defp prepare_review_submission(task, repository, _account, attrs) do
    with {:ok, findings_json} <- encode_document(attrs),
         {:ok, _findings} <- Tarakan.Scans.ScanFormat.parse(findings_json),
         {:ok, commit} <- Scans.verify_commit_sha(repository, task.commit_sha) do
      provenance = attrs |> Map.get("provenance", "agent") |> to_string()
      review_kind = task.kind

      if review_kind not in @finding_kinds do
        {:error, :document_not_allowed}
      else
        client_prompt = Map.get(attrs, "prompt_version") || "review"

        scan_attrs = %{
          "commit_sha" => task.commit_sha,
          "provenance" => provenance,
          "review_kind" => review_kind,
          "model" => Map.get(attrs, "model"),
          "prompt_version" => client_prompt,
          "notes" => Map.get(attrs, "summary") || Map.get(attrs, "notes"),
          "findings_json" => findings_json,
          "commit_committed_at" =>
            task.commit_committed_at || normalize_commit_precision(commit.committed_at)
        }

        trial = Scan.submission_changeset(%Scan{}, scan_attrs)

        if trial.valid? do
          {:ok,
           %{
             scan_attrs: scan_attrs,
             provenance: provenance,
             client_prompt_version: client_prompt
           }}
        else
          {:error, %{trial | action: :insert}}
        end
      end
    end
  end

  defp accept_linked_review_allowed?(%ReviewTask{linked_review_id: nil}), do: true

  defp accept_linked_review_allowed?(%ReviewTask{linked_review_id: review_id}) do
    case Repo.get(Scan, review_id) do
      %Scan{visibility: "restricted"} -> false
      %Scan{} -> true
      nil -> false
    end
  end

  defp encode_document(attrs) do
    case Map.get(attrs, "document") do
      doc when is_map(doc) ->
        case Jason.encode(doc) do
          {:ok, json} -> {:ok, json}
          {:error, _} -> {:error, :invalid_document}
        end

      doc when is_binary(doc) ->
        trimmed = String.trim(doc)

        case Jason.decode(trimmed) do
          {:ok, _} -> {:ok, trimmed}
          {:error, _} -> {:error, :invalid_document}
        end

      _other ->
        {:error, :document_required}
    end
  end

  defp attempt_prompt_version(client, request_id, n) do
    suffix = "#req#{request_id}v#{n}"
    prefix_max = max(100 - String.length(suffix), 0)

    client =
      client
      |> to_string()
      |> String.trim()
      |> case do
        "" -> "review"
        other -> other
      end
      |> String.slice(0, prefix_max)

    client <> suffix
  end

  defp normalize_commit_precision(nil), do: nil
  defp normalize_commit_precision(%DateTime{} = dt), do: DateTime.add(dt, 0, :microsecond)

  defp ensure_target_review_on_repository(changeset, %Repository{id: repository_id}) do
    case Ecto.Changeset.get_field(changeset, :target_review_id) do
      nil ->
        :ok

      target_id ->
        case Repo.get(Scan, target_id) do
          %Scan{repository_id: ^repository_id} -> :ok
          %Scan{} -> {:error, :target_review_mismatch}
          nil -> {:error, :target_review_missing}
        end
    end
  end

  defp assert_active_claimant!(locked, %Account{id: account_id}) do
    cond do
      locked.status != "claimed" or locked.claimed_by_id != account_id ->
        Repo.rollback(:not_claimant)

      not ReviewTask.claim_active?(locked) ->
        Repo.rollback(:claim_expired)

      true ->
        :ok
    end
  end

  defp stringify_keys(attrs) when is_map(attrs) do
    Map.new(attrs, fn
      {key, value} when is_atom(key) -> {Atom.to_string(key), value}
      {key, value} when is_binary(key) -> {key, value}
    end)
  end

  def accept_task(%ReviewTask{} = task, actor, attrs),
    do: review_task(task, actor, "accept", "accepted", attrs)

  def request_changes(%ReviewTask{} = task, actor, attrs),
    do: review_task(task, actor, "request_changes", "changes_requested", attrs)

  def reject_task(%ReviewTask{} = task, actor, attrs),
    do: review_task(task, actor, "reject", "rejected", attrs)

  @doc "Discloses an accepted result after a separate attributable decision."
  def disclose_task(%ReviewTask{} = task, %Account{} = account, visibility, attrs) do
    disclose_task(task, Scope.for_account(account), visibility, attrs)
  end

  def disclose_task(
        %ReviewTask{} = task,
        %Scope{account: %Account{} = account} = scope,
        visibility,
        attrs
      )
      when visibility in ["public_summary", "public"] and is_map(attrs) do
    with :ok <- authorize(scope, :disclose_task, task),
         :ok <- verify_disclosure_identity(task),
         {:ok, decision_changeset} <- decision_changeset("disclose", attrs) do
      transition(task, scope, :review_task_disclosed, :disclose_task, fn locked, _fresh_scope ->
        cond do
          locked.status != "accepted" ->
            Repo.rollback(:invalid_state)

          true ->
            insert_decision!(decision_changeset, locked, account)
            now = DateTime.utc_now()

            locked
            |> Ecto.Changeset.change(
              visibility: visibility,
              disclosed_at: now,
              disclosed_by_id: account.id,
              sensitive_data_reviewed_at: if(visibility == "public", do: now),
              sensitive_data_reviewed_by_id: if(visibility == "public", do: account.id)
            )
            |> Repo.update!()
        end
      end)
    end
  end

  def disclose_task(%ReviewTask{}, %Scope{}, _visibility, _attrs),
    do: {:error, :invalid_visibility}

  def disclose_task(%ReviewTask{}, _actor, _visibility, _attrs),
    do: {:error, :unauthorized}

  def cancel_task(%ReviewTask{} = task, %Account{} = account, attrs) do
    cancel_task(task, Scope.for_account(account), attrs)
  end

  def cancel_task(%ReviewTask{} = task, %Scope{account: %Account{} = account} = scope, attrs) do
    with :ok <- authorize(scope, :cancel_task, task),
         {:ok, decision_changeset} <- decision_changeset("cancel", attrs) do
      transition(task, scope, :review_task_cancelled, :cancel_task, fn locked, _fresh_scope ->
        cond do
          ReviewTask.terminal?(locked) ->
            Repo.rollback(:closed)

          locked.status in ["claimed", "submitted"] ->
            Repo.rollback(:active_work)

          true ->
            insert_decision!(decision_changeset, locked, account)

            locked
            |> Ecto.Changeset.change(
              status: "cancelled",
              reviewed_at: DateTime.utc_now(),
              reviewed_by_id: account.id,
              claimed_at: nil,
              claim_expires_at: nil
            )
            |> Repo.update!()
        end
      end)
    end
  end

  def cancel_task(%ReviewTask{}, _actor, _attrs), do: {:error, :unauthorized}

  @doc "Immediately removes a task and its contribution from public view after moderation."
  def quarantine_task(%ReviewTask{} = task, %Scope{} = scope, reason)
      when is_binary(reason) do
    with :ok <- authorize(scope, :moderate, task),
         {:ok, decision_changeset} <- decision_changeset("cancel", %{"reason" => reason}) do
      transition(
        task,
        scope,
        :review_task_quarantined,
        :moderate,
        fn locked, fresh_scope ->
          if locked.status == "cancelled" do
            locked
          else
            insert_decision!(decision_changeset, locked, fresh_scope.account)

            locked
            |> Ecto.Changeset.change(
              status: "cancelled",
              visibility: "restricted",
              reviewed_at: DateTime.utc_now(),
              reviewed_by_id: fresh_scope.account_id,
              claimed_at: nil,
              claim_expires_at: nil,
              completed_at: nil
            )
            |> Repo.update!()
          end
        end
      )
    end
  end

  def quarantine_task(%ReviewTask{}, _actor, _reason), do: {:error, :unauthorized}

  defp review_task(task, %Account{} = account, action, status, attrs) do
    review_task(task, Scope.for_account(account), action, status, attrs)
  end

  defp review_task(
         %ReviewTask{} = task,
         %Scope{account: %Account{} = account} = scope,
         action,
         status,
         attrs
       ) do
    with :ok <- authorize(scope, :review_contribution, task),
         {:ok, decision_changeset} <- decision_changeset(action, attrs) do
      transition(
        task,
        scope,
        event_for_review(status),
        :review_contribution,
        fn locked, _fresh_scope ->
          cond do
            locked.status != "submitted" ->
              Repo.rollback(:invalid_state)

            locked.created_by_id == account.id or locked.claimed_by_id == account.id or
                contribution_by?(locked, account.id) ->
              Repo.rollback(:not_independent)

            status == "accepted" and not accept_linked_review_allowed?(locked) ->
              Repo.rollback(:linked_review_restricted)

            true ->
              insert_decision!(decision_changeset, locked, account)
              now = DateTime.utc_now()

              locked
              |> Ecto.Changeset.change(
                status: status,
                visibility: "public",
                reviewed_at: now,
                reviewed_by_id: account.id,
                completed_at: if(status == "accepted", do: now),
                claimed_at: if(status == "changes_requested", do: nil, else: locked.claimed_at),
                claim_expires_at: nil
              )
              |> Repo.update!()
          end
        end
      )
    end
  end

  defp review_task(%ReviewTask{}, _actor, _action, _status, _attrs),
    do: {:error, :unauthorized}

  defp transition(task, scope, event, policy_action, fun) do
    result =
      Repo.transaction(fn ->
        repository_id = task_repository_id!(task.id)
        repository = lock_repository!(repository_id)
        fresh_scope = lock_scope_account(scope)
        locked = locked_task!(task.id)

        if locked.repository_id != repository.id, do: Repo.rollback(:invalid_state)

        locked = %{locked | repository: repository}
        authorize_locked!(fresh_scope, policy_action, locked)

        case fun.(locked, fresh_scope) do
          {:noop, unchanged} ->
            {:noop, preload_task(unchanged)}

          %ReviewTask{} = updated ->
            record_audit(fresh_scope, event, updated, locked.status)
            updated = preload_task(updated)

            {:updated, updated,
             %{repository_promoted?: repository_promoted?(repository, updated.repository)}}
        end
      end)

    broadcast_result(result, event)
  end

  defp locked_task!(id) do
    Repo.one!(from task in ReviewTask, where: task.id == ^id, lock: "FOR UPDATE")
  end

  defp task_repository_id!(task_id) do
    Repo.one!(from task in ReviewTask, where: task.id == ^task_id, select: task.repository_id)
  end

  defp lock_repository!(repository_id) do
    Repo.one!(
      from repository in Repository,
        where: repository.id == ^repository_id,
        lock: "FOR UPDATE"
    )
  end

  defp lock_scope_account(%Scope{account_id: account_id} = scope) do
    account =
      Repo.one!(from account in Account, where: account.id == ^account_id, lock: "FOR UPDATE")

    case Accounts.refresh_scope_for_account(account, scope) do
      {:ok, fresh_scope} -> fresh_scope
      {:error, reason} -> Repo.rollback(reason)
    end
  end

  defp authorize_locked!(_scope, nil, _task), do: :ok

  defp authorize_locked!(scope, action, task) do
    case authorize(scope, action, task) do
      :ok -> :ok
      {:error, reason} -> Repo.rollback(reason)
    end
  end

  # Visibility is the only content gate; task status is workflow, not
  # disclosure. Only globally listed repositories expose their queue to
  # unrelated viewers - pending and quarantined listings pull it.
  defp visible_to(query, nil, %Repository{listing_status: "listed"}) do
    where(query, [task], task.visibility in ["public_summary", "public"])
  end

  defp visible_to(query, nil, _repository), do: where(query, [task], false)

  defp visible_to(query, %Scope{account: %Account{id: account_id}} = scope, repository) do
    if Scope.token_scope?(scope, "tasks:read") do
      if authorized?(scope, :view_restricted_task, repository) do
        query
      else
        contributed_task_ids =
          from contribution in Contribution,
            where: contribution.account_id == ^account_id,
            select: contribution.review_task_id

        publicly_visible? = repository.listing_status == "listed"

        where(
          query,
          [task],
          (^publicly_visible? and task.visibility in ["public_summary", "public"]) or
            task.created_by_id == ^account_id or
            task.claimed_by_id == ^account_id or task.reviewed_by_id == ^account_id or
            task.id in subquery(contributed_task_ids)
        )
      end
    else
      visible_to(query, nil, repository)
    end
  end

  defp visible_to(query, _scope, _repository), do: visible_to(query, nil, nil)

  defp visible_to_scope?(%ReviewTask{} = task, nil), do: publicly_listed?(task)

  defp visible_to_scope?(%ReviewTask{} = task, %Scope{account: %Account{id: account_id}} = scope) do
    publicly_listed?(task) or
      (Scope.token_scope?(scope, "tasks:read") and
         (task.created_by_id == account_id or task.claimed_by_id == account_id or
            task.reviewed_by_id == account_id or
            Enum.any?(task.contributions, &(&1.account_id == account_id)) or
            authorized?(scope, :view_restricted_task, task)))
  end

  defp visible_to_scope?(_task, _scope), do: false

  defp publicly_listed?(%ReviewTask{repository: %Repository{listing_status: "listed"}} = task),
    do: ReviewTask.public?(task)

  defp publicly_listed?(_task), do: false

  defp expose_task(%ReviewTask{} = task, scope) do
    if full_task_access?(task, scope) do
      task
    else
      public_task(task)
    end
  end

  defp full_task_access?(
         %ReviewTask{} = task,
         %Scope{account: %Account{id: account_id}} = scope
       ) do
    Scope.token_scope?(scope, "tasks:read") and
      (task.created_by_id == account_id or task.claimed_by_id == account_id or
         task.reviewed_by_id == account_id or
         Enum.any?(task.contributions, &(&1.account_id == account_id)) or
         authorized?(scope, :view_restricted_task, task))
  end

  defp full_task_access?(_task, _scope), do: false

  # Contribution exposure follows the task's visibility alone: full public
  # tasks carry their evidence, summaries strip it, restricted tasks carry
  # nothing. Decision records stay private to authorized participants.
  defp public_task(%ReviewTask{visibility: visibility, contribution: contribution} = task) do
    public_contribution =
      case {visibility, contribution} do
        {"public", contribution} when not is_nil(contribution) ->
          contribution

        {"public_summary", contribution} when not is_nil(contribution) ->
          %{contribution | evidence: nil}

        _other ->
          nil
      end

    %{
      task
      | contribution: public_contribution,
        contributions: List.wrap(public_contribution),
        decisions: []
    }
  end

  defp active_claim_count(account_id) do
    now = DateTime.utc_now()

    Repo.aggregate(
      from(task in ReviewTask,
        where:
          task.claimed_by_id == ^account_id and
            ((task.status == "claimed" and task.claim_expires_at > ^now) or
               task.status == "submitted")
      ),
      :count
    )
  end

  defp enforce_proposal_quota!(%Scope{account: %Account{platform_role: role}})
       when role in ["moderator", "admin"],
       do: :ok

  defp enforce_proposal_quota!(%Scope{account: %Account{} = account}) do
    count = proposal_count(account.id)
    limit = proposal_limit(account)

    if count >= limit, do: Repo.rollback(:proposal_limit), else: :ok
  end

  defp proposal_quota_precheck(%Scope{account: %Account{platform_role: role}})
       when role in ["moderator", "admin"],
       do: :ok

  defp proposal_quota_precheck(%Scope{account: %Account{} = account}) do
    if proposal_count(account.id) >= proposal_limit(account),
      do: {:error, :proposal_limit},
      else: :ok
  end

  defp proposal_preflight(%Scope{account: %Account{platform_role: role}}, %Repository{})
       when role in ["moderator", "admin"],
       do: :ok

  defp proposal_preflight(%Scope{platform_role: role}, %Repository{})
       when role in ["moderator", "admin"],
       do: :ok

  defp proposal_preflight(%Scope{account_id: account_id}, %Repository{id: repository_id}) do
    # Burst limits. Exact duplicate jobs are rejected separately under row lock.
    with :ok <- rate_check({:task_proposal, account_id}, 10, 60, :proposal_rate_limited),
         :ok <-
           rate_check(
             {:task_proposal_repository, account_id, repository_id},
             6,
             60,
             :proposal_rate_limited
           ),
         :ok <-
           rate_check(
             {:task_proposal_repository_any, repository_id},
             20,
             60,
             :proposal_rate_limited
           ) do
      :ok
    end
  end

  defp proposal_count(account_id) do
    cutoff = DateTime.add(DateTime.utc_now(), -1, :day)

    Repo.aggregate(
      from(task in ReviewTask,
        where: task.created_by_id == ^account_id and task.inserted_at >= ^cutoff
      ),
      :count
    )
  end

  defp proposal_limit(%Account{trust_tier: "reviewer"}), do: 30
  defp proposal_limit(%Account{state: "active"}), do: 15
  defp proposal_limit(%Account{}), do: 3

  defp claim_mutation_preflight(%Scope{account: %Account{platform_role: role}})
       when role in ["moderator", "admin"],
       do: :ok

  defp claim_mutation_preflight(%Scope{platform_role: role})
       when role in ["moderator", "admin"],
       do: :ok

  defp claim_mutation_preflight(%Scope{account_id: account_id}) do
    rate_check(
      {:task_claim_mutation, account_id},
      @claim_mutation_limit,
      @claim_mutation_window_seconds,
      :claim_rate_limited
    )
  end

  defp rate_check(key, limit, window_seconds, error) do
    case Tarakan.RateLimiter.check(key, limit, window_seconds) do
      :ok -> :ok
      {:error, _reason, _retry_after} -> {:error, error}
    end
  end

  # Concurrent active claims. Operators are not capped.
  defp claim_limit(%Account{platform_role: role}) when role in ["moderator", "admin"],
    do: 1_000_000

  defp claim_limit(%Account{state: "probation"}), do: 1
  defp claim_limit(%Account{}), do: 3

  defp claimable_now?(%ReviewTask{status: "claimed"} = task),
    do: not ReviewTask.claim_active?(task)

  defp claimable_now?(%ReviewTask{} = task), do: ReviewTask.claimable?(task)

  defp release_status(%ReviewTask{latest_contribution_id: nil}), do: "open"
  defp release_status(%ReviewTask{}), do: "changes_requested"

  defp next_contribution_version(task_id) do
    (Repo.one(
       from contribution in Contribution,
         where: contribution.review_task_id == ^task_id,
         select: max(contribution.version)
     ) || 0) + 1
  end

  defp contribution_by?(%ReviewTask{latest_contribution_id: nil}, _account_id), do: false

  defp contribution_by?(%ReviewTask{id: task_id}, account_id) do
    Repo.exists?(
      from contribution in Contribution,
        where: contribution.review_task_id == ^task_id and contribution.account_id == ^account_id
    )
  end

  defp decision_changeset(action, attrs) do
    attrs = attrs |> stringify_keys() |> Map.put("action", action)
    %ReviewDecision{} |> ReviewDecision.changeset(attrs) |> valid_changeset()
  end

  defp insert_decision!(changeset, task, account) do
    changeset
    |> Ecto.Changeset.put_change(:review_task_id, task.id)
    |> Ecto.Changeset.put_change(:account_id, account.id)
    |> Repo.insert!()
  end

  defp event_for_review("accepted"), do: :review_task_accepted
  defp event_for_review("changes_requested"), do: :review_task_changes_requested
  defp event_for_review("rejected"), do: :review_task_rejected

  defp maybe_limit(query, nil), do: query
  defp maybe_limit(query, limit), do: limit(query, ^limit)

  defp maybe_active_only(query, true) do
    where(
      query,
      [task],
      task.status in ["proposed", "open", "claimed", "submitted", "changes_requested"]
    )
  end

  defp maybe_active_only(query, false), do: query

  defp valid_changeset(%Ecto.Changeset{valid?: true} = changeset), do: {:ok, changeset}

  defp valid_changeset(%Ecto.Changeset{} = changeset),
    do: {:error, %{changeset | action: :insert}}

  defp verify_commit(repository, changeset) do
    sha = Ecto.Changeset.get_field(changeset, :commit_sha)

    if Repository.hosted?(repository) do
      verify_hosted_commit(repository, sha)
    else
      verify_github_commit(repository, sha)
    end
  end

  # Tarakan-hosted repos are the source of truth locally - never call GitHub.
  defp verify_hosted_commit(%Repository{} = repository, sha) do
    dir = Tarakan.HostedRepositories.Storage.dir(repository)

    case Tarakan.Git.Local.read_commit(dir, sha) do
      {:ok, commit} -> {:ok, commit}
      :miss -> {:error, :commit_not_found}
    end
  end

  defp verify_github_commit(repository, sha) do
    with {:identity_before, {:ok, _metadata}} <-
           {:identity_before, GitHub.verify_public_identity(repository)},
         {:commit, {:ok, commit}} <-
           {:commit, GitHub.fetch_commit(repository.owner, repository.name, sha)},
         :ok <- ensure_requested_commit(commit, sha),
         {:identity_after, {:ok, _metadata}} <-
           {:identity_after, GitHub.verify_public_identity(repository)} do
      {:ok, commit}
    else
      {:commit, {:error, :not_found}} ->
        {:error, :commit_not_found}

      {step, {:error, reason}} when step in [:identity_before, :identity_after] ->
        if reason in [:not_found, :not_public],
          do: {:error, :identity_changed},
          else: {:error, reason}

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

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

  defp ensure_requested_commit(%{sha: sha}, sha), do: :ok
  defp ensure_requested_commit(_commit, _requested_sha), do: {:error, :commit_mismatch}

  defp verify_disclosure_identity(%ReviewTask{repository_id: repository_id}) do
    case Repo.get(Repository, repository_id) do
      %Repository{} = repository ->
        if Repository.hosted?(repository) do
          if Tarakan.HostedRepositories.Storage.exists?(repository),
            do: :ok,
            else: {:error, :not_found}
        else
          case GitHub.verify_public_identity(repository) do
            {:ok, _metadata} -> :ok
            {:error, reason} -> {:error, reason}
          end
        end

      nil ->
        {:error, :not_found}
    end
  end

  defp normalize_precision(nil), do: nil
  defp normalize_precision(%DateTime{} = datetime), do: DateTime.add(datetime, 0, :microsecond)

  defp contribution_satisfies_capability?("human", provenance),
    do: provenance in ["human", "hybrid"]

  defp contribution_satisfies_capability?("agent", provenance),
    do: provenance in ["agent", "hybrid"]

  defp contribution_satisfies_capability?("hybrid", provenance), do: provenance == "hybrid"

  defp preload_task(task_or_tasks) do
    Repo.preload(
      task_or_tasks,
      [
        :repository,
        :created_by,
        :claimed_by,
        :reviewed_by,
        :disclosed_by,
        :sensitive_data_reviewed_by,
        contribution: :account,
        contributions: :account,
        decisions: :account,
        linked_review: [:findings, :submitted_by],
        target_review: [:findings, :submitted_by],
        target_canonical_finding: []
      ],
      force: true
    )
  end

  defp authorize(scope, action, resource) do
    case Policy.authorize(scope, action, resource) do
      :ok -> :ok
      {:error, _reason} = error -> error
    end
  end

  defp authorized?(scope, action, resource) do
    match?(:ok, authorize(scope, action, resource))
  end

  defp promote_pending_repository!(%Repository{listing_status: "pending"} = repository, scope) do
    promoted =
      repository
      |> Repository.listing_changeset(%{listing_status: "listed"})
      |> Repo.update!()

    case Audit.record(scope, :repository_listing_status_updated, promoted, %{
           from_state: "pending",
           to_state: "listed",
           metadata: %{reason: "independent_review_task_publication"}
         }) do
      {:ok, _event} -> promoted
      {:error, reason} -> Repo.rollback({:audit_failed, reason})
    end
  end

  defp promote_pending_repository!(%Repository{} = repository, _scope), do: repository

  defp repository_promoted?(%Repository{listing_status: "pending"}, %Repository{
         listing_status: "listed"
       }),
       do: true

  defp repository_promoted?(_before, _after), do: false

  defp record_audit(scope, event, task, from_state) do
    case Audit.record(scope, event, task, %{
           from_state: from_state,
           to_state: task.status,
           metadata: %{status: task.status, visibility: task.visibility}
         }) do
      {:ok, _event} -> :ok
      {:error, reason} -> Repo.rollback({:audit_failed, reason})
    end
  end

  defp broadcast_result({:ok, {:updated, task, metadata}}, event) do
    broadcast(task.repository_id, {event, task.id})

    if metadata.repository_promoted? do
      Repositories.broadcast_registration(task.repository)
      Activity.broadcast_registration(task.repository)

      _ =
        Tarakan.Infestations.schedule_refresh_for_repository_after_commit(task.repository_id,
          reason: :listing_change
        )
    end

    {:ok, task}
  end

  defp broadcast_result({:ok, {:noop, task}}, _event), do: {:ok, task}

  defp broadcast_result({:error, reason}, _event), do: {:error, reason}

  defp topic(repository_id), do: "review_tasks:#{repository_id}"

  defp broadcast(repository_id, message) do
    Phoenix.PubSub.broadcast(Tarakan.PubSub, topic(repository_id), message)
  end

  defp present_or(value, default) do
    if blank_string?(value), do: default, else: value
  end

  defp blank_string?(value) when value in [nil, ""], do: true
  defp blank_string?(value) when is_binary(value), do: String.trim(value) == ""
  defp blank_string?(_), do: false

  defp maybe_pin_commit_from_target_review(attrs, "verify_findings") do
    case parse_positive_int(attrs["target_review_id"]) do
      nil ->
        attrs

      target_id ->
        case Repo.get(Scan, target_id) do
          %Scan{commit_sha: sha} when is_binary(sha) and sha != "" ->
            if blank_string?(attrs["commit_sha"]),
              do: Map.put(attrs, "commit_sha", sha),
              else: attrs

          _missing ->
            attrs
        end
    end
  end

  defp maybe_pin_commit_from_target_review(attrs, _kind), do: attrs

  defp parse_positive_int(value) when is_integer(value) and value > 0, do: value

  defp parse_positive_int(value) when is_binary(value) do
    case Integer.parse(String.trim(value)) do
      {int, ""} when int > 0 -> int
      _ -> nil
    end
  end

  defp parse_positive_int(_), do: nil

  defp default_task_title(%Repository{} = repository, kind, attrs) do
    slug = "#{repository.owner}/#{repository.name}"
    short = short_commit_label(attrs["commit_sha"])

    case kind do
      "verify_findings" ->
        report =
          case parse_positive_int(attrs["target_review_id"]) do
            nil -> "report"
            id -> "report ##{id}"
          end

        "Check #{report} · #{slug}" <> if(short != "", do: " @ #{short}", else: "")

      "write_fix" ->
        "Propose a fix · #{slug}" <> if(short != "", do: " @ #{short}", else: "")

      other ->
        "#{kind_label(other)} · #{slug}" <> if(short != "", do: " @ #{short}", else: "")
    end
    |> String.slice(0, 160)
  end

  defp default_task_description(kind, attrs) do
    case kind do
      "code_review" ->
        "Independent security review of this commit. Report concrete defects with file paths, severity, and remediation."

      "threat_model" ->
        "Map assets, trust boundaries, and abuse paths at this commit. Report code-backed weaknesses where assumptions fail."

      "privacy_review" ->
        "Trace personal and sensitive data through collection, storage, logging, and sharing. Report concrete privacy failures."

      "business_logic" ->
        "Test workflow invariants for authorization gaps, replay, races, and quota bypasses. Report exploitable sequences."

      "verify_findings" ->
        report =
          case parse_positive_int(attrs["target_review_id"]) do
            nil -> "the linked report"
            id -> "report ##{id}"
          end

        "Independently confirm or dispute each finding in #{report} against the pinned commit. Record verdicts with evidence."

      "write_fix" ->
        "Propose a minimal, testable patch for the defect described here. Prefer the smallest safe change."

      _other ->
        "Complete this security job against the pinned commit with clear evidence."
    end
  end

  defp short_commit_label(sha) when is_binary(sha) and byte_size(sha) >= 7 do
    String.slice(String.downcase(String.trim(sha)), 0, 7)
  end

  defp short_commit_label(_), do: ""

  defp kind_label("code_review"), do: "Security report"
  defp kind_label("threat_model"), do: "Threat model"
  defp kind_label("privacy_review"), do: "Privacy review"
  defp kind_label("business_logic"), do: "Business logic"
  defp kind_label("verify_findings"), do: "Check report"
  defp kind_label("write_fix"), do: "Write fix"
  defp kind_label(other) when is_binary(other), do: other
  defp kind_label(_), do: "Security job"
end
