"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAgentVariantsRouter = void 0;
const path_1 = __importDefault(require("path"));
const express_1 = require("express");
const v3_1 = require("zod/v3");
const logger_1 = require("../utils/logger");
const agent_variants_1 = require("../mcp/agent-variants");
const VariantHistoryService_1 = require("../services/VariantHistoryService");
const PrototypeDeployService_1 = require("../services/PrototypeDeployService");
const evaluateFlags_1 = require("../config/evaluateFlags");
const featureFlagUserKey_1 = require("../config/featureFlagUserKey");
const log = (0, logger_1.createLogger)('AgentVariantsRouter');
const previewPortBody = v3_1.z.object({
    variantId: v3_1.z.string().min(1).nullable(),
});
const commitBody = v3_1.z.object({
    variantId: v3_1.z.string().min(1),
});
const deployBody = v3_1.z.object({
    variantId: v3_1.z.string().min(1),
    projectPath: v3_1.z.string().optional(),
});
/**
 * Express routes that back the iframe variants chip.
 *
 * - GET  /api/variants/active             — current active session state for the chip
 * - GET  /api/variants/:sessionId         — explicit session lookup
 * - POST /api/variants/:sessionId/preview-port  — switch proxy to a variant's
 *   dev server (or null to restore the user's default dev server)
 * - GET  /api/variants/:sessionId/static/:variantId — serve static preview HTML
 * - GET  /api/variants/:sessionId/design-md/:artifactId — serve raw DESIGN.md
 * - GET  /api/variants/:sessionId/design-md/:artifactId/view — serve DesignMD page
 * - POST /api/variants/:sessionId/commit  — user picked this variant in the
 *   chip; orchestrator enqueues the diff onto pending changes
 * - POST /api/variants/:sessionId/cancel  — dismiss the variants session
 */
