defmodule Ash.Reactor.ChangeStep do
  @moduledoc """
  The Reactor step which is used to execute change steps.
  """

  use Reactor.Step
  alias Ash.Changeset

  @doc false
  @impl true
  def run(arguments, context, options) do
    with {:ok, changeset} <- initial_changeset(arguments.initial, arguments.arguments),
         {:ok, changeset} <- maybe_must_be_valid(changeset, options[:only_when_valid?], :bypass),
         {:ok, changeset} <- apply_where_clauses(changeset, options[:where], context),
         {:ok, changeset} <- apply_change(changeset, options[:change], context) do
      fail_if_invalid? = Keyword.get(options, :fail_if_invalid?, false)
      maybe_must_be_valid(changeset, fail_if_invalid?, :error)
    else
      {:bypass, changeset} -> {:ok, changeset}
      {:error, reason} -> {:error, reason}
    end
  end

  defp initial_changeset(module, arguments) when is_atom(module),
    do: initial_changeset(Changeset.new(module), arguments)

  defp initial_changeset(changeset, arguments) when is_struct(changeset, Changeset),
    do: {:ok, Changeset.force_set_arguments(changeset, arguments)}

  defp maybe_must_be_valid(changeset, true, _) when changeset.valid?, do: {:ok, changeset}
  defp maybe_must_be_valid(changeset, true, tag), do: {tag, changeset}
  defp maybe_must_be_valid(changeset, _, _), do: {:ok, changeset}

  defp apply_where_clauses(changeset, nil, context),
    do: apply_where_clauses(changeset, [], context)

  defp apply_where_clauses(changeset, clauses, context) when is_list(clauses) do
    Enum.reduce_while(clauses, {:ok, changeset}, fn clause, {:ok, changeset} ->
      case apply_validation(changeset, clause, context) do
        :ok -> {:cont, {:ok, changeset}}
        {:error, _} -> {:halt, {:bypass, changeset}}
      end
    end)
  end

  defp apply_validation(changeset, {module, opts}, context) do
    opts =
      Ash.Expr.fill_template(
        opts,
        actor: context[:actor],
        tenant: changeset.to_tenant,
        args: changeset.arguments,
        context: changeset.context,
        changeset: changeset
      )

    Ash.Resource.Validation.validate(module, changeset, opts, context)
  rescue
    error -> {:error, error}
  end

  defp apply_validation(changeset, module, context),
    do: apply_validation(changeset, {module, []}, context)

  defp apply_change(changeset, {module, opts}, context) do
    opts =
      Ash.Expr.fill_template(
        opts,
        actor: context[:actor],
        tenant: changeset.to_tenant,
        args: changeset.arguments,
        context: changeset.context,
        changeset: changeset
      )

    with {:ok, opts} <- module.init(opts) do
      {:ok, Ash.Resource.Change.change(module, changeset, opts, context)}
    end
  end

  defp apply_change(changeset, module, context) when is_atom(module),
    do: apply_change(changeset, {module, []}, context)
end
