defmodule Tarakan.Fixes.FixTemplate do
  @moduledoc """
  A settled fix, captured so the same pattern can be closed elsewhere.

  Cross-repository patterns mean the same class of bug lives in codebases that
  never share a line of code. Once one of them is fixed, the record holds
  something no scanner produces on its own: a worked example of the remedy,
  pinned to the commit that applied it. That example is the template other
  repositories carrying the pattern get to start from.

  The diff is captured when the source repository is mirrored and the fixing
  commit is known; otherwise the template carries only the narrative summary
  drawn from the check evidence that settled the fix.
  """

  use Ecto.Schema
  import Ecto.Changeset

  alias Tarakan.Accounts.Account
  alias Tarakan.Fixes.FixPropagation
  alias Tarakan.Repositories.Repository
  alias Tarakan.Scans.CanonicalFinding

  @max_diff_bytes 60_000

  schema "fix_templates" do
    field :pattern_key, :string
    field :fix_commit_sha, :string
    field :title, :string
    field :summary, :string
    field :diff, :string
    field :diff_truncated, :boolean, default: false

    belongs_to :source_canonical_finding, CanonicalFinding
    belongs_to :source_repository, Repository
    belongs_to :created_by, Account
    has_many :propagations, FixPropagation

    timestamps(type: :utc_datetime_usec)
  end

  def max_diff_bytes, do: @max_diff_bytes

  @doc false
  def changeset(template, attrs) do
    template
    |> cast(attrs, [
      :pattern_key,
      :source_canonical_finding_id,
      :source_repository_id,
      :created_by_id,
      :fix_commit_sha,
      :title,
      :summary,
      :diff,
      :diff_truncated
    ])
    |> validate_required([
      :pattern_key,
      :source_canonical_finding_id,
      :source_repository_id,
      :title,
      :summary
    ])
    |> validate_length(:title, max: 200)
    |> validate_length(:summary, min: 20, max: 10_000)
    |> truncate_diff()
    |> unique_constraint(:source_canonical_finding_id)
  end

  # A whole-commit diff can be arbitrarily large; the template only needs
  # enough for an agent to recognise the shape of the remedy.
  defp truncate_diff(changeset) do
    case get_change(changeset, :diff) do
      diff when is_binary(diff) and byte_size(diff) > @max_diff_bytes ->
        changeset
        |> put_change(:diff, binary_part(diff, 0, @max_diff_bytes))
        |> put_change(:diff_truncated, true)

      _otherwise ->
        changeset
    end
  end
end
