defmodule Tarakan.Scans.FindingCheck do
  @moduledoc "An independent check on one canonical finding at one commit."

  use Ecto.Schema
  import Ecto.Changeset

  @verdicts ~w(confirmed disputed fixed)
  @provenances ~w(agent human hybrid)
  @repro_statuses ~w(pass fail inconclusive)

  schema "canonical_finding_checks" do
    field :commit_sha, :string
    field :verdict, :string
    field :provenance, :string, default: "human"
    field :notes, :string
    field :evidence, :string
    field :client_ip_hash, :binary
    # An adversarial check was asked to break the finding. "confirmed" here means
    # the attack was attempted and failed, which is a stronger signal than an
    # ordinary agreement.
    field :adversarial, :boolean, default: false
    # Client-attested reproduction: the worker ran the PoC on its own machine.
    # The artifact and runtime are kept verbatim so the same thing can be
    # re-executed later, by a person or by a server-side runner.
    field :repro_status, :string
    field :repro_runtime, :string
    field :repro_artifact, :string
    field :repro_transcript, :string

    belongs_to :canonical_finding, Tarakan.Scans.CanonicalFinding
    belongs_to :scan_finding, Tarakan.Scans.Finding
    belongs_to :account, Tarakan.Accounts.Account

    timestamps(type: :utc_datetime_usec)
  end

  def verdicts, do: @verdicts
  def repro_statuses, do: @repro_statuses

  @doc false
  def changeset(check, attrs) do
    check
    |> cast(attrs, [
      :commit_sha,
      :verdict,
      :provenance,
      :notes,
      :evidence,
      :client_ip_hash,
      :adversarial,
      :repro_status,
      :repro_runtime,
      :repro_artifact,
      :repro_transcript
    ])
    |> validate_required([:commit_sha, :verdict, :provenance, :notes])
    |> validate_format(:commit_sha, ~r/^[0-9a-f]{40}$/)
    |> validate_inclusion(:verdict, @verdicts)
    |> validate_inclusion(:provenance, @provenances)
    |> validate_length(:notes, min: 20, max: 2000)
    |> validate_length(:evidence, max: 10_000)
    |> validate_inclusion(:repro_status, @repro_statuses)
    |> validate_length(:repro_artifact, max: 20_000)
    |> validate_length(:repro_transcript, max: 40_000)
    |> validate_length(:repro_runtime, max: 40)
    |> validate_repro_artifact()
    |> unique_constraint([:canonical_finding_id, :account_id, :commit_sha],
      name: :canonical_finding_checks_unique_actor_commit_index,
      message: "you already checked this finding at this commit"
    )
    |> check_constraint(:verdict, name: :canonical_finding_checks_verdict_must_be_valid)
    |> check_constraint(:repro_status, name: :canonical_finding_checks_repro_status_valid)
    |> check_constraint(:repro_artifact, name: :canonical_finding_checks_repro_needs_artifact)
    |> check_constraint(:provenance, name: :canonical_finding_checks_provenance_must_be_valid)
  end

  # A reproduction outcome without the thing that produced it is unfalsifiable,
  # which defeats the point of asking for one.
  defp validate_repro_artifact(changeset) do
    status = get_field(changeset, :repro_status)
    artifact = get_field(changeset, :repro_artifact)

    if not is_nil(status) and (is_nil(artifact) or String.trim(artifact) == "") do
      add_error(changeset, :repro_artifact, "is required when reporting a reproduction")
    else
      changeset
    end
  end
end
