/**
 * Request context middleware and role_grant checking helpers.
 *
 * Two-phase identity resolution:
 *
 * 1. **Authentication (middleware)** — `create_request_context_middleware`,
 *    `bearer_auth`, and `daemon_token_middleware` validate the credential
 *    (session cookie, bearer token, daemon token) and set `c.var.account_id`
 *    + `c.var.credential_type` on the Hono context. They do not resolve
 *    an acting actor or load role_grants; `REQUEST_CONTEXT_KEY` stays null at
 *    this stage, so account-grain identity is the only thing known.
 * 2. **Authorization (route-spec wrapper / RPC dispatcher)** — after input
 *    validation, the per-route layer inspects the route. If the input
 *    schema declared `acting?: ActingActor` (reference equality with the
 *    canonical `ActingActor` schema) or the auth requires role_grants
 *    (`role` / `keeper`), `apply_authorization_phase` resolves the actor
 *    against `c.var.account_id` plus the validated `acting` value via
 *    `resolve_acting_actor`, builds the `{account, actor, role_grants}`
 *    context via `build_request_context`, and sets it on
 *    `REQUEST_CONTEXT_KEY` before auth guards fire. Authenticated routes
 *    that don't need an actor still get an account-only context via
 *    `build_account_context` so handler signatures stay uniform.
 *
 * Account-grain operations (logout, password_change, account_verify,
 * etc.) declare neither `acting` nor role_grant-requiring auth, so no actor
 * is resolved and their handlers see a `RequestContext` with
 * `actor: null` + empty `role_grants`. They never trigger `actor_required`,
 * which is what makes multi-actor logout work without first picking a
 * persona.
 *
 * `build_request_context` loads `account → actor → role_grants` and verifies
 * the `actor.account_id === account.id` binding. `refresh_role_grants`
 * reloads role_grants on an existing context.
 *
 * @module
 */
import { is_role_grant_active } from './account_schema.js';
import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from './account_queries.js';
import { query_role_grant_find_active_for_actor } from './role_grant_queries.js';
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
import { is_public_auth, needs_actor } from '../http/auth_shape.js';
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_ACTOR_REQUIRED, ERROR_ACTOR_NOT_ON_ACCOUNT, ERROR_NO_ACTORS_ON_ACCOUNT, ERROR_ACCOUNT_VANISHED, } from '../http/error_schemas.js';
/** Hono context variable name for the request context. */
export const REQUEST_CONTEXT_KEY = 'request_context';
/**
 * Hono context variable name for the authenticated session token hash.
 *
 * Set by `create_request_context_middleware` after a successful session lookup.
 * `null` when the request is unauthenticated or authenticated via a non-session
 * credential (bearer token, daemon token). Exposed so handlers can scope
 * per-session resources (e.g., SSE stream identity for targeted disconnection
 * on `session_revoke`) without re-hashing the token.
 */
export const AUTH_SESSION_TOKEN_HASH_KEY = 'auth_session_token_hash';
/**
 * Get the request context from a Hono context, or `null` if unauthenticated.
 *
 * @param c - the Hono context
 * @returns the request context, or `null`
 */
export const get_request_context = (c) => {
    return c.get(REQUEST_CONTEXT_KEY) ?? null;
};
/**
 * Get the request context, throwing if unauthenticated.
 *
 * Use in route handlers where the dispatcher's authorization phase guarantees
 * a context exists (i.e., routes with `auth: {type: 'authenticated'}` or
 * stricter). Prefer this over `get_request_context(c)!` for explicit error
 * handling.
 *
 * @param c - the Hono context
 * @returns the request context (never null)
 * @throws Error if no request context is set (dispatcher misconfiguration)
 */
export const require_request_context = (c) => {
    const ctx = get_request_context(c);
    if (!ctx) {
        throw new Error('require_request_context: no request context — is the dispatcher authorization phase wired?');
    }
    return ctx;
};
/**
 * Check if a request context has an active role_grant for a given role.
 *
 * Checks the role_grants already loaded in the context (no DB query).
 * Null-tolerant — `null` ctx (unauthenticated) returns `false`. Symmetric
 * with `has_scoped_role` / `has_any_scoped_role` so the three helpers
 * compose freely in the same predicate (e.g.
 * `has_role(auth, ADMIN) || has_scoped_role(auth, role, scope)`).
 *
 * @param ctx - the request context, or `null` for unauthenticated callers
 * @param role - the role to check
 * @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
 * @returns `true` if the actor has an active role_grant for the role
 */
