"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObserveDashboardController = void 0;
exports.isMongoId = isMongoId;
const observeDashboard_1 = require("../../services/observe/observeDashboard");
const frameworkParser_1 = require("../../services/observe/frameworkParser");
const logger_1 = __importDefault(require("../../../logger"));
const observeCharts_1 = require("../../services/observe/observeCharts");
const database_1 = require("../../../database");
const tables_1 = require("../../../database/tables");
const drizzle_orm_1 = require("drizzle-orm");
const lodash_1 = __importDefault(require("lodash"));
const pulseService_1 = require("../../services/pulseService");
const metadata_1 = require("../../../redteam/constants/metadata");
const aggregatedTraceDagInsert_1 = require("../../services/aggregatedTraceDagInsert");
const ragParser_1 = require("../../services/observe/ragParser");
function isMongoId(id) {
    if (typeof id !== "string")
        return false;
    return /^[a-fA-F0-9]{24}$/.test(id);
}
// Helper function to merge attributes
function mergeAttributes(raw) {
    const merged = {
        ...(raw?.attributes ?? {}),
    };
    // Common key spelling differences
    const alias = (from, to) => {
        if (merged[to] === undefined && merged[from] !== undefined)
            merged[to] = merged[from];
    };
    for (const [k, v] of Object.entries(merged)) {
        if (k.startsWith('gen_ai.')) {
            const nk = `genai.${k.slice('gen_ai.'.length)}`;
            if (merged[nk] === undefined)
                merged[nk] = v;
        }
    }
    alias('gen_ai.system', 'genai.system');
    alias('gen_ai.framework', 'genai.framework');
    alias('gen_ai.workflow.type', 'genai.workflow.type');
    alias('gen_ai.tool.name', 'genai.tool.name');
    alias('gen_ai.usage.total_tokens', 'genai.usage.totaltokens');
    return merged;
}
// Helper function to normalize timestamp (string or numeric Unix ns/ms/s) to ISO string
function normalizeTimestamp(value) {
    if (value == null)
        return new Date(0).toISOString();
    if (typeof value === 'number' && Number.isFinite(value)) {
        let ms = value;
        if (value >= 1e15)
            ms = value / 1e6; // nanoseconds -> ms
        else if (value >= 1e12)
            ms = value; // already milliseconds
        else if (value > 0)
            ms = value * 1000; // seconds -> ms
        const d = new Date(ms);
        return Number.isFinite(d.getTime()) ? d.toISOString() : new Date(0).toISOString();
    }
    const s = typeof value === 'string' ? value.trim() : '';
    if (!s)
        return new Date(0).toISOString();
    const direct = new Date(s);
    if (Number.isFinite(direct.getTime()))
        return s;
    let t = s.replace(' ', 'T');
    const m = t.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.(\d+))?$/);
    if (m) {
        const base = m[1];
        const frac = (m[3] ?? '').slice(0, 3).padEnd(3, '0');
        t = `${base}.${frac}Z`;
    }
    const dt = new Date(t);
    if (Number.isFinite(dt.getTime()))
        return t;
    return new Date(0).toISOString();
}
// Build span.events from eventsTimestamp / eventsName / eventsAttributes (new span format)
function buildEventsFromArrays(raw) {
    if (raw?.events && Array.isArray(raw.events) && raw.events.length > 0)
        return raw.events;
    const ts = raw?.eventsTimestamp;
    const names = raw?.eventsName;
    const attrs = raw?.eventsAttributes;
    if (!Array.isArray(ts) || !Array.isArray(names) || ts.length === 0)
        return undefined;
    return ts.map((t, i) => ({
        timestamp: String(t),
        name: typeof names[i] === 'string' ? names[i] : '',
        attributes: (Array.isArray(attrs) && attrs[i] && typeof attrs[i] === 'object') ? attrs[i] : {}
    }));
}
// Helper function to normalize a span (supports both OTLP-style and new UI export style with topPills, eventsTimestamp, etc.)
function normalizeSpan(raw) {
    const attributes = mergeAttributes(raw);
    const resourceAttributes = raw?.resourceAttributes ?? {};
    const traceId = String(raw?.traceId ?? attributes.traceId ?? 'unknown-trace');
    const id = String(raw?.id ?? attributes.id ?? `${traceId}:${Math.random().toString(16).slice(2)}`);
    const serviceName = String(raw?.serviceName ??
        resourceAttributes?.['service.name'] ??
        attributes?.['service.name'] ??
        attributes?.['serviceName'] ??
        'unknown') || 'unknown';
    const timestamp = normalizeTimestamp(raw?.timestamp ?? attributes?.timestamp);
    let duration = typeof raw?.duration === 'number' ? raw.duration : typeof attributes?.duration === 'number' ? attributes.duration : 0;
    // New span format may send duration in microseconds (e.g. 718400 = 718.4ms); normalize to nanoseconds for pipeline
    if (typeof duration === 'number' && duration >= 1e3 && duration < 1e6)
        duration = Math.round(duration * 1e3);
    const parentSpanIdRaw = raw?.parentSpanId ?? attributes?.parentSpanId;
    const parentSpanId = parentSpanIdRaw ? String(parentSpanIdRaw) : null;
    const events = raw?.events ?? buildEventsFromArrays(raw);
    return {
        id,
        traceId,
        parentSpanId,
        name: String(raw?.name ?? attributes?.name ?? 'span'),
        duration,
        timestamp,
        serviceName,
        attributes,
        events,
        resourceAttributes: raw?.resourceAttributes,
    };
}
// Helper function to normalize spans array
function normalizeSpans(spans) {
    const normalizedSpans = [];
    for (const item of spans) {
        if (Array.isArray(item)) {
            normalizedSpans.push(...item.map(normalizeSpan));
        }
        else {
            normalizedSpans.push(normalizeSpan(item));
        }
    }
    return normalizedSpans;
}
exports.ObserveDashboardController = {
    getObserveDashboardConfig: async (req, res) => {
        const { workspaceId } = req.params;
        const { applicationId, fetchVersions } = req?.query ?? {};
        if (!isMongoId(workspaceId)) {
            res.status(400).json({
                success: false,
                message: 'Invalid workspace ID',
            });
            return;
        }
        const fetchVersion = fetchVersions === 'true';
        try {
            const dashboard = await observeDashboard_1.observeDashboardService.getObserveDashboardConfig(workspaceId, applicationId, fetchVersion);
            res.status(200).json({
                success: true,
                message: 'Observe dashboard config fetched successfully',
                data: dashboard,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching observe dashboard config: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch observe dashboard config',
            });
        }
    },
    parseTrace: async (req, res) => {
        try {
            const { spans } = req.body;
            if (!spans || !Array.isArray(spans)) {
                res.status(400).json({
                    success: false,
                    message: 'Invalid request: spans must be an array',
                });
                return;
            }
            // Normalize spans if needed (handle nested arrays)
            const normalizedSpans = normalizeSpans(spans);
            if (normalizedSpans.length === 0) {
                res.status(400).json({
                    success: false,
                    message: 'No spans provided',
                });
                return;
            }
            // Group by traceId
            const traceMap = new Map();
            for (const span of normalizedSpans) {
                const traceId = span.traceId || 'default-trace';
                if (!traceMap.has(traceId)) {
                    traceMap.set(traceId, []);
                }
                traceMap.get(traceId).push(span);
            }
            // Parse each trace
            const parser = new frameworkParser_1.MultiFrameworkParser();
            const results = [];
            for (const [traceId, traceSpans] of traceMap.entries()) {
                try {
                    const parseResult = parser.parse(traceSpans);
                    results.push({
                        traceId,
                        ...parseResult,
                    });
                }
                catch (error) {
                    logger_1.default.error(`Error parsing trace ${traceId}: ${error}`);
                    // Continue with other traces
                }
            }
            res.status(200).json({
                success: true,
                message: 'Traces parsed successfully',
                data: results,
            });
        }
        catch (error) {
            logger_1.default.error(`Error parsing traces: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to parse traces',
                error: error instanceof Error ? error.message : 'Unknown error',
            });
        }
    },
    getTraceDag: async (req, res) => {
        try {
            const { spans } = req.body;
            if (!spans || !Array.isArray(spans)) {
                res.status(400).json({
                    success: false,
                    message: 'Invalid request: spans must be an array',
                });
                return;
            }
            const normalizedSpans = normalizeSpans(spans);
            if (normalizedSpans.length === 0) {
                res.status(400).json({
                    success: false,
                    message: 'No spans provided',
                });
                return;
            }
            // Group by traceId
            const traceMap = new Map();
            for (const span of normalizedSpans) {
                const traceId = span.traceId || 'default-trace';
                if (!traceMap.has(traceId)) {
                    traceMap.set(traceId, []);
                }
                traceMap.get(traceId).push(span);
            }
            // Parse each trace
            const parser = new frameworkParser_1.MultiFrameworkParser();
            const results = [];
            for (const [traceId, traceSpans] of traceMap.entries()) {
                try {
                    const { dag, promptEntityId, responseEntityId, nodeIdsWIthNameMap } = parser.getDagData(spans);
                    const { nodes: transformedNodes, edges: transformedEdges } = (0, aggregatedTraceDagInsert_1.buildDagWithPromptAndResponse)(spans, dag, promptEntityId, responseEntityId);
                    results.push({
                        traceId,
                        nodes: transformedNodes,
                        edges: transformedEdges,
                        promptEntityId,
                        responseEntityId,
                        nodeIdsWIthNameMap,
                    });
                }
                catch (error) {
                    logger_1.default.error(`Error parsing DAG for trace ${traceId}: ${error}`);
                    // Continue with other traces
                }
            }
            res.status(200).json({
                success: true,
                message: 'DAG data fetched successfully',
                data: results,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching DAG data: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch DAG data',
                error: error instanceof Error ? error.message : 'Unknown error',
            });
        }
    },
    /**
     * Linear flow: same parser as getTraceViewDag (getTraceViewDagData).
     * For identical nodes/edges to trace-view/dag, use the same spans (e.g. POST /trace-view/dag with body { traceId, spans }).
     */
    getTraceLinearFlow: async (req, res) => {
        try {
            const { spans } = req.body;
            if (!spans || !Array.isArray(spans)) {
                res.status(400).json({
                    success: false,
                    message: 'Invalid request: spans must be an array',
                });
                return;
            }
            const normalizedSpans = normalizeSpans(spans);
            if (normalizedSpans.length === 0) {
                res.status(400).json({
                    success: false,
                    message: 'No spans provided',
                });
                return;
            }
            // Group by traceId
            const traceMap = new Map();
            for (const span of normalizedSpans) {
                const traceId = span.traceId || 'default-trace';
                if (!traceMap.has(traceId)) {
                    traceMap.set(traceId, []);
                }
                traceMap.get(traceId).push(span);
            }
            // Parse each trace
            const parser = new frameworkParser_1.MultiFrameworkParser();
            const results = [];
            for (const [traceId, traceSpans] of traceMap.entries()) {
                try {
                    const linearFlowData = parser.getLinearFlowData(traceSpans);
                    results.push({
                        traceId,
                        ...linearFlowData,
                    });
                }
                catch (error) {
                    logger_1.default.error(`Error parsing linear flow for trace ${traceId}: ${error}`);
                    // Continue with other traces
                }
            }
            res.status(200).json({
                success: true,
                message: 'Linear flow data fetched successfully',
                data: results,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching linear flow data: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch linear flow data',
                error: error instanceof Error ? error.message : 'Unknown error',
            });
        }
    },
    getTraceDetails: async (req, res) => {
        try {
            const { spans } = req.body;
            if (!spans || !Array.isArray(spans)) {
                res.status(400).json({
                    success: false,
                    message: 'Invalid request: spans must be an array',
                });
                return;
            }
            const normalizedSpans = normalizeSpans(spans);
            if (normalizedSpans.length === 0) {
                res.status(400).json({
                    success: false,
                    message: 'No spans provided',
                });
                return;
            }
            // Group by traceId
            const traceMap = new Map();
            for (const span of normalizedSpans) {
                const traceId = span.traceId || 'default-trace';
                if (!traceMap.has(traceId)) {
                    traceMap.set(traceId, []);
                }
                traceMap.get(traceId).push(span);
            }
            // Parse each trace
            const parser = new frameworkParser_1.MultiFrameworkParser();
            const results = [];
            for (const [traceId, traceSpans] of traceMap.entries()) {
                try {
                    const detailsData = parser.getDetailsData(traceSpans);
                    results.push({
                        traceId,
                        ...detailsData,
                    });
                }
                catch (error) {
                    logger_1.default.error(`Error parsing details for trace ${traceId}: ${error}`);
                    // Continue with other traces
                }
            }
            res.status(200).json({
                success: true,
                message: 'Details data fetched successfully',
                data: results,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching details data: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch details data',
                error: error instanceof Error ? error.message : 'Unknown error',
            });
        }
    },
    getSpansByTraceId: async (req, res) => {
        try {
            const traceId = lodash_1.default.isString(req.body?.traceId) ? req.body.traceId.trim() : '';
            if (!traceId) {
                res.status(400).json({
                    success: false,
                    message: 'traceId is required',
                });
                return;
            }
            const rawSpans = await pulseService_1.pulseService.getReportData({ traceId });
            const spans = Array.isArray(rawSpans) ? rawSpans : [];
            const normalizedSpans = normalizeSpans(spans);
            res.status(200).json({
                success: true,
                message: 'Spans fetched successfully',
                data: normalizedSpans,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching spans by traceId: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch spans by traceId',
                error: error instanceof Error ? error.message : 'Unknown error',
            });
        }
    },
    getTraceRagDetails: async (req, res) => {
        try {
            const { spans } = req.body;
            if (!spans || !Array.isArray(spans)) {
                res.status(400).json({
                    success: false,
                    message: 'Invalid request: spans must be an array',
                });
                return;
            }
            const normalizedSpans = normalizeSpans(spans);
            if (normalizedSpans.length === 0) {
                res.status(400).json({
                    success: false,
                    message: 'No spans provided',
                });
                return;
            }
            const traceMap = new Map();
            for (const span of normalizedSpans) {
                const traceId = span.traceId || 'default-trace';
                if (!traceMap.has(traceId)) {
                    traceMap.set(traceId, []);
                }
                traceMap.get(traceId).push(span);
            }
            const results = [];
            for (const [traceId, traceSpans] of traceMap.entries()) {
                try {
                    const ragCalls = (0, ragParser_1.parseRAG)(traceSpans);
                    const cards = (0, ragParser_1.buildTraceRagDetailsCards)(ragCalls, traceSpans);
                    results.push({ traceId, cards });
                }
                catch (error) {
                    logger_1.default.error(`Error parsing RAG details for trace ${traceId}: ${error}`);
                }
            }
            res.status(200).json({
                success: true,
                message: 'RAG details fetched successfully',
                data: results,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching RAG details: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch RAG details',
                error: error instanceof Error ? error.message : 'Unknown error',
            });
        }
    },
    getPromptTraces: async (req, res) => {
        try {
            const payload = req.body ?? {};
            const { orgId, projectId } = payload;
            if (!lodash_1.default.isString(projectId) || lodash_1.default.isEmpty(projectId?.trim?.())) {
                res.status(400).json({
                    success: false,
                    message: 'projectId is required',
                });
                return;
            }
            const { applicationId, applicationIds } = payload;
            let resolvedApplicationIds = [];
            if (Array.isArray(applicationIds) && applicationIds.length > 0) {
                resolvedApplicationIds = applicationIds.map((x) => String(x)).filter(Boolean);
            }
            else if (lodash_1.default.isString(applicationId) && !lodash_1.default.isEmpty(applicationId) && applicationId !== 'undefined' && applicationId !== 'null') {
                resolvedApplicationIds = [String(applicationId)];
            }
            if (lodash_1.default.isEmpty(resolvedApplicationIds)) {
                const db = await (0, database_1.getDb)();
                const applicationsInProject = await db
                    .select({ id: tables_1.applicationsTable.id })
                    .from(tables_1.applicationsTable)
                    .innerJoin(tables_1.pulseMonitoringTable, (0, drizzle_orm_1.eq)(tables_1.applicationsTable.id, tables_1.pulseMonitoringTable.applicationId))
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId)))
                    .orderBy(tables_1.applicationsTable.createdAt);
                resolvedApplicationIds = applicationsInProject.map((a) => a.id).filter(Boolean);
            }
            const data = await observeCharts_1.observeChartsService.getPromptTraces({
                ...payload,
                applicationIds: resolvedApplicationIds,
            });
            res.status(200).json({
                success: true,
                message: 'Prompt traces fetched successfully',
                data,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching prompt traces: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch prompt traces',
            });
        }
    },
    getExceptionTraces: async (req, res) => {
        try {
            const payload = req.body ?? {};
            const { projectId } = payload;
            if (!lodash_1.default.isString(projectId) || lodash_1.default.isEmpty(projectId?.trim?.())) {
                res.status(400).json({
                    success: false,
                    message: 'projectId is required',
                });
                return;
            }
            const { applicationId, applicationIds } = payload;
            let resolvedApplicationIds = [];
            if (Array.isArray(applicationIds) && applicationIds.length > 0) {
                resolvedApplicationIds = applicationIds.map((x) => String(x)).filter(Boolean);
            }
            else if (lodash_1.default.isString(applicationId) && !lodash_1.default.isEmpty(applicationId) && applicationId !== 'undefined' && applicationId !== 'null') {
                resolvedApplicationIds = [String(applicationId)];
            }
            if (lodash_1.default.isEmpty(resolvedApplicationIds)) {
                const db = await (0, database_1.getDb)();
                const applicationsInProject = await db
                    .select({ id: tables_1.applicationsTable.id })
                    .from(tables_1.applicationsTable)
                    .innerJoin(tables_1.pulseMonitoringTable, (0, drizzle_orm_1.eq)(tables_1.applicationsTable.id, tables_1.pulseMonitoringTable.applicationId))
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId)))
                    .orderBy(tables_1.applicationsTable.createdAt);
                resolvedApplicationIds = applicationsInProject.map((a) => a.id).filter(Boolean);
            }
            const data = await observeCharts_1.observeChartsService.getExceptionTraces({
                ...payload,
                applicationIds: resolvedApplicationIds,
            });
            res.status(200).json({
                success: true,
                message: 'Exception traces fetched successfully',
                data,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching exception traces: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch exception traces',
            });
        }
    },
    getNodeTraces: async (req, res) => {
        try {
            const payload = req.body ?? {};
            const { projectId, nodeId, nodeType } = payload;
            if (!lodash_1.default.isString(projectId) || lodash_1.default.isEmpty(projectId?.trim?.())) {
                res.status(400).json({
                    success: false,
                    message: 'projectId is required',
                });
                return;
            }
            if (!lodash_1.default.isString(nodeId) || lodash_1.default.isEmpty(nodeId?.trim?.()) || !lodash_1.default.isString(nodeType) || lodash_1.default.isEmpty(nodeType?.trim?.())) {
                res.status(400).json({
                    success: false,
                    message: 'nodeId and nodeType are required',
                });
                return;
            }
            const { applicationId, applicationIds } = payload;
            let resolvedApplicationIds = [];
            if (Array.isArray(applicationIds) && applicationIds.length > 0) {
                resolvedApplicationIds = applicationIds.map((x) => String(x)).filter(Boolean);
            }
            else if (lodash_1.default.isString(applicationId) && applicationId !== 'undefined' && applicationId !== 'null') {
                resolvedApplicationIds = [String(applicationId)];
            }
            if (lodash_1.default.isEmpty(resolvedApplicationIds)) {
                const db = await (0, database_1.getDb)();
                const applicationsInProject = await db
                    .select({ id: tables_1.applicationsTable.id })
                    .from(tables_1.applicationsTable)
                    .innerJoin(tables_1.pulseMonitoringTable, (0, drizzle_orm_1.eq)(tables_1.applicationsTable.id, tables_1.pulseMonitoringTable.applicationId))
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId)))
                    .orderBy(tables_1.applicationsTable.createdAt);
                resolvedApplicationIds = applicationsInProject.map((a) => a.id).filter(Boolean);
            }
            const data = await observeCharts_1.observeChartsService.getNodeTraces({
                ...payload,
                applicationIds: resolvedApplicationIds,
            });
            res.status(200).json({
                success: true,
                message: 'Node traces fetched successfully',
                data,
            });
        }
        catch (error) {
            logger_1.default.error(`Error fetching node traces: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch node traces',
            });
        }
    },
    downloadPromptsCSV: async (req, res) => {
        // Security: Set timeout for the entire request (30 minutes)
        const timeout = setTimeout(() => {
            if (!res.headersSent) {
                res.status(408).json({
                    success: false,
                    message: 'Request timeout - download took too long',
                });
            }
            else {
                res.end();
            }
        }, 30 * 60 * 1000); // 30 minutes
        try {
            const payload = req.body ?? {};
            const { projectId } = payload;
            if (!lodash_1.default.isString(projectId) || lodash_1.default.isEmpty(projectId?.trim?.())) {
                clearTimeout(timeout);
                res.status(400).json({
                    success: false,
                    message: 'projectId is required',
                });
                return;
            }
            const { applicationId, applicationIds } = payload;
            let resolvedApplicationIds = [];
            if (Array.isArray(applicationIds) && applicationIds.length > 0) {
                resolvedApplicationIds = applicationIds.map((x) => String(x)).filter(Boolean);
            }
            else if (lodash_1.default.isString(applicationId) && !lodash_1.default.isEmpty(applicationId) && applicationId !== 'undefined' && applicationId !== 'null') {
                resolvedApplicationIds = [String(applicationId)];
            }
            if (lodash_1.default.isEmpty(resolvedApplicationIds)) {
                const db = await (0, database_1.getDb)();
                const applicationsInProject = await db
                    .select({ id: tables_1.applicationsTable.id })
                    .from(tables_1.applicationsTable)
                    .innerJoin(tables_1.pulseMonitoringTable, (0, drizzle_orm_1.eq)(tables_1.applicationsTable.id, tables_1.pulseMonitoringTable.applicationId))
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId)))
                    .orderBy(tables_1.applicationsTable.createdAt);
                resolvedApplicationIds = applicationsInProject.map((a) => a.id).filter(Boolean);
            }
            // Set headers for CSV download
            res.setHeader('Content-Type', 'text/csv; charset=utf-8');
            res.setHeader('Content-Disposition', `attachment; filename="prompts-${Date.now()}.csv"`);
            res.setHeader('Cache-Control', 'no-cache');
            // CSV headers
            const headers = [
                'Trace ID',
                'Session ID',
                'Tokens',
                'Duration (ms)',
                'Prompt',
                'Response',
                'Application ID',
                'Timestamp',
                'Functional Evaluation Pass Count',
                'Functional Evaluation Fail Count',
                'Security Evaluation Pass Count',
                'Security Evaluation Fail Count',
            ];
            // Helper function to escape CSV values
            const escapeCsv = (value) => {
                if (value === null || value === undefined)
                    return '';
                const str = String(value);
                return `"${str.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ')}"`;
            };
            res.write('\uFEFF');
            res.write(headers.map(h => escapeCsv(h)).join(',') + '\n');
            const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
            let totalBytesWritten = 0;
            const BOM_SIZE = 3; // UTF-8 BOM
            const HEADER_SIZE = Buffer.byteLength(headers.map(h => escapeCsv(h)).join(',') + '\n', 'utf8');
            totalBytesWritten = BOM_SIZE + HEADER_SIZE;
            // Fetch all data using the download method
            const traces = await observeCharts_1.observeChartsService.getAllPromptTracesForDownload({
                ...payload,
                applicationIds: resolvedApplicationIds,
            });
            const MAX_ROWS = 1000000; // 1 million rows max
            if (traces.length > MAX_ROWS) {
                clearTimeout(timeout);
                if (!res.headersSent) {
                    res.status(413).json({
                        success: false,
                        message: `Too many rows (${traces.length}). Maximum ${MAX_ROWS} rows allowed.`,
                    });
                }
                else {
                    res.end();
                }
                return;
            }
            // Stream rows to response with size checking
            for (const row of traces) {
                const csvRow = [
                    row.id || '',
                    row.sessionId || '',
                    row.tokens || 0,
                    row.duration || 0,
                    row.prompt || '',
                    row.response || '',
                    row.applicationId || '',
                    row.createdAt || '',
                    row.functionalComponentPassCount || 0,
                    row.functionalComponentFailCount || 0,
                    row.securityComponentPassCount || 0,
                    row.securityComponentFailCount || 0,
                ];
                const rowData = csvRow.map(escapeCsv).join(',') + '\n';
                const rowSize = Buffer.byteLength(rowData, 'utf8');
                if (totalBytesWritten + rowSize > MAX_FILE_SIZE) {
                    logger_1.default.warn(`CSV download exceeded size limit. Stopping at ${totalBytesWritten} bytes.`);
                    break;
                }
                res.write(rowData);
                totalBytesWritten += rowSize;
            }
            clearTimeout(timeout);
            res.end();
        }
        catch (error) {
            clearTimeout(timeout);
            logger_1.default.error(`Error downloading prompts CSV: ${error}`);
            if (!res.headersSent) {
                res.status(500).json({
                    success: false,
                    message: 'Failed to download prompts CSV',
                });
            }
            else {
                res.end();
            }
        }
    },
    downloadExceptionsCSV: async (req, res) => {
        // Security: Set timeout for the entire request (30 minutes)
        const timeout = setTimeout(() => {
            if (!res.headersSent) {
                res.status(408).json({
                    success: false,
                    message: 'Request timeout - download took too long',
                });
            }
            else {
                res.end();
            }
        }, 30 * 60 * 1000); // 30 minutes
        try {
            const payload = req.body ?? {};
            const { projectId } = payload;
            if (!lodash_1.default.isString(projectId) || lodash_1.default.isEmpty(projectId?.trim?.())) {
                clearTimeout(timeout);
                res.status(400).json({
                    success: false,
                    message: 'projectId is required',
                });
                return;
            }
            const { applicationId, applicationIds } = payload;
            let resolvedApplicationIds = [];
            if (Array.isArray(applicationIds) && applicationIds.length > 0) {
                resolvedApplicationIds = applicationIds.map((x) => String(x)).filter(Boolean);
            }
            else if (lodash_1.default.isString(applicationId) && !lodash_1.default.isEmpty(applicationId) && applicationId !== 'undefined' && applicationId !== 'null') {
                resolvedApplicationIds = [String(applicationId)];
            }
            if (lodash_1.default.isEmpty(resolvedApplicationIds)) {
                const db = await (0, database_1.getDb)();
                const applicationsInProject = await db
                    .select({ id: tables_1.applicationsTable.id })
                    .from(tables_1.applicationsTable)
                    .innerJoin(tables_1.pulseMonitoringTable, (0, drizzle_orm_1.eq)(tables_1.applicationsTable.id, tables_1.pulseMonitoringTable.applicationId))
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId)))
                    .orderBy(tables_1.applicationsTable.createdAt);
                resolvedApplicationIds = applicationsInProject.map((a) => a.id).filter(Boolean);
            }
            // Set headers for CSV download
            res.setHeader('Content-Type', 'text/csv; charset=utf-8');
            res.setHeader('Content-Disposition', `attachment; filename="exceptions-${Date.now()}.csv"`);
            res.setHeader('Cache-Control', 'no-cache');
            // CSV headers
            const headers = [
                'Trace ID',
                'Session ID',
                'Tokens',
                'Duration (ms)',
                'Prompt',
                'Response',
                'Exception Type',
                'Exception Message',
                'Application ID',
                'Timestamp',
                'Functional Evaluation Pass Count',
                'Functional Evaluation Fail Count',
                'Security Evaluation Pass Count',
                'Security Evaluation Fail Count',
            ];
            // Helper function to escape CSV values
            const escapeCsv = (value) => {
                if (value === null || value === undefined)
                    return '';
                const str = String(value);
                return `"${str.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ')}"`;
            };
            res.write('\uFEFF');
            res.write(headers.map(h => escapeCsv(h)).join(',') + '\n');
            const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
            let totalBytesWritten = 0;
            const BOM_SIZE = 3; // UTF-8 BOM
            const HEADER_SIZE = Buffer.byteLength(headers.map(h => escapeCsv(h)).join(',') + '\n', 'utf8');
            totalBytesWritten = BOM_SIZE + HEADER_SIZE;
            // Fetch all data using the download method
            const traces = await observeCharts_1.observeChartsService.getAllExceptionTracesForDownload({
                ...payload,
                applicationIds: resolvedApplicationIds,
            });
            const MAX_ROWS = 1000000; // 1 million rows max
            if (traces.length > MAX_ROWS) {
                clearTimeout(timeout);
                if (!res.headersSent) {
                    res.status(413).json({
                        success: false,
                        message: `Too many rows (${traces.length}). Maximum ${MAX_ROWS} rows allowed.`,
                    });
                }
                else {
                    res.end();
                }
                return;
            }
            // Stream rows to response with size checking
            for (const row of traces) {
                const csvRow = [
                    row.id || '',
                    row.sessionId || '',
                    row.tokens || 0,
                    row.duration || 0,
                    row.prompt || '',
                    row.response || '',
                    row.exceptionType || '',
                    row.exception || '',
                    row.applicationId || '',
                    row.createdAt || '',
                    row.functionalComponentPassCount || 0,
                    row.functionalComponentFailCount || 0,
                    row.securityComponentPassCount || 0,
                    row.securityComponentFailCount || 0,
                ];
                const rowData = csvRow.map(escapeCsv).join(',') + '\n';
                const rowSize = Buffer.byteLength(rowData, 'utf8');
                if (totalBytesWritten + rowSize > MAX_FILE_SIZE) {
                    logger_1.default.warn(`CSV download exceeded size limit. Stopping at ${totalBytesWritten} bytes.`);
                    break;
                }
                res.write(rowData);
                totalBytesWritten += rowSize;
            }
            clearTimeout(timeout);
            res.end();
        }
        catch (error) {
            clearTimeout(timeout);
            logger_1.default.error(`Error downloading exceptions CSV: ${error}`);
            if (!res.headersSent) {
                res.status(500).json({
                    success: false,
                    message: 'Failed to download exceptions CSV',
                });
            }
            else {
                res.end();
            }
        }
    },
    /**
     * Get filter options for Agent Flow panel: unique agents and tools from aggregated_trace_dag_nodes
     * (no date-range filter). Returns separate "Agents" and "Tools" categories; only one can be selected at a time.
     */
    getFilterOptions: async (req, res) => {
        try {
            const payload = req.query ?? {};
            const orgId = payload.orgId ?? payload.workspaceId;
            const projectId = payload.projectId;
            const applicationId = payload.applicationId;
            if (!lodash_1.default.isString(projectId) || lodash_1.default.isEmpty(projectId?.trim?.())) {
                res.status(400).json({
                    success: false,
                    message: 'projectId is required',
                });
                return;
            }
            const db = await (0, database_1.getDb)();
            let monitoringConfig = null;
            let resolvedApplicationIds = [];
            if (lodash_1.default.isString(applicationId) && !lodash_1.default.isEmpty(applicationId) && applicationId !== 'undefined' && applicationId !== 'null') {
                const [row] = await db
                    .select({ metrics: tables_1.pulseMonitoringTable.metrics, pluginsByCategory: tables_1.pulseMonitoringTable.pluginsByCategory })
                    .from(tables_1.pulseMonitoringTable)
                    .where((0, drizzle_orm_1.eq)(tables_1.pulseMonitoringTable.applicationId, applicationId))
                    .limit(1);
                if (row) {
                    monitoringConfig = {
                        metrics: Array.isArray(row?.metrics) ? row.metrics : [],
                        pluginsByCategory: row?.pluginsByCategory && typeof row.pluginsByCategory === 'object' && !Array.isArray(row.pluginsByCategory)
                            ? row.pluginsByCategory
                            : {},
                    };
                    resolvedApplicationIds = [applicationId];
                }
            }
            if (!monitoringConfig) {
                const applicationsInProject = await db
                    .select({ id: tables_1.applicationsTable.id })
                    .from(tables_1.applicationsTable)
                    .innerJoin(tables_1.pulseMonitoringTable, (0, drizzle_orm_1.eq)(tables_1.applicationsTable.id, tables_1.pulseMonitoringTable.applicationId))
                    .where((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId))
                    .orderBy(tables_1.applicationsTable.createdAt);
                resolvedApplicationIds = (applicationsInProject ?? []).map((r) => r.id);
                if (resolvedApplicationIds.length > 0) {
                    const [firstRow] = await db
                        .select({ metrics: tables_1.pulseMonitoringTable.metrics, pluginsByCategory: tables_1.pulseMonitoringTable.pluginsByCategory })
                        .from(tables_1.pulseMonitoringTable)
                        .where((0, drizzle_orm_1.eq)(tables_1.pulseMonitoringTable.applicationId, resolvedApplicationIds[0]))
                        .limit(1);
                    if (firstRow) {
                        monitoringConfig = {
                            metrics: Array.isArray(firstRow?.metrics) ? firstRow.metrics : [],
                            pluginsByCategory: firstRow?.pluginsByCategory && typeof firstRow.pluginsByCategory === 'object' && !Array.isArray(firstRow.pluginsByCategory)
                                ? firstRow.pluginsByCategory
                                : {},
                        };
                    }
                }
            }
            if (!monitoringConfig) {
                monitoringConfig = { metrics: [], pluginsByCategory: {} };
            }
            const functionalMetrics = (monitoringConfig.metrics ?? []).map((m) => ({
                type: m?.type ?? m?.metric ?? String(m),
                displayLabel: m?.displayLabel ?? m?.label ?? m?.type ?? m?.metric ?? String(m),
            })).filter((m) => !!m.type);
            const pluginIdsSet = new Set();
            const pluginsByCategory = monitoringConfig.pluginsByCategory ?? {};
            if (typeof pluginsByCategory === 'object' && !Array.isArray(pluginsByCategory)) {
                for (const arr of Object.values(pluginsByCategory)) {
                    if (Array.isArray(arr)) {
                        arr.forEach((id) => pluginIdsSet.add(String(id).trim()));
                    }
                }
            }
            const securityMetrics = Array.from(pluginIdsSet).filter(Boolean).map((pluginId) => ({
                pluginId,
                displayName: metadata_1.displayNameOverrides[pluginId] ?? pluginId,
            }));
            let agents = [];
            let tools = [];
            if (lodash_1.default.isString(orgId) && !lodash_1.default.isEmpty(orgId)) {
                const result = await observeCharts_1.observeChartsService.getUniqueAgentsAndTools({
                    orgId,
                    projectId,
                    applicationId: resolvedApplicationIds.length === 1 ? resolvedApplicationIds[0] : undefined,
                    applicationIds: resolvedApplicationIds.length > 0 ? resolvedApplicationIds : undefined,
                });
                agents = result.agents ?? [];
                tools = result.tools ?? [];
            }
            res.status(200).json({
                success: true,
                message: 'Filter options fetched successfully',
                data: {
                    functionalMetrics,
                    securityMetrics,
                    agents,
                    tools,
                },
            });
        }
        catch (error) {
            logger_1.default.error(`Error getting filter options: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to get filter options',
            });
        }
    },
    getDashboardDag: async (req, res) => {
        try {
            const payload = req.body ?? {};
            const { orgId, projectId } = payload;
            if (!lodash_1.default.isString(projectId) || lodash_1.default.isEmpty(projectId?.trim?.())) {
                res.status(400).json({
                    success: false,
                    message: 'projectId is required',
                });
                return;
            }
            const { applicationId, applicationIds } = payload;
            let resolvedApplicationIds = [];
            if (Array.isArray(applicationIds) && applicationIds.length > 0) {
                resolvedApplicationIds = applicationIds.map((x) => String(x)).filter(Boolean);
            }
            else if (lodash_1.default.isString(applicationId) && !lodash_1.default.isEmpty(applicationId) && applicationId !== 'undefined' && applicationId !== 'null') {
                resolvedApplicationIds = [String(applicationId)];
            }
            if (lodash_1.default.isEmpty(resolvedApplicationIds)) {
                const db = await (0, database_1.getDb)();
                const applicationsInProject = await db
                    .select({ id: tables_1.applicationsTable.id })
                    .from(tables_1.applicationsTable)
                    .innerJoin(tables_1.pulseMonitoringTable, (0, drizzle_orm_1.eq)(tables_1.applicationsTable.id, tables_1.pulseMonitoringTable.applicationId))
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId)))
                    .orderBy(tables_1.applicationsTable.createdAt);
                resolvedApplicationIds = (applicationsInProject ?? []).map((r) => r.id);
            }
            const data = await observeCharts_1.observeChartsService.getDashboardDag({
                ...payload,
                applicationIds: resolvedApplicationIds,
            });
            if (resolvedApplicationIds.length > 0 && data?.metadata) {
                const db = await (0, database_1.getDb)();
                const [monitoring] = await db
                    .select({
                    samplingFrequency: tables_1.pulseMonitoringTable.samplingFrequency,
                    samplingCount: tables_1.pulseMonitoringTable.samplingCount,
                    samplingPercentage: tables_1.pulseMonitoringTable.samplingPercentage,
                })
                    .from(tables_1.pulseMonitoringTable)
                    .where((0, drizzle_orm_1.eq)(tables_1.pulseMonitoringTable.applicationId, resolvedApplicationIds[0]))
                    .limit(1);
                if (monitoring) {
                    data.metadata.samplingFrequency = monitoring.samplingFrequency ?? undefined;
                    data.metadata.samplingCount = monitoring.samplingCount ?? undefined;
                    data.metadata.samplingPercentage = monitoring.samplingPercentage ?? undefined;
                }
            }
            res.status(200).json({
                success: true,
                message: 'Dashboard DAG fetched successfully',
                data,
            });
        }
        catch (error) {
            logger_1.default.error(`Error getting dashboard DAG: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to get dashboard DAG',
            });
        }
    },
    /**
     * Trace view DAG: same parser as /trace/linear-flow (getTraceViewDagData).
     * Accepts traceId (fetch spans from ClickHouse) or spans (use provided); when both are sent, spans are used so edges match linear-flow.
     */
    getTraceViewDag: async (req, res) => {
        try {
            const payload = req.body ?? {};
            const traceIdParam = lodash_1.default.isString(payload.traceId) ? payload.traceId.trim() : '';
            const bodySpans = Array.isArray(payload.spans) ? payload.spans : undefined;
            let normalizedSpans;
            let effectiveTraceId;
            if (bodySpans && bodySpans.length > 0) {
                normalizedSpans = normalizeSpans(bodySpans);
                const byTrace = new Map();
                for (const s of normalizedSpans) {
                    const tid = s.traceId ?? 'default-trace';
                    if (!byTrace.has(tid))
                        byTrace.set(tid, []);
                    byTrace.get(tid).push(s);
                }
                if (traceIdParam && byTrace.has(traceIdParam)) {
                    normalizedSpans = byTrace.get(traceIdParam);
                    effectiveTraceId = traceIdParam;
                }
                else if (traceIdParam && byTrace.size > 0) {
                    const first = byTrace.keys().next().value;
                    normalizedSpans = byTrace.get(first);
                    effectiveTraceId = first;
                }
                else if (byTrace.size > 0) {
                    const first = byTrace.keys().next().value;
                    normalizedSpans = byTrace.get(first);
                    effectiveTraceId = first;
                }
                else {
                    normalizedSpans = [];
                    effectiveTraceId = traceIdParam || 'default-trace';
                }
            }
            else if (traceIdParam) {
                effectiveTraceId = traceIdParam;
                const rawSpans = await pulseService_1.pulseService.getReportData({ traceId: traceIdParam });
                normalizedSpans = normalizeSpans(Array.isArray(rawSpans) ? rawSpans : []);
            }
            else {
                res.status(400).json({
                    success: false,
                    message: 'Invalid request: traceId or spans is required',
                });
                return;
            }
            const parser = new frameworkParser_1.MultiFrameworkParser();
            const results = [];
            if (normalizedSpans.length === 0) {
                results.push({
                    traceId: effectiveTraceId,
                    input: undefined,
                    output: undefined,
                    renderLinear: { nodes: [], edges: [] },
                });
            }
            else {
                try {
                    const { input, output, renderLinear } = parser.getLinearFlowData(normalizedSpans);
                    results.push({
                        traceId: effectiveTraceId,
                        input,
                        output,
                        renderLinear: { nodes: renderLinear.nodes, edges: renderLinear.edges },
                    });
                }
                catch (error) {
                    logger_1.default.error(`Error building trace view DAG for trace ${effectiveTraceId}: ${error}`);
                    results.push({
                        traceId: effectiveTraceId,
                        input: undefined,
                        output: undefined,
                        renderLinear: { nodes: [], edges: [] },
                    });
                }
            }
            res.status(200).json({
                success: true,
                message: 'Trace view DAG fetched successfully',
                data: results,
            });
        }
        catch (error) {
            logger_1.default.error(`Error getting trace view DAG: ${error}`);
            res.status(500).json({
                success: false,
                message: 'Failed to get trace view DAG',
            });
        }
    },
};
//# sourceMappingURL=observe-dashboard-controller.js.map