/**
 * App surface generation — JSON-serializable attack surface from route and middleware specs.
 *
 * Pure schema helpers (`is_null_schema`, `schema_to_surface`, `middleware_applies`,
 * `merge_error_schemas`) live in `http/schema_helpers.ts`.
 *
 * @module
 */
import { z } from 'zod';
import { schema_to_surface, middleware_applies, merge_error_schemas, is_null_schema, is_strict_object_schema, } from './schema_helpers.js';
// --- Surface generation ---
/**
 * Collect error schemas from all middleware that applies to a route path.
 *
 * @returns merged middleware error schemas, or `null` if none
 */
export const collect_middleware_errors = (middleware, route_path) => {
    const errors = {};
    for (const mw of middleware) {
        if (mw.errors && middleware_applies(mw.path, route_path)) {
            Object.assign(errors, mw.errors);
        }
    }
    return Object.keys(errors).length > 0 ? errors : null;
};
/**
 * Convert env schema to surface entries using `.meta()` metadata.
 *
 * @param schema - Zod object schema with `.meta()` on fields
 */
export const env_schema_to_surface = (schema) => {
    const entries = [];
    for (const [name, field_schema] of Object.entries(schema.shape)) {
        const field = field_schema;
        const meta = field.meta();
        const undef_result = field.safeParse(undefined);
        entries.push({
            name,
            description: meta?.description ?? '',
            sensitivity: meta?.sensitivity ?? null,
            has_default: undef_result.success && undef_result.data !== undefined,
            optional: undef_result.success,
        });
    }
    return entries;
};
/**
 * Convert SSE event specs to surface entries.
 */
export const events_to_surface = (event_specs) => {
    return event_specs.map((spec) => ({
        method: spec.method,
        description: spec.description,
        channel: spec.channel ?? null,
        params_schema: schema_to_surface(spec.params),
    }));
};
/**
 * Generate a JSON-serializable attack surface from middleware, route specs,
 * and optional env/event metadata.
 */
export const generate_app_surface = (options) => {
    const { route_specs, middleware_specs, env_schema, event_specs, rpc_endpoints, ws_endpoints } = options;
    const diagnostics = [];
    // Spec-level diagnostics: check for non-strict input schemas
    for (const r of route_specs) {
        if (!is_null_schema(r.input) && !is_strict_object_schema(r.input)) {
            diagnostics.push({
                level: 'warning',
                category: 'schema',
                message: 'Input schema is not z.strictObject() — unknown keys will be silently stripped',
                source: `${r.method} ${r.path} input`,
            });
        }
    }
    return {
        diagnostics,
        middleware: middleware_specs.map((m) => {
            let mw_error_schemas = null;
            if (m.errors) {
                const schemas = {};
                for (const [status, schema] of Object.entries(m.errors)) {
                    const json_schema = schema_to_surface(schema);
                    if (json_schema !== null) {
                        schemas[status] = json_schema;
                    }
                }
                if (Object.keys(schemas).length > 0) {
                    mw_error_schemas = schemas;
                }
            }
            return { name: m.name, path: m.path, error_schemas: mw_error_schemas };
        }),
        routes: route_specs.map((r) => {
            const applicable_middleware = middleware_specs
                .filter((m) => middleware_applies(m.path, r.path))
                .map((m) => m.name);
            // Merge auto-derived + middleware + explicit error schemas
            const mw_errors = collect_middleware_errors(middleware_specs, r.path);
            const merged_errors = merge_error_schemas(r, mw_errors);
            let error_schemas = null;
            if (merged_errors) {
                const schemas = {};
                for (const [status, schema] of Object.entries(merged_errors)) {
                    const json_schema = schema_to_surface(schema);
                    if (json_schema !== null) {
                        schemas[status] = json_schema;
                    }
                }
                if (Object.keys(schemas).length > 0) {
                    error_schemas = schemas;
                }
            }
            return {
                method: r.method,
                path: r.path,
                auth: r.auth,
                applicable_middleware,
                description: r.description,
                is_mutation: r.method !== 'GET',
                transaction: r.transaction ?? r.method !== 'GET',
                rate_limit_key: r.rate_limit ?? null,
                params_schema: r.params ? schema_to_surface(r.params) : null,
                query_schema: r.query ? schema_to_surface(r.query) : null,
                input_schema: schema_to_surface(r.input),
                output_schema: schema_to_surface(r.output),
                error_schemas,
            };
        }),
        rpc_endpoints: rpc_endpoints?.length
            ? rpc_endpoints.map((ep) => ({
                path: ep.path,
                methods: ep.actions.map((a) => ({
                    name: a.spec.method,
                    auth: a.spec.auth,
                    input_schema: schema_to_surface(a.spec.input),
                    output_schema: schema_to_surface(a.spec.output),
                    side_effects: a.spec.side_effects,
                    description: a.spec.description,
                    rate_limit_key: a.spec.rate_limit ?? null,
                })),
            }))
            : [],
        ws_endpoints: ws_endpoints?.length
            ? ws_endpoints.map((ep) => ({
                path: ep.path,
                allowed_origins: ep.allowed_origins.map((re) => re.toString()),
                required_roles: ep.required_roles ?? [],
                // `local_call` specs are frontend-side helpers — registry-only
                // on the backend, never dispatched over WS. Drop them from the
                // surface so attack-surface tests reflect dispatchable methods
                // only. Notifications are kept (server → client emit).
                methods: ep.actions
                    .filter((a) => a.spec.kind !== 'local_call')
                    .map((a) => ({
                    name: a.spec.method,
                    kind: a.spec.kind,
                    // `request_response` carries a `RouteAuth`; notifications
                    // have `auth: null` (server-pushed, no inbound dispatch).
                    auth: a.spec.auth,
                    input_schema: schema_to_surface(a.spec.input),
                    output_schema: schema_to_surface(a.spec.output),
                    description: a.spec.description,
                    side_effects: a.spec.side_effects,
                    rate_limit_key: a.spec.kind === 'request_response' ? (a.spec.rate_limit ?? null) : null,
                })),
            }))
            : [],
        env: env_schema ? env_schema_to_surface(env_schema) : [],
        events: event_specs?.length ? events_to_surface(event_specs) : [],
    };
};
/**
 * Create an `AppSurfaceSpec` — the surface bundled with its source specs.
 */
export const create_app_surface_spec = (options) => {
    const surface = generate_app_surface(options);
    return {
        surface,
        route_specs: options.route_specs,
        middleware_specs: options.middleware_specs,
        rpc_endpoints: options.rpc_endpoints ?? [],
        ws_endpoints: options.ws_endpoints ? [...options.ws_endpoints] : [],
    };
};