export const has_role = (ctx, role, now = new Date()) => ctx?.role_grants.some((p) => p.role === role && is_role_grant_active(p, now)) ?? false;
/**
 * Whether the request context holds an active role_grant for `role` at `scope_id`.
 *
 * Walks the in-memory `ctx.role_grants` snapshot loaded once per request by
 * the route-spec / RPC dispatcher's authorization phase (when the route
 * declares `acting?: ActingActor` or has role_grant-requiring auth); zero DB
 * roundtrip per check. The "freshness" framing of a SQL re-query is
 * illusory because the race window is between predicate and the actual
 * mutation, not predicate and authorization load. Closing that race needs
 * a transactional re-check inside the UPDATE/INSERT, which neither style
 * provides.
 *
 * Null-tolerant — `null` ctx (unauthenticated) and account-grain
 * contexts (`actor: null`, empty `role_grants`) both return `false`. Same
 * convention as `has_role`; lets the helper drop into public
 * (`{account: 'none', actor: 'none'}`) and account-grain
 * (`{account: 'required', actor: 'none'}`) handlers without a manual
 * narrow. See `cell_authorize` for the resource-side analog.
 *
 * `scope_id` semantics: in-memory `role_grant.scope_id` is `string | null`, so
 * JS `===` matches the SQL `IS NOT DISTINCT FROM` semantics exactly:
 *
 * - `scope_id === null` matches global role_grants (`scope_id IS NULL`).
 * - `scope_id === '<uuid>'` matches role_grants bound to that exact scope.
 *
 * @param ctx - the request context, or `null` for unauthenticated callers
 * @param role - the role to check
 * @param scope_id - the scope to check (`null` for global)
 * @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
 * @returns `true` iff the actor holds an active role_grant for the role at the requested scope
 */
export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
    if (!ctx)
        return false;
    return ctx.role_grants.some((p) => p.role === role && p.scope_id === scope_id && is_role_grant_active(p, now));
};
/**
 * Whether the request context holds an active role_grant for any role in `roles`
 * at `scope_id`. Empty `roles` short-circuits to `false` — documents intent
 * at the call site ("zero roles trivially admit no-one"). Same scope and
 * null-tolerance semantics as `has_scoped_role`.
 *
 * @param ctx - the request context, or `null` for unauthenticated callers
 * @param roles - the roles that would admit the caller (any-of)
 * @param scope_id - the scope to check (`null` for global)
 * @param now - current time (defaults to `new Date()`, pass for testability)
 * @returns `true` iff the actor holds an active role_grant for any role in `roles` at the requested scope
 */
export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
    if (!ctx)
        return false;
    if (roles.length === 0)
        return false;
    return ctx.role_grants.some((p) => roles.includes(p.role) && p.scope_id === scope_id && is_role_grant_active(p, now));
};
/**
 * Resolve the acting actor for an authenticated request.
 *
 * Called from the route-spec / RPC dispatcher's authorization phase
 * with the authenticated account id and the validated `acting` value
 * (from the request payload). Applies the uniform resolution rules:
 *
 * - `acting_actor_id` omitted + 1 actor → use it.
 * - `acting_actor_id` omitted + 0 actors → `no_actors` (defensive —
 *   signup / bootstrap always create an actor in the same tx, so this
 *   is a server error).
 * - `acting_actor_id` omitted + multiple actors → `actor_required` with
 *   the available list so the client can prompt; never pick silently.
 * - `acting_actor_id` present + matches an actor on the account → use it.
 * - `acting_actor_id` present + does not match → `actor_not_on_account`.
 *   The available list is intentionally not echoed in this branch (treat
 *   as opaque rejection).
 *
 * @param deps - query dependencies
 * @param account_id - the authenticated account
 * @param acting_actor_id - the requested acting actor id, or `undefined`
 */
export const resolve_acting_actor = async (deps, account_id, acting_actor_id) => {
    const actors = await query_actors_by_account(deps, account_id);
    if (actors.length === 0)
        return { ok: false, reason: 'no_actors' };
    if (acting_actor_id == null) {
        if (actors.length === 1)
            return { ok: true, actor_id: actors[0].id };
        return {
            ok: false,
            reason: 'actor_required',
            available: actors.map((a) => ({ id: a.id, name: a.name })),
        };
    }
    const match = actors.find((a) => a.id === acting_actor_id);
    if (!match)
        return { ok: false, reason: 'actor_not_on_account' };
    return { ok: true, actor_id: match.id };
};
/**
 * Create middleware that authenticates the account from a session cookie.
 *
 * Reads the session identity (set by session middleware), looks up the
 * `auth_session`, and on a valid session sets `c.var.auth_account_id`,
 * `CREDENTIAL_TYPE_KEY = 'session'`, and `AUTH_SESSION_TOKEN_HASH_KEY`.
 * Touches the session (fire-and-forget). Does not load actor or role_grants;
 * `REQUEST_CONTEXT_KEY` is left null — the route-spec / RPC dispatcher
 * authorization phase resolves the acting actor and builds the full
 * `RequestContext` when the route needs one.
 *
 * Invalid / missing session leaves all keys null and calls `next()` —
 * `require_auth` / `require_role` enforce.
 *
 * @param deps - query dependencies (pool-level db for middleware)
 * @param log - the logger instance
 * @param session_context_key - the Hono context key where session middleware stored the session token
 * @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
 */
