defmodule Tarakan.Billing.Watchlist do
  @moduledoc """
  A named set of repositories (`owner/name` refs) an account wants finding
  alerts for. Entries are plain strings so watchlists survive renames and
  can point at repositories Tarakan has not indexed yet.
  """

  use Ecto.Schema

  import Ecto.Changeset

  alias Tarakan.Accounts.Account

  @max_entries 500
  @entry_format ~r/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/

  schema "watchlists" do
    field :name, :string
    field :entries, {:array, :string}, default: []
    field :notify, :boolean, default: true
    field :last_notified_at, :utc_datetime_usec

    belongs_to :account, Account

    timestamps(type: :utc_datetime_usec)
  end

  @doc "Maximum number of `owner/name` entries per watchlist."
  def max_entries, do: @max_entries

  @doc false
  def changeset(watchlist, attrs) do
    watchlist
    |> cast(attrs, [:name, :entries, :notify])
    |> validate_required([:name])
    |> validate_length(:name, max: 100)
    |> validate_entries()
    |> put_normalized_entries()
  end

  defp validate_entries(changeset) do
    validate_change(changeset, :entries, fn :entries, entries ->
      entries = List.wrap(entries)

      cond do
        length(entries) > @max_entries ->
          [entries: "must have at most #{@max_entries} entries"]

        Enum.any?(entries, fn entry -> not is_binary(entry) or entry == "" end) ->
          [entries: "must be non-empty strings"]

        true ->
          []
      end
    end)
  end

  # Normalization (trim, drop blanks, shape-check, dedupe) runs after the raw
  # validations so the count cap is measured against what was submitted.
  defp put_normalized_entries(changeset) do
    case get_change(changeset, :entries) do
      nil ->
        changeset

      entries when is_list(entries) ->
        normalized =
          entries
          |> Enum.map(&String.trim/1)
          |> Enum.uniq()

        if Enum.all?(normalized, &(&1 =~ @entry_format)) do
          put_change(changeset, :entries, normalized)
        else
          add_error(changeset, :entries, "must use the owner/name shape")
        end
    end
  end
end
