/**
 * Role system — builtin roles, role specs, and extensible role schema factory.
 *
 * Defines the authorization policy vocabulary: which roles exist, their
 * required credential types, the scope kinds each role applies to, and
 * the grant paths through which each role can be granted. Each role
 * gets a structured `RoleSpec`; the factory `create_role_schema` merges
 * builtins with consumer-declared specs and validates every cross-axis
 * field against the corresponding open registries
 * (`create_credential_type_schema`, `create_scope_kind_schema`,
 * `create_grant_path_schema`) at construction time so misconfigurations
 * fire at server startup, not at first call.
 *
 * `RoleSpec` carries the four cross-axis fields that the dispatcher
 * branches on: credential type, scope kind, grant path, and the
 * role-name itself. v1 keeps the cross-axis fields informative-only
 * (registry-membership validation, no INSERT-time enforcement); v2 may
 * add `(role, scope_kind)` enforcement once the shape is clear from
 * real consumer usage.
 *
 * @module
 */
import { z } from 'zod';
import { CREDENTIAL_TYPE_DAEMON_TOKEN, } from './credential_type_schema.js';
import { GRANT_PATH_ADMIN, GRANT_PATH_BOOTSTRAP, } from './grant_path_schema.js';
/** Valid role name: lowercase letters and underscores, no leading/trailing underscore. */
export const RoleName = z
    .string()
    .regex(/^[a-z][a-z_]*[a-z]$|^[a-z]$/, 'Role names must be lowercase letters and underscores (a-z_), no leading/trailing underscore');
// Builtin roles — provided by fuz_app, always available.
/** System-level role. Requires daemon token (filesystem proof). Controls the keep. */
export const ROLE_KEEPER = 'keeper';
/** App-level administrative role. Granted via the admin path. */
export const ROLE_ADMIN = 'admin';
/** The builtin role names as a const tuple. */
export const BUILTIN_ROLES = [ROLE_KEEPER, ROLE_ADMIN];
/** Zod schema for builtin roles only. */
export const BuiltinRole = z.enum(BUILTIN_ROLES);
/**
 * Builtin role specs, keyed by role name. Not overridable by consumers
 * — read once at startup by `create_role_schema` and the action
 * factories that fall back to builtins when no consumer `roles` is
 * supplied. `ReadonlyMap` encodes the contract; runtime mutation has
 * no effect on already-built role schemas (the factory copies entries
 * into a fresh `Map`).
 */
