defmodule Ash.Resource.Transformers.CacheActionInputs do
  @moduledoc "Stores the set of valid input keys for each action"
  use Spark.Dsl.Transformer

  alias Spark.Dsl.Transformer

  def transform(dsl_state) do
    final_attributes_to_require =
      dsl_state
      |> Ash.Resource.Info.attributes()
      |> Enum.reject(&(&1.allow_nil? || &1.generated?))

    dsl_state =
      Transformer.persist(dsl_state, :attributes_to_require, final_attributes_to_require)

    dsl_state
    |> Ash.Resource.Info.actions()
    |> Enum.reduce(dsl_state, fn action, dsl_state ->
      inputs =
        action.arguments
        |> Enum.map(& &1.name)
        |> Enum.concat(Map.get(action, :accept, []))
        |> Enum.flat_map(&[&1, to_string(&1)])

      argument_names = action.arguments |> Enum.map(& &1.name)

      attributes_to_require_for_action =
        if action.type in [:read, :action] do
          []
        else
          accepted =
            action.accept
            |> Kernel.++(action.require_attributes)
            |> Kernel.--(action.allow_nil_input)
            |> Kernel.--(argument_names)

          dsl_state
          |> Ash.Resource.Info.attributes()
          |> Enum.reject(
            &(&1.name not in accepted || !&1.writable? || &1.generated? ||
                (&1.allow_nil? && &1.name not in action.require_attributes))
          )
        end

      inputs
      |> Enum.reduce(dsl_state, fn input, dsl_state ->
        Transformer.persist(dsl_state, {:action_inputs, action.name, input}, true)
      end)
      |> Transformer.persist({:action_inputs, action.name}, MapSet.new(inputs))
      |> then(&cache_action_select(&1, action))
      |> Transformer.persist(
        {:attributes_to_require, action.name},
        attributes_to_require_for_action
      )
    end)
    |> then(&{:ok, &1})
  end

  defp cache_action_select(dsl_state, %{type: type}) when type in [:read, :action] do
    dsl_state
  end

  defp cache_action_select(dsl_state, action) do
    if action.action_select do
      Transformer.persist(
        dsl_state,
        {:action_select, action.name},
        Enum.uniq(action.action_select ++ Ash.Resource.Info.primary_key(dsl_state))
      )
    else
      changes = Ash.Resource.Info.changes(dsl_state, action.type) ++ action.changes
      notifiers = Ash.Resource.Info.notifiers(dsl_state) ++ action.notifiers

      case changes ++ notifiers do
        [] ->
          dsl_state
          |> Transformer.persist(
            {:action_select, action.name},
            Ash.Resource.Info.primary_key(dsl_state)
          )

        _ ->
          attributes =
            dsl_state
            |> Ash.Resource.Info.attributes()

          select =
            attributes
            |> Enum.filter(&(&1.select_by_default? || &1.primary_key?))
            |> Enum.map(& &1.name)
            |> case do
              [] ->
                case attributes do
                  [%{name: name} | _] ->
                    [name]

                  _ ->
                    []
                end

              other ->
                other
            end

          dsl_state
          |> Transformer.persist({:action_select, action.name}, select)
      end
    end
  end
end
