defmodule Tarakan.Market.Bounty do
  @moduledoc """
  A contract (a "bounty" internally): a funded reward for security work on a public target.

  Exactly one target reference must be set, matching `target_type`
  (`repository_id`, `pattern_key`, or `canonical_finding_id`). The shape is
  enforced at the changeset level (see `validate_target/1`); the database only
  constrains the enum-like columns.

  Funding is either fiat (Stripe Checkout escrow, `amount_cents`) or platform
  credits (`credit_amount`, debited from the sponsor at creation). The
  platform `take_rate` is snapshotted at creation so later config changes
  never rewrite open offers.
  """

  use Ecto.Schema
  import Ecto.Changeset

  alias Tarakan.Accounts.Account
  alias Tarakan.Market.BountyClaim
  alias Tarakan.Repositories.Repository
  alias Tarakan.Scans.CanonicalFinding

  @target_types ~w(repository infestation finding)
  @fundings ~w(fiat credits)
  @statuses ~w(pending_funding open claimed fulfilled payout_pending paid cancelled expired)

  schema "bounties" do
    field :public_id, Ecto.UUID, autogenerate: true
    field :target_type, :string
    field :pattern_key, :string
    field :title, :string
    field :description, :string
    field :funding, :string
    field :amount_cents, :integer
    field :credit_amount, :integer
    field :take_rate, :decimal
    field :status, :string, default: "pending_funding"
    field :stripe_checkout_session_id, :string
    field :expires_at, :utc_datetime_usec

    belongs_to :sponsor_account, Account
    belongs_to :repository, Repository
    belongs_to :canonical_finding, CanonicalFinding
    belongs_to :winner_account, Account

    has_many :claims, BountyClaim

    timestamps(type: :utc_datetime_usec)
  end

  def target_types, do: @target_types
  def fundings, do: @fundings
  def statuses, do: @statuses

  # Only Tarakan.Market builds the attrs map for this changeset - public form
  # params never reach it directly - so programmatic fields (target refs,
  # take_rate, status) are safe to cast, and they must be cast so the
  # validations below can see them.
  @doc false
  def changeset(bounty, attrs) do
    bounty
    |> cast(attrs, [
      :target_type,
      :repository_id,
      :pattern_key,
      :canonical_finding_id,
      :title,
      :description,
      :funding,
      :amount_cents,
      :credit_amount,
      :take_rate,
      :status,
      :expires_at
    ])
    |> validate_required([:target_type, :title, :description, :funding, :take_rate])
    |> validate_inclusion(:target_type, @target_types)
    |> validate_inclusion(:funding, @fundings)
    |> validate_inclusion(:status, @statuses)
    |> validate_length(:title, max: 200)
    |> validate_length(:description, max: 5_000)
    |> validate_amount()
    |> validate_target()
    |> unique_constraint(:public_id)
    |> foreign_key_constraint(:repository_id)
    |> foreign_key_constraint(:canonical_finding_id)
    |> check_constraint(:target_type, name: :bounties_target_type_must_be_valid)
    |> check_constraint(:funding, name: :bounties_funding_must_be_valid)
    |> check_constraint(:status, name: :bounties_status_must_be_valid)
  end

  defp validate_amount(changeset) do
    case get_field(changeset, :funding) do
      "fiat" ->
        changeset
        |> validate_required([:amount_cents], message: "must be at least $1.00")
        |> validate_number(:amount_cents,
          greater_than_or_equal_to: 100,
          message: "must be at least $1.00"
        )

      "credits" ->
        changeset
        |> validate_required([:credit_amount])
        |> validate_number(:credit_amount, greater_than: 0)

      _other ->
        changeset
    end
  end

  # Exactly one target reference, and it must match target_type. The database
  # cannot express the type/ref pairing, so this stays changeset-only.
  defp validate_target(changeset) do
    type = get_field(changeset, :target_type)

    refs = [
      repository_id: get_field(changeset, :repository_id),
      pattern_key: present(get_field(changeset, :pattern_key)),
      canonical_finding_id: get_field(changeset, :canonical_finding_id)
    ]

    set = for {key, value} <- refs, not is_nil(value), do: key

    expected =
      case type do
        "repository" -> [:repository_id]
        "infestation" -> [:pattern_key]
        "finding" -> [:canonical_finding_id]
        _other -> []
      end

    if set == expected do
      changeset
    else
      add_error(changeset, :target_type, "must reference exactly one #{type} target")
    end
  end

  defp present(nil), do: nil
  defp present(""), do: nil
  defp present(value), do: value
end