export const builtin_role_specs_by_name = new Map([
    [
        ROLE_KEEPER,
        {
            name: ROLE_KEEPER,
            description: 'System-level role; controls the keep. Requires the daemon-token credential and lands via the bootstrap grant path.',
            required_credential_types: [CREDENTIAL_TYPE_DAEMON_TOKEN],
            applicable_scope_kinds: [],
            grant_paths: [GRANT_PATH_BOOTSTRAP],
        },
    ],
    [
        ROLE_ADMIN,
        {
            name: ROLE_ADMIN,
            description: 'App-level administrative role. Web-grantable through the admin path; manages users and content.',
            required_credential_types: [],
            applicable_scope_kinds: [],
            grant_paths: [GRANT_PATH_ADMIN],
        },
    ],
]);
const validate_registry_membership = (role_name, field, values, registry) => {
    if (!registry || !values)
        return;
    for (const value of values) {
        if (!registry.has(value)) {
            throw new Error(`Role "${role_name}" declares ${field}="${value}" which is not a registered ${field.replace(/s$/, '')}`);
        }
    }
};
/**
 * Create a role schema and spec map that extends the builtins with
 * app-defined roles.
 *
 * Call once at server init. The returned `Role` schema validates role
 * strings at I/O boundaries (grant endpoint, role_grant queries). The
 * `role_specs` map is read by middleware for `required_credential_types`
 * checks and by admin / self-service factories to derive their default
 * eligibility filters from `RoleSpec.grant_paths`.
 *
 * Construction-time guards (all fire on misconfiguration):
 *
 * 1. Every `consumer_roles[i].name` matches `RoleName` regex.
 * 2. No two consumer roles share a name.
 * 3. No consumer role collides with a builtin (`keeper` / `admin`).
 * 4. When `options.credential_types` is supplied, every entry in
 *    `required_credential_types` is registered in that map.
 * 5. When `options.scope_kinds` is supplied, every entry in
 *    `applicable_scope_kinds` is registered in that map. (Builtins
 *    declare empty `applicable_scope_kinds`, so they pass any registry.)
 * 6. When `options.grant_paths` is supplied, every entry in
 *    `grant_paths` is registered in that map. (Builtins use only
 *    `'admin'` and `'bootstrap'`, both of which are builtin grant
 *    paths, so they pass the default registry from
 *    `create_grant_path_schema()`.)
 *
 * @param consumer_roles - app-defined role specs
 * @param options - optional registries for cross-axis validation
 * @returns `{Role, role_specs}` — Zod schema and full spec map
 *
 * @throws Error if any `consumer_roles` entry fails any of the construction-time guards above
 *
 * @example
 * ```ts
 * // visiones — opt into all four registries for full construction-time validation
 * const credential_types = create_credential_type_schema();
 * const scope_kinds = create_scope_kind_schema({
 *   classroom: {description: 'A classroom — teacher and student role_grants scope here.'},
 * });
 * const grant_paths = create_grant_path_schema();
 *
 * const {Role, role_specs} = create_role_schema(
 *   [
 *     {
 *       name: 'teacher',
 *       description: 'Educator role. Web-grantable; applies at classroom scope.',
 *       grant_paths: ['admin'],
 *       applicable_scope_kinds: ['classroom'],
 *     },
 *   ],
 *   {credential_types, scope_kinds, grant_paths},
 * );
 * ```
 */
export const create_role_schema = (consumer_roles, options = {}) => {
    const credential_types_registry = options.credential_types?.credential_types ?? null;
    const scope_kinds_registry = options.scope_kinds?.scope_kinds ?? null;
    const grant_paths_registry = options.grant_paths?.grant_paths ?? null;
    const seen = new Set();
    for (const spec of consumer_roles) {
        const parsed = RoleName.safeParse(spec.name);
        if (!parsed.success) {
            throw new Error(`Invalid role name "${spec.name}": ${parsed.error.issues[0].message}`);
        }
        if (builtin_role_specs_by_name.has(spec.name)) {
            throw new Error(`App role "${spec.name}" collides with builtin role`);
        }
        if (seen.has(spec.name)) {
            throw new Error(`Duplicate role name "${spec.name}"`);
        }
        seen.add(spec.name);
        validate_registry_membership(spec.name, 'required_credential_types', spec.required_credential_types, credential_types_registry);
        validate_registry_membership(spec.name, 'applicable_scope_kinds', spec.applicable_scope_kinds, scope_kinds_registry);
        validate_registry_membership(spec.name, 'grant_paths', spec.grant_paths, grant_paths_registry);
    }
    const role_specs = new Map(builtin_role_specs_by_name);
    for (const spec of consumer_roles) {
        role_specs.set(spec.name, spec);
    }
    const all_names = [...role_specs.keys()];
    const Role = z.enum(all_names);
    return { Role, role_specs };
};
/**
 * Predicate over a `RoleSpec` map: does the named role include the given
 * grant path? Returns `false` for unknown roles. Used by
 * `admin_actions.create_admin_actions` (path = `'admin'`) and
 * `self_service_role_actions.create_self_service_role_actions` (path =
 * `'self_service'`) to derive their default eligibility filters.
 */
export const role_has_grant_path = (role_specs, role, grant_path) => {
    const spec = role_specs.get(role);
    return !!spec?.grant_paths?.includes(grant_path);
};
/** Filter helper: list every role whose `grant_paths` includes the given path. */
export const list_roles_with_grant_path = (role_specs, grant_path) => {
    const out = [];
    for (const [name, spec] of role_specs) {
        if (spec.grant_paths?.includes(grant_path))
            out.push(name);
    }
    return out;
};
