defmodule PhilomenaQuery.Ecto.QueryValidator do
  @moduledoc """
  Query string validation for Ecto.

  It enables the following usage pattern by taking a fn of the compiler:

      defmodule Filter do
        import PhilomenaQuery.Ecto.QueryValidator

        # ...

        def changeset(filter, attrs, user) do
          filter
          |> cast(attrs, [:complex])
          |> validate_required([:complex])
          |> validate_query([:complex], with: &Query.compile(&1, user: user))
        end
      end

  """

  import Ecto.Changeset
  alias PhilomenaQuery.Parse.String

  @doc """
  Validates a query string using the provided attribute(s) and compiler.

  Returns the changeset as-is, or with an `"is invalid"` error added to validated field.

  ## Examples

      # With single attribute
      filter
      |> cast(attrs, [:complex])
      |> validate_query(:complex, &Query.compile(&1, user: user))

      # With list of attributes
      filter
      |> cast(attrs, [:spoilered_complex, :hidden_complex])
      |> validate_query([:spoilered_complex, :hidden_complex], &Query.compile(&1, user: user))

  """
  def validate_query(changeset, attr_or_attr_list, callback)

  def validate_query(changeset, attr_list, callback) when is_list(attr_list) do
    Enum.reduce(attr_list, changeset, fn attr, changeset ->
      validate_query(changeset, attr, callback)
    end)
  end

  def validate_query(changeset, attr, callback) do
    if changed?(changeset, attr) do
      validate_assuming_changed(changeset, attr, callback)
    else
      changeset
    end
  end

  defp validate_assuming_changed(changeset, attr, callback) do
    with value when is_binary(value) <- fetch_change!(changeset, attr) || "",
         value <- String.normalize(value),
         {:ok, _} <- callback.(value) do
      changeset
    else
      {:error, msg} ->
        add_error(changeset, attr, "is invalid: #{msg}")

      _ ->
        add_error(changeset, attr, "is invalid")
    end
  end
end
