import { createRequire } from 'node:module';
import { createApiRouter } from './router.js';
import { registerCMSRoutes } from './handlers.js';
import { parseCookieHeader } from './route-helpers.js';
import { initDB, isDBInitialized } from '../db.js';
import { validateCsrfToken } from '../security/index.js';
import { createRateLimiter, warnStrictRateLimitConfigAtBoot } from '../security/rate-limit.js';
import { initServerSentry } from '../diagnostics/sentry.js';
import { getClientIp } from '../security/client-ip.js';
import { autoSeedAdmin } from '../setup/index.js';
import { setStorageAdapter, getStorageAdapter } from '../storage/index.js';
import { autoWireStorageAdapter, isAutoWireableStorageProvider } from '../storage/auto-wire.js';
import { getActuateConfig, setActuateConfig, setActuateCoreVersion } from '../config/runtime.js';
import { recordApiRequest } from '../diagnostics/api-metrics.js';
import { registerSectionTypes, resetCustomSectionTypes, resolveCustomSectionTypes, } from '../sections/registry.js';
/**
 * Routes that may legitimately receive a state-mutating request without a
 * CSRF token. Matched by exact prefix on the rewritten (CMS-relative) path,
 * not by `pathname.includes(...)` — without exact matching, an attacker could
 * craft a path like `/collections/auth/login/foo` to bypass CSRF on every
 * other endpoint.
 *
 *   /auth/login           — initial credential POST has no session yet
 *   /auth/logout          — clears state; CSRF would just block sign-out
 *   /auth/totp/login      — second factor in the login dance
 *   /auth/forgot-password — anonymous; pre-account
 *   /auth/reset-password  — anonymous; pre-account
 *   /auth/oauth/...       — OAuth init + callback redirects
 *   /setup/create-admin   — only reachable on a fresh install
 *   /forms/:id/submit     — public form submissions (regex below)
 *   /cron/...             — Vercel Cron / external schedulers; auth is via
 *                           Authorization: Bearer ${CRON_SECRET} instead
 */
const CSRF_EXEMPT_PATHS = [
    '/auth/login',
    '/auth/logout',
    '/auth/totp/login',
    '/auth/forgot-password',
    '/auth/reset-password',
    '/auth/oauth/',
    '/setup/create-admin',
    '/cron/',
    // Called from the public site frontend (no session/CSRF token) to log a 404
    // for broken-link tracking. CSRF has no value here — it only increments a hit
    // counter, carries no auth, and the handler bounds its inputs. Still subject
    // to the global per-IP rate limiter above.
    '/redirects/record-404',
    // `/dev/login` is the localhost-only auth bypass — already triple-gated
    // (env flag + non-prod + loopback IP). CSRF on a redirect-style login
    // doesn't help here and would break the simple browser-navigation flow.
    '/dev/login',
];
function isCsrfExemptPath(pathname) {
    if (CSRF_EXEMPT_PATHS.some((p) => pathname === p ||
        pathname.startsWith(`${p}/`) ||
        (p.endsWith('/') && pathname.startsWith(p)))) {
        return true;
    }
    // Public form submission endpoints (legacy by-id + slug-based public route).
    return (/^\/forms\/[^/]+\/submit$/.test(pathname) || /^\/public\/forms\/[^/]+\/submit$/.test(pathname));
}
/**
 * Detect requests authenticated with an Actuate API key (`act_sk_...`) so we
 * can skip the CSRF check for them. API keys are opaque secrets — they cannot
 * be obtained by a browser via a cross-site request the way session cookies
 * can, so the CSRF protection has no value here.
 *
 * We do NOT verify the key here — full verification still happens inside
 * `extractSession`. This is only a fast format check so the CSRF middleware
 * can defer to that downstream check.
 */