const HISTORY_STATUSES = new Set([
    'completed',
    'committed',
    'rejected',
    'cancelled',
]);
const createAgentVariantsRouter = (orchestrator, bridge, telemetry, variantHistoryOverride, deployServiceOverride) => {
    const router = (0, express_1.Router)();
    const variantHistory = variantHistoryOverride ?? new VariantHistoryService_1.VariantHistoryService();
    const deployService = deployServiceOverride ?? new PrototypeDeployService_1.PrototypeDeployService();
    const resolveProjectPath = (raw) => {
        if (typeof raw === 'string' && raw.length > 0) {
            return path_1.default.isAbsolute(raw) ? raw : null;
        }
        return bridge?.getProjectPath() ?? null;
    };
    router.get('/variants/active', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        res.json(orchestrator.buildActiveSnapshot());
    });
    /**
     * Server-Sent Events stream for the iframe chip. Pushes a fresh
     * ActiveSnapshot whenever the orchestrator's state changes (propose,
     * approve, report-complete, dev-server-up, terminal, cancel, commit) so
     * the chip never has to poll.
     *
     * Heartbeats every 30s as comments (`: heartbeat\n\n`) to keep
     * intermediate proxies from killing the connection on idle. EventSource
     * auto-reconnects on disconnect.
     */
    router.get('/variants/events', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        res.set({
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache, no-transform',
            Connection: 'keep-alive',
            'X-Accel-Buffering': 'no',
        });
        res.flushHeaders?.();
        const writeSnapshot = (snapshot) => {
            try {
                res.write(`data: ${JSON.stringify(snapshot)}\n\n`);
            }
            catch (err) {
                log.warn('SSE write failed (client likely disconnected)', err);
            }
        };
        // Initial state so the chip can render immediately.
        writeSnapshot(orchestrator.buildActiveSnapshot());
        const unsubscribe = orchestrator.subscribeToEvents(writeSnapshot);
        const heartbeat = setInterval(() => {
            try {
                res.write(': heartbeat\n\n');
            }
            catch {
                // ignore — close handler will clean up
            }
        }, 30_000);
        req.on('close', () => {
            clearInterval(heartbeat);
            unsubscribe();
            try {
                res.end();
            }
            catch {
                // already ended
            }
        });
    });
    router.get('/variants/history', async (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const projectPath = resolveProjectPath(req.query.projectPath);
        if (!projectPath) {
            res.status(400).json({
                errorCode: 'RUNTIME_VALIDATION_FAILED',
                message: 'No projectPath available — pass ?projectPath=<abs> or open a project in the visual editor.',
            });
            return;
        }
        const statusParam = typeof req.query.status === 'string' ? req.query.status : undefined;
        if (statusParam && !HISTORY_STATUSES.has(statusParam)) {
            res.status(400).json({
                errorCode: 'SCHEMA_VALIDATION_FAILED',
                message: `Invalid status '${statusParam}' — expected one of ${[...HISTORY_STATUSES].join(', ')}.`,
            });
            return;
        }
        const limitRaw = req.query.limit;
        const limit = typeof limitRaw === 'string' && limitRaw.length > 0
            ? Number.parseInt(limitRaw, 10)
            : undefined;
        if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
            res.status(400).json({
                errorCode: 'SCHEMA_VALIDATION_FAILED',
                message: `Invalid limit '${limitRaw}' — expected a positive integer.`,
            });
            return;
        }
        try {
            const entries = await variantHistory.listVariants({
                projectPath,
                limit,
                status: statusParam,
            });
            res.json({
                projectPath,
                variants: entries.map((entry) => ({
                    sessionId: entry.manifest.sessionId,
                    variantId: entry.manifest.variantId,
                    label: entry.manifest.label,
                    brief: entry.manifest.brief,
                    kind: manifestKind(entry.manifest),
                    status: entry.manifest.status,
                    createdAt: entry.manifest.createdAt,
                    statusChangedAt: entry.manifest.statusChangedAt,
                    changedFilesCount: entry.manifest.changedFilesCount,
                    hasStaticPreview: entry.hasStaticPreview,
                    preview: entry.manifest.preview,
                    artifact: entry.manifest.artifact,
                })),
            });
        }
        catch (err) {
            sendError(res, err);
        }
    });
    router.get([
        '/variants/history/:sessionId/:variantId/preview',
        '/variants/history/:sessionId/:variantId/preview/{*assetPath}',
    ], async (req, res) => serveHistoryPreview(req, res, variantHistory, resolveProjectPath));
    router.get('/variants/history/:sessionId/:variantId', async (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const projectPath = resolveProjectPath(req.query.projectPath);
        if (!projectPath) {
            res.status(400).json({
                errorCode: 'RUNTIME_VALIDATION_FAILED',
                message: 'No projectPath available — pass ?projectPath=<abs> or open a project in the visual editor.',
            });
            return;
        }
        try {
            const detail = await variantHistory.getVariant({
                projectPath,
                sessionId: req.params.sessionId,
                variantId: req.params.variantId,
            });
            if (!detail) {
                res.status(404).json({
                    errorCode: 'WORK_ITEM_NOT_FOUND',
                    message: 'No persisted variant for that session + variant.',
                });
                return;
            }
            res.json({
                projectPath,
                variant: {
                    ...detail.manifest,
                    kind: manifestKind(detail.manifest),
                    diff: detail.diff,
                    files: detail.files,
                    designMarkdown: detail.designMarkdown,
                },
            });
        }
        catch (err) {
            sendError(res, err);
        }
    });
    router.get('/variants/:sessionId', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const sessionId = req.params.sessionId;
        if (!orchestrator.hasSession(sessionId)) {
            res.status(404).json({ error: 'Unknown variants sessionId' });
            return;
        }
        try {
            res.json(stateForSession(orchestrator, sessionId));
        }
        catch (err) {
            sendError(res, err);
        }
    });
    router.post('/variants/:sessionId/preview-port', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const sessionId = req.params.sessionId;
        const parsed = previewPortBody.safeParse(req.body);
        if (!parsed.success) {
            res.status(400).json({
                errorCode: 'SCHEMA_VALIDATION_FAILED',
                message: parsed.error.message,
            });
            return;
        }
        if (!bridge) {
            res.status(503).json({
                errorCode: 'RUNTIME_VALIDATION_FAILED',
                message: 'Bridge not available — cannot retarget proxy',
            });
            return;
        }
        try {
            const port = parsed.data.variantId
                ? (orchestrator.getDevServerPort(sessionId, parsed.data.variantId) ??
                    null)
                : null;
            bridge.switchPreviewPort(port);
            res.json({ port });
        }
        catch (err) {
            sendError(res, err);
        }
    });
    router.get('/variants/:sessionId/static/:variantId', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const { sessionId, variantId } = req.params;
        try {
            const html = orchestrator.getStaticPreviewHtml(sessionId, variantId);
            if (!html) {
                res.status(404).json({
                    errorCode: 'WORK_ITEM_NOT_FOUND',
                    message: 'Static preview not found',
                });
                return;
            }
            // Only inject `<base href>` when the variant actually has sibling
            // assets to serve. Otherwise the inlined HTML is self-contained and
            // the base tag would only serve to mis-resolve any incidental
            // relative URLs (e.g. `<a href="something">`) against the asset
            // sub-path. The hasAssets check also avoids the trailing-slash
            // fragment-link quirk for the common no-assets case.
            const hasAssets = orchestrator.hasStaticPreviewAssets(sessionId, variantId);
            if (hasAssets && !req.originalUrl.includes('?') && !req.originalUrl.endsWith('/')) {
                // Document URL must end in `/` so the browser's `document.URL` and
                // the `<base href>` agree. Otherwise fragment-only links
                // (`<a href="#section">`) resolve to the base-href URL — different
                // from the current URL — and the browser does a full navigation,
                // hitting the asset route with an empty path → 404. Redirect to
                // the canonical trailing-slash form before serving.
                res.redirect(301, req.originalUrl + '/');
                return;
            }
            const finalHtml = hasAssets
                ? injectBaseHref(html, 
                // Trailing slash is required so `./avatar.glb` resolves to
                // `.../static/<vid>/avatar.glb`, not `.../static/avatar.glb`.
                `/api/variants/${encodeURIComponent(sessionId)}/static/${encodeURIComponent(variantId)}/`)
                : html;
            // Constrain the blast radius of agent-generated scripts. The HTML is
            // served same-origin so a perfectly restrictive policy is impossible
            // without architectural surgery (separate origin per preview), but
            // we can still:
            //   - `frame-ancestors 'self'` — only Rivet's own UI can embed the
            //     preview, so a malicious page can't trick the user.
            //   - script-src / style-src / connect-src limited to inline +
            //     unpkg.com (the de-facto ESM CDN we observe in agent output)
            //     + data:/blob: for inline assets + 'self' so the iframe can
            //     fetch sibling assets from `/api/variants/.../static/.../`.
            //   - `connect-src 'self'` still permits same-origin fetches to
            //     other `/api/*` endpoints — accepted as a known limitation
            //     until previews move to an isolated origin. Tracked as
            //     follow-up: every state-changing route already enforces
            //     `rejectNonLocalRequest`, and the agent JS is sandboxed inside
            //     an iframe whose `allow-same-origin` we keep so the visual
            //     editor can target elements.
            const previewCsp = [
                "default-src 'self' data: blob:",
                "script-src 'self' 'unsafe-inline' https://unpkg.com blob:",
                "style-src 'self' 'unsafe-inline'",
                "img-src 'self' data: blob: https:",
                "font-src 'self' data: https://unpkg.com",
                "connect-src 'self' https://unpkg.com blob:",
                "frame-ancestors 'self'",
                "base-uri 'self'",
            ].join('; ');
            res
                .status(200)
                .type('html')
                .set('Cache-Control', 'no-store')
                .set('Content-Security-Policy', previewCsp)
                .set('X-Content-Type-Options', 'nosniff')
                .send(finalHtml);
        }
        catch (err) {
            sendError(res, err);
        }
    });
    router.get('/variants/:sessionId/design-md/:artifactId', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const { sessionId, artifactId } = req.params;
        try {
            const markdown = orchestrator.getDesignContextMarkdown(sessionId, artifactId);
            if (!markdown) {
                res.status(404).json({
                    errorCode: 'WORK_ITEM_NOT_FOUND',
                    message: 'DesignMD artifact not found',
                });
                return;
            }
            res
                .status(200)
                .type('text/markdown')
                .set('Cache-Control', 'no-store')
                .send(markdown);
        }
        catch (err) {
            sendError(res, err);
        }
    });
    router.get('/variants/:sessionId/design-md/:artifactId/view', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const { sessionId, artifactId } = req.params;
        try {
            const html = orchestrator.getDesignContextViewerHtml(sessionId, artifactId);
            if (!html) {
                res.status(404).json({
                    errorCode: 'WORK_ITEM_NOT_FOUND',
                    message: 'DesignMD artifact not found',
                });
                return;
            }
            res
                .status(200)
                .type('html')
                .set('Cache-Control', 'no-store')
                .set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'")
                .send(html);
        }
        catch (err) {
            sendError(res, err);
        }
    });
    /**
     * Serve sibling assets the variant's HTML references (e.g. `./avatar.glb`,
     * `./hero.png`). The agent reports `output.assetBase` alongside the inline
     * HTML; the orchestrator sandboxes it inside the fresh-session workspace
     * and exposes a resolver. Path-traversal-rejected requests + missing files
     * return 404. Loopback-only.
     */
    router.get('/variants/:sessionId/static/:variantId/{*assetPath}', (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const params = req.params;
        const toStr = (v) => Array.isArray(v) ? v.join('/') : typeof v === 'string' ? v : '';
        const sessionId = toStr(params.sessionId);
        const variantId = toStr(params.variantId);
        const requestedPath = toStr(params.assetPath);
        try {
            const abs = orchestrator.resolveStaticPreviewAssetPath(sessionId, variantId, requestedPath);
            if (!abs) {
                res.status(404).json({
                    errorCode: 'WORK_ITEM_NOT_FOUND',
                    message: 'Static preview asset not found',
                });
                return;
            }
            // `dotfiles: 'allow'` because workspacePath is typically nested
            // under a `.rivet/` directory (Rivet's per-project state dir) —
            // sendFile's default 'ignore' setting 404s anything with a hidden
            // segment, which would always trigger here.
            res.sendFile(abs, {
                dotfiles: 'allow',
                headers: { 'Cache-Control': 'no-store' },
            });
        }
        catch (err) {
            sendError(res, err);
        }
    });
    router.post('/variants/:sessionId/commit', async (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const sessionId = req.params.sessionId;
        const parsed = commitBody.safeParse(req.body);
        if (!parsed.success) {
            res.status(400).json({
                errorCode: 'SCHEMA_VALIDATION_FAILED',
                message: parsed.error.message,
            });
            return;
        }
        try {
            const projectContext = orchestrator.getProjectContext(sessionId);
            const isFresh = projectContext.kind === 'fresh';
            const result = await orchestrator.commitVariant({
                sessionId,
                variantId: parsed.data.variantId,
                skipTeardown: isFresh,
            });
            if (result.enqueued &&
                result.payloadKind === 'project-created' &&
                isFresh) {
                if (!bridge || !result.destinationPath) {
                    telemetry?.trackAgentVariantsActiveProjectTransition({
                        sessionId,
                        success: false,
                        reasonCode: 'bridge_unavailable',
                        destinationPath: result.destinationPath ?? null,
                    });
                    res.status(503).json({
                        errorCode: 'RUNTIME_VALIDATION_FAILED',
                        message: 'Variant sent, but active-project transition is unavailable. Keep this session open and run open_visual_editor on the destination project.',
                        destinationPath: result.destinationPath ?? null,
                    });
                    return;
                }
                try {
                    bridge.rebindProjectPath(result.destinationPath);
                    // Do NOT call switchPreviewPort(null) here on the fresh vite_app
                    // path: commitVariant has already spawned a fresh dev server at
                    // destinationPath and retargeted the proxy to its port via the
                    // orchestrator's switchPreviewPort callback. Calling null here
                    // would route the proxy to the default upstream (the user's repo,
                    // which has no dev server) and the iframe would show "preview
                    // disconnected" — exactly the regression we're fixing. The
                    // committed dev server lives until the next propose / shutdown.
                    await orchestrator.cleanupCommittedSession(sessionId);
                    telemetry?.trackAgentVariantsActiveProjectTransition({
                        sessionId,
                        success: true,
                        reasonCode: 'rebind_succeeded',
                        destinationPath: result.destinationPath,
                    });
                }
                catch {
                    telemetry?.trackAgentVariantsActiveProjectTransition({
                        sessionId,
                        success: false,
                        reasonCode: 'rebind_failed',
                        destinationPath: result.destinationPath,
                    });
                    res.status(500).json({
                        errorCode: 'RUNTIME_VALIDATION_FAILED',
                        message: 'Variant sent, but active-project handoff failed. Destination project is preserved; keep this variants session open and retry send.',
                        destinationPath: result.destinationPath,
                    });
                    return;
                }
            }
            else if (bridge && result.enqueued) {
                try {
                    bridge.switchPreviewPort(null);
                }
                catch (err) {
                    log.warn('switchPreviewPort(null) after commit failed', err);
                }
            }
            res.json(result);
        }
        catch (err) {
            sendError(res, err);
        }
    });
    /**
     * Deploy a fresh-project (Vite) variant to the public Prototype Preview Host
     * and return a shareable URL. Builds locally, uploads the static bundle with
     * the user's Rivet JWT, and records the deployment in the variant manifest so
     * the chip can re-show ("Copy link") without rebuilding.
     */
    router.post('/variants/:sessionId/deploy', async (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const sessionId = req.params.sessionId;
        // Gated behind the `deployPrototype` flag (default off). Mirrors the UI
        // gate so the endpoint can't be driven while the feature is dark.
        const flags = await (0, evaluateFlags_1.getFeatureFlags)({
            userKey: (0, featureFlagUserKey_1.getConfigFeatureFlagUserKey)({ headers: req.headers }),
        });
        if (!flags.deployPrototype.isEnabled) {
            res.status(403).json({
                errorCode: 'FEATURE_DISABLED',
                message: 'Prototype deploy is not enabled.',
            });
            return;
        }
        const parsed = deployBody.safeParse(req.body);
        if (!parsed.success) {
            res.status(400).json({
                errorCode: 'SCHEMA_VALIDATION_FAILED',
                message: parsed.error.message,
            });
            return;
        }
        const projectPath = resolveProjectPath(parsed.data.projectPath);
        if (!projectPath) {
            res.status(400).json({
                errorCode: 'RUNTIME_VALIDATION_FAILED',
                message: 'No projectPath available — pass projectPath or open a project in the visual editor.',
            });
            return;
        }
        try {
            const variant = await variantHistory.getVariant({
                projectPath,
                sessionId,
                variantId: parsed.data.variantId,
            });
            if (!variant) {
                res.status(404).json({
                    errorCode: 'WORK_ITEM_NOT_FOUND',
                    message: 'Variant not found in history.',
                });
                return;
            }
            const filesDir = path_1.default.join(variant.variantDir, 'files');
            const preview = variant.manifest.preview;
            let result;
            if (preview.kind === 'static') {
                // Already a built static bundle (e.g. a history static-preview
                // variant) — upload files/ directly, no build needed. This is what
                // makes deploying a clicked variant from history work.
                result = await deployService.deployStatic(filesDir);
            }
            else if (variant.manifest.projectKind === 'fresh' &&
                variant.manifest.artifact.kind === 'project') {
                // Buildable fresh Vite app — compile then upload.
                result = await deployService.deploy(filesDir);
            }
            else {
                res.status(422).json({
                    errorCode: 'RUNTIME_VALIDATION_FAILED',
                    message: 'This prototype can\'t be deployed — only fresh app directions or static previews are supported.',
                });
                return;
            }
            const { deployId, shareUrl } = result;
            const deployedAt = new Date().toISOString();
            await variantHistory.recordDeployment({
                projectPath,
                sessionId,
                variantId: parsed.data.variantId,
                deployment: { deployId, shareUrl, deployedAt },
            });
            res.json({ deployId, shareUrl, deployedAt });
        }
        catch (err) {
            log.error('Prototype deploy failed', err);
            const message = err instanceof Error ? err.message : 'Deploy failed.';
            const detail = err && typeof err === 'object' && 'detail' in err
                ? String(err.detail ?? '')
                : '';
            res.status(500).json({
                errorCode: 'RUNTIME_VALIDATION_FAILED',
                message,
                ...(detail ? { detail } : {}),
            });
        }
    });
    router.post('/variants/:sessionId/cancel', async (req, res) => {
        if (rejectNonLocalRequest(req, res))
            return;
        const sessionId = req.params.sessionId;
        try {
            const result = await orchestrator.cancel({
                sessionId,
                reason: typeof req.body?.reason === 'string' ? req.body.reason : undefined,
            });
            if (bridge) {
                try {
                    bridge.switchPreviewPort(null);
                }
                catch (err) {
                    log.warn('switchPreviewPort(null) after cancel failed', err);
                }
            }
            res.json(result);
        }
        catch (err) {
            sendError(res, err);
        }
    });
    return router;
};
exports.createAgentVariantsRouter = createAgentVariantsRouter;
/** Serve one saved static preview file from a variant history snapshot. */
const serveHistoryPreview = async (req, res, variantHistory, resolveProjectPath) => {
    if (rejectNonLocalRequest(req, res))
        return;
    const projectPath = resolveProjectPath(req.query.projectPath);
    if (!projectPath) {
        res.status(400).json({
            errorCode: 'RUNTIME_VALIDATION_FAILED',
            message: 'No projectPath available — pass ?projectPath=<abs> or open a project in the visual editor.',
        });
        return;
    }
    const rawAssetPath = req.params.assetPath;
    let assetPath = null;
    if (Array.isArray(rawAssetPath)) {
        assetPath = rawAssetPath.join('/');
    }
    else if (typeof rawAssetPath === 'string') {
        assetPath = rawAssetPath;
    }
    const sessionId = Array.isArray(req.params.sessionId)
        ? req.params.sessionId[0]
        : req.params.sessionId;
    const variantId = Array.isArray(req.params.variantId)
        ? req.params.variantId[0]
        : req.params.variantId;
    try {
        const file = await variantHistory.readStaticPreviewFile({
            projectPath,
            sessionId,
            variantId,
            assetPath,
        });
        if (!file) {
            res.status(404).json({
                errorCode: 'WORK_ITEM_NOT_FOUND',
                message: 'No static preview file persisted for that variant.',
            });
            return;
        }
        const contentType = path_1.default.extname(file.absolutePath) || 'html';
        res
            .status(200)
            .type(contentType)
            .set('Cache-Control', 'no-store')
            .send(file.body);
    }
    catch (err) {
        if (err instanceof Error && err.message.includes('invalid assetPath')) {
            res.status(400).json({
                errorCode: 'SCHEMA_VALIDATION_FAILED',
                message: err.message,
            });
            return;
        }
        sendError(res, err);
    }
};
function rejectNonLocalRequest(req, res) {
    const remoteAddress = req.socket.remoteAddress;
    if (isLoopbackAddress(remoteAddress))
        return false;
    res.status(403).json({
        errorCode: 'RUNTIME_VALIDATION_FAILED',
        message: 'Variant session routes are only accepted from loopback clients.',
    });
    return true;
}
function isLoopbackAddress(remoteAddress) {
    if (!remoteAddress)
        return false;
    return (remoteAddress === '127.0.0.1' ||
        remoteAddress === '::1' ||
        remoteAddress.startsWith('::ffff:127.'));
}
function stateForSession(orchestrator, sessionId) {
    const stage = orchestrator.getStage(sessionId);
    const briefs = orchestrator.getBriefs(sessionId);
    const progress = orchestrator.getProgress(sessionId);
    const summary = stage === 'ready' ||
        stage === 'degraded' ||
        stage === 'failed' ||
        stage === 'cancelled'
        ? orchestrator.getSummary(sessionId)
        : null;
    const variants = orchestrator.getVariants(sessionId);
    const projectContext = orchestrator.getProjectContext(sessionId);
    const activeProjectContext = toActiveProjectContext(projectContext);
    const destinationPath = activeProjectContext.kind === 'fresh'
        ? activeProjectContext.workspacePath
        : undefined;
    return {
        sessionId,
        stage,
        briefs,
        progress,
        summary,
        variants,
        projectContext: activeProjectContext,
        ...(destinationPath ? { destinationPath } : {}),
    };
}
const toActiveProjectContext = (projectContext) => {
    if (projectContext.kind === 'existing') {
        return { kind: 'existing' };
    }
    return {
        kind: 'fresh',
        workspacePath: projectContext.workspacePath,
        workspaceRoot: projectContext.workspaceRoot,
        framework: projectContext.framework,
        designContext: projectContext.designContext?.map((entry) => entry.kind === 'slug'
            ? { kind: 'slug', slug: entry.slug }
            : { kind: 'markdown', label: entry.label }),
        ...(projectContext.sourceContext
            ? {
                sourceContext: {
                    sourceCount: projectContext.sourceContext.sourceUrls?.length ?? 0,
                    isSourceGrounded: Boolean(projectContext.sourceContext.artifact),
                },
            }
            : {}),
    };
};
/** Map persisted artifact shape to the public history summary kind. */
const manifestKind = (manifest) => {
    return manifest.artifact.kind === 'diff' ? 'diff' : 'project-created';
};
/**
 * Inject a `<base href>` tag into the variant HTML so relative URLs
 * resolve against the asset-serving sub-path. Idempotent: if the HTML
 * already declares a `<base>` tag we leave it alone (the agent opted in
 * to its own resolution rules). Falls back to prefixing the document if
 * `<head>` can't be located.
 */
