defmodule SpazioSolazzo.Accounts.User do
  @moduledoc """
  Represents a user in the system with magic link authentication.
  """

  use Ash.Resource,
    otp_app: :spazio_solazzo,
    domain: SpazioSolazzo.Accounts,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshAuthentication]

  authentication do
    add_ons do
      log_out_everywhere do
        apply_on_password_change? true
      end
    end

    tokens do
      enabled? true
      token_resource SpazioSolazzo.Accounts.Token
      signing_secret SpazioSolazzo.Secrets
      store_all_tokens? true
      require_token_presence_for_authentication? true
    end

    strategies do
      magic_link do
        identity_field :email
        registration_enabled? true
        require_interaction? true
        sender SpazioSolazzo.Accounts.User.Senders.SendMagicLinkEmail
      end

      remember_me :remember_me
    end
  end

  postgres do
    table "users"
    repo SpazioSolazzo.Repo
  end

  actions do
    defaults [:read]

    read :get_by_email do
      description "Looks up a user by their email"
      argument :email, :ci_string, allow_nil?: false
      get? true
      filter expr(email == ^arg(:email))
    end

    create :sign_in_with_magic_link do
      description "Sign in or register a user with magic link."

      argument :token, :string do
        description "The token from the magic link that was sent to the user"
        allow_nil? false
      end

      argument :remember_me, :boolean do
        description "Whether to generate a remember me token"
        allow_nil? true
      end

      argument :name, :string do
        description "User's full name (required for new users)"
        allow_nil? true
      end

      argument :phone_number, :string do
        description "User's phone number (required for new users)"
        allow_nil? true
      end

      upsert? true
      upsert_identity :unique_email
      upsert_fields [:email, :name, :phone_number]

      # Uses the information from the token to create or sign in the user
      change AshAuthentication.Strategy.MagicLink.SignInChange

      # Conditionally validate name and phone_number for new users
      change SpazioSolazzo.Accounts.User.Changes.ParseRegistrationFields

      change {AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenChange,
              strategy_name: :remember_me}

      metadata :token, :string do
        allow_nil? false
      end
    end

    action :request_magic_link do
      argument :email, :ci_string, allow_nil?: false
      run AshAuthentication.Strategy.MagicLink.Request
    end

    update :update_profile do
      description "Update user profile (name and phone number)"
      accept [:name, :phone_number]
      require_atomic? false
    end

    update :make_admin do
      accept []
      change set_attribute(:role, :admin)
    end

    destroy :terminate_account do
      description "Delete user account with optional booking data removal"
      require_atomic? false

      argument :delete_history, :boolean do
        description "Whether to permanently delete all booking history"
        default false
      end

      change SpazioSolazzo.Accounts.User.Changes.HandleBookingsOnAccountDeletion
    end
  end

  policies do
    bypass AshAuthentication.Checks.AshAuthenticationInteraction do
      authorize_if always()
    end

    policy action(:make_admin) do
      authorize_if never()
    end

    policy action_type(:read) do
      authorize_if always()
    end

    policy action_type(:update) do
      authorize_if expr(id == ^actor(:id))
    end

    policy action_type(:destroy) do
      authorize_if expr(id == ^actor(:id))
    end
  end

  # Make sure it is loaded in all read actions
  preparations do
    prepare build(load: [:is_admin])
  end

  # Make sure it is loaded in all write actions
  changes do
    change load(:is_admin)
  end

  attributes do
    uuid_primary_key :id

    attribute :email, :ci_string do
      allow_nil? false
      public? true
    end

    attribute :name, :string do
      allow_nil? false
      public? true
    end

    attribute :phone_number, :string do
      allow_nil? true
      public? true
    end

    attribute :role, :atom do
      allow_nil? false
      public? false
      constraints one_of: [:customer, :admin]
      default :customer
    end
  end

  calculations do
    calculate :is_admin, :boolean, expr(role == :admin)
  end

  identities do
    identity :unique_email, [:email]
  end
end
