defmodule Ash.Policy.Check.ChangingAttributes do
  @moduledoc "This check is true when attribute changes correspond to the provided options."
  use Ash.Policy.FilterCheck

  @impl true
  def describe(opts) do
    message =
      Enum.map_join(opts[:changing], " and ", fn
        {key, further} ->
          field_message =
            Enum.map_join(further, ", ", fn
              {:to, to_value} ->
                "to #{inspect(to_value)}"

              {:from, from_value} ->
                "from #{inspect(from_value)}"
            end)

          "#{key} #{field_message}"

        key ->
          "#{key}"
      end)

    "changing #{message}"
  end

  @impl true
  def filter(_actor, %{changeset: %Ash.Changeset{} = changeset}, options) do
    Enum.reduce_while(options[:changing], true, fn {attribute, opts}, expr ->
      if Keyword.has_key?(opts, :from) && changeset.action_type == :create do
        {:halt, false}
      else
        if Ash.Changeset.changing_attribute?(changeset, attribute) do
          case {Keyword.fetch(opts, :from), Keyword.fetch(opts, :to)} do
            {:error, :error} ->
              {:cont, expr}

            {{:ok, from}, {:ok, to}} ->
              if expr == true do
                {:cont, expr(^ref(attribute) == ^from and ^atomic_ref(attribute) == ^to)}
              else
                {:cont,
                 expr(^expr and ^ref(attribute) == ^from and ^atomic_ref(attribute) == ^to)}
              end

            {{:ok, from}, :error} ->
              if expr == true do
                {:cont, expr(^ref(attribute) == ^from)}
              else
                {:cont, expr(^expr and ^ref(attribute) == ^from)}
              end

            {:error, {:ok, to}} ->
              if expr == true do
                {:cont, expr(^atomic_ref(attribute) == ^to)}
              else
                {:cont, expr(^expr and ^atomic_ref(attribute) == ^to)}
              end
          end
        else
          {:halt, false}
        end
      end
    end)
  end

  def filter(_, _, _), do: false
end