export const create_request_context_middleware = (deps, log, session_context_key = 'auth_session_id') => {
    return async (c, next) => {
        c.set(REQUEST_CONTEXT_KEY, null);
        c.set(ACCOUNT_ID_KEY, null);
        c.set(CREDENTIAL_TYPE_KEY, null);
        c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
        c.set(AUTH_API_TOKEN_ID_KEY, null);
        const session_token = c.get(session_context_key) ?? null;
        if (!session_token) {
            await next();
            return;
        }
        const token_hash = hash_session_token(session_token);
        const session = await query_session_get_valid(deps, token_hash);
        if (!session) {
            await next();
            return;
        }
        c.set(ACCOUNT_ID_KEY, session.account_id);
        c.set(CREDENTIAL_TYPE_KEY, 'session');
        c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
        // Touch session (fire-and-forget, don't block the request)
        void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
        await next();
    };
};
/**
 * Middleware that requires authentication.
 *
 * Returns 401 if the auth middleware did not set `c.var.auth_account_id`.
 */
export const require_auth = async (c, next) => {
    if (c.get(ACCOUNT_ID_KEY) == null) {
        return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
    }
    await next();
};
/**
 * Create middleware that requires the actor to hold any of the given
 * roles globally (`scope_id IS NULL`).
 *
 * Returns 401 if unauthenticated, 403 if none of the roles are present.
 * Reads `REQUEST_CONTEXT_KEY` because role-gated routes always run the
 * dispatcher's authorization phase before this guard (the phase sets
 * the actor-bound `RequestContext`).
 *
 * Uses `has_any_scoped_role(ctx, roles, null)` so the gate matches
 * **global / unscoped role_grants only**. A scoped role_grant
 * (`{role: 'admin', scope_id: <some uuid>}`) does not unlock route-spec
 * gates that are inherently global. The same scope-aware check is
 * mirrored in `actions/action_rpc.ts` (HTTP RPC dispatcher) and
 * `actions/register_action_ws.ts` (WS dispatcher) so all three
 * transports agree.
 *
 * Multi-role disjunction (any-of) lets `auth.roles: ['admin', 'steward']`
 * specs translate to one middleware that admits either role. Single-role
 * routes pass `[role_name]`; the array shape is uniform.
 *
 * @param roles - the roles to admit (any-of)
 */
export const require_role = (roles) => {
    return async (c, next) => {
        if (c.get(ACCOUNT_ID_KEY) == null) {
            return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
        }
        const ctx = get_request_context(c);
        if (!ctx || !has_any_scoped_role(ctx, roles, null)) {
            return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_roles: roles }, 403);
        }
        await next();
    };
};
/**
 * Create middleware that requires the request's `credential_type` to be
 * one of the given values.
 *
 * Returns 401 if unauthenticated, 403 with
 * `ERROR_CREDENTIAL_TYPE_REQUIRED` + `required_credential_types` echoing
 * the spec's allowlist when the wire-side credential isn't in it.
 * Body shape is symmetric with the role gate (`ERROR_INSUFFICIENT_PERMISSIONS` +
 * `required_roles`) and matches what the RPC dispatcher's post-auth
 * gate emits for the same condition. Today's only credential gate is
 * keeper (`['daemon_token']`); future gates (`agent_token`,
 * `group_actor_token`) reuse this literal and label themselves through
 * the array.
 *
 * @param credential_types - allowed credential types (any-of)
 */
export const require_credential_types = (credential_types) => {
    return async (c, next) => {
        if (c.get(ACCOUNT_ID_KEY) == null) {
            return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
        }
        const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? null;
        if (!credential_type || !credential_types.includes(credential_type)) {
            return c.json({
                error: ERROR_CREDENTIAL_TYPE_REQUIRED,
                required_credential_types: credential_types,
            }, 403);
        }
        await next();
    };
};
/**
 * Reload active role_grants from the database, returning a new request context.
 *
 * Useful for long-lived WebSocket connections where role_grants may change
 * (grant or revoke) during the connection lifetime. Call periodically
 * or after receiving a revocation signal.
 *
 * Returns a new `RequestContext` with updated role_grants — the original
 * context is not mutated, making concurrent calls safe. Throws when
 * `ctx.actor` is null; account-grain contexts have no role_grants to refresh.
 *
 * @param ctx - the request context to refresh
 * @param deps - query dependencies
 * @returns a new `RequestContext` with fresh role_grants
 * @throws Error when called on an account-grain context (`actor: null`)
 */
