"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const config_1 = require("../config");
const auth_1 = require("../middleware/auth");
const enterpriseDb_1 = require("../services/enterpriseDb");
const analysisResultSnapshotStore_1 = require("../services/analysisResultSnapshotStore");
const multiTraceComparisonStore_1 = require("../services/multiTraceComparisonStore");
const comparisonResultService_1 = require("../services/comparisonResultService");
const comparisonAiConclusionService_1 = require("../services/comparisonAiConclusionService");
const comparisonHtmlReportService_1 = require("../services/comparisonHtmlReportService");
const reportRoutes_1 = require("./reportRoutes");
const standardMetricBackfillService_1 = require("../services/standardMetricBackfillService");
const traceProcessorService_1 = require("../services/traceProcessorService");
const traceProcessorLeaseStore_1 = require("../services/traceProcessorLeaseStore");
const traceProcessorLeaseModeDecision_1 = require("../services/traceProcessorLeaseModeDecision");
const traceProcessorRamBudget_1 = require("../services/traceProcessorRamBudget");
const workingTraceProcessor_1 = require("../services/workingTraceProcessor");
const rbac_1 = require("../services/rbac");
const router = express_1.default.Router();
function optionalString(value) {
    return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function stringArray(value) {
    if (!Array.isArray(value))
        return [];
    return value
        .filter((item) => typeof item === 'string')
        .map(item => item.trim())
        .filter(Boolean);
}
function uniqueStrings(values) {
    const seen = new Set();
    const result = [];
    for (const value of values) {
        if (seen.has(value))
            continue;
        seen.add(value);
        result.push(value);
    }
    return result;
}
function optionalBoolean(value) {
    return value === true || value === 'true' || value === '1';
}
function comparisonResponseView(req) {
    return {
        significantOnly: optionalBoolean(req.body?.significantOnly) || optionalBoolean(req.query?.significantOnly),
    };
}
function applyComparisonResponseView(comparison, view) {
    if (!comparison.result || !view.significantOnly)
        return comparison;
    return {
        ...comparison,
        result: (0, comparisonResultService_1.applyComparisonResultViewOptions)(comparison.result, view),
    };
}
function exportFilename(comparisonId) {
    const safeId = comparisonId.replace(/[^a-zA-Z0-9._-]/g, '_');
    return `smartperfetto-comparison-${safeId}.html`;
}
function reportArtifactExists(db, reportId) {
    const row = db.prepare('SELECT 1 FROM report_artifacts WHERE id = ? LIMIT 1').get(reportId);
    return Boolean(row);
}
function enterpriseLeasesEnabled() {
    return (0, config_1.resolveFeatureConfig)().enterprise;
}
function markLeaseReadyIfNew(lease, scope) {
    if (lease.state !== 'pending')
        return lease;
    const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
    const starting = store.markStarting(scope, lease.id);
    return store.markReady(scope, starting.id);
}
async function backfillSnapshotMetrics(input) {
    if (!enterpriseLeasesEnabled()) {
        const backfill = await (0, standardMetricBackfillService_1.backfillStandardMetrics)({
            snapshot: input.snapshot,
            metricKeys: input.metricKeys,
            traceProcessor: input.traceProcessor,
            repository: input.snapshotRepository,
            scope: input.scope,
        });
        return backfill.snapshot;
    }
    const trace = await input.traceProcessor.getOrLoadTrace(input.snapshot.traceId);
    const stats = workingTraceProcessor_1.TraceProcessorFactory.getStats();
    const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
    const estimatedNewLeaseRssBytes = typeof trace?.size === 'number'
        ? (0, traceProcessorRamBudget_1.estimateTraceProcessorRssBytes)(trace.size)
        : undefined;
    const decision = (0, traceProcessorLeaseModeDecision_1.buildTraceProcessorLeaseModeDecision)({
        traceId: input.snapshot.traceId,
        holderType: 'metric_backfill',
        estimatedSqlMs: 2000,
        leases: store.listLeases(input.scope, { traceId: input.snapshot.traceId }),
        processors: stats.processors,
        ramBudget: stats.ramBudget,
        ...(estimatedNewLeaseRssBytes !== undefined ? { estimatedNewLeaseRssBytes } : {}),
    });
    let lease = null;
    try {
        lease = store.acquireHolder(input.scope, input.snapshot.traceId, {
            holderType: 'metric_backfill',
            holderRef: input.holderRef,
            sessionId: input.snapshot.sessionId,
            runId: input.snapshot.runId,
            metadata: {
                requestedMetricKeys: input.metricKeys,
                leaseModeReason: decision.reason,
                leaseModeSignals: decision.signals,
            },
        }, { mode: decision.mode });
        lease = markLeaseReadyIfNew(lease, input.scope);
        await input.traceProcessor.ensureProcessorForLease(input.snapshot.traceId, lease.id, lease.mode, input.scope);
        const backfill = await input.traceProcessor.runWithLease({
            traceId: input.snapshot.traceId,
            leaseId: lease.id,
            mode: lease.mode,
            leaseScope: input.scope,
        }, () => (0, standardMetricBackfillService_1.backfillStandardMetrics)({
            snapshot: input.snapshot,
            metricKeys: input.metricKeys,
            traceProcessor: input.traceProcessor,
            repository: input.snapshotRepository,
            scope: input.scope,
            queryOptions: {
                leaseId: lease.id,
                leaseMode: lease.mode,
                leaseScope: input.scope,
            },
        }));
        return backfill.snapshot;
    }
    finally {
        if (lease) {
            try {
                store.releaseHolder(input.scope, lease.id, 'metric_backfill', input.holderRef);
            }
            catch (error) {
                console.warn(`[ComparisonRoutes] Failed to release metric_backfill lease ${lease.id}:`, error);
            }
        }
    }
}
async function completeComparisonRun(input) {
    const result = (0, comparisonResultService_1.buildDeterministicComparisonResult)(input.snapshots, {
        baselineSnapshotId: input.baselineSnapshotId,
        metricKeys: input.metricKeys,
    });
    result.conclusion = await (0, comparisonAiConclusionService_1.generateAiComparisonConclusion)({
        result,
        query: input.query,
        providerId: input.providerId,
        providerScope: input.scope,
    });
    const report = (0, comparisonHtmlReportService_1.persistComparisonHtmlReport)({
        comparison: input.comparison,
        result,
        scope: input.scope,
    });
    result.reportId = report.reportId;
    result.reportUrl = report.reportUrl;
    result.reportExportUrl = `${report.reportUrl}/export`;
    const persistedReportId = reportArtifactExists(input.db, report.reportId)
        ? report.reportId
        : undefined;
    return input.repository.updateRun(input.scope, input.comparison.id, {
        status: 'completed',
        baselineSnapshotId: input.baselineSnapshotId,
        result,
        ...(persistedReportId ? { reportId: persistedReportId } : {}),
    }) || input.comparison;
}
router.post('/', async (req, res) => {
    const context = (0, auth_1.requireRequestContext)(req);
    if (!(0, rbac_1.hasRbacPermission)(context, 'comparison:create')) {
        (0, rbac_1.sendForbidden)(res, 'comparison:create permission is required');
        return;
    }
    const baselineSnapshotId = optionalString(req.body?.baselineSnapshotId);
    const candidateSnapshotIds = uniqueStrings(stringArray(req.body?.candidateSnapshotIds));
    const query = optionalString(req.body?.query) || 'Compare selected analysis results';
    const providerId = optionalString(req.body?.providerId);
    const metricKeys = (0, comparisonResultService_1.resolveComparisonMetricKeys)(stringArray(req.body?.metricKeys));
    const allowTraceBackfill = req.body?.allowTraceBackfill === true;
    const inputSnapshotIds = uniqueStrings([
        ...(baselineSnapshotId ? [baselineSnapshotId] : []),
        ...candidateSnapshotIds,
    ]);
    if (!baselineSnapshotId || inputSnapshotIds.length < 2) {
        res.status(400).json({
            success: false,
            error: 'baselineSnapshotId and at least one candidateSnapshotId are required',
        });
        return;
    }
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        const snapshotRepository = (0, analysisResultSnapshotStore_1.createAnalysisResultSnapshotRepository)(db);
        const scope = {
            tenantId: context.tenantId,
            workspaceId: context.workspaceId,
            userId: context.userId,
        };
        const snapshots = [];
        for (const snapshotId of inputSnapshotIds) {
            const snapshot = snapshotRepository.getSnapshot(scope, snapshotId);
            if (!snapshot) {
                res.status(404).json({
                    success: false,
                    error: 'Analysis result snapshot not found',
                    snapshotId,
                });
                return;
            }
            snapshots.push(snapshot);
        }
        if (allowTraceBackfill) {
            const traceProcessor = (0, traceProcessorService_1.getTraceProcessorService)();
            for (const [index, snapshot] of snapshots.entries()) {
                try {
                    const backfilledSnapshot = await backfillSnapshotMetrics({
                        snapshot,
                        metricKeys,
                        traceProcessor,
                        snapshotRepository,
                        scope,
                        holderRef: `comparison-backfill:${snapshot.id}:${Date.now()}`,
                    });
                    snapshots[index] = backfilledSnapshot;
                }
                catch (error) {
                    console.warn('[ComparisonRoutes] Standard metric backfill failed:', {
                        snapshotId: snapshot.id,
                        traceId: snapshot.traceId,
                        error: error instanceof Error ? error.message : String(error),
                    });
                }
            }
        }
        const repository = (0, multiTraceComparisonStore_1.createMultiTraceComparisonRunRepository)(db);
        const created = repository.createRun(scope, {
            baselineSnapshotId,
            candidateSnapshotIds,
            query,
            metricKeys,
            status: 'running',
        });
        let comparison = created;
        try {
            comparison = await completeComparisonRun({
                db,
                repository,
                comparison: created,
                snapshots,
                baselineSnapshotId,
                metricKeys,
                query,
                providerId,
                scope,
            });
        }
        catch (error) {
            repository.updateRun(scope, created.id, {
                status: 'failed',
                error: error instanceof Error ? error.message : String(error),
            });
            throw error;
        }
        res.status(201).json({
            success: true,
            comparison: applyComparisonResponseView(comparison, comparisonResponseView(req)),
        });
    }
    catch (error) {
        console.error('[ComparisonRoutes] Failed to create comparison:', error);
        res.status(500).json({
            success: false,
            error: 'Failed to create comparison',
        });
    }
    finally {
        db.close();
    }
});
router.patch('/:comparisonId/baseline', async (req, res) => {
    const context = (0, auth_1.requireRequestContext)(req);
    if (!(0, rbac_1.hasRbacPermission)(context, 'comparison:create')) {
        (0, rbac_1.sendForbidden)(res, 'comparison:create permission is required');
        return;
    }
    const comparisonId = optionalString(req.params.comparisonId);
    const baselineSnapshotId = optionalString(req.body?.baselineSnapshotId);
    const providerId = optionalString(req.body?.providerId);
    if (!comparisonId || !baselineSnapshotId) {
        res.status(400).json({
            success: false,
            error: 'comparisonId and baselineSnapshotId are required',
        });
        return;
    }
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        const scope = {
            tenantId: context.tenantId,
            workspaceId: context.workspaceId,
            userId: context.userId,
        };
        const repository = (0, multiTraceComparisonStore_1.createMultiTraceComparisonRunRepository)(db);
        const comparison = repository.getRun(scope, comparisonId);
        if (!comparison) {
            res.status(404).json({
                success: false,
                error: 'Comparison run not found',
            });
            return;
        }
        if (!comparison.inputSnapshotIds.includes(baselineSnapshotId)) {
            res.status(400).json({
                success: false,
                error: 'baselineSnapshotId must be one of the comparison input snapshots',
            });
            return;
        }
        const snapshotRepository = (0, analysisResultSnapshotStore_1.createAnalysisResultSnapshotRepository)(db);
        const snapshots = [];
        for (const snapshotId of comparison.inputSnapshotIds) {
            const snapshot = snapshotRepository.getSnapshot(scope, snapshotId);
            if (!snapshot) {
                res.status(404).json({
                    success: false,
                    error: 'Analysis result snapshot not found',
                    snapshotId,
                });
                return;
            }
            snapshots.push(snapshot);
        }
        const requestedMetricKeys = stringArray(req.body?.metricKeys);
        const metricKeys = (0, comparisonResultService_1.resolveComparisonMetricKeys)(requestedMetricKeys.length > 0
            ? requestedMetricKeys
            : comparison.result?.matrix.rows.map(row => row.metricKey));
        const updated = await completeComparisonRun({
            db,
            repository,
            comparison,
            snapshots,
            baselineSnapshotId,
            metricKeys,
            query: comparison.query,
            providerId,
            scope,
        });
        res.json({
            success: true,
            comparison: applyComparisonResponseView(updated, comparisonResponseView(req)),
        });
    }
    catch (error) {
        console.error('[ComparisonRoutes] Failed to switch comparison baseline:', error);
        res.status(500).json({
            success: false,
            error: 'Failed to switch comparison baseline',
        });
    }
    finally {
        db.close();
    }
});
router.get('/:comparisonId/report/export', (req, res) => {
    const context = (0, auth_1.requireRequestContext)(req);
    if (!(0, rbac_1.hasRbacPermission)(context, 'comparison:read')) {
        (0, rbac_1.sendForbidden)(res, 'comparison:read permission is required');
        return;
    }
    const comparisonId = optionalString(req.params.comparisonId);
    if (!comparisonId) {
        res.status(400).json({
            success: false,
            error: 'comparisonId is required',
        });
        return;
    }
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        const repository = (0, multiTraceComparisonStore_1.createMultiTraceComparisonRunRepository)(db);
        const comparison = repository.getRun({
            tenantId: context.tenantId,
            workspaceId: context.workspaceId,
            userId: context.userId,
        }, comparisonId);
        if (!comparison?.result) {
            res.status(404).json({
                success: false,
                error: 'Comparison report not found',
            });
            return;
        }
        const view = comparisonResponseView(req);
        const viewComparison = applyComparisonResponseView(comparison, view);
        const reportId = viewComparison.result?.reportId;
        const cachedReport = !view.significantOnly && reportId ? reportRoutes_1.reportStore.get(reportId) : undefined;
        const html = cachedReport?.html || (0, comparisonHtmlReportService_1.renderComparisonHtmlReport)({
            comparisonId: viewComparison.id,
            query: viewComparison.query,
            result: viewComparison.result,
        });
        res.setHeader('Content-Type', 'text/html; charset=utf-8');
        res.setHeader('Content-Disposition', `attachment; filename="${exportFilename(viewComparison.id)}"`);
        res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
        res.setHeader('Pragma', 'no-cache');
        res.setHeader('Expires', '0');
        res.send(html);
    }
    catch (error) {
        console.error('[ComparisonRoutes] Failed to export comparison report:', error);
        res.status(500).json({
            success: false,
            error: 'Failed to export comparison report',
        });
    }
    finally {
        db.close();
    }
});
router.get('/:comparisonId', (req, res) => {
    const context = (0, auth_1.requireRequestContext)(req);
    if (!(0, rbac_1.hasRbacPermission)(context, 'comparison:read')) {
        (0, rbac_1.sendForbidden)(res, 'comparison:read permission is required');
        return;
    }
    const comparisonId = optionalString(req.params.comparisonId);
    if (!comparisonId) {
        res.status(400).json({
            success: false,
            error: 'comparisonId is required',
        });
        return;
    }
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        const repository = (0, multiTraceComparisonStore_1.createMultiTraceComparisonRunRepository)(db);
        const comparison = repository.getRun({
            tenantId: context.tenantId,
            workspaceId: context.workspaceId,
            userId: context.userId,
        }, comparisonId);
        if (!comparison) {
            res.status(404).json({
                success: false,
                error: 'Comparison run not found',
            });
            return;
        }
        res.json({
            success: true,
            comparison: applyComparisonResponseView(comparison, comparisonResponseView(req)),
        });
    }
    catch (error) {
        console.error('[ComparisonRoutes] Failed to read comparison:', error);
        res.status(500).json({
            success: false,
            error: 'Failed to read comparison',
        });
    }
    finally {
        db.close();
    }
});
router.get('/:comparisonId/stream', (req, res) => {
    const context = (0, auth_1.requireRequestContext)(req);
    if (!(0, rbac_1.hasRbacPermission)(context, 'comparison:read')) {
        (0, rbac_1.sendForbidden)(res, 'comparison:read permission is required');
        return;
    }
    const comparisonId = optionalString(req.params.comparisonId);
    if (!comparisonId) {
        res.status(400).json({
            success: false,
            error: 'comparisonId is required',
        });
        return;
    }
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        const repository = (0, multiTraceComparisonStore_1.createMultiTraceComparisonRunRepository)(db);
        const comparison = repository.getRun({
            tenantId: context.tenantId,
            workspaceId: context.workspaceId,
            userId: context.userId,
        }, comparisonId);
        if (!comparison) {
            res.status(404).json({
                success: false,
                error: 'Comparison run not found',
            });
            return;
        }
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive',
        });
        res.write('retry: 3000\n');
        res.write(`event: comparison_state\n`);
        res.write(`data: ${JSON.stringify({ comparison: applyComparisonResponseView(comparison, comparisonResponseView(req)) })}\n\n`);
        res.end();
    }
    catch (error) {
        console.error('[ComparisonRoutes] Failed to stream comparison:', error);
        if (!res.headersSent) {
            res.status(500).json({
                success: false,
                error: 'Failed to stream comparison',
            });
        }
        else {
            res.end();
        }
    }
    finally {
        db.close();
    }
});
exports.default = router;
//# sourceMappingURL=comparisonRoutes.js.map