# Role Registry Type Contracts
#
# Schema for site/config/roles.ncl — the canonical role list that
# rbac.ncl groups, DB user_roles seeds, and the Rust Role enum must agree with.

let RustVariants = ["Admin", "Moderator", "User", "Guest", "Custom"] in

{
  OneOf = fun allowed => std.contract.from_predicate (fun v => std.array.elem v allowed),

  # Single role definition — one entry per role in the application
  RoleEntry = {
    name
      | String
      | doc m%"
          Canonical role identifier.
          Must match: JWT claims, DB user_roles.name, and rbac.ncl group names.
        "%,

    description
      | String
      | doc "Human-readable description for admin UI and documentation",

    db_seed
      | Bool
      | doc m%"
          Whether this role is inserted into user_roles on initial DB migration.
          Set false for virtual roles (e.g. guest) that have no DB row.
        "%
      | default = true,

    is_system
      | Bool
      | doc "System roles cannot be deleted or renamed via admin UI"
      | default = false,

    parent_roles
      | Array String
      | doc m%"
          Role names inherited by this role (BFS expansion, same semantics as
          rbac.ncl parent_groups). Names must resolve to other entries in this array.
        "%
      | default = [],

    rust_variant
      | OneOf RustVariants
      | doc m%"
          Corresponding Rust enum variant in rustelo_core_lib::auth::Role.
          Use "Custom" for roles that have no dedicated variant.
        "%
      | default = "Custom",

    db_level
      | Number
      | doc "Privilege level for UI ordering (higher = more privilege)"
      | default = 0,

    is_default
      | Bool
      | doc m%"
          Role assigned automatically on new user registration.
          Exactly one role in the registry must have is_default = true.
          Must match registration_role in site/config/auth.toml.
        "%
      | default = false,
  },

  # Validates that exactly one role carries is_default = true.
  ExactlyOneDefaultRole =
    std.contract.custom (fun _label value =>
      let defaults = value.roles |> std.array.filter (fun r => r.is_default) in
      let n = std.array.length defaults in
      if n == 1 then
        'Ok value
      else
        'Error {
          message =
            "Exactly one role must have is_default = true, found "
            ++ std.string.from_number n,
        }
    ),

  # Root contract applied to site/config/roles.ncl
  RolesConfig = {
    roles
      | Array RoleEntry
      | doc m%"
          Authoritative role registry.

          Invariants enforced at eval time:
          - Exactly one entry has is_default = true (enforced by ExactlyOneDefaultRole)
          - Every rbac.ncl group name must appear here
          - Every parent_role reference must resolve to another entry in this array
          - DB migrations must seed all entries where db_seed = true
          - registration_role in auth.toml must match the entry with is_default = true
        "%,
  },
}
