import { createHash, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
import { AppError, appErrorToHttpBody, NotFoundError, ValidationError, toAppError } from '../errors.js';
import { shouldAutoCapture } from '../error-log/auto-capture.js';
import { computeAtomGateStats } from '../operations/atom-gate-stats.js';
import { computeAtomGraphDensity } from '../operations/atom-graph-density.js';
import { predictImpact } from '../retrieval/impact-predictor.js';
import { getRetrievalPolicy } from '../retrieval/policy.js';
import { streamAtomGraphJsonl } from '../operations/atom-graph-export.js';
import { getCatchupMetadata } from '../operations/catchup.js';
import { createUserStyleAtom } from '../user-style/store-helpers.js';
import { validateAppendAgentSessionNoteInput, validateCaptureAgentLearningSignalInput, validateContextSearchInput, validateBackupRetentionInput, validateCollectErrorLogsInput, validateContextQualityReportInput, validateCleanupOperationsInput, validateCreateBackupInput, validateCreateErrorLogReflectionDraftInput, validateErrorLogInput, validateErrorLogListInput, validateErrorLogPatchInput, validateFeedbackInput, validateFinishAgentSessionInput, validateIngestFilesRequest, validateKnowledgeInput, validateKnowledgePatchInput, validateKnowledgeConflictPatchInput, validateKnowledgeGapPatchInput, validateKnowledgeRelationInput, validateKnowledgeRelationPatchInput, validateKnowledgeReviewFilter, validateKnowledgeStatusQuery, validateLearningProposalPatchInput, validateLearningProposalTypeQuery, validateLearningReviewStatusQuery, validateMaintenanceApplyInput, validateMaintenanceProposeInput, validateRecordAgentContextDecisionInput, validateReflectionDraftPatchInput, validateReflectionDraftInput, validateReflectionDraftReviewInput, validateRestoreBackupInput, validateResolveErrorLogInput, validateStartAgentSessionInput, } from '../validation.js';
const RAW_RESPONSE = Symbol('raw_http_response');
export function createHttpServer(services) {
    const router = new HttpRouter(services);
    const server = createServer(async (request, response) => {
        await router.handle(request, response);
    });
    server.requestTimeout = 60_000;
    server.headersTimeout = 10_000;
    return server;
}
export async function handleHttpRequest(services, request, response) {
    const router = new HttpRouter(services);
    await router.handle(request, response);
}
class HttpRouter {
    services;
    routes;
    constructor(services) {
        this.services = services;
        this.routes = createRoutes();
    }
    async handle(request, response) {
        const method = request.method?.toUpperCase() ?? 'GET';
        const url = new URL(request.url ?? '/', 'http://localhost');
        try {
            const matched = this.matchRoute(method, url);
            if (!matched) {
                throw new HttpError(404, `No route for ${method} ${url.pathname}`);
            }
            if (!matched.route.public) {
                authenticate(request, this.services.config);
            }
            const body = await matched.route.handle({
                services: this.services,
                request,
                url,
                params: matched.params,
            });
            if (isRawHttpResponse(body)) {
                sendRaw(response, body);
            }
            else {
                sendJson(response, 200, body);
            }
        }
        catch (error) {
            const appError = toAppError(error);
            await maybeCaptureHttpError(this.services, request, url, method, appError);
            sendJson(response, appError.status, appErrorToHttpBody(appError));
        }
    }
    matchRoute(method, url) {
        for (const route of this.routes) {
            if (route.method !== method) {
                continue;
            }
            const params = route.match(url);
            if (params) {
                return { route, params };
            }
        }
        return undefined;
    }
}
function createRoutes() {
    return [
        {
            method: 'GET',
            match: exactPath('/health'),
            public: true,
            handle: ({ services }) => ({
                ok: true,
                service: 'tuberosa',
                store: services.config.storage.store,
                durability: services.config.storage.store === 'postgres' ? 'persistent' : 'ephemeral',
                backupDir: services.config.backup.dir,
                cache: services.config.storage.cache,
                modelProvider: services.config.model.provider,
            }),
        },
        {
            method: 'POST',
            match: exactPath('/context/search'),
            handle: async ({ services, request }) => {
                const body = validateContextSearchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const pack = await services.retrieval.searchContext(body);
                services.operations.requestPhysicalMirror('context-searched');
                return pack;
            },
        },
        {
            method: 'GET',
            match: exactPath('/context/packs'),
            handle: ({ services, url }) => services.operations.listContextPacks(readListRecordsOptions(url)),
        },
        {
            method: 'GET',
            match: pathPattern(/^\/context\/packs\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const pack = await services.retrieval.getContextPack(params.id);
                if (!pack) {
                    throw new NotFoundError('Context pack not found.');
                }
                return pack;
            },
        },
        {
            method: 'POST',
            match: exactPath('/context/feedback'),
            handle: async ({ services, request }) => {
                const body = validateFeedbackInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const result = await services.retrieval.recordFeedback(body);
                services.operations.requestPhysicalMirror('context-feedback-recorded');
                return result;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/atoms\/([^/]+)\/resurrect$/, ['id']),
            handle: async ({ services, params }) => {
                const atom = await services.store.updateAtom(params.id, {
                    status: 'active',
                    lastReusedAt: new Date().toISOString(),
                });
                if (!atom) {
                    throw new NotFoundError('Atom not found.');
                }
                services.operations.requestPhysicalMirror('atom-resurrected');
                return { atom };
            },
        },
        {
            method: 'POST',
            match: exactPath('/user-style-atoms'),
            handle: async ({ services, request }) => {
                const body = await readJsonBody(request, services.config.http.maxRequestBytes);
                const userId = (typeof body.userId === 'string' && body.userId) || services.config.userStyle.userId;
                if (!userId) {
                    throw new ValidationError('userId required (set TUBEROSA_USER_ID or include in body).');
                }
                if (typeof body.claim !== 'string' || !body.claim.trim()) {
                    throw new ValidationError('claim is required.');
                }
                if (typeof body.type !== 'string' || !['convention', 'gotcha', 'decision', 'fact'].includes(body.type)) {
                    throw new ValidationError('type must be one of: convention, gotcha, decision, fact.');
                }
                const priority = (body.priority ?? 'coding_preference');
                if (!['personal_workflow', 'coding_preference'].includes(priority)) {
                    throw new ValidationError('priority must be personal_workflow or coding_preference.');
                }
                const atom = await createUserStyleAtom(services.store, {
                    userId,
                    claim: body.claim,
                    type: body.type,
                    priority,
                    trigger: (body.trigger ?? {}),
                    evidence: Array.isArray(body.evidence) ? body.evidence : undefined,
                    pitfalls: Array.isArray(body.pitfalls) ? body.pitfalls : undefined,
                    sessionId: typeof body.sessionId === 'string' ? body.sessionId : undefined,
                });
                services.operations.requestPhysicalMirror('user-style-recorded');
                return { atom };
            },
        },
        {
            method: 'GET',
            match: exactPath('/user-style-atoms'),
            handle: async ({ services, url }) => {
                const userId = url.searchParams.get('userId') ?? services.config.userStyle.userId;
                if (!userId) {
                    throw new ValidationError('userId query parameter required (or set TUBEROSA_USER_ID).');
                }
                const atoms = await services.store.listAtoms({
                    project: undefined,
                    scope: 'user',
                    userId,
                    limit: 100,
                });
                return { atoms };
            },
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/user-style-atoms\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const patch = await readJsonBody(request, services.config.http.maxRequestBytes);
                const existing = await services.store.getAtom(params.id);
                if (!existing || existing.scope !== 'user') {
                    throw new NotFoundError('User-style atom not found.');
                }
                const safePatch = {
                    tier: patch.tier,
                    status: patch.status,
                    pitfalls: Array.isArray(patch.pitfalls) ? patch.pitfalls : undefined,
                };
                const updated = await services.store.updateAtom(params.id, safePatch);
                return { atom: updated };
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/atom-gate/stats'),
            handle: ({ services, url }) => {
                const project = url.searchParams.get('project') ?? undefined;
                const window = url.searchParams.get('window');
                const windowDays = window === '30d' ? 30 : 7;
                return computeAtomGateStats(services.store, { project, windowDays });
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/atom-graph/density'),
            handle: ({ services, url }) => {
                const project = url.searchParams.get('project');
                if (!project) {
                    throw new ValidationError('project query parameter is required.');
                }
                return computeAtomGraphDensity(services.store, { project });
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/organization/atom-graph.jsonl'),
            handle: async ({ services, url }) => {
                const project = url.searchParams.get('project');
                if (!project) {
                    throw new ValidationError('project query parameter is required.');
                }
                // Buffered NDJSON. Atom graphs are bounded by listAtoms's 10k cap; the
                // payload stays small in practice. True chunked streaming requires
                // wider router changes — left as a follow-up.
                const lines = [];
                for await (const line of streamAtomGraphJsonl(services.store, { project })) {
                    lines.push(line);
                }
                const body = lines.length > 0 ? `${lines.join('\n')}\n` : '';
                return rawResponse('application/x-ndjson; charset=utf-8', body);
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/atom-graph/impact'),
            handle: async ({ services, request }) => {
                const body = await readJsonBody(request, services.config.http.maxRequestBytes);
                if (!body || typeof body !== 'object' || Array.isArray(body)) {
                    throw new ValidationError('Request body must be a JSON object.');
                }
                const record = body;
                const project = record.project;
                if (typeof project !== 'string' || project.trim().length === 0) {
                    throw new ValidationError('project must be a non-empty string.');
                }
                const files = readImpactStringArray(record.files, 'files');
                const symbols = readImpactStringArray(record.symbols, 'symbols');
                const depth = typeof record.depth === 'number' && Number.isFinite(record.depth) && record.depth >= 1
                    ? Math.min(4, Math.floor(record.depth))
                    : undefined;
                const policy = getRetrievalPolicy().graph;
                return predictImpact(services.store, { project, files, symbols, policy, depth });
            },
        },
        {
            method: 'POST',
            match: exactPath('/agent-sessions'),
            handle: async ({ services, request }) => {
                const body = validateStartAgentSessionInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const result = await services.agentSessions.startSession(body);
                services.operations.requestPhysicalMirror('agent-session-started');
                return result;
            },
        },
        {
            method: 'GET',
            match: exactPath('/agent-sessions'),
            handle: ({ services, url }) => services.operations.listAgentSessions(readListRecordsOptions(url)),
        },
        {
            method: 'GET',
            match: pathPattern(/^\/agent-sessions\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const session = await services.operations.getAgentSession(params.id);
                if (!session) {
                    throw new NotFoundError('Agent session not found.');
                }
                return session;
            },
        },
        {
            method: 'GET',
            match: pathPattern(/^\/agent-sessions\/([^/]+)\/context-decisions$/, ['id']),
            handle: ({ services, params, url }) => services.operations.listAgentContextDecisions({
                sessionId: params.id,
                limit: readLimit(url),
            }),
        },
        {
            method: 'POST',
            match: pathPattern(/^\/agent-sessions\/([^/]+)\/context-decision$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateRecordAgentContextDecisionInput(await readJsonBody(request, services.config.http.maxRequestBytes), params.id);
                const result = await services.agentSessions.recordContextDecision(body);
                services.operations.requestPhysicalMirror('agent-context-decision-recorded');
                return result;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/agent-sessions\/([^/]+)\/learning-signals$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateCaptureAgentLearningSignalInput(await readJsonBody(request, services.config.http.maxRequestBytes), params.id);
                const result = await services.agentSessions.captureLearningSignal(body);
                services.operations.requestPhysicalMirror('agent-learning-signal-captured');
                return result;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/agent-sessions\/([^/]+)\/finish$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateFinishAgentSessionInput(await readJsonBody(request, services.config.http.maxRequestBytes), params.id);
                const result = await services.agentSessions.finishSession(body);
                services.operations.requestPhysicalMirror('agent-session-finished');
                return result;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/agent-sessions\/([^/]+)\/notes$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateAppendAgentSessionNoteInput(await readJsonBody(request, services.config.http.maxRequestBytes), params.id);
                const result = await services.agentSessions.appendSessionNote(body);
                services.operations.requestPhysicalMirror('agent-session-note-appended');
                return result;
            },
        },
        {
            method: 'POST',
            match: exactPath('/ingest/files'),
            handle: async ({ services, request }) => {
                const body = validateIngestFilesRequest(await readJsonBody(request, services.config.http.maxRequestBytes));
                const result = await services.ingestion.ingestFiles(body.project, body.files, { mode: body.mode });
                if (body.files.length > 1) {
                    services.operations.requestWriteThroughBackup('bulk-ingest-files');
                }
                services.operations.requestPhysicalMirror('files-ingested');
                return result;
            },
        },
        {
            method: 'POST',
            match: exactPath('/knowledge'),
            handle: async ({ services, request }) => {
                const body = validateKnowledgeInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const knowledge = await services.ingestion.ingestKnowledge(body);
                services.operations.requestPhysicalMirror('knowledge-ingested');
                return knowledge;
            },
        },
        {
            method: 'GET',
            match: exactPath('/knowledge'),
            handle: ({ services, url }) => services.operations.listKnowledge({
                project: url.searchParams.get('project') ?? undefined,
                query: url.searchParams.get('q') ?? undefined,
                status: validateKnowledgeStatusQuery(url.searchParams.get('status')),
                review: validateKnowledgeReviewFilter(url.searchParams.get('review')),
                limit: readLimit(url),
            }),
        },
        {
            method: 'GET',
            match: pathPattern(/^\/knowledge\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const knowledge = await services.operations.getKnowledge(params.id);
                if (!knowledge) {
                    throw new NotFoundError('Knowledge item not found.');
                }
                return knowledge;
            },
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/knowledge\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateKnowledgePatchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const knowledge = await services.operations.updateKnowledge(params.id, body);
                if (!knowledge) {
                    throw new NotFoundError('Knowledge item not found.');
                }
                return knowledge;
            },
        },
        {
            method: 'GET',
            match: exactPath('/labels'),
            handle: ({ services, url }) => services.operations.listLabels({
                project: url.searchParams.get('project') ?? undefined,
                limit: readLimit(url),
            }),
        },
        {
            method: 'GET',
            match: exactPath('/operations/relations'),
            handle: ({ services, url }) => services.operations.listKnowledgeRelations(readRelationListOptions(url)),
        },
        {
            method: 'POST',
            match: exactPath('/operations/relations'),
            handle: async ({ services, request }) => {
                const body = validateKnowledgeRelationInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                return services.operations.createKnowledgeRelation(body);
            },
        },
        {
            method: 'GET',
            match: pathPattern(/^\/operations\/relations\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const relation = await services.operations.getKnowledgeRelation(params.id);
                if (!relation) {
                    throw new NotFoundError('Knowledge relation not found.');
                }
                return relation;
            },
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/operations\/relations\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateKnowledgeRelationPatchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const relation = await services.operations.updateKnowledgeRelation(params.id, body);
                if (!relation) {
                    throw new NotFoundError('Knowledge relation not found.');
                }
                return relation;
            },
        },
        {
            method: 'DELETE',
            match: pathPattern(/^\/operations\/relations\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const deleted = await services.operations.deleteKnowledgeRelation(params.id);
                if (!deleted) {
                    throw new NotFoundError('Knowledge relation not found.');
                }
                return { deleted: true };
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/conflicts'),
            handle: ({ services, url }) => services.operations.listKnowledgeConflicts(readConflictListOptions(url)),
        },
        {
            method: 'POST',
            match: exactPath('/operations/import-pack'),
            handle: async ({ services, request }) => {
                const body = (await readJsonBody(request, services.config.http.maxRequestBytes));
                if (typeof body.from !== 'string' || body.from.length === 0) {
                    throw new ValidationError('from is required');
                }
                let safeFrom;
                try {
                    const { assertSafeBundlePath } = await import('../security/safe-paths.js');
                    safeFrom = await assertSafeBundlePath(services.config.backup.importBaseDir, body.from);
                }
                catch (err) {
                    throw new ValidationError(err.message);
                }
                const { importPack } = await import('../export/importer.js');
                return importPack(services.store, {
                    from: safeFrom,
                    project: typeof body.project === 'string' ? body.project : undefined,
                    dryRun: Boolean(body.dryRun),
                    onConflict: body.onConflict === 'skip' ? 'skip' : 'review',
                });
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/export-pack'),
            handle: async ({ services, request }) => {
                const body = (await readJsonBody(request, services.config.http.maxRequestBytes));
                if (typeof body.project !== 'string' || typeof body.out !== 'string') {
                    throw new ValidationError('project and out are required');
                }
                let safeOut;
                try {
                    const { assertSafeBundlePath } = await import('../security/safe-paths.js');
                    safeOut = await assertSafeBundlePath(services.config.backup.exportBaseDir, body.out);
                }
                catch (err) {
                    throw new ValidationError(err.message);
                }
                const { exportPack } = await import('../export/exporter.js');
                return exportPack(services.store, {
                    project: body.project,
                    out: safeOut,
                    includeChunks: body.includeChunks === undefined ? true : Boolean(body.includeChunks),
                    includeArchived: Boolean(body.includeArchived),
                });
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/atom-import-conflicts'),
            handle: async ({ services, url }) => {
                const project = url.searchParams.get('project') ?? undefined;
                const status = url.searchParams.get('status') ?? 'open';
                return services.store.listAtomImportConflicts({ project, status, limit: 100 });
            },
        },
        {
            method: 'GET',
            match: pathPattern(/^\/operations\/atom-import-conflicts\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const row = await services.store.getAtomImportConflict(params.id);
                if (!row)
                    throw new NotFoundError('Atom import conflict not found.');
                return row;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/operations\/atom-import-conflicts\/([^/]+)\/resolve$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = (await readJsonBody(request, services.config.http.maxRequestBytes));
                const action = body.action;
                if (action !== 'keep_local'
                    && action !== 'take_imported'
                    && action !== 'merged'
                    && action !== 'dismissed') {
                    throw new ValidationError('action must be keep_local|take_imported|merged|dismissed');
                }
                const updated = await services.store.resolveAtomImportConflict(params.id, action, body.mergedSnapshot, typeof body.notes === 'string' ? body.notes : undefined);
                if (!updated)
                    throw new NotFoundError('Atom import conflict not found.');
                return updated;
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/conflicts/detect'),
            handle: ({ services, url }) => services.operations.detectKnowledgeConflicts({
                project: url.searchParams.get('project') ?? undefined,
                limit: readLimit(url),
            }),
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/operations\/conflicts\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateKnowledgeConflictPatchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const conflict = await services.operations.updateKnowledgeConflict(params.id, body);
                if (!conflict) {
                    throw new NotFoundError('Knowledge conflict not found.');
                }
                return conflict;
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/knowledge-gaps'),
            handle: ({ services, url }) => services.operations.listKnowledgeGaps(readKnowledgeGapListOptions(url)),
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/operations\/knowledge-gaps\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateKnowledgeGapPatchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const gap = await services.operations.updateKnowledgeGap(params.id, body);
                if (!gap) {
                    throw new NotFoundError('Knowledge gap not found.');
                }
                return gap;
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/learning-proposals'),
            handle: ({ services, url }) => services.operations.listLearningProposals(readLearningProposalListOptions(url)),
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/operations\/learning-proposals\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateLearningProposalPatchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const proposal = await services.operations.updateLearningProposal(params.id, body);
                if (!proposal) {
                    throw new NotFoundError('Learning proposal not found.');
                }
                return proposal;
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/organization/project-map'),
            handle: ({ services, url }) => services.operations.exportProjectMap({
                project: url.searchParams.get('project') ?? undefined,
                limit: readLimit(url),
            }),
        },
        {
            method: 'GET',
            match: exactPath('/operations/organization/knowledge-graph.jsonl'),
            handle: ({ services, url }) => services.operations.exportKnowledgeGraphJsonl({
                project: url.searchParams.get('project') ?? undefined,
                limit: readLimit(url),
            }),
        },
        {
            method: 'GET',
            match: exactPath('/operations/organization/readable-summary'),
            handle: ({ services, url }) => services.operations.exportReadableSummary({
                project: url.searchParams.get('project') ?? undefined,
                limit: readLimit(url),
            }),
        },
        {
            method: 'GET',
            match: exactPath('/operations/context-quality'),
            handle: ({ services, url }) => services.operations.collectContextQualityFeedback(readContextQualityReportOptions(url)),
        },
        {
            method: 'GET',
            match: pathPattern(/^\/operations\/session\/([^/]+)\/replay$/, ['id']),
            handle: async ({ services, params }) => {
                const bundle = await services.sessionReplay.readReplay(params.id);
                if (!bundle) {
                    throw new NotFoundError('replay not found');
                }
                return bundle;
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/maintenance/preview'),
            handle: async ({ services, request }) => {
                const body = validateMaintenanceProposeInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                return services.maintenance.propose(body);
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/maintenance/apply'),
            handle: async ({ services, request }) => {
                const body = validateMaintenanceApplyInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const result = await services.maintenance.apply(body);
                services.operations.requestPhysicalMirror('maintenance-applied');
                return result;
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/catchup'),
            handle: () => {
                const catchup = getCatchupMetadata();
                return { catchup };
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/error-logs'),
            handle: async ({ services, request }) => {
                const body = validateErrorLogInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                return services.errorLogs.recordLog(body);
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/error-logs'),
            handle: ({ services, url }) => services.errorLogs.listLogs(readErrorLogListOptions(url)),
        },
        {
            method: 'GET',
            match: exactPath('/operations/error-logs/collection'),
            handle: ({ services, url }) => services.errorLogInsights.collect(readErrorLogCollectionOptions(url)),
        },
        {
            method: 'POST',
            match: exactPath('/operations/error-logs/reflection-drafts'),
            handle: async ({ services, request }) => {
                const body = validateCreateErrorLogReflectionDraftInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const result = await services.errorLogInsights.createReflectionDraft(body);
                services.operations.requestPhysicalMirror('error-log-reflection-created');
                return result;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/operations\/error-logs\/([^/]+)\/resolve$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateResolveErrorLogInput(await readJsonBody(request, services.config.http.maxRequestBytes), params.id);
                const result = await services.errorLogInsights.resolve(body);
                if (!result) {
                    throw new NotFoundError('Error log not found.');
                }
                return result;
            },
        },
        {
            method: 'GET',
            match: pathPattern(/^\/operations\/error-logs\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const log = await services.errorLogs.getLog(params.id);
                if (!log) {
                    throw new NotFoundError('Error log not found.');
                }
                return log;
            },
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/operations\/error-logs\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateErrorLogPatchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const log = await services.errorLogs.updateLog(params.id, body);
                if (!log) {
                    throw new NotFoundError('Error log not found.');
                }
                return log;
            },
        },
        {
            method: 'GET',
            match: exactPath('/feedback-events'),
            handle: ({ services, url }) => services.operations.listFeedbackEvents(readListRecordsOptions(url)),
        },
        {
            method: 'POST',
            match: exactPath('/reflection-drafts'),
            handle: async ({ services, request }) => {
                const body = validateReflectionDraftInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const draft = await services.reflection.createDraft(body);
                services.operations.requestPhysicalMirror('reflection-created');
                return draft;
            },
        },
        {
            method: 'GET',
            match: exactPath('/reflection-drafts'),
            handle: ({ services, url }) => services.operations.listReflectionDrafts(readListRecordsOptions(url)),
        },
        {
            method: 'GET',
            match: pathPattern(/^\/reflection-drafts\/([^/]+)$/, ['id']),
            handle: async ({ services, params }) => {
                const draft = await services.operations.getReflectionDraft(params.id);
                if (!draft) {
                    throw new NotFoundError('Reflection draft not found.');
                }
                return draft;
            },
        },
        {
            method: 'PATCH',
            match: pathPattern(/^\/reflection-drafts\/([^/]+)$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateReflectionDraftPatchInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                const draft = await services.reflection.updateDraft(params.id, body);
                if (!draft) {
                    throw new NotFoundError('Reflection draft not found.');
                }
                services.operations.requestPhysicalMirror('reflection-updated');
                return draft;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/reflection-drafts\/([^/]+)\/review$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateReflectionDraftReviewInput({
                    ...await readJsonBody(request, services.config.http.maxRequestBytes),
                    id: params.id,
                });
                const draft = await services.reflection.reviewDraft(body);
                if (!draft) {
                    throw new NotFoundError('Reflection draft not found.');
                }
                services.operations.requestWriteThroughBackup('reflection-reviewed');
                services.operations.requestPhysicalMirror('reflection-reviewed');
                return draft;
            },
        },
        {
            method: 'GET',
            match: pathPattern(/^\/reflection-drafts\/([^/]+)\/recommendation$/, ['id']),
            handle: async ({ services, params }) => {
                const recommendation = await services.reflection.recommendDraft(params.id);
                if (!recommendation) {
                    throw new NotFoundError('Reflection draft not found.');
                }
                return recommendation;
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/reflection-drafts\/([^/]+)\/approve$/, ['id']),
            handle: async ({ services, params }) => {
                const draft = await services.reflection.approveDraft(params.id);
                if (!draft) {
                    throw new NotFoundError('Reflection draft not found.');
                }
                services.operations.requestWriteThroughBackup('reflection-approved');
                services.operations.requestPhysicalMirror('reflection-approved');
                return draft;
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/import-files'),
            handle: async ({ services, request }) => {
                const body = validateIngestFilesRequest(await readJsonBody(request, services.config.http.maxRequestBytes));
                return services.operations.importFiles(body);
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/cleanup'),
            handle: async ({ services, request }) => {
                const body = validateCleanupOperationsInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                return services.operations.cleanup(body);
            },
        },
        {
            method: 'POST',
            match: exactPath('/operations/backups'),
            handle: async ({ services, request }) => {
                const body = validateCreateBackupInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                return services.operations.createBackup(body);
            },
        },
        {
            method: 'GET',
            match: exactPath('/operations/backups'),
            handle: ({ services }) => services.operations.listBackups(),
        },
        {
            method: 'GET',
            match: exactPath('/operations/backups/status'),
            handle: ({ services }) => services.operations.getBackupStatus(),
        },
        {
            method: 'POST',
            match: exactPath('/operations/backups/prune'),
            handle: async ({ services, request }) => {
                const body = validateBackupRetentionInput(await readJsonBody(request, services.config.http.maxRequestBytes));
                return services.operations.pruneBackups(body);
            },
        },
        {
            method: 'POST',
            match: pathPattern(/^\/operations\/backups\/([^/]+)\/verify$/, ['id']),
            handle: ({ services, params }) => services.operations.verifyBackup({ backupIdOrPath: params.id }),
        },
        {
            method: 'POST',
            match: pathPattern(/^\/operations\/backups\/([^/]+)\/restore$/, ['id']),
            handle: async ({ services, request, params }) => {
                const body = validateRestoreBackupInput(await readJsonBody(request, services.config.http.maxRequestBytes), params.id);
                return services.operations.restoreBackup(body);
            },
        },
    ];
}
export async function readJsonBody(request, maxBytes) {
    const contentLength = Number(request.headers['content-length'] ?? 0);
    if (contentLength > maxBytes) {
        throw new HttpError(413, `Request body exceeds ${maxBytes} bytes.`);
    }
    const chunks = [];
    let totalBytes = 0;
    for await (const chunk of request) {
        const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
        totalBytes += buffer.length;
        if (totalBytes > maxBytes) {
            throw new HttpError(413, `Request body exceeds ${maxBytes} bytes.`);
        }
        chunks.push(buffer);
    }
    if (chunks.length === 0) {
        return {};
    }
    try {
        return JSON.parse(Buffer.concat(chunks).toString('utf8'));
    }
    catch {
        throw new HttpError(400, 'Invalid JSON body.');
    }
}
function authenticate(request, config) {
    if (!isAuthorizedRequest(request, config)) {
        throw new HttpError(401, 'Unauthorized.');
    }
}
function readProvidedApiKey(request) {
    const auth = request.headers.authorization;
    if (auth?.startsWith('Bearer ')) {
        return auth.slice('Bearer '.length).trim();
    }
    const header = request.headers['x-tuberosa-api-key'];
    return Array.isArray(header) ? header[0] : header;
}
export function isAuthorizedApiKey(provided, apiKey) {
    if (!apiKey) {
        return true;
    }
    return Boolean(provided && secureEqual(provided, apiKey));
}
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
export function isLoopbackRequest(request) {
    const remote = request.socket?.remoteAddress;
    return typeof remote === 'string' && LOOPBACK_ADDRESSES.has(remote);
}
export function isAuthorizedRequest(request, config) {
    const provided = readProvidedApiKey(request);
    if (config.http.apiKey) {
        return Boolean(provided && secureEqual(provided, config.http.apiKey));
    }
    if (!config.http.requireApiKeyForNonLoopback) {
        return true;
    }
    return isLoopbackRequest(request);
}
function secureEqual(left, right) {
    const leftDigest = createHash('sha256').update(left).digest();
    const rightDigest = createHash('sha256').update(right).digest();
    return timingSafeEqual(leftDigest, rightDigest);
}
const SECURITY_HEADERS = {
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'Referrer-Policy': 'no-referrer',
};
function sendJson(response, status, body) {
    let encoded;
    try {
        encoded = JSON.stringify(body, null, 2);
    }
    catch (stringifyError) {
        // Fallback for circular refs / BigInts so the error handler never re-throws.
        encoded = JSON.stringify({
            code: 'internal_error',
            status: 500,
            message: 'Failed to serialize response body.',
            detail: stringifyError instanceof Error ? stringifyError.message : String(stringifyError),
        });
        status = 500;
    }
    response.writeHead(status, {
        'Content-Type': 'application/json; charset=utf-8',
        'Content-Length': Buffer.byteLength(encoded),
        ...SECURITY_HEADERS,
    });
    response.end(encoded);
}
function rawResponse(contentType, body, status = 200) {
    return {
        [RAW_RESPONSE]: true,
        status,
        contentType,
        body,
    };
}
function isRawHttpResponse(value) {
    return typeof value === 'object' && value !== null && value[RAW_RESPONSE] === true;
}
function sendRaw(response, body) {
    const encoded = Buffer.isBuffer(body.body) ? body.body : Buffer.from(body.body, 'utf8');
    response.writeHead(body.status, {
        'Content-Type': body.contentType,
        'Content-Length': encoded.length,
        ...SECURITY_HEADERS,
    });
    response.end(encoded);
}
export class HttpError extends AppError {
    constructor(status, message) {
        super({ code: httpErrorCode(status), status, message });
    }
}
function httpErrorCode(status) {
    if (status === 404) {
        return 'not_found';
    }
    if (status === 413) {
        return 'ingestion_limit';
    }
    return 'validation_error';
}
function exactPath(pathname) {
    return (url) => (url.pathname === pathname ? {} : undefined);
}
function pathPattern(pattern, keys) {
    return (url) => {
        const match = pattern.exec(url.pathname);
        if (!match) {
            return undefined;
        }
        try {
            return Object.fromEntries(keys.map((key, index) => [key, decodeURIComponent(match[index + 1])]));
        }
        catch {
            throw new HttpError(400, 'Invalid path parameter.');
        }
    };
}
function readLimit(url) {
    const rawLimit = url.searchParams.get('limit');
    if (!rawLimit) {
        return 25;
    }
    if (!/^\d+$/.test(rawLimit)) {
        throw new HttpError(400, 'Query parameter "limit" must be a positive integer.');
    }
    const limit = Number.parseInt(rawLimit, 10);
    if (!Number.isFinite(limit) || limit < 1) {
        throw new HttpError(400, 'Query parameter "limit" must be a positive integer.');
    }
    return Math.min(limit, 100);
}
function readImpactStringArray(value, field) {
    if (value === undefined || value === null)
        return [];
    if (!Array.isArray(value)) {
        throw new ValidationError(`${field} must be an array of strings.`);
    }
    return value.map((entry, index) => {
        if (typeof entry !== 'string' || entry.trim().length === 0) {
            throw new ValidationError(`${field}[${index}] must be a non-empty string.`);
        }
        return entry;
    });
}
function readListRecordsOptions(url) {
    return {
        project: url.searchParams.get('project') ?? undefined,
        status: url.searchParams.get('status') ?? undefined,
        limit: readLimit(url),
    };
}
function readErrorLogListOptions(url) {
    return validateErrorLogListInput({
        project: url.searchParams.get('project') ?? undefined,
        category: url.searchParams.get('category') ?? undefined,
        severity: url.searchParams.get('severity') ?? undefined,
        status: url.searchParams.get('status') ?? undefined,
        query: url.searchParams.get('q') ?? url.searchParams.get('query') ?? undefined,
        tag: url.searchParams.get('tag') ?? undefined,
        limit: readLimit(url),
    });
}
function readErrorLogCollectionOptions(url) {
    return validateCollectErrorLogsInput({
        project: url.searchParams.get('project') ?? undefined,
        categories: readRepeatedQuery(url, 'category', 'categories'),
        severities: readRepeatedQuery(url, 'severity', 'severities'),
        statuses: readRepeatedQuery(url, 'status', 'statuses'),
        query: url.searchParams.get('q') ?? url.searchParams.get('query') ?? undefined,
        tag: url.searchParams.get('tag') ?? undefined,
        since: url.searchParams.get('since') ?? undefined,
        until: url.searchParams.get('until') ?? undefined,
        limit: readOptionalQueryNumber(url, 'limit'),
        offset: readOptionalQueryNumber(url, 'offset'),
    });
}
function readRepeatedQuery(url, singular, plural) {
    const values = [
        ...url.searchParams.getAll(singular),
        ...url.searchParams.getAll(plural).flatMap((value) => value.split(',')),
    ].map((value) => value.trim()).filter(Boolean);
    return values.length > 0 ? values : undefined;
}
function readOptionalQueryNumber(url, key) {
    const value = url.searchParams.get(key);
    return value === null ? undefined : Number(value);
}
function readRelationListOptions(url) {
    const inferred = url.searchParams.get('inferred');
    if (inferred !== null && inferred !== 'true' && inferred !== 'false') {
        throw new HttpError(400, 'Query parameter "inferred" must be true or false.');
    }
    return {
        project: url.searchParams.get('project') ?? undefined,
        fromKnowledgeId: url.searchParams.get('fromKnowledgeId') ?? undefined,
        targetKnowledgeId: url.searchParams.get('targetKnowledgeId') ?? undefined,
        targetValue: url.searchParams.get('targetValue') ?? undefined,
        relationType: (url.searchParams.get('relationType') ?? undefined),
        inferred: inferred === null ? undefined : inferred === 'true',
        limit: readLimit(url),
    };
}
function readConflictListOptions(url) {
    const status = url.searchParams.get('status');
    if (status !== null && !['open', 'resolved', 'dismissed'].includes(status)) {
        throw new HttpError(400, 'Query parameter "status" must be open, resolved, or dismissed.');
    }
    return {
        project: url.searchParams.get('project') ?? undefined,
        status: (status ?? undefined),
        limit: readLimit(url),
    };
}
function readKnowledgeGapListOptions(url) {
    return {
        project: url.searchParams.get('project') ?? undefined,
        status: validateLearningReviewStatusQuery(url.searchParams.get('status')),
        sourceSessionId: url.searchParams.get('sourceSessionId') ?? undefined,
        contextPackId: url.searchParams.get('contextPackId') ?? undefined,
        limit: readLimit(url),
    };
}
function readLearningProposalListOptions(url) {
    return {
        project: url.searchParams.get('project') ?? undefined,
        status: validateLearningReviewStatusQuery(url.searchParams.get('status')),
        proposalType: validateLearningProposalTypeQuery(url.searchParams.get('proposalType') ?? url.searchParams.get('type')),
        sourceSessionId: url.searchParams.get('sourceSessionId') ?? undefined,
        contextPackId: url.searchParams.get('contextPackId') ?? undefined,
        affectedKnowledgeId: url.searchParams.get('affectedKnowledgeId') ?? undefined,
        limit: readLimit(url),
    };
}
function readContextQualityReportOptions(url) {
    return validateContextQualityReportInput({
        project: url.searchParams.get('project') ?? undefined,
        feedbackType: url.searchParams.get('feedbackType') ?? undefined,
        limit: readLimit(url),
    });
}
async function maybeCaptureHttpError(services, request, url, method, error) {
    if (!shouldAutoCapture(services, error)) {
        return;
    }
    try {
        await services.errorLogs.recordLog({
            project: url.searchParams.get('project') ?? undefined,
            category: categoryForAppError(error),
            severity: error.status >= 500 ? 'error' : 'warning',
            title: `HTTP ${method} ${url.pathname} failed`,
            summary: `${error.code}: ${error.message}`,
            message: error.message,
            stack: error.stack,
            operation: `${method} ${url.pathname}`,
            agentTool: 'http',
            metadata: {
                surface: 'http',
                code: error.code,
                status: error.status,
                path: url.pathname,
                method,
                userAgent: request.headers['user-agent'],
            },
        });
    }
    catch (captureError) {
        console.error('[error-log]', captureError instanceof Error ? captureError.message : String(captureError));
    }
}
function categoryForAppError(error) {
    switch (error.code) {
        case 'cache_error':
            return 'cache';
        case 'model_provider_error':
            return 'model_provider';
        case 'store_error':
            return 'database';
        default:
            return 'http';
    }
}