export const refresh_role_grants = async (ctx, deps) => {
    if (!ctx.actor) {
        throw new Error('refresh_role_grants: account-grain context has no actor / role_grants to refresh');
    }
    const role_grants = await query_role_grant_find_active_for_actor(deps, ctx.actor.id);
    return { ...ctx, role_grants };
};
/**
 * Build a full `RequestContext` from an account id and an explicit
 * actor id (already resolved via `resolve_acting_actor`).
 *
 * Loads `account` + the named `actor` + the actor's active role_grants.
 * Verifies the `actor.account_id === account.id` binding so downstream
 * handlers can trust `ctx.actor.account_id === ctx.account.id`. Returns
 * `null` when the account is missing, the actor is missing, or the
 * actor doesn't belong to the supplied account.
 *
 * Called by the route-spec / RPC dispatcher's authorization phase for
 * routes that need an acting actor; account-grain routes use
 * `build_account_context` instead.
 *
 * @param deps - query dependencies
 * @param account_id - the account to build context for
 * @param actor_id - the actor this request acts as
 * @returns a request context, or `null` if account/actor not found or mismatched
 */
export const build_request_context = async (deps, account_id, actor_id) => {
    const account = await query_account_by_id(deps, account_id);
    if (!account)
        return null;
    const actor = await query_actor_by_id(deps, actor_id);
    if (!actor)
        return null;
    if (actor.account_id !== account.id)
        return null;
    const role_grants = await query_role_grant_find_active_for_actor(deps, actor.id);
    return { account, actor, role_grants };
};
/**
 * Build an account-only `RequestContext` (no actor, no role_grants) from
 * an account id.
 *
 * Used by the dispatcher's authorization phase for authenticated routes
 * that don't need an acting actor — account-grain operations (logout,
 * password change, account self-service). Lets handlers read
 * `auth.account.id` / `auth.account.username` uniformly with role_grant-bound
 * routes; the cost is one extra `query_account_by_id` per request.
 *
 * Returns `null` when the account row is missing (e.g. deleted between
 * the auth middleware's session lookup and the dispatcher) — caller
 * surfaces that as a 500 since it represents a torn read.
 *
 * @param deps - query dependencies
 * @param account_id - the account to build context for
 * @returns an account-only request context, or `null` if the account is missing
 */
export const build_account_context = async (deps, account_id) => {
    const account = await query_account_by_id(deps, account_id);
    if (!account)
        return null;
    return { account, actor: null, role_grants: [] };
};
/**
 * Apply the dispatcher's authorization phase against the flat-record
 * `RouteAuth` shape. Shared by the route-spec wrapper, the HTTP RPC
 * dispatcher, and the per-message WS dispatcher. Phase order:
 * pre-validation 401 → input validation 400 → authorization phase →
 * post-authorization 403.
 *
 * Pure data — the function does not touch a Hono context. Each transport
 * passes `account_id` (extracted from its own credential surface) and
 * binds the returned `AuthorizationResult` to its wire shape. The REST
 * pipeline additionally writes `REQUEST_CONTEXT_KEY` on `c` for downstream
 * `require_role` / `require_credential_types` middleware that still reads
 * the resolved context off the Hono context.
 *
 * Branching by `auth.account` × `auth.actor`:
 *
 * - Both `'none'` → `{ok: true, request_context: null}`. Public actions
 *   never see a `RequestContext`.
 * - `account_id == null` on any non-public route → same null
 *   `request_context`. The `'required'` callers were already rejected at
 *   the pre-validation gate in the dispatcher; only genuine anonymous
 *   access on an `'optional'` axis lands here.
 * - `actor === 'none'` → builds account-only context via
 *   `build_account_context`. Null lookup → `account_vanished` 500 failure.
 * - `actor === 'required'` → resolves the actor from `acting_value` (or
 *   single-actor account); failures map to 400 / 500.
 * - `actor === 'optional'` → same as `'required'` except multi-actor
 *   accounts without an `acting` value fall back to account-only context
 *   (no `actor_required` 400). Bad `acting` ids still 400.
 *
 * 500 branches stay distinct: `ERROR_NO_ACTORS_ON_ACCOUNT` (signup
 * invariant violation), `ERROR_ACCOUNT_VANISHED` (torn read after
 * resolve).
 */
