defmodule Mv.Authorization.Role do
  @moduledoc """
  Represents a user role that references a permission set.

  Roles are stored in the database and link users to permission sets.
  Each role has a `permission_set_name` that references one of the four
  hardcoded permission sets defined in `Mv.Authorization.PermissionSets`.

  ## Fields

  - `name` - Unique role name (e.g., "Vorstand", "Admin")
  - `description` - Human-readable description of the role
  - `permission_set_name` - Must be one of: "own_data", "read_only", "normal_user", "admin"
  - `is_system_role` - If true, role cannot be deleted (protects critical roles like "Mitglied")

  ## Relationships

  - `has_many :users` - Users assigned to this role

  ## Validations

  - `permission_set_name` must be a valid permission set (checked against PermissionSets.all_permission_sets/0)
  - `name` must be unique
  - System roles cannot be deleted (enforced via validation)

  ## Examples

      # Create a new role
      {:ok, role} = Mv.Authorization.create_role(%{
        name: "Vorstand",
        description: "Board member with read access",
        permission_set_name: "read_only"
      })
      
      # List all roles
      {:ok, roles} = Mv.Authorization.list_roles()
  """
  use Ash.Resource,
    domain: Mv.Authorization,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer]

  postgres do
    table "roles"
    repo Mv.Repo

    references do
      # Prevent deletion of roles that are assigned to users
      reference :users, on_delete: :restrict
    end
  end

  code_interface do
    define :create_role
    define :list_roles, action: :read
    define :update_role
    define :destroy_role, action: :destroy
  end

  actions do
    defaults [:read]

    create :create_role do
      primary? true
      # is_system_role is intentionally excluded - should only be set via seeds/internal actions
      accept [:name, :description, :permission_set_name]
      # Note: In Ash 3.0, require_atomic? is not available for create actions
      # Custom validations will still work
    end

    create :create_role_with_system_flag do
      description "Internal action to create roles, allowing `is_system_role` to be set. Used by seeds and migrations."
      accept [:name, :description, :permission_set_name, :is_system_role]
    end

    update :update_role do
      primary? true
      # is_system_role is intentionally excluded - should only be set via seeds/internal actions
      accept [:name, :description, :permission_set_name]
      # Required because custom validation functions cannot be executed atomically
      require_atomic? false
    end

    destroy :destroy do
      # Required because custom validation functions cannot be executed atomically
      require_atomic? false
    end
  end

  policies do
    policy action_type([:read, :create, :update, :destroy]) do
      description "Role access: read for all permission sets, create/update/destroy for admin only (PermissionSets)"
      authorize_if Mv.Authorization.Checks.HasPermission
    end
  end

  validations do
    validate one_of(
               :permission_set_name,
               Mv.Authorization.PermissionSets.all_permission_sets()
               |> Enum.map(&Atom.to_string/1)
             ),
             message:
               "must be one of: #{Mv.Authorization.PermissionSets.all_permission_sets() |> Enum.map_join(", ", &Atom.to_string/1)}"

    validate fn changeset, _context ->
               if changeset.data.is_system_role do
                 {:error,
                  field: :is_system_role,
                  message:
                    "Cannot delete system role. System roles are required for the application to function."}
               else
                 :ok
               end
             end,
             on: [:destroy]
  end

  attributes do
    uuid_v7_primary_key :id

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

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

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

    attribute :is_system_role, :boolean do
      allow_nil? false
      default false
      public? true
    end

    timestamps()
  end

  relationships do
    has_many :users, Mv.Accounts.User do
      destination_attribute :role_id
    end
  end

  identities do
    identity :unique_name, [:name]
  end

  @doc """
  Loads the "Mitglied" role without authorization (for bootstrap operations).

  This is a helper function to avoid code duplication when loading the default
  role in changes, migrations, and test setup.

  ## Returns

  - `{:ok, %Mv.Authorization.Role{}}` - The "Mitglied" role
  - `{:ok, nil}` - Role doesn't exist
  - `{:error, term()}` - Error during lookup

  ## Examples

      {:ok, mitglied_role} = Mv.Authorization.Role.get_mitglied_role()
      # => {:ok, %Mv.Authorization.Role{name: "Mitglied", ...}}

      {:ok, nil} = Mv.Authorization.Role.get_mitglied_role()
      # => Role doesn't exist (e.g., in test environment before seeds run)
  """
  @spec get_mitglied_role() :: {:ok, t() | nil} | {:error, term()}
  def get_mitglied_role do
    require Ash.Query

    __MODULE__
    |> Ash.Query.filter(name == "Mitglied")
    |> Ash.read_one(authorize?: false, domain: Mv.Authorization)
  end

  @doc """
  Returns the Admin role if it exists.

  Used by release tasks (e.g. seed_admin) and OIDC role sync to assign the admin role.
  """
  @spec get_admin_role() :: {:ok, t() | nil} | {:error, term()}
  def get_admin_role do
    require Ash.Query

    __MODULE__
    |> Ash.Query.filter(name == "Admin")
    |> Ash.read_one(authorize?: false, domain: Mv.Authorization)
  end
end
