"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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.findStartupDataInFindings = exports.findSessionDataInFindings = exports.findFrameDataInFindings = void 0;
exports.resolveFollowUp = resolveFollowUp;
exports.findEntityInFindings = findEntityInFindings;
const entityRegistry_1 = require("./entityRegistry");
// =============================================================================
// ID Normalization Utilities
// =============================================================================
/**
 * Normalize an ID for comparison (handles string/number mismatch).
 * Returns string representation for consistent comparison.
 */
function normalizeId(id) {
    if (id === null || id === undefined)
        return '';
    return String(id);
}
function normalizeLooseNumericId(id) {
    if (id === null || id === undefined)
        return null;
    if (typeof id === 'number' && Number.isFinite(id))
        return String(Math.trunc(id));
    const s = String(id).trim();
    if (!s)
        return null;
    const compact = s.replace(/[,\s，_]/g, '');
    if (!/^\d+$/.test(compact))
        return null;
    return compact;
}
/**
 * Compare two IDs with type normalization.
 */
function idsMatch(a, b) {
    const an = normalizeLooseNumericId(a);
    const bn = normalizeLooseNumericId(b);
    if (an !== null && bn !== null)
        return an === bn;
    return normalizeId(a) === normalizeId(b);
}
/**
 * Get a field value from an object, trying multiple key variations.
 * Supports snake_case and camelCase.
 */
function getField(obj, ...keys) {
    for (const key of keys) {
        if (obj[key] !== undefined)
            return obj[key];
        // Try camelCase conversion
        const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
        if (obj[camelKey] !== undefined)
            return obj[camelKey];
        // Try snake_case conversion
        const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
        if (obj[snakeKey] !== undefined)
            return obj[snakeKey];
    }
    return undefined;
}
// =============================================================================
// Main Resolution Function
// =============================================================================
/**
 * Resolve a follow-up query using session context.
 */
