lib/pg_rest/filter.ex

defmodule PgRest.Filter do
  @moduledoc """
  Applies parsed filter ASTs to Ecto queries.
  """

  import Ecto.Query
  import PgRest.Utils, only: [safe_to_atom: 1]

  @doc """
  Applies a list of parsed filter ASTs to an Ecto query.
  """
  @spec apply_all(Ecto.Queryable.t(), [map()]) :: Ecto.Query.t()
  def apply_all(query, filters) when is_list(filters) do
    Enum.reduce(filters, query, &apply_filter/2)
  end

  @doc """
  Applies a single filter AST node to an Ecto query.

  Handles simple field filters, logical operators (`:and`, `:or`, `:not`),
  and all PostgREST comparison operators.
  """
  @spec apply_filter(map(), Ecto.Queryable.t()) :: Ecto.Query.t()
  def apply_filter(%{logic: :and, conditions: conditions}, query) do
    Enum.reduce(conditions, query, &apply_filter/2)
  end

  def apply_filter(%{logic: :or, conditions: conditions}, query) do
    dynamic = build_or_dynamic(conditions)
    where(query, ^dynamic)
  end

  def apply_filter(%{logic: :not, condition: condition}, query) do
    dynamic = build_dynamic(condition)
    where(query, [r], not (^dynamic))
  end

  def apply_filter(%{field: field, operator: op, value: value}, query) do
    field_atom = safe_to_atom(field)
    apply_op(query, field_atom, op, value)
  end

  # Comparison operators
  defp apply_op(query, field, :eq, value) do
    where(query, [r], field(r, ^field) == ^value)
  end

  defp apply_op(query, field, :neq, value) do
    where(query, [r], field(r, ^field) != ^value)
  end

  defp apply_op(query, field, :gt, value) do
    where(query, [r], field(r, ^field) > ^value)
  end

  defp apply_op(query, field, :gte, value) do
    where(query, [r], field(r, ^field) >= ^value)
  end

  defp apply_op(query, field, :lt, value) do
    where(query, [r], field(r, ^field) < ^value)
  end

  defp apply_op(query, field, :lte, value) do
    where(query, [r], field(r, ^field) <= ^value)
  end

  # Pattern matching — PostgREST only converts * to %, no auto-wrap
  defp apply_op(query, field, :like, value) do
    pattern = convert_wildcards(value)
    where(query, [r], like(field(r, ^field), ^pattern))
  end

  defp apply_op(query, field, :ilike, value) do
    pattern = convert_wildcards(value)
    where(query, [r], ilike(field(r, ^field), ^pattern))
  end

  # POSIX regex
  defp apply_op(query, field, :match, value) do
    where(query, [r], fragment("? ~ ?", field(r, ^field), ^value))
  end

  defp apply_op(query, field, :imatch, value) do
    where(query, [r], fragment("? ~* ?", field(r, ^field), ^value))
  end

  # IN operator
  defp apply_op(query, field, :in, values) when is_list(values) do
    where(query, [r], field(r, ^field) in ^values)
  end

  # IS NULL
  defp apply_op(query, field, :is_null, true) do
    where(query, [r], is_nil(field(r, ^field)))
  end

  defp apply_op(query, field, :is_null, false) do
    where(query, [r], not is_nil(field(r, ^field)))
  end

  # IS boolean
  defp apply_op(query, field, :is, true) do
    where(query, [r], field(r, ^field) == true)
  end

  defp apply_op(query, field, :is, false) do
    where(query, [r], field(r, ^field) == false)
  end

  # IS DISTINCT FROM
  defp apply_op(query, field, :isdistinct, value) do
    where(query, [r], fragment("? IS DISTINCT FROM ?", field(r, ^field), ^value))
  end

  # Array containment
  defp apply_op(query, field, :cs, value) do
    array_val = parse_pg_array(value)
    where(query, [r], fragment("? @> ?", field(r, ^field), ^array_val))
  end

  defp apply_op(query, field, :cd, value) do
    array_val = parse_pg_array(value)
    where(query, [r], fragment("? <@ ?", field(r, ^field), ^array_val))
  end

  # Array overlap
  defp apply_op(query, field, :ov, value) do
    array_val = parse_pg_array(value)
    where(query, [r], fragment("? && ?", field(r, ^field), ^array_val))
  end

  # Range operators
  defp apply_op(query, field, :sl, value) do
    where(query, [r], fragment("? << ?", field(r, ^field), ^value))
  end

  defp apply_op(query, field, :sr, value) do
    where(query, [r], fragment("? >> ?", field(r, ^field), ^value))
  end

  defp apply_op(query, field, :nxr, value) do
    where(query, [r], fragment("? &< ?", field(r, ^field), ^value))
  end

  defp apply_op(query, field, :nxl, value) do
    where(query, [r], fragment("? &> ?", field(r, ^field), ^value))
  end

  defp apply_op(query, field, :adj, value) do
    where(query, [r], fragment("? -|- ?", field(r, ^field), ^value))
  end

  # Full-text search — use to_tsvector with language config
  defp apply_op(query, field, :fts, {lang, value}) do
    where(
      query,
      [r],
      fragment("to_tsvector(?, ?) @@ to_tsquery(?, ?)", ^lang, field(r, ^field), ^lang, ^value)
    )
  end

  defp apply_op(query, field, :plfts, {lang, value}) do
    where(
      query,
      [r],
      fragment(
        "to_tsvector(?, ?) @@ plainto_tsquery(?, ?)",
        ^lang,
        field(r, ^field),
        ^lang,
        ^value
      )
    )
  end

  defp apply_op(query, field, :phfts, {lang, value}) do
    where(
      query,
      [r],
      fragment(
        "to_tsvector(?, ?) @@ phraseto_tsquery(?, ?)",
        ^lang,
        field(r, ^field),
        ^lang,
        ^value
      )
    )
  end

  defp apply_op(query, field, :wfts, {lang, value}) do
    where(
      query,
      [r],
      fragment(
        "to_tsvector(?, ?) @@ websearch_to_tsquery(?, ?)",
        ^lang,
        field(r, ^field),
        ^lang,
        ^value
      )
    )
  end

  # Helpers

  defp build_or_dynamic([first | rest]) do
    first_dynamic = build_dynamic(first)

    Enum.reduce(rest, first_dynamic, fn condition, acc ->
      condition_dynamic = build_dynamic(condition)
      dynamic([r], ^acc or ^condition_dynamic)
    end)
  end

  # Comparison
  defp build_dynamic(%{field: field, operator: :eq, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) == ^value)
  end

  defp build_dynamic(%{field: field, operator: :neq, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) != ^value)
  end

  defp build_dynamic(%{field: field, operator: :gt, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) > ^value)
  end

  defp build_dynamic(%{field: field, operator: :gte, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) >= ^value)
  end

  defp build_dynamic(%{field: field, operator: :lt, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) < ^value)
  end

  defp build_dynamic(%{field: field, operator: :lte, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) <= ^value)
  end

  # Pattern matching — no auto-wrap for ilike
  defp build_dynamic(%{field: field, operator: :like, value: value}) do
    field_atom = safe_to_atom(field)
    pattern = convert_wildcards(value)
    dynamic([r], like(field(r, ^field_atom), ^pattern))
  end

  defp build_dynamic(%{field: field, operator: :ilike, value: value}) do
    field_atom = safe_to_atom(field)
    pattern = convert_wildcards(value)
    dynamic([r], ilike(field(r, ^field_atom), ^pattern))
  end

  # POSIX regex
  defp build_dynamic(%{field: field, operator: :match, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? ~ ?", field(r, ^field_atom), ^value))
  end

  defp build_dynamic(%{field: field, operator: :imatch, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? ~* ?", field(r, ^field_atom), ^value))
  end

  # IN
  defp build_dynamic(%{field: field, operator: :in, value: values}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) in ^values)
  end

  # IS NULL
  defp build_dynamic(%{field: field, operator: :is_null, value: true}) do
    field_atom = safe_to_atom(field)
    dynamic([r], is_nil(field(r, ^field_atom)))
  end

  defp build_dynamic(%{field: field, operator: :is_null, value: false}) do
    field_atom = safe_to_atom(field)
    dynamic([r], not is_nil(field(r, ^field_atom)))
  end

  # IS boolean
  defp build_dynamic(%{field: field, operator: :is, value: val}) do
    field_atom = safe_to_atom(field)
    dynamic([r], field(r, ^field_atom) == ^val)
  end

  # IS DISTINCT FROM
  defp build_dynamic(%{field: field, operator: :isdistinct, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? IS DISTINCT FROM ?", field(r, ^field_atom), ^value))
  end

  # Array operators
  defp build_dynamic(%{field: field, operator: :cs, value: value}) do
    field_atom = safe_to_atom(field)
    array_val = parse_pg_array(value)
    dynamic([r], fragment("? @> ?", field(r, ^field_atom), ^array_val))
  end

  defp build_dynamic(%{field: field, operator: :cd, value: value}) do
    field_atom = safe_to_atom(field)
    array_val = parse_pg_array(value)
    dynamic([r], fragment("? <@ ?", field(r, ^field_atom), ^array_val))
  end

  defp build_dynamic(%{field: field, operator: :ov, value: value}) do
    field_atom = safe_to_atom(field)
    array_val = parse_pg_array(value)
    dynamic([r], fragment("? && ?", field(r, ^field_atom), ^array_val))
  end

  # Range operators
  defp build_dynamic(%{field: field, operator: :sl, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? << ?", field(r, ^field_atom), ^value))
  end

  defp build_dynamic(%{field: field, operator: :sr, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? >> ?", field(r, ^field_atom), ^value))
  end

  defp build_dynamic(%{field: field, operator: :nxr, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? &< ?", field(r, ^field_atom), ^value))
  end

  defp build_dynamic(%{field: field, operator: :nxl, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? &> ?", field(r, ^field_atom), ^value))
  end

  defp build_dynamic(%{field: field, operator: :adj, value: value}) do
    field_atom = safe_to_atom(field)
    dynamic([r], fragment("? -|- ?", field(r, ^field_atom), ^value))
  end

  # FTS
  defp build_dynamic(%{field: field, operator: :fts, value: {lang, value}}) do
    field_atom = safe_to_atom(field)

    dynamic(
      [r],
      fragment(
        "to_tsvector(?, ?) @@ to_tsquery(?, ?)",
        ^lang,
        field(r, ^field_atom),
        ^lang,
        ^value
      )
    )
  end

  defp build_dynamic(%{field: field, operator: :plfts, value: {lang, value}}) do
    field_atom = safe_to_atom(field)

    dynamic(
      [r],
      fragment(
        "to_tsvector(?, ?) @@ plainto_tsquery(?, ?)",
        ^lang,
        field(r, ^field_atom),
        ^lang,
        ^value
      )
    )
  end

  defp build_dynamic(%{field: field, operator: :phfts, value: {lang, value}}) do
    field_atom = safe_to_atom(field)

    dynamic(
      [r],
      fragment(
        "to_tsvector(?, ?) @@ phraseto_tsquery(?, ?)",
        ^lang,
        field(r, ^field_atom),
        ^lang,
        ^value
      )
    )
  end

  defp build_dynamic(%{field: field, operator: :wfts, value: {lang, value}}) do
    field_atom = safe_to_atom(field)

    dynamic(
      [r],
      fragment(
        "to_tsvector(?, ?) @@ websearch_to_tsquery(?, ?)",
        ^lang,
        field(r, ^field_atom),
        ^lang,
        ^value
      )
    )
  end

  defp convert_wildcards(value) do
    String.replace(value, "*", "%")
  end

  defp parse_pg_array(value) when is_binary(value) do
    value
    |> String.trim_leading("{")
    |> String.trim_trailing("}")
    |> String.split(",")
    |> Enum.map(&String.trim/1)
  end

  defp parse_pg_array(value) when is_list(value), do: value
end