defmodule Ash.Reactor.Dsl.BulkUpdate do
  @moduledoc """
  The `bulk_update` entity for the `Ash.Reactor` reactor extension.
  """

  @bulk_actions_default_to_errors? Application.compile_env(
                                     :ash,
                                     :bulk_actions_default_to_errors?,
                                     false
                                   )

  defstruct __identifier__: nil,
            action_step?: true,
            action: nil,
            actor: [],
            allow_stream_with: :keyset,
            assume_casted?: false,
            async?: true,
            atomic_update: nil,
            authorize_changeset_with: :filter,
            authorize_query_with: :filter,
            authorize_query?: true,
            authorize?: nil,
            batch_size: nil,
            context: nil,
            description: nil,
            domain: nil,
            guards: [],
            filter: %{},
            initial: nil,
            inputs: [],
            load: nil,
            lock: nil,
            max_concurrency: 0,
            name: nil,
            notification_metadata: %{},
            notify?: false,
            page: [],
            read_action: nil,
            resource: nil,
            return_errors?: @bulk_actions_default_to_errors?,
            return_records?: false,
            return_stream?: false,
            reuse_values?: false,
            rollback_on_error?: true,
            select: [],
            skip_unknown_inputs: [],
            sorted?: false,
            stop_on_error?: @bulk_actions_default_to_errors?,
            strategy: [:atomic],
            stream_batch_size: nil,
            stream_with: nil,
            success_state: :success,
            tenant: [],
            timeout: :infinity,
            transaction: false,
            transform: nil,
            type: :bulk_update,
            undo_action: nil,
            undo: :never,
            wait_for: []

  @type t :: %__MODULE__{
          __identifier__: any,
          action_step?: true,
          action: atom,
          actor: nil | Ash.Reactor.Dsl.Actor.t(),
          allow_stream_with: :keyset | :offset | :full_read,
          assume_casted?: boolean,
          async?: boolean,
          atomic_update: %{optional(atom) => Ash.Expr.t()},
          authorize_changeset_with: :filter | :error,
          authorize_query_with: :filter | :error,
          authorize_query?: boolean,
          authorize?: boolean | nil,
          batch_size: nil | pos_integer(),
          context: nil | Ash.Reactor.Dsl.Context.t(),
          description: String.t() | nil,
          domain: Ash.Domain.t(),
          guards: [Reactor.Guard.Build.t()],
          filter:
            %{optional(String.t()) => %{required(String.t()) => String.t() | number | boolean}}
            | Keyword.t(Keyword.t(String.t() | number | boolean)),
          initial: Reactor.Template.t(),
          inputs: [Ash.Reactor.Dsl.Inputs.t()],
          load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
          lock: nil | Ash.DataLayer.lock_type(),
          max_concurrency: non_neg_integer(),
          name: atom,
          notification_metadata: map | Reactor.Template.t(),
          notify?: boolean,
          page: Keyword.t(),
          read_action: atom,
          resource: module,
          return_errors?: boolean,
          return_records?: boolean,
          return_stream?: boolean,
          reuse_values?: boolean,
          rollback_on_error?: boolean,
          select: [atom],
          skip_unknown_inputs: list(atom | String.t()),
          sorted?: boolean,
          stop_on_error?: boolean,
          strategy: :atomic | :atomic_batches | :stream,
          stream_batch_size: nil | pos_integer(),
          stream_with: nil | :keyset | :offset | :full_read,
          success_state: :success | :partial_success,
          tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
          timeout: nil | timeout,
          transaction: :all | :batch | false,
          type: :bulk_create,
          undo_action: nil,
          undo: :never
        }

  @doc false
  def __entity__,
    do: %Spark.Dsl.Entity{
      name: :bulk_update,
      describe: """
      Declares a step which will call an update action on a resource with a collection of inputs.

      > ### Check the docs! {: .warning}
      >
      > Make sure to thoroughly read and understand the documentation in `Ash.bulk_update/4` before using.  Read each option and note the default values.  By default, bulk updates don't return records or errors, and don't emit notifications.

      Caveats/differences from `Ash.bulk_update/4`:

      1. `max_concurrency` specifies the number of tasks that Ash will start to process batches, and has no effect on Reactor concurrency targets.  It's could be possible to create a very large number of processes if a number of steps are running bulk actions with a high degree of concurrency.
      2. Setting `notify?` to `true` will cause both `notify?` and `return_notifications?` to be set to true in the underlying call to `Ash.bulk_create/4`. Notifications will then be managed by the `Ash.Reactor.Notifications` Reactor middleware.
      3. If you specify an undo action it must be a generic action which takes the bulk result as it's only argument.

      #{Ash.Reactor.Dsl.Action.__shared_undo_docs__()}
      """,
      examples: [
        """
        bulk_update :publish_posts, MyApp.Post, :publish do
          initial input(:posts),
          actor result(:get_user)
        end
        """
      ],
      no_depend_modules: [:domain, :resource],
      target: __MODULE__,
      args: [:name, :resource, {:optional, :action}],
      identifier: :name,
      imports: [Reactor.Dsl.Argument],
      entities: [
        actor: [Ash.Reactor.Dsl.Actor.__entity__()],
        context: [Ash.Reactor.Dsl.Context.__entity__()],
        guards: [Reactor.Dsl.Guard.__entity__(), Reactor.Dsl.Where.__entity__()],
        inputs: [Ash.Reactor.Dsl.Inputs.__entity__()],
        tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
        wait_for: [Reactor.Dsl.WaitFor.__entity__()]
      ],
      singleton_entity_keys: [:actor, :context, :tenant],
      recursive_as: :steps,
      schema:
        [
          allow_stream_with: [
            type: {:in, [:keyset, :offset, :full_read]},
            doc:
              "The 'worst' strategy allowed to be used to fetch records if the :stream strategy is chosen. See the `Ash.stream!/2` docs for more.",
            required: false,
            default: :keyset
          ],
          assume_casted?: [
            type: :boolean,
            doc:
              "Whether or not to cast attributes and arguments as input. This is an optimization for cases where the input is already casted and/or not in need of casting",
            required: false,
            default: false
          ],
          atomic_update: [
            type: :map,
            keys: [*: [type: {:struct, Ash.Expr}]],
            doc:
              "A map of atomic updates to apply. See `Ash.Changeset.atomic_update/3` for more.",
            required: false
          ],
          authorize_changeset_with: [
            type: {:in, [:filter, :error]},
            doc:
              "If set to `:error`, instead of filtering unauthorized changes, unauthorized changes will raise an appropriate forbidden error",
            required: false,
            default: :filter
          ],
          authorize_query_with: [
            type: {:in, [:filter, :error]},
            doc:
              "If set to `:error`, instead of filtering unauthorized query results, unauthorized query results will raise an appropriate forbidden error",
            required: false,
            default: :filter
          ],
          authorize_query?: [
            type: :boolean,
            doc:
              "If a query is given, determines whether or not authorization is run on that query.",
            required: false,
            default: true
          ],
          batch_size: [
            type: {:or, [nil, :pos_integer]},
            doc:
              "The number of records to include in each batch. Defaults to the `default_limit` or `max_page_size` of the action, or 100.",
            required: false
          ],
          filter: [
            type: {:or, [:map, :keyword_list]},
            doc: "A filter to apply to records. This is also applied to a stream of inputs.",
            required: false
          ],
          initial: [
            type: Reactor.Template.type(),
            required: true,
            doc:
              "A collection of inputs to pass to the create action. Must implement the `Enumerable` protocol."
          ],
          lock: [
            type: :any,
            doc: "A lock statement to add onto the query.",
            required: false
          ],
          max_concurrency: [
            type: :non_neg_integer,
            doc:
              "If set to a value greater than 0, up to that many tasks will be started to run batches asynchronously.",
            required: false,
            default: 0
          ],
          notification_metadata: [
            type: {:or, [:map, Reactor.Template.type()]},
            doc:
              "Metadata to be merged into the metadata field for all notifications sent from this operation.",
            required: false,
            default: %{}
          ],
          notify?: [
            type: :boolean,
            doc:
              "Whether or not to generate any notifications. This may be intensive for large bulk actions.",
            required: false,
            default: false
          ],
          page: [
            type: :keyword_list,
            doc: "Pagination options, see `Ash.read/2` for more.",
            required: false,
            default: []
          ],
          read_action: [
            type: :atom,
            doc: "The action to use when building the read query.",
            required: false
          ],
          return_errors?: [
            type: :boolean,
            doc:
              "Whether or not to return all of the errors that occur. Defaults to false to account for large inserts.",
            required: false,
            default: @bulk_actions_default_to_errors?
          ],
          return_records?: [
            type: :boolean,
            doc:
              "Whether or not to return all of the records that were inserted. Defaults to false to account for large inserts.",
            required: false,
            default: false
          ],
          return_stream?: [
            type: :boolean,
            doc: "If set to `true`, instead of an `Ash.BulkResult`, a mixed stream is returned.",
            required: false,
            default: false
          ],
          reuse_values?: [
            type: :boolean,
            doc:
              "Whether calculations are allowed to reuse values that have already been loaded, or must refetch them from the data layer.",
            required: false,
            default: false
          ],
          rollback_on_error?: [
            type: :boolean,
            doc:
              "Whether or not to rollback the transaction on error, if the resource is in a transaction.",
            required: false,
            default: true
          ],
          select: [
            type: {:wrap_list, :atom},
            doc:
              "A select statement to apply to records. Ignored if `return_records?` is not `true`.",
            required: false
          ],
          skip_unknown_inputs: [
            type: {:wrap_list, {:or, [:atom, :string]}},
            doc:
              "A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys.",
            required: false
          ],
          sorted?: [
            type: :boolean,
            doc:
              "Whether or not to sort results by their input position, in cases where `return_records?` is set to `true`.",
            required: false,
            default: false
          ],
          stop_on_error?: [
            type: :boolean,
            doc:
              "If `true`, the first encountered error will stop the action and be returned. Otherwise, errors will be skipped.",
            required: false,
            default: @bulk_actions_default_to_errors?
          ],
          strategy: [
            type: {:list, {:in, [:atomic, :atomic_batches, :stream]}},
            doc:
              "The strategy or strategies to enable. `:stream` is used in all cases if the data layer does not support atomics.",
            required: false,
            default: [:atomic]
          ],
          stream_batch_size: [
            type: :pos_integer,
            doc: "Batch size to use if provided a query and the query must be streamed.",
            required: false
          ],
          stream_with: [
            type: {:in, [:keyset, :offset, :full_read]},
            doc:
              "The specific strategy to use to fetch records. See `Ash.stream!/2` docs for more.",
            required: false
          ],
          success_state: [
            type: {:in, [:success, :partial_success]},
            doc:
              "Bulk results can be entirely or partially successful. Chooses the `Ash.BulkResult` state to consider the step a success.",
            required: false,
            default: :success
          ],
          timeout: [
            type: :timeout,
            doc:
              "If none is provided, the timeout configured on the domain is used (which defaults to `30_000`).",
            required: false
          ],
          transaction: [
            type: {:in, [:all, :batch, false]},
            doc:
              "Whether or not to wrap the entire execution in a transaction, each batch, or not at all.",
            required: false,
            default: :batch
          ]
        ]
        |> Spark.Options.merge(
          Ash.Reactor.Dsl.Action.__shared_action_option_schema__(),
          "Shared action options"
        )
    }
end