function resolveFollowUp(intent, sessionContext) {
    if (!intent.followUpType || intent.followUpType === 'initial' || !sessionContext) {
        return {
            isFollowUp: false,
            resolvedParams: intent.extractedParams || {},
            confidence: 1.0,
        };
    }
    // FIX: Safe spread - handle undefined extractedParams
    const params = { ...(intent.extractedParams || {}) };
    const focusIntervals = [];
    const details = [];
    for (const entity of intent.referencedEntities || []) {
        const resolved = resolveEntityFromFindings(entity, sessionContext);
        if (resolved) {
            Object.assign(params, resolved.params);
            details.push(`${entity.type}:${entity.id} → ${Object.keys(resolved.params).join(', ')}`);
            if (intent.followUpType === 'drill_down') {
                // Try to build interval from finding
                if (resolved.finding) {
                    const interval = buildFocusInterval(entity, resolved.finding, focusIntervals.length);
                    if (interval) {
                        focusIntervals.push(interval);
                        continue;
                    }
                }
                // Fallback: build interval from resolved params if we have timestamps
                const fallbackInterval = buildFallbackInterval(entity, resolved.params, focusIntervals.length);
                if (fallbackInterval) {
                    focusIntervals.push(fallbackInterval);
                }
            }
        }
    }
    // If still no intervals but we have drill-down with entity params, try to construct from params alone
    if (intent.followUpType === 'drill_down' && focusIntervals.length === 0) {
        const minimalInterval = buildMinimalIntervalFromParams(params, intent.referencedEntities || []);
        if (minimalInterval) {
            focusIntervals.push(minimalInterval);
            details.push('Constructed minimal interval from params');
        }
    }
    return {
        isFollowUp: true,
        resolvedParams: params,
        focusIntervals: focusIntervals.length > 0 ? focusIntervals : undefined,
        suggestedStrategy: getSuggestedStrategy(intent.followUpType, params),
        confidence: calculateConfidence(intent, params, focusIntervals),
        resolutionDetails: details.length > 0 ? details.join('; ') : undefined,
    };
}
// =============================================================================
// Entity Resolution
// =============================================================================
function resolveEntityFromFindings(entity, sessionContext) {
    // Get the entity ID - prefer value if available, fall back to id
    const entityId = entity.value !== undefined ? entity.value : entity.id;
    if (entityId === undefined)
        return null;
    for (const finding of sessionContext.getAllFindings()) {
        const d = finding.details;
        if (!d)
            continue;
        // Frame entity matching
        if (entity.type === 'frame') {
            const findingFrameId = getField(d, 'frame_id', 'frameId');
            if (findingFrameId !== undefined && idsMatch(findingFrameId, entityId)) {
                return {
                    params: buildFrameParams(entityId, d),
                    finding,
                };
            }
        }
        // Session entity matching
        if (entity.type === 'session') {
            const findingSessionId = getField(d, 'session_id', 'sessionId');
            if (findingSessionId !== undefined && idsMatch(findingSessionId, entityId)) {
                return {
                    params: buildSessionParams(entityId, d),
                    finding,
                };
            }
        }
        // Startup entity matching
        if (entity.type === 'startup') {
            const findingStartupId = getField(d, 'startup_id', 'startupId');
            if (findingStartupId !== undefined && idsMatch(findingStartupId, entityId)) {
                return {
                    params: buildStartupParams(entityId, d),
                    finding,
                };
            }
        }
        // Process entity matching
        if (entity.type === 'process') {
            const processName = getField(d, 'process_name', 'processName', 'package');
            if (processName !== undefined && idsMatch(processName, entityId)) {
                return {
                    params: buildProcessParams(entityId, d),
                    finding,
                };
            }
        }
    }
    // No finding match - return just the entity id as param
    const paramKey = (0, entityRegistry_1.getEntityParamKey)(entity.type);
    return { params: { [paramKey]: entityId }, finding: null };
}
// =============================================================================
// Parameter Building
// =============================================================================
function buildFrameParams(entityId, d) {
    return {
        frame_id: entityId,
        start_ts: getField(d, 'start_ts', 'startTs'),
        end_ts: getField(d, 'end_ts', 'endTs'),
        process_name: getField(d, 'process_name', 'processName', 'package'),
        session_id: getField(d, 'session_id', 'sessionId'),
        ...(getField(d, 'dur', 'duration') && { duration: getField(d, 'dur', 'duration') }),
        ...(getField(d, 'vsync_id', 'vsyncId') && { vsync_id: getField(d, 'vsync_id', 'vsyncId') }),
        ...(getField(d, 'jank_type', 'jankType') && { jank_type: getField(d, 'jank_type', 'jankType') }),
        ...(getField(d, 'dur_ms', 'durMs') && { dur_ms: getField(d, 'dur_ms', 'durMs') }),
        ...(getField(d, 'main_start_ts', 'mainStartTs') && { main_start_ts: getField(d, 'main_start_ts', 'mainStartTs') }),
        ...(getField(d, 'main_end_ts', 'mainEndTs') && { main_end_ts: getField(d, 'main_end_ts', 'mainEndTs') }),
        ...(getField(d, 'render_start_ts', 'renderStartTs') && { render_start_ts: getField(d, 'render_start_ts', 'renderStartTs') }),
        ...(getField(d, 'render_end_ts', 'renderEndTs') && { render_end_ts: getField(d, 'render_end_ts', 'renderEndTs') }),
        ...(getField(d, 'pid') && { pid: getField(d, 'pid') }),
        ...(getField(d, 'layer_name', 'layerName') && { layer_name: getField(d, 'layer_name', 'layerName') }),
    };
}
function buildSessionParams(entityId, d) {
    return {
        session_id: entityId,
        start_ts: getField(d, 'start_ts', 'startTs'),
        end_ts: getField(d, 'end_ts', 'endTs'),
        process_name: getField(d, 'process_name', 'processName', 'package'),
        ...(getField(d, 'frame_count', 'frameCount') && { frame_count: getField(d, 'frame_count', 'frameCount') }),
        ...(getField(d, 'jank_count', 'jankCount') && { jank_count: getField(d, 'jank_count', 'jankCount') }),
    };
}
function buildStartupParams(entityId, d) {
    return {
        startup_id: entityId,
        start_ts: getField(d, 'start_ts', 'startTs'),
        end_ts: getField(d, 'end_ts', 'endTs'),
        package: getField(d, 'package', 'process_name', 'processName'),
        startup_type: getField(d, 'startup_type', 'startupType'),
        dur_ms: getField(d, 'dur_ms', 'durMs'),
        ttid_ms: getField(d, 'ttid_ms', 'ttidMs'),
        ttfd_ms: getField(d, 'ttfd_ms', 'ttfdMs'),
    };
}
function buildProcessParams(entityId, d) {
    const startTs = getField(d, 'start_ts', 'startTs');
    const endTs = getField(d, 'end_ts', 'endTs');
    return {
        process_name: entityId,
        ...(startTs && { start_ts: startTs }),
        ...(endTs && { end_ts: endTs }),
    };
}
// =============================================================================
// Interval Building
// =============================================================================
function buildFocusInterval(entity, finding, index) {
    const d = finding.details;
    if (!d)
        return null;
    const startTs = getField(d, 'start_ts', 'startTs');
    const endTs = getField(d, 'end_ts', 'endTs');
    if (!startTs || !endTs)
        return null;
    const entityId = entity.value !== undefined ? entity.value : entity.id;
    const label = (0, entityRegistry_1.buildEntityLabel)(entity.type, entityId);
    return {
        id: typeof entityId === 'number' ? entityId : index,
        processName: getField(d, 'process_name', 'processName', 'package') || '',
        startTs: String(startTs),
        endTs: String(endTs),
        priority: 1,
        label,
        metadata: {
            sourceEntityType: entity.type,
            sourceEntityId: entityId,
            // Normalize all metadata to snake_case for consistency
            frameId: getField(d, 'frame_id', 'frameId'),
            sessionId: getField(d, 'session_id', 'sessionId'),
            startupId: getField(d, 'startup_id', 'startupId'),
            startupType: getField(d, 'startup_type', 'startupType'),
            jankType: getField(d, 'jank_type', 'jankType'),
            durMs: getField(d, 'dur_ms', 'durMs'),
            ttidMs: getField(d, 'ttid_ms', 'ttidMs'),
            ttfdMs: getField(d, 'ttfd_ms', 'ttfdMs'),
            mainStartTs: getField(d, 'main_start_ts', 'mainStartTs'),
            mainEndTs: getField(d, 'main_end_ts', 'mainEndTs'),
            renderStartTs: getField(d, 'render_start_ts', 'renderStartTs'),
            renderEndTs: getField(d, 'render_end_ts', 'renderEndTs'),
            pid: getField(d, 'pid'),
            layerName: getField(d, 'layer_name', 'layerName'),
            vsyncMissed: getField(d, 'vsync_missed', 'vsyncMissed'),
        },
    };
}
/**
 * Build a fallback interval from resolved params when no finding is available.
 * This handles cases where entity is recognized but not in findings.
 */