function injectBaseHref(html, baseHref) {
    if (/<base\s/i.test(html))
        return html;
    const tag = `<base href="${baseHref.replace(/"/g, '&quot;')}">`;
    const headMatch = html.match(/<head[^>]*>/i);
    if (headMatch) {
        const insertAt = headMatch.index + headMatch[0].length;
        return html.slice(0, insertAt) + tag + html.slice(insertAt);
    }
    // Headless HTML (e.g. `<!doctype html><html><body>…`) — placing the base
    // tag before the doctype would trigger quirks mode in strict parsers.
    // Insert after the doctype if there is one, otherwise prepend.
    const doctypeMatch = html.match(/<!doctype[^>]*>/i);
    if (doctypeMatch) {
        const insertAt = doctypeMatch.index + doctypeMatch[0].length;
        return html.slice(0, insertAt) + tag + html.slice(insertAt);
    }
    return tag + html;
}
function sendError(res, err) {
    if (err instanceof agent_variants_1.AgentVariantsError) {
        log.warn(`agent-variants route error: ${err.code}: ${err.detail}`);
        res.status(statusForCode(err.code)).json({
            errorCode: err.code,
            message: err.detail,
        });
        return;
    }
    log.error('Unexpected error in agent-variants route', err);
    const message = err instanceof Error ? err.message : 'Unknown error';
    res.status(500).json({ errorCode: 'RUNTIME_VALIDATION_FAILED', message });
}
function statusForCode(code) {
    switch (code) {
        case 'SESSION_NOT_FOUND':
        case 'WORK_ITEM_NOT_FOUND':
            return 404;
        case 'INVALID_STAGE_ACTION':
        case 'INVALID_TRANSITION':
        case 'STALE_LEASE':
        case 'LEASE_EXPIRED':
        case 'LEASE_MISMATCH':
        case 'PENDING_CHANGE_CONFLICT':
        case 'MISSING_REQUIRED_INPUT':
            return 409;
        case 'SCHEMA_VALIDATION_FAILED':
        case 'SOURCE_CONTEXT_INVALID':
        case 'SOURCE_CONTEXT_TOO_LARGE':
            return 400;
        default:
            return 500;
    }
}
//# sourceMappingURL=agentVariants.js.map