function isApiKeyRequest(request) {
    const auth = request.headers.get('Authorization');
    if (!auth?.startsWith('Bearer '))
        return false;
    const token = auth.slice(7);
    return /^act_sk_[a-f0-9]{32,}$/i.test(token);
}
const _require = createRequire(import.meta.url);
const { version: CMS_CORE_VERSION } = _require('../../package.json');
let cachedGraphQL = null;
export function handleActuateAPI(options = {}) {
    setActuateCoreVersion(CMS_CORE_VERSION);
    // A remote `platform.storage` shorthand that still needs a server-side adapter
    // wired on the first request (the config itself stays client-safe). Resolved
    // once, lazily, in the request handler — see `storageWireChecked` below.
    let pendingStorageProvider = null;
    if (options.config) {
        // Cast through `unknown` only to satisfy the typed setter; the public
        // signature accepts `unknown` deliberately because consumers may have
        // their own typings. Plugins and downstream code use `getActuateConfig()`
        // which gives them the ActuateCMSConfig shape.
        setActuateConfig(options.config);
        warnStrictRateLimitConfigAtBoot();
        initServerSentry();
        const config = options.config;
        // Register config-declared custom section types so coerce, validation, and
        // discovery (REST + MCP) treat them as first-class. Reset first so a
        // re-configure (tests, multi-tenant boots) never leaks stale types.
        resetCustomSectionTypes();
        registerSectionTypes(resolveCustomSectionTypes(config.sections));
        const hooks = [];
        if (Array.isArray(config.plugins)) {
            for (const plugin of config.plugins) {
                if (Array.isArray(plugin?.hooks)) {
                    hooks.push(...plugin.hooks);
                }
            }
        }
        ;
        config._pluginHooks = hooks;
        // Wire up the platform storage adapter so /media/upload routes through
        // it instead of dynamic-importing @vercel/blob directly. This honors
        // platform-vercel's `token`/`prefix` options and lets consumers ship
        // their own adapter (S3, R2, etc.) by registering one in the config.
        const platformStorage = config.platform?.storage;
        if (platformStorage &&
            typeof platformStorage === 'object' &&
            typeof platformStorage.upload === 'function') {
            try {
                setStorageAdapter(platformStorage);
            }
            catch (err) {
                if (process.env.NODE_ENV !== 'test') {
                    console.warn('[Actuate CMS] Failed to register platform storage adapter:', err);
                }
            }
        }
        // `platform.storage: { provider: 'vercel-blob' }` is a client-safe shorthand
        // (the config is imported by the admin client bundle, so it must not contain
        // a real adapter, which pulls in `node:*`). It registers nothing by itself,
        // so capture the provider and auto-wire the matching platform adapter
        // server-side on the first request (see `storageWireChecked`).
        const declaredProvider = platformStorage &&
            typeof platformStorage === 'object' &&
            typeof platformStorage.upload !== 'function'
            ? platformStorage.provider
            : undefined;
        if (declaredProvider && declaredProvider !== 'local' && !getStorageAdapter()) {
            pendingStorageProvider = declaredProvider;
        }
    }
    const router = createApiRouter();
    const rateLimiter = createRateLimiter({ windowMs: 60000, maxRequests: 100 });
    router.use(async (request, next) => {
        // Rate-limit per trusted client IP. When we cannot resolve a trustworthy
        // IP we fall back to a single global bucket — that's deliberately strict
        // so an attacker who's spoofing X-Forwarded-For shares the bucket with
        // every other un-authenticated client instead of getting their own.
        const ip = getClientIp(request);
        const bucket = ip === 'unknown' ? 'rl:_unknown' : `rl:${ip}`;
        const rateLimitResult = await rateLimiter.check(bucket);
        if (!rateLimitResult.allowed) {
            return new Response(JSON.stringify({ error: 'Too many requests' }), {
                status: 429,
                headers: {
                    'Content-Type': 'application/json',
                    'Retry-After': rateLimitResult.retryAfter?.toString() ?? '60',
                },
            });
        }
        if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
            // The router rewrites the URL to drop the `/api/cms` prefix before
            // dispatch, so by the time we run the path is already CMS-relative
            // (e.g. `/auth/login`). Match exact prefixes — never `includes()`.
            if (!isCsrfExemptPath(new URL(request.url).pathname) && !isApiKeyRequest(request)) {
                const csrfToken = request.headers.get('x-csrf-token');
                const csrfCookie = parseCookieHeader(request.headers.get('cookie') ?? '')['actuate_csrf'];
                if (!csrfToken || !csrfCookie || !validateCsrfToken(csrfToken, csrfCookie)) {
                    return new Response(JSON.stringify({ error: 'Invalid CSRF token' }), {
                        status: 403,
                        headers: { 'Content-Type': 'application/json' },
                    });
                }
            }
        }
        return next();
    });
    registerCMSRoutes(router);
    let dbChecked = false;
    let storageWireChecked = false;
    /**
     * Resolve the `platform.storage` shorthand into a real adapter on the first
     * request (server-only, so importing a platform package is safe). Memoized via
     * `storageWireChecked`; emits the guidance warning only if auto-wiring fails
     * (e.g. the platform package isn't installed).
     */
    async function ensureStorageAdapter() {
        if (storageWireChecked)
            return;
        storageWireChecked = true;
        if (!pendingStorageProvider || getStorageAdapter())
            return;
        const provider = pendingStorageProvider;
        const wired = await autoWireStorageAdapter(provider);
        if (!wired && process.env.NODE_ENV !== 'test') {
            const hint = isAutoWireableStorageProvider(provider)
                ? `install the matching platform package (e.g. \`@actuate-media/platform-${provider === 's3' ? 'aws' : 'vercel'}\`) so it can be auto-wired, or register an adapter server-side via setStorageAdapter()`
                : 'register a storage adapter server-side via setStorageAdapter() in your API route';
            console.warn(`[Actuate CMS] platform.storage.provider is "${provider}" but no storage adapter ` +
                `could be registered. The storage shorthand is client-safe and does not wire uploads ` +
                `on its own — ${hint}. Until then, media and appearance uploads will fail.`);
        }
    }
    return async function handler(request) {
        // Capture the start *before* the try-block — we want the duration
        // to include DB init / setup wiring too, since those costs are
        // visible to the user as latency.
        const startedAt = performance.now();
        const pathname = (() => {
            try {
                return new URL(request.url).pathname;
            }
            catch {
                return '/';
            }
        })();
        const response = await runHandler(request);
        // Fire-and-forget — must NOT delay the response. Wrapping in a
        // try/catch on the synchronous parts of `recordApiRequest` is
        // belt-and-braces since the function itself swallows DB errors.
        try {
            recordApiRequest({
                pathname,
                status: response.status,
                durationMs: performance.now() - startedAt,
            });
        }
        catch {
            /* metric pipeline must never break user requests */
        }
        return response;
    };
    async function runHandler(request) {
        try {
            await ensureStorageAdapter();
            if (!isDBInitialized()) {
                const client = options.prismaClient ??
                    (options.prismaClientGetter ? await options.prismaClientGetter() : null);
                if (client) {
                    initDB(client);
                    autoSeedAdmin(client).catch(() => { });
                }
            }
            if (!dbChecked) {
                dbChecked = true;
                try {
                    const client = options.prismaClient;
                    if (client) {
                        const required = ['document', 'user', 'session', 'media'];
                        const missing = required.filter((m) => !(m in client));
                        if (missing.length > 0) {
                            console.warn(`[Actuate CMS] Prisma client is missing models: ${missing.join(', ')}. ` +
                                'CMS features that depend on these models will return empty results. ' +
                                'Add the CMS schema to your project: see @actuate-media/cms-core/prisma/schema');
                        }
                    }
                }
                catch { }
            }
            const url = new URL(request.url);
            const originalPath = url.pathname;
            if (originalPath === '/api/cms/graphql' || originalPath.startsWith('/api/cms/graphql')) {
                const secret = process.env.CMS_SECRET;
                if (!secret || secret.length < 32) {
                    return new Response(JSON.stringify({ error: 'Server misconfiguration' }), {
                        status: 500,
                        headers: { 'Content-Type': 'application/json' },
                    });
                }
                const ip = getClientIp(request);
                const bucket = ip === 'unknown' ? 'rl:_unknown' : `rl:${ip}`;
                const rateLimitResult = await rateLimiter.check(bucket);
                if (!rateLimitResult.allowed) {
                    return new Response(JSON.stringify({ error: 'Too many requests' }), {
                        status: 429,
                        headers: {
                            'Content-Type': 'application/json',
                            'Retry-After': rateLimitResult.retryAfter?.toString() ?? '60',
                        },
                    });
                }
                if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method) &&
                    !isApiKeyRequest(request)) {
                    const csrfToken = request.headers.get('x-csrf-token');
                    const csrfCookie = parseCookieHeader(request.headers.get('cookie') ?? '')['actuate_csrf'];
                    if (!csrfToken || !csrfCookie || !validateCsrfToken(csrfToken, csrfCookie)) {
                        return new Response(JSON.stringify({ error: 'Invalid CSRF token' }), {
                            status: 403,
                            headers: { 'Content-Type': 'application/json' },
                        });
                    }
                }
                const config = getActuateConfig();
                if (config) {
                    if (!cachedGraphQL) {
                        const { createGraphQLHandler } = await import('../graphql/index.js');
                        cachedGraphQL = createGraphQLHandler({ config, secret });
                    }
                    return cachedGraphQL.handle(request);
                }
            }
            const cmsPrefix = '/api/cms';
            if (originalPath.startsWith(cmsPrefix)) {
                const strippedPath = originalPath.slice(cmsPrefix.length) || '/';
                const rewrittenUrl = new URL(strippedPath, url.origin);
                rewrittenUrl.search = url.search;
                const rewrittenRequest = new Request(rewrittenUrl.toString(), {
                    method: request.method,
                    headers: request.headers,
                    body: request.body,
                    // @ts-expect-error -- `duplex` is required for streaming bodies but
                    // not yet in the lib.dom RequestInit type. Using @ts-expect-error
                    // (vs @ts-ignore) ensures this directive itself fails when a future
                    // TS lib update adds the property, prompting us to remove the cast.
                    duplex: 'half',
                });
                return router.handle(rewrittenRequest);
            }
            return router.handle(request);
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            const stack = err instanceof Error ? err.stack : undefined;
            console.error(`[Actuate CMS] Unhandled error in API handler: ${message}`, stack);
            const isDev = process.env.NODE_ENV !== 'production';
            return new Response(JSON.stringify({
                error: 'Internal CMS error',
                ...(isDev && { detail: message, stack }),
            }), {
                status: 500,
                headers: { 'Content-Type': 'application/json' },
            });
        }
    }
}
//# sourceMappingURL=handler-factory.js.map