defmodule Ash.Resource.Attribute do
  @moduledoc "Represents an attribute on a resource"

  defstruct [
    :name,
    :type,
    :allow_nil?,
    :generated?,
    :primary_key?,
    :public?,
    :writable?,
    :always_select?,
    :select_by_default?,
    :default,
    :update_default,
    :description,
    :source,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    sortable?: true,
    constraints: []
  ]

  defmodule Helpers do
    @moduledoc false

    @doc """
    Defines create and update timestamp attributes.

    Shorthand for `d:Ash.Resource.Dsl.attributes.create_timestamp` and
    `d:Ash.Resource.Dsl.attributes.update_timestamp` with the attribute names
    `:inserted_at` and `:updated_at` respectively.  Any options passed to this
    helper are passed to both timestamp macros.
    """
    defmacro timestamps(opts \\ []) do
      quote do
        require Ash.Resource
        Ash.Resource.timestamps(unquote(opts))
      end
    end
  end

  @type t :: %__MODULE__{
          name: atom(),
          constraints: Keyword.t(),
          type: Ash.Type.t(),
          primary_key?: boolean(),
          public?: boolean(),
          sortable?: boolean(),
          select_by_default?: boolean(),
          default: nil | term | (-> term),
          update_default: nil | term | (-> term) | (Ash.Resource.record() -> term),
          sensitive?: boolean(),
          writable?: boolean()
        }

  @schema [
    name: [
      type: :atom,
      doc: "The name of the attribute.",
      required: true
    ],
    type: [
      type: Ash.OptionsHelpers.ash_type(),
      doc: "The type of the attribute. See `Ash.Type` for more.",
      required: true
    ],
    constraints: [
      type: :keyword_list,
      doc: """
      Constraints to provide to the type when casting the value. For more, see `Ash.Type`.
      """
    ],
    description: [
      type: :string,
      doc: "An optional description for the attribute."
    ],
    sensitive?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the attribute value contains sensitive information, like PII(Personally Identifiable Information). See the [Sensitive Data guide](/documentation/topics/security/sensitive-data.md) for more.
      """
    ],
    source: [
      type: :atom,
      doc: """
      If the field should be mapped to a different name in the data layer. Support varies by data layer.
      """
    ],
    select_by_default?: [
      type: :boolean,
      default: true,
      doc: """
      Whether or not the attribute is selected by default.
      """
    ],
    always_select?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not to ensure this attribute is always selected when reading from the database, regardless of applied select statements.
      """
    ],
    primary_key?: [
      type: :boolean,
      default: false,
      doc: """
      Whether the attribute is the primary key. Composite primary key is also possible by using `primary_key? true` in more than one attribute. If primary_key? is true, allow_nil? must be false.
      """
    ],
    allow_nil?: [
      type: :boolean,
      default: true,
      doc: """
      Whether or not the attribute can be set to nil. If nil value is given error is raised.
      """
    ],
    generated?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the value may be generated by the data layer.
      """
    ],
    writable?: [
      type: :boolean,
      default: true,
      doc: """
      Whether or not the value can be written to. Non-writable attributes can still be written with `Ash.Changeset.force_change_attribute/3`.
      """
    ],
    public?: [
      type: :boolean,
      default: false,
      doc: """
      Whether or not the attribute should be shown over public interfaces. See the [sensitive data guide](/documentation/topics/security/sensitive-data.md) for more.
      """
    ],
    default: [
      type: {:or, [{:mfa_or_fun, 0}, :literal]},
      doc: """
      A value to be set on all creates, unless a value is being provided already.  Note: The default value is casted according to the type's `Ash.Type.*` module, before it is saved.  For `:string`, for example, if `constraints: [allow_empty?: _]` is false, the value `\"\"` will be cast to `nil`.  See the `:constraints` option, the `:allow_nil?` option, and the relevant `Ash.Type.*` documentation.
      """
    ],
    update_default: [
      type: {:or, [{:mfa_or_fun, 0}, :literal]},
      doc: "A value to be set on all updates, unless a value is being provided already."
    ],
    filterable?: [
      type: {:or, [:boolean, {:in, [:simple_equality]}]},
      default: true,
      doc: """
      Whether or not the attribute can be referenced in filters.
      """
    ],
    sortable?: [
      default: true,
      type: :boolean,
      doc: """
      Whether or not the attribute can be referenced in sorts.
      """
    ],
    match_other_defaults?: [
      type: :boolean,
      default: false,
      doc: """
      Ensures that other attributes that use the same "lazy" default (a function or an mfa), use the same default value. Has no effect unless `default` is a zero argument function.
      """
    ]
  ]

  @create_timestamp_schema @schema
                           |> Spark.Options.Helpers.set_default!(:writable?, false)
                           |> Spark.Options.Helpers.set_default!(:default, &DateTime.utc_now/0)
                           |> Spark.Options.Helpers.set_default!(:match_other_defaults?, true)
                           |> Spark.Options.Helpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
                           |> Spark.Options.Helpers.set_default!(:allow_nil?, false)
                           |> Ash.OptionsHelpers.hide_all_except([:name])

  @update_timestamp_schema @schema
                           |> Spark.Options.Helpers.set_default!(:writable?, false)
                           |> Spark.Options.Helpers.set_default!(:match_other_defaults?, true)
                           |> Spark.Options.Helpers.set_default!(:default, &DateTime.utc_now/0)
                           |> Spark.Options.Helpers.set_default!(
                             :update_default,
                             &DateTime.utc_now/0
                           )
                           |> Spark.Options.Helpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
                           |> Spark.Options.Helpers.set_default!(:allow_nil?, false)
                           |> Ash.OptionsHelpers.hide_all_except([:name])

  @uuid_primary_key_schema @schema
                           |> Spark.Options.Helpers.set_default!(:public?, true)
                           |> Spark.Options.Helpers.set_default!(:writable?, false)
                           |> Spark.Options.Helpers.set_default!(:default, &Ash.UUID.generate/0)
                           |> Spark.Options.Helpers.set_default!(:primary_key?, true)
                           |> Spark.Options.Helpers.set_default!(:type, :uuid)
                           |> Keyword.delete(:allow_nil?)
                           |> Ash.OptionsHelpers.hide_all_except([:name])

  @uuid_v7_primary_key_schema @schema
                              |> Spark.Options.Helpers.set_default!(:public?, true)
                              |> Spark.Options.Helpers.set_default!(:writable?, false)
                              |> Spark.Options.Helpers.set_default!(
                                :default,
                                &Ash.UUIDv7.generate/0
                              )
                              |> Spark.Options.Helpers.set_default!(:primary_key?, true)
                              |> Spark.Options.Helpers.set_default!(:type, :uuid_v7)
                              |> Keyword.delete(:allow_nil?)
                              |> Ash.OptionsHelpers.hide_all_except([:name])

  @integer_primary_key_schema @schema
                              |> Spark.Options.Helpers.set_default!(:public?, true)
                              |> Spark.Options.Helpers.set_default!(:writable?, false)
                              |> Spark.Options.Helpers.set_default!(:primary_key?, true)
                              |> Spark.Options.Helpers.set_default!(:generated?, true)
                              |> Spark.Options.Helpers.set_default!(:type, :integer)
                              |> Keyword.delete(:allow_nil?)
                              |> Ash.OptionsHelpers.hide_all_except([:name])

  def transform(attribute) do
    Ash.Type.set_type_transformation(%{attribute | source: attribute.source || attribute.name})
  end

  @doc false
  def attribute_schema, do: @schema
  def create_timestamp_schema, do: @create_timestamp_schema
  def update_timestamp_schema, do: @update_timestamp_schema
  def uuid_primary_key_schema, do: @uuid_primary_key_schema
  def uuid_v7_primary_key_schema, do: @uuid_v7_primary_key_schema
  def integer_primary_key_schema, do: @integer_primary_key_schema
end