function buildFallbackInterval(entity, params, index) {
    const startTs = getField(params, 'start_ts', 'startTs');
    const endTs = getField(params, 'end_ts', 'endTs');
    // Can't build interval without timestamps
    if (!startTs || !endTs)
        return null;
    const entityId = entity.value !== undefined ? entity.value : entity.id;
    const label = (0, entityRegistry_1.buildEntityLabel)(entity.type, entityId);
    return {
        id: typeof entityId === 'number' ? entityId : index,
        processName: getField(params, 'process_name', 'processName', 'package') || '',
        startTs: String(startTs),
        endTs: String(endTs),
        priority: 1,
        label,
        metadata: {
            sourceEntityType: entity.type,
            sourceEntityId: entityId,
            fallback: true, // Mark as fallback-constructed
            ...params,
        },
    };
}
/**
 * Build a minimal interval from params alone when no finding match.
 * This is the last resort for drill-down scenarios.
 */
function buildMinimalIntervalFromParams(params, entities) {
    // Need at least an entity ID to be meaningful
    const frameId = getField(params, 'frame_id', 'frameId');
    const sessionId = getField(params, 'session_id', 'sessionId');
    const startupId = getField(params, 'startup_id', 'startupId');
    const entityId = frameId || sessionId || startupId;
    if (!entityId)
        return null;
    const startTs = getField(params, 'start_ts', 'startTs');
    const endTs = getField(params, 'end_ts', 'endTs');
    // If we have timestamps, use them
    if (startTs && endTs) {
        const entityType = frameId ? 'frame' : (sessionId ? 'session' : 'startup');
        const label = (0, entityRegistry_1.buildEntityLabel)(entityType, entityId);
        return {
            id: typeof entityId === 'number' ? entityId : 0,
            processName: getField(params, 'process_name', 'processName', 'package') || '',
            startTs: String(startTs),
            endTs: String(endTs),
            priority: 1,
            label,
            metadata: {
                sourceEntityType: entityType,
                sourceEntityId: entityId,
                minimal: true, // Mark as minimal construction
                ...params,
            },
        };
    }
    // No timestamps - still create interval but mark it as needing enrichment
    // The DirectDrillDownExecutor will need to handle this case
    const entityType = frameId ? 'frame' : (sessionId ? 'session' : 'startup');
    const entity = entities.find(e => e.type === entityType);
    const label = (0, entityRegistry_1.buildEntityLabel)(entityType, entityId);
    return {
        id: typeof entityId === 'number' ? entityId : 0,
        processName: getField(params, 'process_name', 'processName', 'package') || '',
        startTs: '0', // Placeholder - executor will need to query for real timestamps
        endTs: '0',
        priority: 1,
        label,
        metadata: {
            sourceEntityType: entityType,
            sourceEntityId: entityId,
            needsEnrichment: true, // Signal that timestamps need to be fetched
            ...params,
        },
    };
}
// =============================================================================
// Strategy Suggestion
// =============================================================================
function getSuggestedStrategy(followUpType, params) {
    if (followUpType === 'drill_down') {
        if (getField(params, 'frame_id', 'frameId'))
            return (0, entityRegistry_1.getEntityDrillDownStrategy)('frame');
        if (getField(params, 'session_id', 'sessionId'))
            return (0, entityRegistry_1.getEntityDrillDownStrategy)('session');
        if (getField(params, 'startup_id', 'startupId'))
            return (0, entityRegistry_1.getEntityDrillDownStrategy)('startup');
    }
    if (followUpType === 'compare')
        return 'comparison';
    return undefined;
}
// =============================================================================
// Confidence Calculation
// =============================================================================
function calculateConfidence(intent, params, focusIntervals) {
    let confidence = 0.5;
    const hasTimestamps = Object.keys(params).some(k => k.includes('_ts') || k.includes('Ts'));
    if ((intent.referencedEntities?.length || 0) > 0 && hasTimestamps) {
        confidence += 0.3;
    }
    if (focusIntervals.length > 0) {
        // Higher confidence if intervals don't need enrichment
        const needsEnrichment = focusIntervals.some(i => i.metadata?.needsEnrichment);
        confidence += needsEnrichment ? 0.1 : 0.2;
    }
    return Math.min(confidence, 1.0);
}
// =============================================================================
// Public Utilities
// =============================================================================
/**
 * Find entity data in findings by type and id.
 * Uses normalized ID comparison.
 */
function findEntityInFindings(type, id, sessionContext) {
    const keys = (0, entityRegistry_1.getEntityIdFieldKeys)(type);
    for (const finding of sessionContext.getAllFindings()) {
        const d = finding.details;
        if (!d)
            continue;
        for (const key of keys) {
            if (d[key] !== undefined && idsMatch(d[key], id)) {
                return d;
            }
        }
    }
    return null;
}
// Convenience aliases
const findFrameDataInFindings = (frameId, ctx) => findEntityInFindings('frame', frameId, ctx);
exports.findFrameDataInFindings = findFrameDataInFindings;
const findSessionDataInFindings = (sessionId, ctx) => findEntityInFindings('session', sessionId, ctx);
exports.findSessionDataInFindings = findSessionDataInFindings;
const findStartupDataInFindings = (startupId, ctx) => findEntityInFindings('startup', startupId, ctx);
exports.findStartupDataInFindings = findStartupDataInFindings;
//# sourceMappingURL=followUpHandler.js.map