defmodule Ash.Resource.Info do
  @moduledoc "Introspection for resources"

  alias Spark.Dsl.Extension

  @doc """
  Returns the statically configured domain for the resource.
  """
  def domain(resource) do
    Spark.Dsl.Extension.get_persisted(resource, :domain)
  end

  @doc """
  Retrieves a relationship path from the resource related by path, to the provided resource.
  """
  def reverse_relationship(resource, path, acc \\ [])

  def reverse_relationship(_, [], acc),
    do: acc

  def reverse_relationship(resource, [name | rest], acc) do
    resource
    |> relationships()
    |> Enum.find(fn relationship ->
      relationship.name == name
    end)
    |> case do
      nil ->
        nil

      relationship ->
        relationship.destination
        |> relationships()
        |> Enum.find(fn candidate ->
          reverse_relationship?(relationship, candidate)
        end)
        |> case do
          nil ->
            nil

          destination_relationship ->
            reverse_relationship(relationship.destination, rest, [
              destination_relationship.name | acc
            ])
        end
    end
  end

  defp reverse_relationship?(rel, destination_rel) do
    rel.source == destination_rel.destination &&
      rel.destination == destination_rel.source &&
      rel.source_attribute == destination_rel.destination_attribute &&
      rel.destination_attribute == destination_rel.source_attribute &&
      Map.fetch(rel, :source_attribute_on_join_resource) ==
        Map.fetch(destination_rel, :destination_attribute_on_join_resource) &&
      Map.fetch(rel, :destination_attribute_on_join_resource) ==
        Map.fetch(destination_rel, :source_attribute_on_join_resource) &&
      is_nil(destination_rel.context) &&
      is_nil(rel.context)
  end

  @doc """
  The list of code interface definitions.
  """
  @spec interfaces(Spark.Dsl.t() | Ash.Resource.t()) :: [Ash.Resource.Interface.t()]
  def interfaces(resource) do
    resource
    |> Extension.get_entities([:code_interface])
    |> Enum.filter(fn
      %Ash.Resource.Interface{} ->
        true

      _ ->
        false
    end)
  end

  @doc "Get an interface by name from the resource"
  @spec interface(Spark.Dsl.t() | Ash.Resource.t(), atom) :: Ash.Resource.Interface.t() | nil
  def interface(resource, name) do
    resource
    |> interfaces()
    |> Enum.find(&(&1.name == name))
  end

  @doc """
  The list of code interface calculation definitions.
  """
  @spec calculation_interfaces(Spark.Dsl.t() | Ash.Resource.t()) :: [
          Ash.Resource.CalculationInterface.t()
        ]
  def calculation_interfaces(resource) do
    resource
    |> Extension.get_entities([:code_interface])
    |> Enum.filter(fn
      %Ash.Resource.CalculationInterface{} ->
        true

      _ ->
        false
    end)
  end

  @doc "Get an calculation interface by name from the resource"
  @spec calculation_interface(Spark.Dsl.t() | Ash.Resource.t(), atom) ::
          Ash.Resource.CalculationInterface.t() | nil
  def calculation_interface(resource, name) do
    resource
    |> calculation_interfaces()
    |> Enum.find(&(&1.name == name))
  end

  @doc """
  The domain to define the interface for, when defining it in the resource
  """
  @spec code_interface_domain(Spark.Dsl.t() | Ash.Resource.t()) :: atom | nil
  def code_interface_domain(resource) do
    Extension.get_opt(resource, [:code_interface], :domain, nil)
  end

  @doc """
  Whether or not to define the interface on the resource
  """
  @spec define_interface?(Spark.Dsl.t() | Ash.Resource.t()) :: boolean
  def define_interface?(resource) do
    Extension.get_opt(resource, [:code_interface], :define?, true)
  end

  @doc """
  Whether or not the resource is an embedded resource
  """
  @spec embedded?(Spark.Dsl.t() | Ash.Resource.t()) :: boolean
  def embedded?(resource) do
    Extension.get_persisted(resource, :embedded?, false)
  end

  @doc """
  The description of the resource
  """
  @spec description(Spark.Dsl.t() | Ash.Resource.t()) :: String.t() | nil
  def description(resource) do
    Extension.get_opt(resource, [:resource], :description, nil)
  end

  @doc """
  A list of simple notifiers (require no DSL, used to avoid compile time dependencies)
  """
  @spec simple_notifiers(Spark.Dsl.t() | Ash.Resource.t()) :: list(module)
  def simple_notifiers(resource) do
    Extension.get_persisted(resource, :simple_notifiers, [])
  end

  @doc """
  The trace_name of the resource
  """
  @spec trace_name(Spark.Dsl.t() | Ash.Resource.t()) :: String.t() | nil
  def trace_name(resource) do
    Extension.get_opt(resource, [:resource], :trace_name, nil) || to_string(short_name(resource))
  end

  @doc """
  The short_name of the resource
  """
  # sobelow_skip ["DOS.StringToAtom"]
  @spec short_name(Spark.Dsl.t() | Ash.Resource.t()) :: atom | nil
  def short_name(resource) when is_map(resource) do
    Extension.get_opt(resource, [:resource], :short_name, nil) ||
      resource
      |> Extension.get_persisted(:module)
      |> Module.split()
      |> List.last()
      |> Macro.underscore()
      |> String.to_atom()
  end

  def short_name(resource) do
    Extension.get_opt(resource, [:resource], :short_name, nil) || resource.default_short_name()
  end

  @doc """
  Whether to include private fields in the inspect output.
  `show_inspect_fields` takes precedence over this option.
  """
  @spec inspect_private_fields?(Spark.Dsl.t() | Ash.Resource.t()) :: boolean
  def inspect_private_fields?(resource) do
    Extension.get_opt(resource, [:resource], :inspect_private_fields?, true)
  end

  @doc """
  A deny-list of fields to hide from the inspect output.
  Takes precedence over `show_inspect_fields`.
  """
  @spec hide_inspect_fields(Spark.Dsl.t() | Ash.Resource.t()) :: list(atom)
  def hide_inspect_fields(resource) do
    Extension.get_opt(resource, [:resource], :hide_inspect_fields, [])
  end

  @doc """
  An allow-list of fields to show in the inspect output.
  """
  @spec show_inspect_fields(Spark.Dsl.t() | Ash.Resource.t()) :: list(atom) | nil
  def show_inspect_fields(resource) do
    Extension.get_opt(resource, [:resource], :show_inspect_fields, nil)
  end

  @doc """
  The plural_name of the resource
  """
  def plural_name(resource) do
    Extension.get_opt(resource, [:resource], :plural_name, nil)
  end

  @doc """
  The configured default target attribute for atomic validations.
  """
  @spec atomic_validation_default_target_attribute(Spark.Dsl.t() | Ash.Resource.t()) :: atom | nil
  def atomic_validation_default_target_attribute(resource) do
    Extension.get_opt(resource, [:resource], :atomic_validation_default_target_attribute, nil)
  end

  @doc """
  The base filter of the resource
  """
  @spec base_filter(Spark.Dsl.t() | Ash.Resource.t()) :: term
  def base_filter(resource) do
    Extension.get_opt(resource, [:resource], :base_filter, nil)
  end

  @doc """
  The default context of the resource
  """
  @spec default_context(Spark.Dsl.t() | Ash.Resource.t()) :: term
  def default_context(resource) do
    Extension.get_opt(resource, [:resource], :default_context, nil)
  end

  @doc "A list of identities for the resource"
  @spec identities(Spark.Dsl.t() | Ash.Resource.t()) :: [Ash.Resource.Identity.t()]
  def identities(resource) do
    Extension.get_entities(resource, [:identities])
  end

  @doc "Get an identity by name from the resource"
  @spec identity(Spark.Dsl.t() | Ash.Resource.t(), atom) :: Ash.Resource.Identity.t() | nil
  def identity(resource, name) do
    resource
    |> identities()
    |> Enum.find(&(&1.name == name))
  end

  @doc "A list of authorizers to be used when accessing"
  @spec authorizers(Spark.Dsl.t() | Ash.Resource.t()) :: [module]
  def authorizers(resource) do
    Extension.get_persisted(resource, :authorizers, [])
  end

  @doc "A list of notifiers to be used when accessing"
  @spec notifiers(Spark.Dsl.t() | Ash.Resource.t()) :: [module]
  def notifiers(resource) do
    Extension.get_persisted(resource, :notifiers, []) ++ simple_notifiers(resource)
  end

  @doc "A list of all validations for the resource for a given action type"
  @spec validations(Spark.Dsl.t() | Ash.Resource.t(), :create | :update | :destroy | :read) :: [
          Ash.Resource.Validation.t()
        ]
  def validations(resource, type) do
    Extension.get_persisted(resource, :validations_by_on)[type] ||
      resource
      |> validations()
      |> Enum.filter(&(type in &1.on))
  end

  @doc "A list of all validations for the resource"
  @spec validations(Spark.Dsl.t() | Ash.Resource.t()) :: [Ash.Resource.Validation.t()]
  def validations(resource) do
    Extension.get_entities(resource, [:validations])
  end

  @doc "A list of all changes for the resource for a given action type"
  @spec changes(Spark.Dsl.t() | Ash.Resource.t(), :create | :update | :destroy) ::
          list(
            Ash.Resource.Validation.t()
            | Ash.Resource.Change.t()
          )
  def changes(resource, type) do
    Extension.get_persisted(resource, :changes_by_on)[type] ||
      resource
      |> changes()
      |> Enum.filter(&(type in &1.on))
  end

  @doc "A list of all changes for the resource"
  @spec changes(Spark.Dsl.t() | Ash.Resource.t()) ::
          list(Ash.Resource.Validation.t() | Ash.Resource.Change.t())
  def changes(resource) do
    Extension.get_entities(resource, [:changes])
  end

  @spec preparations(Spark.Dsl.t() | Ash.Resource.t(), action_type :: :read | :action) ::
          list(Ash.Resource.Preparation.t())
  def preparations(resource, type \\ :read) do
    Extension.get_entities(resource, [:preparations])
    |> Enum.filter(&(type in &1.on))
  end

  @doc "Whether or not a given module is a resource module"
  @spec resource?(module) :: boolean
  def resource?(module) when is_atom(module) do
    Spark.Dsl.is?(module, Ash.Resource)
  end

  def resource?(_), do: false

  @doc "A list of field names corresponding to the primary key"
  @spec primary_key(Spark.Dsl.t() | Ash.Resource.t()) :: list(atom)
  def primary_key(resource) do
    Spark.Dsl.Extension.get_persisted(resource, :primary_key, [])
  end

  @doc "A list of unique keys and information for a resource"
  @spec unique_keys(Spark.Dsl.t() | Ash.Resource.t()) ::
          list(%{type: atom, keys: list(atom), nils_distinct?: boolean()})
  def unique_keys(resource) do
    Spark.Dsl.Extension.get_persisted(resource, :unique_keys, [])
  end

  @doc "Whether or not all primary key attributes can be compared with simple_equality"
  @spec primary_key_simple_equality?(Spark.Dsl.t() | Ash.Resource.t()) :: boolean()
  def primary_key_simple_equality?(resource) do
    Spark.Dsl.Extension.get_persisted(resource, :primary_key_simple_equality?, [])
  end

  @doc "Returns all relationships of a resource"
  @spec relationships(Spark.Dsl.t() | Ash.Resource.t()) ::
          list(Ash.Resource.Relationships.relationship())
  def relationships(resource) do
    Extension.get_entities(resource, [:relationships])
  end

  @doc "Get a relationship by name or path"
  @spec relationship(Spark.Dsl.t() | Ash.Resource.t(), atom | String.t() | [atom | String.t()]) ::
          Ash.Resource.Relationships.relationship() | nil
  def relationship(resource, [name]) do
    relationship(resource, name)
  end

  def relationship(resource, [name | rest]) do
    case relationship(resource, name) do
      nil ->
        nil

      relationship ->
        relationship(relationship.destination, rest)
    end
  end

  def relationship(resource, relationship_name)
      when is_binary(relationship_name) or is_atom(relationship_name) do
    case Extension.get_persisted(resource, :relationships_by_name) do
      value when is_map(value) ->
        value[relationship_name]

      nil ->
        resource
        |> relationships()
        |> Enum.find(&(to_string(&1.name) == to_string(relationship_name)))
    end
  end

  def relationship(_, _), do: nil

  @doc "Returns all public relationships of a resource"
  @spec public_relationships(Spark.Dsl.t() | Ash.Resource.t()) ::
          list(Ash.Resource.Relationships.relationship())
  def public_relationships(resource) do
    resource
    |> relationships()
    |> Enum.filter(& &1.public?)
  end

  @doc "The required belongs_to relationships"
  def required_belongs_to_relationships(resource) do
    Extension.get_persisted(resource, :required_belongs_to_relationships) ||
      Enum.filter(
        relationships(resource),
        &(&1.type == :belongs_to && !&1.allow_nil?)
      )
  end

  def attributes_to_require(resource) do
    Extension.get_persisted(resource, :attributes_to_require)
  end

  def attributes_to_require(resource, action_name) do
    Extension.get_persisted(resource, {:attributes_to_require, action_name})
  end

  @doc "Get a public relationship by name or path"
  def public_relationship(resource, [name]) do
    public_relationship(resource, name)
  end

  def public_relationship(resource, [name | rest]) do
    case public_relationship(resource, name) do
      nil ->
        nil

      relationship ->
        public_relationship(relationship.destination, rest)
    end
  end

  def public_relationship(resource, relationship_name) do
    case relationship(resource, relationship_name) do
      %{public?: true} = relationship -> relationship
      _ -> nil
    end
  end

  @doc "The multitenancy strategy for a resource"
  @spec multitenancy_strategy(Spark.Dsl.t() | Ash.Resource.t()) :: :context | :attribute | nil
  def multitenancy_strategy(resource) do
    Spark.Dsl.Extension.get_opt(resource, [:multitenancy], :strategy, nil)
  end

  @doc "The multitenancy attribute for a resource"
  @spec multitenancy_attribute(Spark.Dsl.t() | Ash.Resource.t()) :: atom | nil
  def multitenancy_attribute(resource) do
    Spark.Dsl.Extension.get_opt(resource, [:multitenancy], :attribute, nil)
  end

  @doc "The function to parse the tenant from the attribute"
  @spec multitenancy_parse_attribute(Spark.Dsl.t() | Ash.Resource.t()) :: {atom, atom, list(any)}
  def multitenancy_parse_attribute(resource) do
    Spark.Dsl.Extension.get_opt(
      resource,
      [:multitenancy],
      :parse_attribute,
      {__MODULE__, :_identity, []}
    )
  end

  @doc false
  def _identity(x), do: x

  @doc "The MFA to parse the tenant from the attribute"
  @spec multitenancy_global?(Spark.Dsl.t() | Ash.Resource.t()) :: atom | nil
  def multitenancy_global?(resource) do
    Spark.Dsl.Extension.get_opt(resource, [:multitenancy], :global?, nil)
  end

  @doc "The template for creating the tenant name"
  @spec multitenancy_template(Spark.Dsl.t() | Ash.Resource.t()) :: atom | nil
  def multitenancy_template(resource) do
    Spark.Dsl.Extension.get_opt(resource, [:multitenancy], :template, nil)
  end

  @doc "Returns all calculations of a resource"
  @spec calculations(Spark.Dsl.t() | Ash.Resource.t()) :: list(Ash.Resource.Calculation.t())
  def calculations(resource) do
    Extension.get_entities(resource, [:calculations])
  end

  @doc "Get a calculation by name"
  @spec calculation(Spark.Dsl.t() | Ash.Resource.t(), atom | String.t()) ::
          Ash.Resource.Calculation.t() | nil
  def calculation(resource, name) when is_binary(name) or is_atom(name) do
    case Extension.get_persisted(resource, :calculations_by_name) do
      value when is_map(value) ->
        value[name]

      nil ->
        resource
        |> calculations()
        |> Enum.find(&(to_string(&1.name) == to_string(name)))
    end
  end

  def calculation(_, _), do: nil

  @doc "Returns all public calculations of a resource"
  @spec public_calculations(Spark.Dsl.t() | Ash.Resource.t()) ::
          list(Ash.Resource.Calculation.t())
  def public_calculations(resource) do
    resource
    |> Extension.get_entities([:calculations])
    |> Enum.filter(& &1.public?)
  end

  @doc "Get a public calculation by name"
  @spec public_calculation(Spark.Dsl.t() | Ash.Resource.t(), atom | String.t()) ::
          Ash.Resource.Calculation.t() | nil
  def public_calculation(resource, name) when is_binary(name) do
    resource
    |> calculations()
    |> Enum.find(&(to_string(&1.name) == name && &1.public?))
  end

  def public_calculation(resource, name) do
    resource
    |> calculations()
    |> Enum.find(&(&1.name == name && &1.public?))
  end

  @doc """
  Gets the type of an aggregate for a given resource.
  """
  @spec aggregate_type(Spark.Dsl.t() | Ash.Resource.t(), Ash.Resource.Aggregate.t() | atom) ::
          {:ok, Ash.Type.t()} | {:error, String.t()}
  def aggregate_type(resource, aggregate) when is_atom(aggregate) do
    aggregate_type(resource, aggregate(resource, aggregate))
  end

  def aggregate_type(resource, aggregate) do
    attribute =
      if aggregate.field do
        related = Ash.Resource.Info.related(resource, aggregate.relationship_path)
        Ash.Resource.Info.attribute(related, aggregate.field)
      end

    attribute_type =
      if attribute do
        attribute.type
      end

    attribute_constraints =
      if attribute do
        attribute.constraints
      end

    case Ash.Query.Aggregate.kind_to_type(aggregate.kind, attribute_type, attribute_constraints) do
      {:ok, type, _constraints} ->
        {:ok, type}

      other ->
        other
    end
  end

  @doc "Returns all aggregates of a resource"
  @spec aggregates(Spark.Dsl.t() | Ash.Resource.t()) :: list(Ash.Resource.Aggregate.t())
  def aggregates(resource) do
    Extension.get_entities(resource, [:aggregates])
  end

  @doc "Get an aggregate by name"
  @spec aggregate(Spark.Dsl.t() | Ash.Resource.t(), atom | String.t()) ::
          Ash.Resource.Aggregate.t() | nil
  def aggregate(resource, name) when is_binary(name) do
    resource
    |> aggregates()
    |> Enum.find(&(to_string(&1.name) == name))
  end

  def aggregate(resource, name) do
    resource
    |> aggregates()
    |> Enum.find(&(&1.name == name))
  end

  @doc "Returns all public aggregates of a resource"
  @spec public_aggregates(Spark.Dsl.t() | Ash.Resource.t()) :: list(Ash.Resource.Aggregate.t())
  def public_aggregates(resource) do
    resource
    |> Extension.get_entities([:aggregates])
    |> Enum.filter(& &1.public?)
  end

  @doc "Get an aggregate by name"
  @spec public_aggregate(Spark.Dsl.t() | Ash.Resource.t(), atom | String.t()) ::
          Ash.Resource.Aggregate.t() | nil
  def public_aggregate(resource, name) when is_binary(name) do
    resource
    |> aggregates()
    |> Enum.find(&(to_string(&1.name) == name && &1.public?))
  end

  def public_aggregate(resource, name) do
    resource
    |> aggregates()
    |> Enum.find(&(&1.name == name && &1.public?))
  end

  @doc "Returns the primary action of the given type"
  @spec primary_action!(Spark.Dsl.t() | Ash.Resource.t(), Ash.Resource.Actions.action_type()) ::
          Ash.Resource.Actions.action() | no_return
  def primary_action!(resource, type) do
    case primary_action(resource, type) do
      nil ->
        raise "Required primary #{type} action for #{inspect(Extension.get_persisted(resource, :module))}."

      action ->
        action
    end
  end

  @doc "Returns the primary action of a given type"
  @spec primary_action(Spark.Dsl.t() | Ash.Resource.t(), Ash.Resource.Actions.action_type()) ::
          Ash.Resource.Actions.action() | nil
  def primary_action(resource, type) do
    resource
    |> actions()
    |> Enum.find(&(&1.type == type && &1.primary?))
  end

  @doc "Returns the configured default actions"
  @spec default_actions(Spark.Dsl.t() | Ash.Resource.t()) ::
          list(:create | :read | :update | :destroy)
  def default_actions(resource) do
    default =
      if embedded?(resource) do
        [:create, :read, :update, :destroy]
        |> Enum.reject(&Ash.Resource.Info.action(resource, &1))
      else
        []
      end

    Extension.get_opt(
      resource,
      [:actions],
      :defaults,
      default
    )
  end

  @doc "Returns all actions of a resource"
  @spec actions(Spark.Dsl.t() | Ash.Resource.t()) :: [Ash.Resource.Actions.action()]
  def actions(resource) do
    Extension.get_entities(resource, [:actions])
  end

  @doc "Returns the action with the matching name and type on the resource"
  @spec action(Spark.Dsl.t() | Ash.Resource.t(), atom(), Ash.Resource.Actions.action_type() | nil) ::
          Ash.Resource.Actions.action() | nil
  def action(resource, name, type \\ nil) do
    # We used to need type, but we don't anymore since action names are unique
    if type do
      resource
      |> actions()
      |> Enum.find(&(&1.name == name))
      |> case do
        nil ->
          nil

        %{type: ^type} = action ->
          action

        %{type: found_type} ->
          raise ArgumentError, """
          Found an action of type #{found_type} while looking for an action of type #{type}

          Perhaps you've passed a changeset with the incorrect action type?
          """
      end
    else
      resource
      |> actions()
      |> Enum.find(&(&1.name == name))
    end
  end

  @doc "Returns true or false if the input is accepted by the action, as an argument or an attribute"
  @spec action_input?(Ash.Resource.t(), action :: atom(), input :: atom() | String.t()) ::
          boolean()
  def action_input?(resource, action, input) do
    # Extension.get_persisted(resource, {:action_inputs, action, input}) || false

    case Extension.get_persisted(resource, {:action_inputs, action}) do
      nil -> false
      map_set -> input in map_set
    end
  end

  @doc "Returns the list of possible accepted keys by an action"
  @spec action_inputs(Ash.Resource.t(), action :: atom()) ::
          MapSet.t()
  def action_inputs(resource, action) do
    Extension.get_persisted(resource, {:action_inputs, action}) || MapSet.new()
  end

  @doc "Returns the list of attributes that must be selected for an action invocation"
  @spec action_select(Ash.Resource.t(), action :: atom() | Ash.Resource.Actions.action()) ::
          list(atom) | nil
  def action_select(resource, %{name: name}) do
    action_select(resource, name)
  end

  def action_select(resource, action) do
    Extension.get_persisted(resource, {:action_select, action})
  end

  @doc "Returns all attributes of a resource"
  @spec attributes(Spark.Dsl.t() | Ash.Resource.t()) :: [Ash.Resource.Attribute.t()]
  def attributes(resource) do
    Extension.get_entities(resource, [:attributes])
  end

  @spec attribute_names(Spark.Dsl.t() | Ash.Resource.t()) :: MapSet.t()
  def attribute_names(resource) do
    Extension.get_persisted(resource, :attribute_names)
  end

  @doc "Returns all attributes of a resource with lazy non-matching-defaults"
  @spec lazy_non_matching_default_attributes(
          Spark.Dsl.t() | Ash.Resource.t(),
          type :: :create | :update
        ) :: [Ash.Resource.Attribute.t()]
  def lazy_non_matching_default_attributes(resource, :create) do
    Extension.get_persisted(resource, :create_attributes_with_non_matching_lazy_defaults) ||
      Enum.filter(attributes(resource), fn attribute ->
        !attribute.match_other_defaults? &&
          (is_function(attribute.default) or match?({_, _, _}, attribute.default))
      end)
  end

  def lazy_non_matching_default_attributes(resource, :update) do
    Extension.get_persisted(resource, :update_attributes_with_non_matching_lazy_defaults) ||
      Enum.filter(attributes(resource), fn attribute ->
        !attribute.match_other_defaults? &&
          (is_function(attribute.update_default) or match?({_, _, _}, attribute.update_default))
      end)
  end

  @doc "Returns all attributes of a resource with static defaults"
  @spec static_default_attributes(
          Spark.Dsl.t() | Ash.Resource.t(),
          type :: :create | :update
        ) :: [Ash.Resource.Attribute.t()]
  def static_default_attributes(resource, :create) do
    Extension.get_persisted(resource, :create_attributes_with_static_defaults) ||
      resource
      |> attributes()
      |> Enum.filter(fn attribute ->
        not is_nil(attribute.default) &&
          not (is_function(attribute.default) or
                 match?({_, _, _}, attribute.default))
      end)
  end

  def static_default_attributes(resource, :update) do
    Extension.get_persisted(resource, :update_attributes_with_static_defaults) ||
      resource
      |> attributes()
      |> Enum.filter(fn attribute ->
        not is_nil(attribute.update_default) &&
          not (is_function(attribute.update_default) or
                 match?({_, _, _}, attribute.update_default))
      end)
  end

  @doc "Returns all attributes of a resource with lazy matching defaults"
  @spec lazy_matching_default_attributes(
          Spark.Dsl.t() | Ash.Resource.t(),
          type :: :create | :update
        ) :: [Ash.Resource.Attribute.t()]
  def lazy_matching_default_attributes(resource, :create) do
    Extension.get_persisted(resource, :create_attributes_with_matching_defaults) ||
      Enum.filter(attributes(resource), fn attribute ->
        attribute.match_other_defaults? &&
          (is_function(attribute.default) or match?({_, _, _}, attribute.default))
      end)
  end

  def lazy_matching_default_attributes(resource, :update) do
    Extension.get_persisted(resource, :update_attributes_with_matching_defaults) ||
      resource
      |> attributes()
      |> Enum.filter(fn attribute ->
        not is_nil(attribute.update_default) &&
          not (is_function(attribute.update_default) or
                 match?({_, _, _}, attribute.update_default))
      end)
  end

  @doc "Get an attribute name from the resource"
  @spec attribute(Spark.Dsl.t() | Ash.Resource.t(), String.t() | atom) ::
          Ash.Resource.Attribute.t() | nil
  def attribute(resource, name) when is_binary(name) or is_atom(name) do
    case Extension.get_persisted(resource, :attributes_by_name) do
      value when is_map(value) ->
        value[name]

      nil ->
        resource
        |> attributes()
        |> Enum.find(&(to_string(&1.name) == to_string(name)))
    end
  end

  @doc "Returns all public attributes of a resource"
  @spec public_attributes(Spark.Dsl.t() | Ash.Resource.t()) :: [Ash.Resource.Attribute.t()]
  def public_attributes(resource) do
    resource
    |> attributes()
    |> Enum.filter(& &1.public?)
  end

  @doc "Get a public attribute name from the resource"
  @spec public_attribute(Spark.Dsl.t() | Ash.Resource.t(), String.t() | atom) ::
          Ash.Resource.Attribute.t() | nil
  def public_attribute(resource, name) do
    case attribute(resource, name) do
      %{public?: true} = attr -> attr
      _ -> nil
    end
  end

  @spec related(Spark.Dsl.t() | Ash.Resource.t(), atom() | String.t() | [atom() | String.t()]) ::
          Ash.Resource.t() | nil
  def related(resource, relationship) when not is_list(relationship) do
    related(resource, [relationship])
  end

  def related(resource, []), do: resource

  def related(resource, [path | rest]) do
    case relationship(resource, path) do
      %{destination: destination} -> related(destination, rest)
      nil -> nil
    end
  end

  @spec always_selected_attribute_names(Spark.Dsl.t() | Ash.Resource.t()) :: MapSet.t()
  def always_selected_attribute_names(resource) do
    Extension.get_persisted(resource, :always_selected_attribute_names)
  end

  @spec selected_by_default_attribute_names(Spark.Dsl.t() | Ash.Resource.t()) :: MapSet.t()
  def selected_by_default_attribute_names(resource) do
    Extension.get_persisted(resource, :selected_by_default_attribute_names)
  end

  @doc "Returns all attributes, aggregates, calculations and relationships of a resource"
  @spec fields(
          Spark.Dsl.t() | Ash.Resource.t(),
          types :: list(:attributes | :aggregates | :calculations | :relationships)
        ) :: [
          Ash.Resource.Attribute.t()
          | Ash.Resource.Aggregate.t()
          | Ash.Resource.Calculation.t()
          | Ash.Resource.Relationships.relationship()
        ]
  def fields(resource, types \\ [:attributes, :aggregates, :calculations, :relationships]) do
    Enum.flat_map(types, &Extension.get_entities(resource, [&1]))
  end

  @doc "Get a field from a resource by name"
  @spec field(Spark.Dsl.t() | Ash.Resource.t(), String.t() | atom) ::
          Ash.Resource.Attribute.t()
          | Ash.Resource.Aggregate.t()
          | Ash.Resource.Calculation.t()
          | Ash.Resource.Relationships.relationship()
          | nil
  def field(resource, name),
    do:
      attribute(resource, name) ||
        aggregate(resource, name) ||
        calculation(resource, name) ||
        relationship(resource, name)

  @doc "Returns all public attributes, aggregates, calculations and relationships of a resource"
  @spec public_fields(Spark.Dsl.t() | Ash.Resource.t()) :: [
          Ash.Resource.Attribute.t()
          | Ash.Resource.Aggregate.t()
          | Ash.Resource.Calculation.t()
          | Ash.Resource.Relationships.relationship()
        ]
  def public_fields(resource) do
    resource
    |> fields()
    |> Enum.filter(& &1.public?)
  end

  @doc "Get a public field from a resource by name"
  @spec public_field(Spark.Dsl.t() | Ash.Resource.t(), String.t() | atom) ::
          Ash.Resource.Attribute.t()
          | Ash.Resource.Aggregate.t()
          | Ash.Resource.Calculation.t()
          | Ash.Resource.Relationships.relationship()
          | nil
  def public_field(resource, name),
    do:
      public_attribute(resource, name) ||
        public_aggregate(resource, name) ||
        public_calculation(resource, name) ||
        public_relationship(resource, name)

  @doc "Determine if a field is sortable by name"
  @spec sortable?(Spark.Dsl.t() | Ash.Resource.t(), String.t() | atom,
          pagination_type: Ash.Page.type(),
          include_private?: boolean()
        ) ::
          boolean()
  def sortable?(resource, name, opts \\ []) do
    pagination_type = Keyword.get(opts, :pagination_type, :offset)
    include_private? = Keyword.get(opts, :include_private?, true)

    field = if include_private?, do: field(resource, name), else: public_field(resource, name)

    case field do
      nil ->
        false

      %{type: {:array, _}} ->
        false

      %{type: Ash.Type.Map} ->
        false

      %Ash.Resource.Relationships.BelongsTo{} ->
        false

      %Ash.Resource.Relationships.HasOne{} ->
        false

      %Ash.Resource.Relationships.HasMany{} ->
        false

      %Ash.Resource.Relationships.ManyToMany{} ->
        false

      %Ash.Resource.Calculation{calculation: {module, _}} ->
        Code.ensure_compiled(module)
        :erlang.function_exported(module, :expression, 2) && pagination_type == :offset

      %Ash.Resource.Calculation{} ->
        false

      %Ash.Resource.Aggregate{kind: :first, relationship_path: relationship_path} = aggregate ->
        related = related(resource, relationship_path)
        sortable?(related, aggregate.field) && pagination_type == :offset

      %Ash.Resource.Aggregate{} ->
        pagination_type == :offset

      _ ->
        true
    end
  end

  @doc "The data layer of the resource, or nil if it does not have one"
  @spec data_layer(Ash.Resource.t()) :: Ash.DataLayer.t() | nil
  defdelegate data_layer(resource), to: Ash.DataLayer

  @doc """
  Returns a list of extensions in use by the resource.
  """
  @spec extensions(resource :: Ash.Resource.t()) :: list(module())
  defdelegate extensions(resource), to: Spark
end