export const apply_authorization_phase = async (deps, account_id, auth, acting_value) => {
    if (is_public_auth(auth))
        return { ok: true, request_context: null };
    if (account_id == null) {
        // Optional-auth route hit without a credential — leave `RequestContext`
        // null so the handler can branch on it. `'required'` callers already
        // got rejected at the pre-validation gate.
        return { ok: true, request_context: null };
    }
    if (!needs_actor(auth)) {
        const ctx = await build_account_context(deps, account_id);
        if (!ctx)
            return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
        return { ok: true, request_context: ctx };
    }
    // actor 'required' or 'optional' — resolve.
    const acting = await resolve_acting_actor(deps, account_id, acting_value);
    if (!acting.ok) {
        if (acting.reason === 'actor_required') {
            if (auth.actor === 'optional') {
                // Multi-actor account, no pick — fall back to account-only context.
                const ctx = await build_account_context(deps, account_id);
                if (!ctx)
                    return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
                return { ok: true, request_context: ctx };
            }
            return {
                ok: false,
                status: 400,
                body: { error: ERROR_ACTOR_REQUIRED, available: acting.available },
            };
        }
        if (acting.reason === 'actor_not_on_account') {
            return { ok: false, status: 400, body: { error: ERROR_ACTOR_NOT_ON_ACCOUNT } };
        }
        return { ok: false, status: 500, body: { error: ERROR_NO_ACTORS_ON_ACCOUNT } };
    }
    const ctx = await build_request_context(deps, account_id, acting.actor_id);
    if (!ctx)
        return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
    return { ok: true, request_context: ctx };
};
/**
 * Create the route-spec authorization handler used by `apply_route_specs`.
 *
 * Reads `acting` off `c.var.validated_input` (or `c.var.validated_query`
 * for GET routes) — input validation runs first, so the authorization
 * phase consumes the typed Zod field instead of pre-parsing the body.
 * Public routes (`auth.account === 'none' && auth.actor === 'none'`)
 * skip the phase entirely.
 *
 * Per registry-time invariant 2, `auth.actor !== 'none'` ⟺ the input
 * (or query) schema declares `acting?: ActingActor` — so reading from
 * `c.var.validated_input.acting` / `c.var.validated_query.acting` is
 * type-safe.
 *
 * Resolved contexts land on `REQUEST_CONTEXT_KEY` so the post-authorization
 * REST middleware (`require_role`, `require_credential_types`) reads the
 * actor-bound context off `c.var`. The HTTP RPC and WS dispatchers consume
 * the `apply_authorization_phase` outcome directly without round-tripping
 * through `c.var`.
 */
export const create_fuz_authorization_handler = (deps) => {
    return async (c, spec) => {
        // Test escape hatch: harnesses that pre-populate `REQUEST_CONTEXT_KEY`
        // flag `TEST_CONTEXT_PRESET_KEY = true` so the authorization phase
        // trusts the supplied context instead of running DB-backed resolution.
        // Production middleware never sets this flag.
        if (c.get(TEST_CONTEXT_PRESET_KEY))
            return;
        if (is_public_auth(spec.auth))
            return;
        const acting_value = needs_actor(spec.auth) ? extract_validated_acting(c) : undefined;
        const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
        const result = await apply_authorization_phase(deps, account_id, spec.auth, acting_value);
        if (!result.ok)
            return c.json(result.body, result.status);
        if (result.request_context !== null) {
            c.set(REQUEST_CONTEXT_KEY, result.request_context);
        }
        // `request_context: null` — public action or unauthenticated optional axis.
        // Leave `REQUEST_CONTEXT_KEY` null; downstream `require_role` /
        // `require_credential_types` enforce.
        return;
    };
};
/**
 * Read `acting` off the validated input (or validated query) on the Hono
 * context. Input/query validation runs before the authorization phase,
 * so this reads a typed Zod field — not the raw body.
 *
 * Returns `undefined` when `validated_input` / `validated_query` isn't
 * set or doesn't carry `acting`. Per registry-time invariant 2, the
 * dispatcher only calls this when `auth.actor !== 'none'`, which by
 * the biconditional means the input schema declares
 * `acting?: ActingActor`.
 */
const extract_validated_acting = (c) => {
    const validated_input = c.get('validated_input');
    if (validated_input && typeof validated_input.acting === 'string')
        return validated_input.acting;
    const validated_query = c.get('validated_query');
    if (validated_query && typeof validated_query.acting === 'string')
        return validated_query.acting;
    return undefined;
};
