/**
 * Server assembly factory.
 *
 * Consumers provide a pre-initialized `AppBackend` and options (session,
 * origins, routes); `create_app_server()` handles middleware, bootstrap
 * status, surface generation, and Hono app assembly.
 *
 * @module
 */
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { bodyLimit } from 'hono/body-limit';
import { z } from 'zod';
import { session_cookie_options, } from '../auth/session_cookie.js';
import { create_audit_log_sse, audit_log_event_specs, } from '../realtime/sse_auth_guard.js';
import { BaseServerEnv } from './env.js';
import { create_rate_limiter, default_login_account_rate_limit, default_action_account_rate_limit, default_action_ip_rate_limit, } from '../rate_limiter.js';
// Side-effect import: augments Hono's ContextVariableMap so consumers
// that import app_server get type-safe c.get('auth_session_id') etc.
import '../hono_context.js';
import { create_proxy_middleware_spec } from '../http/proxy.js';
import { create_static_middleware } from './static.js';
import { log_startup_summary } from './startup.js';
import { create_app_surface_spec, } from '../http/surface.js';
import { apply_middleware_specs, apply_route_specs, prefix_route_specs, } from '../http/route_spec.js';
import { check_bootstrap_status, create_bootstrap_route_specs, } from '../auth/bootstrap_routes.js';
import { create_surface_route_spec } from '../http/common_routes.js';
import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
import { create_auth_middleware_specs } from '../auth/middleware.js';
import { fuz_auth_guard_resolver } from '../auth/auth_guard_resolver.js';
import { create_fuz_authorization_handler } from '../auth/request_context.js';
import { ERROR_PAYLOAD_TOO_LARGE } from '../http/error_schemas.js';
import { create_rpc_endpoint } from '../actions/action_rpc.js';
import { register_ws_endpoint } from '../actions/register_ws_endpoint.js';
import { create_ws_auth_guard, create_ws_logout_closer, } from '../actions/transports_ws_auth_guard.js';
import { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
/**
 * Assert that `audit_sse` was wired by `create_app_server` and return it
 * as a non-null `AuditLogSse`. Throws a labelled error when the
 * `audit_log_sse` option was not passed to `create_app_server`.
 *
 * Use in route factories that depend on factory-managed audit SSE:
 *
 * ```ts
 * create_route_specs: (ctx) => create_audit_log_route_specs({
 *   stream: require_audit_sse(ctx),
 * }),
 * ```
 *
 * Preferred over `ctx.audit_sse!` — `!` lies to the type system and
 * produces a downstream cannot-read-property crash if a consumer wires
 * the route without enabling the option.
 */
export const require_audit_sse = (source) => {
    if (!source.audit_sse) {
        throw new Error('audit_sse is null — pass `audit_log_sse: true` (or `{role}`) in `AppServerOptions`');
    }
    return source.audit_sse;
};
/** Default maximum request body size: 1 MiB. */
export const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
/**
 * Create a fully assembled Hono app with auth, middleware, and routes.
 *
 * Handles the assembly lifecycle: proxy middleware → auth middleware →
 * bootstrap status → route specs → surface generation → Hono app assembly →
 * static serving. Database migrations belong to the backend lifecycle —
 * pass `migration_namespaces` to `create_app_backend`.
 *
 * When `audit_log_sse` is set, the SSE registry's listener is appended to
 * `backend.deps.audit.on_event_chain` — no shallow-copy of `AppDeps`. The
 * `audit_sse` field on the returned `AppServer` (and the
 * `AppServerContext` passed to `create_route_specs`) is non-null in that
 * case; consumers can call `require_audit_sse(ctx)` / `require_audit_sse(server)`
 * to assert the invariant.
 *
 * @returns assembled Hono app, backend, surface build, and bootstrap status
 */
export const create_app_server = async (options) => {
    const { backend } = options;
    const { deps } = backend;
    const { log } = deps;
    // Rate limiter defaults (undefined = default, null = disable)
    const ip_rate_limiter = options.ip_rate_limiter === undefined ? create_rate_limiter() : options.ip_rate_limiter;
    const login_account_rate_limiter = options.login_account_rate_limiter === undefined
        ? create_rate_limiter(default_login_account_rate_limit)
        : options.login_account_rate_limiter;
    const signup_account_rate_limiter = options.signup_account_rate_limiter === undefined
        ? create_rate_limiter(default_login_account_rate_limit)
        : options.signup_account_rate_limiter;
    const bearer_ip_rate_limiter = options.bearer_ip_rate_limiter === undefined
        ? create_rate_limiter()
        : options.bearer_ip_rate_limiter;
    const action_ip_rate_limiter = options.action_ip_rate_limiter === undefined
        ? create_rate_limiter(default_action_ip_rate_limit)
        : options.action_ip_rate_limiter;
    const action_account_rate_limiter = options.action_account_rate_limiter === undefined
        ? create_rate_limiter(default_action_account_rate_limit)
        : options.action_account_rate_limiter;
    // Factory-managed audit SSE — appends a listener to the bound emitter's
    // chain so SSE fan-out runs alongside the consumer's `on_audit_event`
    // without rebuilding `AppDeps`.
    const audit_sse = options.audit_log_sse
        ? create_audit_log_sse({
            log,
            role: typeof options.audit_log_sse === 'object' ? options.audit_log_sse.role : undefined,
        })
        : null;
    if (audit_sse) {
        deps.audit.on_event_chain.push(audit_sse.on_audit_event);
    }
    // Proxy middleware
    const proxy_spec = create_proxy_middleware_spec({ ...options.proxy, log });
    // Auth middleware
    const auth_middleware = await create_auth_middleware_specs(deps, {
        allowed_origins: options.allowed_origins,
        session_options: options.session_options,
        bearer_ip_rate_limiter,
        daemon_token_state: options.daemon_token_state,
    });
    let middleware_specs = [proxy_spec, ...auth_middleware];
    if (options.transform_middleware) {
        middleware_specs = options.transform_middleware(middleware_specs);
    }
    // Bootstrap status
    // - undefined / 'disabled': no route mounted; placeholder status.
    // - 'surface_only': route mounted but permanently unavailable; status placeholder.
    // - 'live': real disk + lock check via `check_bootstrap_status`.
    const bootstrap_status = options.bootstrap?.mode === 'live'
        ? await check_bootstrap_status(deps, { token_path: options.bootstrap.token_path })
        : { available: false, token_path: null };
    // Surface route ref — factory manages the circular ref
    const surface_ref = {
        surface: {
            middleware: [],
            routes: [],
            rpc_endpoints: [],
            ws_endpoints: [],
            env: [],
            events: [],
            diagnostics: [],
        },
    };
    // Route specs (consumer routes + factory-managed routes)
    const context = {
        deps,
        backend,
        bootstrap_status,
        session_options: options.session_options,
        ip_rate_limiter,
        login_account_rate_limiter,
        signup_account_rate_limiter,
        action_ip_rate_limiter,
        action_account_rate_limiter,
        audit_sse,
    };
    const consumer_routes = options.create_route_specs(context);
    // Factory-managed routes appended after consumer routes
    const factory_routes = [];
    // Bootstrap routes — mounted for 'surface_only' and 'live'; omitted for
    // 'disabled' / undefined. The route handler short-circuits to 403 when
    // `bootstrap_status.available === false`, which is the steady state for
    // 'surface_only' and the post-bootstrap state for 'live'.
    if (options.bootstrap && options.bootstrap.mode !== 'disabled') {
        const bootstrap_routes = create_bootstrap_route_specs(deps, {
            session_options: options.session_options,
            bootstrap_status,
            on_bootstrap: options.bootstrap.mode === 'live' ? options.bootstrap.on_bootstrap : undefined,
            ip_rate_limiter,
        });
        const prefix = options.bootstrap.route_prefix ?? '/api/account';
        factory_routes.push(...prefix_route_specs(prefix, bootstrap_routes));
    }
    // RPC endpoint auto-mount — resolve specs then append their routes so
    // surface generation and live dispatch share one source of truth.
    const resolved_rpc_endpoints = typeof options.rpc_endpoints === 'function'
        ? options.rpc_endpoints(context)
        : options.rpc_endpoints;
    if (resolved_rpc_endpoints) {
        for (const endpoint of resolved_rpc_endpoints) {
            factory_routes.push(...create_rpc_endpoint({
                path: endpoint.path,
                actions: endpoint.actions,
                log,
                action_ip_rate_limiter,
                action_account_rate_limiter,
            }));
        }
    }
    // WS endpoint resolution — done here (alongside RPC) so the captured
    // array threads into surface generation below. Actual mount happens
    // after `apply_route_specs` because `register_ws_endpoint` mutates the
    // live Hono `app` (origin / auth / role / authorization middleware +
    // the `app.get(path, ...)` upgrade route), and `app` does not exist
    // until the assembly phase below.
    const resolved_ws_endpoints = typeof options.ws_endpoints === 'function'
        ? options.ws_endpoints(context)
        : options.ws_endpoints;
    // Surface route (default: enabled)
    if (options.surface_route !== false) {
        factory_routes.push(create_surface_route_spec(surface_ref));
    }
    const route_specs = [...consumer_routes, ...factory_routes];
    // Surface + logging
    const surface_middleware = options.post_route_middleware
        ? [...middleware_specs, ...options.post_route_middleware]
        : middleware_specs;
    const all_event_specs = [
        ...(options.event_specs ?? []),
        ...(audit_sse ? audit_log_event_specs : []),
    ];
    const surface_spec = create_app_surface_spec({
        middleware_specs: surface_middleware,
        route_specs,
        env_schema: options.env_schema ?? BaseServerEnv,
        event_specs: all_event_specs,
        rpc_endpoints: resolved_rpc_endpoints,
        ws_endpoints: resolved_ws_endpoints,
    });
    // Config-level diagnostics (concatenated after spec-level from generate_app_surface)
    const config_diagnostics = [];
    const cookie_opts = options.session_options.cookie_options;
    if (cookie_opts) {
        if (cookie_opts.secure === false) {
            config_diagnostics.push({
                level: 'warning',
                category: 'security',
                message: 'Session cookie secure=false — cookies sent over HTTP',
            });
        }
        if (cookie_opts.sameSite && cookie_opts.sameSite !== session_cookie_options.sameSite) {
            config_diagnostics.push({
                level: 'warning',
                category: 'security',
                message: `Session cookie sameSite='${cookie_opts.sameSite}' — weakened from default '${session_cookie_options.sameSite}'`,
            });
        }
        if (cookie_opts.httpOnly === false) {
            config_diagnostics.push({
                level: 'warning',
                category: 'security',
                message: 'Session cookie httpOnly=false — cookie accessible to JS',
            });
        }
    }
    if (ip_rate_limiter === null) {
        config_diagnostics.push({
            level: 'warning',
            category: 'config',
            message: 'IP rate limiter explicitly disabled (null)',
        });
    }
    if (bearer_ip_rate_limiter === null) {
        config_diagnostics.push({
            level: 'warning',
            category: 'config',
            message: 'Bearer IP rate limiter explicitly disabled (null)',
        });
    }
    if (config_diagnostics.length) {
        surface_spec.surface.diagnostics = [...surface_spec.surface.diagnostics, ...config_diagnostics];
    }
    // Backfill the surface ref — factory owns this lifecycle
    surface_ref.surface = surface_spec.surface;
    log_startup_summary(surface_spec.surface, log, options.env_values);
    // Hono app assembly
    const app = new Hono();
    // Two-queue side-effect flush. `pending_effects` collects eager
    // fire-and-forget promises (audit emits, session touch, api-token
    // usage). `post_commit_effects` collects deferred thunks pushed via
    // `emit_after_commit` (WS notifications, anything that must observe a
    // committed transaction). Both queues drain here, after the handler
    // (and any wrapping `db.transaction`) returns. In test mode both are
    // awaited before the response returns; in production, eager-queue
    // rejections are reported via `on_effect_error`.
    app.use('*', async (c, next) => {
        c.set('pending_effects', []);
        c.set('post_commit_effects', []);
        try {
            await next();
        }
        finally {
            const eager = c.var.pending_effects;
            const deferred = c.var.post_commit_effects;
            if (eager.length || deferred.length) {
                if (options.await_pending_effects) {
                    await flush_pending_effects(eager, log);
                    await flush_post_commit_effects(deferred, log);
                }
                else {
                    const error_ctx = { method: c.req.method, path: c.req.path };
                    const callback = options.on_effect_error;
                    void flush_pending_effects(eager, log, callback ? (reason) => callback(reason, error_ctx) : undefined);
                    // `flush_post_commit_effects` is non-throwing: per-thunk
                    // errors are routed through `log.error` inside the helper,
                    // so production fire-and-forget skips the `on_effect_error`
                    // fan-out (deferred thunks are wrapped end-to-end already).
                    void flush_post_commit_effects(deferred, log);
                }
            }
        }
    });
    if (log.level !== 'off') {
        app.use(logger((msg) => log.info(msg)));
    }
    // Body size limit — rejects oversized payloads before auth/validation.
    // Default 1 MiB; pass null to disable.
    if (options.max_body_size !== null) {
        const max_size = options.max_body_size ?? DEFAULT_MAX_BODY_SIZE;
        app.use(bodyLimit({
            maxSize: max_size,
            onError: (c) => c.json({ error: ERROR_PAYLOAD_TOO_LARGE }, 413),
        }));
    }
    apply_middleware_specs(app, middleware_specs);
    const authorize = create_fuz_authorization_handler({ db: deps.db });
    apply_route_specs(app, route_specs, fuz_auth_guard_resolver, log, deps.db, authorize);
    // WS endpoint auto-mount — must run after `app` exists and
    // `apply_route_specs` has registered the request routes. Each spec
    // becomes a `register_ws_endpoint` call, plus optional `auth_guard`
    // wiring onto the audit chain. `post_route_middleware` and static
    // serving register after this loop, so WS upgrade routes sit
    // adjacent to the consumer routes and ahead of the static fallback —
    // matches the "WS mount is route registration" mental model.
    const mounted_ws_endpoints = {};
    if (resolved_ws_endpoints?.length) {
        if (options.upgradeWebSocket === undefined) {
            throw new Error('create_app_server: ws_endpoints resolved non-empty but upgradeWebSocket is missing. ' +
                "Pass the Hono adapter's upgradeWebSocket helper as a top-level option.");
        }
        // Cross-surface collision: `register_ws_endpoint` mounts a `GET path`
        // upgrade route. If a `RouteSpec` already registered `GET path`,
        // Hono's last-wins semantics would silently shadow the consumer's
        // GET route — fail fast instead.
        const route_spec_get_paths = new Set();
        for (const r of route_specs) {
            if (r.method === 'GET')
                route_spec_get_paths.add(r.path);
        }
        const seen_paths = new Set();
        // Dedupe `auth_guard` wiring by transport reference — two specs
        // sharing one transport instance get a single pair of listeners,
        // otherwise revocation events would fire `close_sockets_for_*`
        // twice per event (idempotent on the transport but log-spammy).
        // Cross-spec OR-semantics: any spec with `auth_guard !== false`
        // wires the guard for that transport; once wired, sibling specs
        // (even with explicit `auth_guard: false`) cannot opt out. To
        // disable, every spec sharing the transport must pass `auth_guard: false`.
        const guarded_transports = new WeakSet();
        for (const endpoint of resolved_ws_endpoints) {
            if (seen_paths.has(endpoint.path)) {
                throw new Error(`create_app_server: duplicate ws_endpoints path: ${endpoint.path}`);
            }
            if (route_spec_get_paths.has(endpoint.path)) {
                throw new Error(`create_app_server: ws_endpoints path collides with a GET RouteSpec: ${endpoint.path}`);
            }
            seen_paths.add(endpoint.path);
            const endpoint_transport = endpoint.transport ?? new BackendWebsocketTransport();
            register_ws_endpoint({
                app,
                path: endpoint.path,
                upgradeWebSocket: options.upgradeWebSocket,
                allowed_origins: endpoint.allowed_origins,
                db: deps.db,
                actions: endpoint.actions,
                transport: endpoint_transport,
                heartbeat: endpoint.heartbeat,
                artificial_delay: endpoint.artificial_delay,
                on_socket_open: endpoint.on_socket_open,
                on_socket_close: endpoint.on_socket_close,
                log,
                required_roles: endpoint.required_roles,
                action_ip_rate_limiter,
                action_account_rate_limiter,
            });
            mounted_ws_endpoints[endpoint.path] = endpoint_transport;
            if (endpoint.auth_guard !== false && !guarded_transports.has(endpoint_transport)) {
                guarded_transports.add(endpoint_transport);
                deps.audit.on_event_chain.push(create_ws_auth_guard(endpoint_transport, log));
                deps.audit.on_event_chain.push(create_ws_logout_closer(endpoint_transport, log));
            }
            if (endpoint.extra_audit_handlers?.length) {
                for (const handler of endpoint.extra_audit_handlers) {
                    deps.audit.on_event_chain.push(handler);
                }
            }
        }
    }
    // Post-route middleware (before static serving)
    if (options.post_route_middleware) {
        apply_middleware_specs(app, options.post_route_middleware);
    }
    // Static file serving
    if (options.static_serving) {
        const { serve_static, root, spa_fallback, is_spa_route } = options.static_serving;
        for (const mw of create_static_middleware(serve_static, { root, spa_fallback, is_spa_route })) {
            app.use('/*', mw);
        }
    }
    return {
        app,
        surface_spec,
        bootstrap_status,
        migration_results: backend.migration_results,
        audit_sse,
        ws_endpoints: mounted_ws_endpoints,
        close: backend.close,
    };
};
