defmodule Mv.Authorization.PermissionSets do
  @moduledoc """
  Defines the four hardcoded permission sets for the application.

  Each permission set specifies:
  - Resource permissions (what CRUD operations on which resources)
  - Page permissions (which LiveView pages can be accessed)
  - Scopes (own, linked, all)

  ## Permission Sets

  1. **own_data** - Default for "Mitglied" role
     - Can only access own user data and linked member/custom field values
     - Cannot create new members or manage system

  2. **read_only** - For "Vorstand" and "Buchhaltung" roles
     - Can read all member data
     - Cannot create, update, or delete

  3. **normal_user** - For "Kassenwart" role
     - Create/Read/Update members (no delete for safety), full CRUD on custom field values
     - Cannot manage custom fields or users

  4. **admin** - For "Admin" role
     - Unrestricted access to all resources
     - Can manage users, roles, custom fields

  ## Usage

      # Get permissions for a role's permission set
      permissions = PermissionSets.get_permissions(:admin)

      # Check if a permission set name is valid
      PermissionSets.valid_permission_set?("read_only")  # => true

      # Convert string to atom safely
      {:ok, atom} = PermissionSets.permission_set_name_to_atom("own_data")

  ## Performance

  All functions are pure and intended to be constant-time. Permission lookups
  are very fast (typically < 1 microsecond in practice) as they are simple
  pattern matches and map lookups with no database queries or external calls.
  """

  @type scope :: :own | :linked | :all
  @type action :: :read | :create | :update | :destroy

  @type resource_permission :: %{
          resource: String.t(),
          action: action(),
          scope: scope(),
          granted: boolean()
        }

  @type permission_set :: %{
          resources: [resource_permission()],
          pages: [String.t()]
        }

  # DRY helpers for shared resource permission lists (used in own_data, read_only, normal_user, admin)
  defp perm(resource, action, scope),
    do: %{resource: resource, action: action, scope: scope, granted: true}

  # All four CRUD actions for a resource with scope :all (used for admin)
  defp perm_all(resource),
    do: [
      perm(resource, :read, :all),
      perm(resource, :create, :all),
      perm(resource, :update, :all),
      perm(resource, :destroy, :all)
    ]

  # User: read/update own credentials only (all non-admin sets allow password changes)
  defp user_own_credentials, do: [perm("User", :read, :own), perm("User", :update, :own)]

  defp group_read_all, do: [perm("Group", :read, :all)]
  defp custom_field_read_all, do: [perm("CustomField", :read, :all)]
  defp membership_fee_type_read_all, do: [perm("MembershipFeeType", :read, :all)]
  defp membership_fee_cycle_read_all, do: [perm("MembershipFeeCycle", :read, :all)]
  defp role_read_all, do: [perm("Role", :read, :all)]

  @doc """
  Returns the list of all valid permission set names.

  ## Examples

      iex> PermissionSets.all_permission_sets()
      [:own_data, :read_only, :normal_user, :admin]
  """
  @spec all_permission_sets() :: [atom()]
  def all_permission_sets do
    [:own_data, :read_only, :normal_user, :admin]
  end

  @doc """
  Returns permissions for the given permission set.

  ## Examples

      iex> permissions = PermissionSets.get_permissions(:admin)
      iex> Enum.any?(permissions.resources, fn p ->
      ...>   p.resource == "User" and p.action == :destroy
      ...> end)
      true

      iex> PermissionSets.get_permissions(:invalid)
      ** (ArgumentError) invalid permission set: :invalid. Must be one of: [:own_data, :read_only, :normal_user, :admin]
  """
  @spec get_permissions(atom()) :: permission_set()

  def get_permissions(set) when set not in [:own_data, :read_only, :normal_user, :admin] do
    raise ArgumentError,
          "invalid permission set: #{inspect(set)}. Must be one of: #{inspect(all_permission_sets())}"
  end

  def get_permissions(:own_data) do
    %{
      resources:
        user_own_credentials() ++
          [
            perm("Member", :read, :linked),
            perm("Member", :update, :linked),
            perm("CustomFieldValue", :read, :linked),
            perm("CustomFieldValue", :update, :linked),
            perm("CustomFieldValue", :create, :linked),
            perm("CustomFieldValue", :destroy, :linked)
          ] ++
          custom_field_read_all() ++
          group_read_all() ++
          [perm("MemberGroup", :read, :linked)] ++
          membership_fee_type_read_all() ++
          [perm("MembershipFeeCycle", :read, :linked)] ++
          role_read_all(),
      pages: [
        # No "/" - Mitglied must not see member index at root (same content as /members).
        # Own profile (sidebar links to /users/:id) and own user edit
        "/users/:id",
        "/users/:id/edit",
        "/users/:id/show/edit",
        # Linked member detail and edit (data access filtered by policy scope: :linked)
        "/members/:id",
        "/members/:id/edit",
        "/members/:id/show/edit"
      ]
    }
  end

  def get_permissions(:read_only) do
    %{
      resources:
        user_own_credentials() ++
          [
            perm("Member", :read, :all),
            perm("CustomFieldValue", :read, :all)
          ] ++
          custom_field_read_all() ++
          group_read_all() ++
          [perm("MemberGroup", :read, :all)] ++
          membership_fee_type_read_all() ++
          membership_fee_cycle_read_all() ++
          role_read_all(),
      pages: [
        "/",
        # Own profile (sidebar links to /users/:id; redirect target must be allowed)
        "/users/:id",
        "/users/:id/edit",
        "/users/:id/show/edit",
        # Member list and CSV export
        "/members",
        "/members/export.csv",
        # Member detail
        "/members/:id",
        # Custom field values overview
        "/custom_field_values",
        # Custom field value detail
        "/custom_field_values/:id",
        # Groups overview
        "/groups",
        # Group detail
        "/groups/:slug",
        # Statistics
        "/statistics"
      ]
    }
  end

  def get_permissions(:normal_user) do
    %{
      resources:
        user_own_credentials() ++
          [
            perm("Member", :read, :all),
            perm("Member", :create, :all),
            perm("Member", :update, :all),
            # destroy intentionally omitted for safety
            perm("CustomFieldValue", :read, :all),
            perm("CustomFieldValue", :create, :all),
            perm("CustomFieldValue", :update, :all),
            perm("CustomFieldValue", :destroy, :all)
          ] ++
          custom_field_read_all() ++
          [
            perm("Group", :read, :all),
            perm("Group", :create, :all),
            perm("Group", :update, :all),
            perm("Group", :destroy, :all)
          ] ++
          [
            perm("MemberGroup", :read, :all),
            perm("MemberGroup", :create, :all),
            perm("MemberGroup", :destroy, :all)
          ] ++
          membership_fee_type_read_all() ++
          [
            perm("MembershipFeeCycle", :read, :all),
            perm("MembershipFeeCycle", :create, :all),
            perm("MembershipFeeCycle", :update, :all),
            perm("MembershipFeeCycle", :destroy, :all)
          ] ++
          role_read_all(),
      pages: [
        "/",
        # Own profile (sidebar links to /users/:id; redirect target must be allowed)
        "/users/:id",
        "/users/:id/edit",
        "/users/:id/show/edit",
        "/members",
        "/members/export.csv",
        # Create member
        "/members/new",
        "/members/:id",
        # Edit member
        "/members/:id/edit",
        "/members/:id/show/edit",
        "/custom_field_values",
        # Custom field value detail
        "/custom_field_values/:id",
        "/custom_field_values/new",
        "/custom_field_values/:id/edit",
        # Groups overview
        "/groups",
        # Create group
        "/groups/new",
        # Group detail
        "/groups/:slug",
        # Edit group
        "/groups/:slug/edit",
        # Statistics
        "/statistics"
      ]
    }
  end

  def get_permissions(:admin) do
    # MemberGroup has no :update action in the domain; use read/create/destroy only
    member_group_perms = [
      perm("MemberGroup", :read, :all),
      perm("MemberGroup", :create, :all),
      perm("MemberGroup", :destroy, :all)
    ]

    %{
      resources:
        perm_all("User") ++
          perm_all("Member") ++
          perm_all("CustomFieldValue") ++
          perm_all("CustomField") ++
          perm_all("Role") ++
          perm_all("Group") ++
          member_group_perms ++
          perm_all("MembershipFeeType") ++
          perm_all("MembershipFeeCycle"),
      pages: [
        # Explicit admin-only pages (for clarity and future restrictions)
        "/settings",
        "/membership_fee_settings",
        # Wildcard: Admin can access all pages
        "*"
      ]
    }
  end

  def get_permissions(invalid) do
    raise ArgumentError,
          "invalid permission set: #{inspect(invalid)}. Must be one of: #{inspect(all_permission_sets())}"
  end

  @doc """
  Checks if a permission set name (string or atom) is valid.

  ## Examples

      iex> PermissionSets.valid_permission_set?("admin")
      true

      iex> PermissionSets.valid_permission_set?(:read_only)
      true

      iex> PermissionSets.valid_permission_set?("invalid")
      false
  """
  @spec valid_permission_set?(any()) :: boolean()
  def valid_permission_set?(name) when is_binary(name) do
    case permission_set_name_to_atom(name) do
      {:ok, _atom} -> true
      {:error, _} -> false
    end
  end

  def valid_permission_set?(name) when is_atom(name) do
    name in all_permission_sets()
  end

  def valid_permission_set?(_), do: false

  @doc """
  Converts a permission set name string to atom safely.

  ## Examples

      iex> PermissionSets.permission_set_name_to_atom("admin")
      {:ok, :admin}

      iex> PermissionSets.permission_set_name_to_atom("invalid")
      {:error, :invalid_permission_set}
  """
  @spec permission_set_name_to_atom(String.t()) ::
          {:ok, atom()} | {:error, :invalid_permission_set}
  def permission_set_name_to_atom(name) when is_binary(name) do
    atom = String.to_existing_atom(name)

    if valid_permission_set?(atom) do
      {:ok, atom}
    else
      {:error, :invalid_permission_set}
    end
  rescue
    ArgumentError -> {:error, :invalid_permission_set}
  end
end
