"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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TraceProcessorService = void 0;
exports.getTraceProcessorService = getTraceProcessorService;
exports.setTraceProcessorServiceForTests = setTraceProcessorServiceForTests;
const events_1 = require("events");
const async_hooks_1 = require("async_hooks");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const uuid_1 = require("../utils/uuid");
const workingTraceProcessor_1 = require("./workingTraceProcessor");
const traceProcessorLeaseStore_1 = require("./traceProcessorLeaseStore");
const traceProcessorConnectionModel_1 = require("./traceProcessorConnectionModel");
const DEFAULT_LEASE_RESTART_BACKOFF_MS = [1000, 5000, 15000];
const DEFAULT_LEASE_RESTART_JITTER_MS = 250;
const LEASE_RESTART_CONFLICT_STATES = new Set(['draining', 'released', 'failed']);
/**
 * Manages trace files and processors using the actual Perfetto Trace Processor WASM
 */
class TraceProcessorService extends events_1.EventEmitter {
    constructor(uploadDir = resolveDefaultTraceUploadDir(), leaseRestartPolicy = {}) {
        super();
        this.leaseRestartPolicy = leaseRestartPolicy;
        this.traces = new Map();
        this.processors = new Map();
        this.uploads = new Map();
        /** Guards against concurrent auto-recovery for the same trace. */
        this.recoveryInProgress = new Map();
        /** Single lease supervisor per processor key; holders wait instead of retrying. */
        this.leaseRestartInProgress = new Map();
        this.queryLeaseContext = new async_hooks_1.AsyncLocalStorage();
        this.uploadDir = path_1.default.resolve(uploadDir);
        this.ensureUploadDir();
    }
    ensureUploadDir() {
        if (!fs_1.default.existsSync(this.uploadDir)) {
            fs_1.default.mkdirSync(this.uploadDir, { recursive: true });
        }
    }
    processorKeyForLease(traceId, leaseId, mode = 'shared') {
        return (0, traceProcessorConnectionModel_1.traceProcessorProcessorKey)(traceId, leaseId, mode);
    }
    runWithLease(context, fn) {
        if (!context)
            return fn();
        return this.queryLeaseContext.run(context, fn);
    }
    runWithLeases(contexts, fn) {
        const traceLeases = {};
        for (const context of contexts) {
            if (!context)
                continue;
            traceLeases[context.traceId] = context;
        }
        if (Object.keys(traceLeases).length === 0)
            return fn();
        return this.queryLeaseContext.run({ traceLeases }, fn);
    }
    resolveLeaseQueryContext(traceId, options = {}) {
        if (options.leaseId) {
            return {
                traceId,
                leaseId: options.leaseId,
                mode: options.leaseMode ?? 'shared',
                ...(options.leaseScope ? { leaseScope: options.leaseScope } : {}),
            };
        }
        const stored = this.queryLeaseContext.getStore();
        if (stored && 'traceLeases' in stored) {
            return stored.traceLeases[traceId];
        }
        return stored?.traceId === traceId ? stored : undefined;
    }
    /**
     * Initialize a trace upload
     */
    async initializeUpload(filename, size) {
        const traceId = (0, uuid_1.uuidv4)();
        const traceInfo = {
            id: traceId,
            filename,
            size,
            uploadTime: new Date(),
            status: 'uploading',
        };
        this.traces.set(traceId, traceInfo);
        this.emit('trace-initialized', traceInfo);
        return traceId;
    }
    /**
     * Initialize a trace upload with a specific ID
     * Use this when you already have a trace ID (e.g., from a file upload)
     */
    async initializeUploadWithId(traceId, filename, size, filePath) {
        const traceInfo = {
            id: traceId,
            filename,
            size,
            ...(filePath ? { filePath } : {}),
            uploadTime: new Date(),
            status: 'uploading',
        };
        this.traces.set(traceId, traceInfo);
        this.emit('trace-initialized', traceInfo);
    }
    /**
     * Handle chunk upload for large files
     */
    async uploadChunk(traceId, chunk, offset) {
        const trace = this.traces.get(traceId);
        if (!trace) {
            throw new Error(`Trace ${traceId} not found`);
        }
        const filePath = this.getTraceFilePath(traceId);
        // Create write stream if not exists
        if (!this.uploads.has(traceId)) {
            const writeStream = fs_1.default.createWriteStream(filePath, { flags: 'w' });
            this.uploads.set(traceId, writeStream);
        }
        const writeStream = this.uploads.get(traceId);
        // Write chunk at specific offset
        return new Promise((resolve, reject) => {
            // For simplicity, we'll append chunks
            // In production, you might want to use random access for better performance
            writeStream.write(chunk, (error) => {
                if (error) {
                    reject(error);
                }
                else {
                    resolve();
                }
            });
        });
    }
    /**
     * Complete the upload and start processing
     */
    async completeUpload(traceId) {
        const trace = this.traces.get(traceId);
        if (!trace) {
            throw new Error(`Trace ${traceId} not found`);
        }
        // Close write stream
        const writeStream = this.uploads.get(traceId);
        if (writeStream) {
            writeStream.end();
            this.uploads.delete(traceId);
        }
        // Update status
        trace.status = 'processing';
        this.emit('trace-status-changed', trace);
        // Start processing
        await this.processTrace(traceId);
    }
    /**
     * Process the uploaded trace file
     */
    async processTrace(traceId) {
        const trace = this.traces.get(traceId);
        if (!trace)
            return;
        try {
            // Detect trace format and OS before creating processor
            const filePath = this.getTraceFilePath(traceId);
            if (filePath && fs_1.default.existsSync(filePath)) {
                try {
                    const { detectTraceFormat } = await Promise.resolve().then(() => __importStar(require('./traceFormatDetector')));
                    const formatInfo = await detectTraceFormat(filePath);
                    trace.traceOs = formatInfo.os;
                    trace.traceFormat = formatInfo.format;
                    console.log(`[TraceProcessorService] Detected trace: os=${formatInfo.os}, format=${formatInfo.format} (${formatInfo.reason})`);
                }
                catch (detectError) {
                    console.warn(`[TraceProcessorService] Format detection failed:`, detectError.message);
                    trace.traceOs = 'unknown';
                    trace.traceFormat = 'unknown';
                }
            }
            // Create a processor instance
            const processor = await this.createProcessor(traceId);
            this.processors.set(traceId, processor);
            // Extract metadata
            const metadata = await this.extractMetadata(processor);
            trace.metadata = metadata;
            trace.status = 'ready';
            this.emit('trace-processed', trace);
            this.emit('trace-status-changed', trace);
        }
        catch (error) {
            console.error(`[TraceProcessorService] Failed to process trace ${traceId}:`, error.message);
            trace.status = 'error';
            trace.error = error.message;
            this.emit('trace-status-changed', trace);
        }
    }
    /**
     * Create a new Trace Processor instance.
     * All traces use the default Perfetto trace_processor_shell,
     * which handles both Perfetto protobuf and ftrace text formats.
     */
    async createProcessor(traceId, leaseContext) {
        const filePath = this.getTraceFilePath(traceId);
        const processorKey = this.processorKeyForLease(traceId, leaseContext?.leaseId, leaseContext?.mode);
        const processor = await workingTraceProcessor_1.TraceProcessorFactory.create(traceId, filePath, {
            processorKey,
            leaseId: leaseContext?.leaseId,
            leaseMode: leaseContext?.mode ?? 'shared',
        });
        // Store reference
        this.processors.set(processorKey, processor);
        return processor;
    }
    /**
     * Extract basic metadata from the trace
     */
    async extractMetadata(processor) {
        try {
            // Query basic trace information
            const result = await processor.query(`
        SELECT
          MIN(ts) as startTime,
          MAX(ts) as endTime,
          COUNT(*) as numEvents
        FROM slice
        UNION ALL
        SELECT
          MIN(ts) as startTime,
          MAX(ts) as endTime,
          COUNT(*) as numEvents
        FROM counter
        LIMIT 1
      `);
            if (result.rows.length > 0) {
                const row = result.rows[0];
                const startTime = row[0];
                const endTime = row[1];
                const numEvents = row[2];
                return {
                    startTime,
                    endTime,
                    duration: endTime - startTime,
                    numEvents,
                };
            }
            return {};
        }
        catch (error) {
            console.error('Failed to extract metadata:', error);
            return {};
        }
    }
    /**
     * Execute a SQL query on a trace.
     * If the processor has died (status=error), attempts to auto-recover once
     * by re-creating the processor from the on-disk trace file.
     * Concurrent callers share the same recovery promise to avoid thundering herd.
     */
    async processorForQuery(traceId, options = {}) {
        const leaseContext = this.resolveLeaseQueryContext(traceId, options);
        const processorKey = this.processorKeyForLease(traceId, leaseContext?.leaseId, leaseContext?.mode);
        let processor = this.processors.get(processorKey);
        if (!processor && leaseContext) {
            processor = await this.ensureProcessorForLease(traceId, leaseContext.leaseId, leaseContext.mode, leaseContext.leaseScope);
        }
        if (!processor) {
            throw new Error(`No processor for trace ${traceId}`);
        }
        // Update last access time for smart cleanup
        this.touchTrace(traceId);
        // Auto-recover dead processor
        if (processor.status === 'error') {
            if (leaseContext?.leaseId) {
                try {
                    processor = await this.restartLeaseProcessor(traceId, leaseContext);
                }
                catch (err) {
                    throw new Error(`HTTP server not ready (auto-recovery failed: ${err.message})`);
                }
                return processor;
            }
            // Serialize concurrent recovery attempts for the same trace
            let recovery = this.recoveryInProgress.get(processorKey);
            if (!recovery) {
                console.log(`[TraceProcessorService] Processor for ${traceId} (${processorKey}) is dead, attempting auto-recovery...`);
                recovery = this.createProcessor(traceId, leaseContext).then((p) => {
                    console.log(`[TraceProcessorService] Auto-recovery succeeded for ${traceId} (${processorKey})`);
                    this.recoveryInProgress.delete(processorKey);
                    return p;
                }, (err) => {
                    console.error(`[TraceProcessorService] Auto-recovery failed for ${traceId} (${processorKey}):`, err.message);
                    this.recoveryInProgress.delete(processorKey);
                    throw err;
                });
                this.recoveryInProgress.set(processorKey, recovery);
            }
            else {
                console.log(`[TraceProcessorService] Waiting for in-progress recovery of ${traceId} (${processorKey})...`);
            }
            try {
                processor = await recovery;
            }
            catch (err) {
                throw new Error(`HTTP server not ready (auto-recovery failed: ${err.message})`);
            }
        }
        return processor;
    }
    async query(traceId, sql, options = {}) {
        const processor = await this.processorForQuery(traceId, options);
        const { leaseId: _leaseId, leaseMode: _leaseMode, leaseScope: _leaseScope, ...queryOptions } = options;
        return await processor.query(sql, queryOptions);
    }
    async queryRaw(traceId, body, options = {}) {
        const processor = await this.processorForQuery(traceId, options);
        const { leaseId: _leaseId, leaseMode: _leaseMode, leaseScope: _leaseScope, ...queryOptions } = options;
        return await processor.queryRaw(body, queryOptions);
    }
    /**
     * Update the last access time for a trace (called on any activity)
     * This prevents active traces from being cleaned up prematurely
     */
    touchTrace(traceId) {
        const trace = this.traces.get(traceId);
        if (trace) {
            trace.lastAccessTime = new Date();
        }
    }
    /**
     * Get trace information
     */
    getTrace(traceId) {
        return this.traces.get(traceId);
    }
    /**
     * Get the HTTP port of the trace processor for a given trace
     * This port can be used by the frontend to connect via HTTP RPC mode
     * Only returns port if the processor is actually ready
     */
    getProcessorPort(traceId) {
        const processor = this.processors.get(traceId);
        // Only return port if processor is ready
        if (processor?.status === 'ready') {
            // Touch trace to prevent cleanup while frontend is using it
            this.touchTrace(traceId);
            return processor.httpPort;
        }
        return undefined;
    }
    /**
     * Get trace info with processor port for frontend
     * Also updates last access time to prevent cleanup
     */
    getTraceWithPort(traceId) {
        const trace = this.traces.get(traceId);
        if (!trace)
            return undefined;
        const processor = this.processors.get(traceId);
        // Only return port if processor is actually ready (not in error state)
        const port = (processor?.status === 'ready') ? processor.httpPort : undefined;
        // Touch trace to prevent cleanup while frontend is accessing it
        if (port) {
            this.touchTrace(traceId);
        }
        return {
            ...trace,
            port,
            processor: processor ? { status: processor.status } : undefined,
        };
    }
    getTraceWithLeasePort(traceId, leaseId, mode) {
        const trace = this.traces.get(traceId);
        if (!trace)
            return undefined;
        const processorKey = this.processorKeyForLease(traceId, leaseId, mode);
        const processor = this.processors.get(processorKey);
        const port = (processor?.status === 'ready') ? processor.httpPort : undefined;
        if (port) {
            this.touchTrace(traceId);
        }
        return {
            ...trace,
            port,
            processor: processor ? { status: processor.status } : undefined,
        };
    }
    /**
     * Register an external RPC connection (frontend already connected to trace_processor)
     * This allows AI analysis to work with traces loaded via external HTTP RPC
     * @param traceId - A generated trace ID for this external connection
     * @param port - The port number where trace_processor is running
     * @param traceName - Display name for the trace
     */
    async registerExternalRpc(traceId, port, traceName) {
        console.log(`[TraceProcessorService] Registering external RPC: ${traceId} on port ${port}`);
        const now = new Date();
        // Create a trace info entry for this external connection
        const traceInfo = {
            id: traceId,
            filename: traceName,
            size: 0, // Unknown size for external traces
            uploadTime: now,
            lastAccessTime: now, // Set initial access time
            status: 'ready', // Assume it's ready since frontend is already connected
        };
        this.traces.set(traceId, traceInfo);
        // Create a proxy processor that uses the existing HTTP RPC connection
        const processor = await workingTraceProcessor_1.TraceProcessorFactory.createFromExternalRpc(traceId, port);
        this.processors.set(traceId, processor);
        console.log(`[TraceProcessorService] External RPC registered successfully: ${traceId}`);
        this.emit('trace-processed', traceInfo);
    }
    /**
     * Load trace from disk if it exists but is not in memory
     * This is useful after server restart when traces are on disk but not loaded
     */
    async loadTraceFromDisk(traceId) {
        // Already in memory
        if (this.traces.has(traceId)) {
            return this.traces.get(traceId);
        }
        // Check if metadata file exists
        const metadataPath = path_1.default.join(this.uploadDir, `${traceId}.json`);
        const tracePath = this.getTraceFilePath(traceId);
        if (!fs_1.default.existsSync(tracePath)) {
            console.log(`[TraceProcessorService] Trace file not found: ${tracePath}`);
            return undefined;
        }
        try {
            let traceInfo;
            // Try to load metadata from JSON file
            if (fs_1.default.existsSync(metadataPath)) {
                const metadataRaw = fs_1.default.readFileSync(metadataPath, 'utf8');
                const metadata = JSON.parse(metadataRaw);
                traceInfo = {
                    id: traceId,
                    filename: metadata.filename || `${traceId}.trace`,
                    size: metadata.size || fs_1.default.statSync(tracePath).size,
                    filePath: tracePath,
                    uploadTime: new Date(metadata.uploadedAt || Date.now()),
                    status: 'ready',
                    traceOs: metadata.traceOs,
                    traceFormat: metadata.traceFormat,
                    metadata: metadata.metadata,
                };
            }
            else {
                // Create basic metadata from trace file
                const stats = fs_1.default.statSync(tracePath);
                traceInfo = {
                    id: traceId,
                    filename: `${traceId}.trace`,
                    size: stats.size,
                    filePath: tracePath,
                    uploadTime: new Date(stats.mtime),
                    status: 'ready',
                };
            }
            // Detect trace format if not already known
            if (!traceInfo.traceOs) {
                try {
                    const { detectTraceFormat } = await Promise.resolve().then(() => __importStar(require('./traceFormatDetector')));
                    const formatInfo = await detectTraceFormat(tracePath);
                    traceInfo.traceOs = formatInfo.os;
                    traceInfo.traceFormat = formatInfo.format;
                    console.log(`[TraceProcessorService] Detected trace from disk: os=${formatInfo.os}, format=${formatInfo.format}`);
                }
                catch { /* default to unknown */ }
            }
            // Register in memory
            this.traces.set(traceId, traceInfo);
            // Create processor
            const processor = await this.createProcessor(traceId);
            this.processors.set(traceId, processor);
            console.log(`[TraceProcessorService] Loaded trace from disk: ${traceId}`);
            return traceInfo;
        }
        catch (error) {
            console.error(`[TraceProcessorService] Failed to load trace from disk:`, error.message);
            return undefined;
        }
    }
    /**
     * Get or load trace - checks memory first, then tries to load from disk
     */
    async getOrLoadTrace(traceId) {
        const trace = this.getTrace(traceId);
        if (trace) {
            return trace;
        }
        return this.loadTraceFromDisk(traceId);
    }
    async ensureProcessorForLease(traceId, leaseId, mode, leaseScope) {
        const key = this.processorKeyForLease(traceId, leaseId, mode);
        const existing = this.processors.get(key);
        if (existing && existing.status === 'ready') {
            this.touchTrace(traceId);
            return existing;
        }
        if (existing && existing.status === 'error') {
            return this.restartLeaseProcessor(traceId, {
                traceId,
                leaseId,
                mode,
                ...(leaseScope ? { leaseScope } : {}),
            });
        }
        const trace = this.traces.get(traceId);
        if (!trace) {
            throw new Error(`Trace ${traceId} not found`);
        }
        const filePath = this.getTraceFilePath(traceId);
        if (!fs_1.default.existsSync(filePath)) {
            throw new Error(`Trace file not found for lease ${leaseId}: ${filePath}`);
        }
        return this.createProcessor(traceId, { leaseId, mode });
    }
    async restartLease(traceId, leaseId, mode, leaseScope) {
        return this.restartLeaseProcessor(traceId, {
            traceId,
            leaseId,
            mode,
            leaseScope,
        });
    }
    async restartLeaseProcessor(traceId, leaseContext) {
        const processorKey = this.processorKeyForLease(traceId, leaseContext.leaseId, leaseContext.mode);
        const inProgress = this.leaseRestartInProgress.get(processorKey);
        if (inProgress) {
            console.log(`[TraceProcessorService] Waiting for lease supervisor restart of ${processorKey}`);
            return inProgress;
        }
        const restart = this.runLeaseRestartSupervisor(traceId, leaseContext, processorKey)
            .finally(() => {
            this.leaseRestartInProgress.delete(processorKey);
        });
        this.leaseRestartInProgress.set(processorKey, restart);
        return restart;
    }
    async runLeaseRestartSupervisor(traceId, leaseContext, processorKey) {
        const backoffMs = this.leaseRestartPolicy.backoffMs ?? DEFAULT_LEASE_RESTART_BACKOFF_MS;
        const attempts = Math.max(1, backoffMs.length);
        let lastError;
        console.warn(`[TraceProcessorService] Lease processor crashed; supervisor restarting ${processorKey}`);
        this.markLeaseCrashedForRestart(leaseContext);
        for (let attempt = 0; attempt < attempts; attempt++) {
            const delayMs = this.restartBackoffDelayMs(backoffMs, attempt);
            if (delayMs > 0) {
                await this.restartSleep(delayMs);
            }
            this.markLeaseRestarting(leaseContext);
            this.destroyProcessorForRestart(processorKey);
            try {
                const processor = await this.createProcessor(traceId, leaseContext);
                this.markLeaseReadyAfterRestart(leaseContext);
                console.log(`[TraceProcessorService] Lease processor restart succeeded for ${processorKey} ` +
                    `(attempt ${attempt + 1}/${attempts})`);
                return processor;
            }
            catch (error) {
                lastError = error;
                console.warn(`[TraceProcessorService] Lease processor restart failed for ${processorKey} ` +
                    `(attempt ${attempt + 1}/${attempts}): ${error?.message || error}`);
            }
        }
        this.markLeaseFailedAfterRestart(leaseContext);
        throw lastError instanceof Error ? lastError : new Error(String(lastError || 'Lease processor restart failed'));
    }
    restartBackoffDelayMs(backoffMs, attemptIndex) {
        const base = backoffMs[Math.min(attemptIndex, backoffMs.length - 1)] ?? 0;
        const jitterMs = this.leaseRestartPolicy.jitterMs ?? DEFAULT_LEASE_RESTART_JITTER_MS;
        if (base <= 0 || jitterMs <= 0)
            return Math.max(0, base);
        const random = this.leaseRestartPolicy.random ?? Math.random;
        return base + Math.floor(random() * jitterMs);
    }
    restartSleep(delayMs) {
        if (this.leaseRestartPolicy.sleep)
            return this.leaseRestartPolicy.sleep(delayMs);
        return new Promise(resolve => {
            const timer = setTimeout(resolve, delayMs);
            if (typeof timer.unref === 'function') {
                timer.unref();
            }
        });
    }
    destroyProcessorForRestart(processorKey) {
        const processor = this.processors.get(processorKey);
        this.processors.delete(processorKey);
        if (workingTraceProcessor_1.TraceProcessorFactory.remove(processorKey))
            return;
        try {
            processor?.destroy();
        }
        catch {
            // Best-effort cleanup before the supervisor creates the replacement.
        }
    }
    markLeaseCrashedForRestart(context) {
        if (!context.leaseScope)
            return;
        const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
        const lease = store.getLeaseById(context.leaseScope, context.leaseId);
        if (!lease)
            throw new Error(`Trace processor lease not found: ${context.leaseId}`);
        if (LEASE_RESTART_CONFLICT_STATES.has(lease.state)) {
            throw new Error(`Trace processor lease ${lease.id} is ${lease.state}`);
        }
        if (lease.state === 'crashed' || lease.state === 'restarting')
            return;
        store.markCrashed(context.leaseScope, lease.id);
    }
    markLeaseRestarting(context) {
        if (!context.leaseScope)
            return;
        const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
        const lease = store.getLeaseById(context.leaseScope, context.leaseId);
        if (!lease)
            throw new Error(`Trace processor lease not found: ${context.leaseId}`);
        if (lease.state === 'restarting')
            return;
        if (lease.state !== 'crashed')
            return;
        store.markRestarting(context.leaseScope, lease.id);
    }
    markLeaseReadyAfterRestart(context) {
        if (!context.leaseScope)
            return;
        (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)().markReady(context.leaseScope, context.leaseId);
    }
    markLeaseFailedAfterRestart(context) {
        if (!context.leaseScope)
            return;
        const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
        const lease = store.getLeaseById(context.leaseScope, context.leaseId);
        if (!lease || lease.state === 'failed' || lease.state === 'released')
            return;
        store.markFailed(context.leaseScope, context.leaseId);
    }
    /**
     * Get all traces
     */
    getAllTraces() {
        return Array.from(this.traces.values());
    }
    /**
     * Delete a trace
     */
    async deleteTrace(traceId) {
        const trace = this.traces.get(traceId);
        if (!trace)
            return;
        // Destroy all processors for this trace, including isolated lease processors.
        for (const [processorKey, processor] of Array.from(this.processors.entries())) {
            if (processor.traceId !== traceId)
                continue;
            workingTraceProcessor_1.TraceProcessorFactory.remove(processorKey);
            this.processors.delete(processorKey);
        }
        // Delete file
        const filePath = this.getTraceFilePath(traceId);
        if (fs_1.default.existsSync(filePath)) {
            fs_1.default.unlinkSync(filePath);
        }
        // Remove from memory
        this.traces.delete(traceId);
        this.emit('trace-deleted', traceId);
    }
    cleanupProcessorsForTraces(traceIds) {
        const traceIdSet = new Set(traceIds);
        let cleaned = 0;
        for (const [processorKey, processor] of Array.from(this.processors.entries())) {
            if (!traceIdSet.has(processor.traceId))
                continue;
            if (!workingTraceProcessor_1.TraceProcessorFactory.remove(processorKey)) {
                processor.destroy();
            }
            this.processors.delete(processorKey);
            cleaned++;
        }
        return cleaned;
    }
    /**
     * Load a trace directly from a file path (for CLI/testing use)
     * This copies the file to the upload directory and processes it
     */
    async loadTraceFromFilePath(filePath) {
        if (!fs_1.default.existsSync(filePath)) {
            throw new Error(`File not found: ${filePath}`);
        }
        const filename = path_1.default.basename(filePath);
        const stats = fs_1.default.statSync(filePath);
        const traceId = (0, uuid_1.uuidv4)();
        // Create trace info
        const traceInfo = {
            id: traceId,
            filename,
            size: stats.size,
            uploadTime: new Date(),
            status: 'processing',
        };
        this.traces.set(traceId, traceInfo);
        this.emit('trace-initialized', traceInfo);
        // Copy file to upload directory
        const destPath = this.getTraceFilePath(traceId);
        fs_1.default.copyFileSync(filePath, destPath);
        traceInfo.filePath = destPath;
        // Process the trace
        await this.processTrace(traceId);
        return traceId;
    }
    /**
     * Get file path for a trace.
     *
     * NOTE: public so that scene-reconstruction's traceHash + report-store
     * code paths can hash the on-disk content. Existence of the file at the
     * returned path is also the source of truth for "is this trace
     * file-backed or external RPC".
     */
    getTraceFilePath(traceId) {
        return this.traces.get(traceId)?.filePath || path_1.default.join(this.uploadDir, `${traceId}.trace`);
    }
    /**
     * Cleanup old traces with smart activity detection
     *
     * A trace is only cleaned up when BOTH conditions are met:
     * 1. Upload time exceeds maxAge (how old the trace is)
     * 2. Last access time exceeds idleTimeout (how long since last activity)
     *
     * This prevents active traces from being cleaned up prematurely while
     * still cleaning up truly abandoned traces.
     *
     * @param maxAge - Maximum age since upload before trace becomes eligible for cleanup (default: 2 hours)
     * @param idleTimeout - Minimum idle time (no queries) before trace can be cleaned (default: 30 minutes)
     */
    async cleanup(maxAge = 2 * 60 * 60 * 1000, // 2 hours since upload
    idleTimeout = 30 * 60 * 1000 // 30 minutes since last access
    ) {
        const now = Date.now();
        const tracesToDelete = [];
        const skippedDueToActivity = [];
        for (const [traceId, trace] of this.traces) {
            const uploadAge = now - trace.uploadTime.getTime();
            // Only consider traces older than maxAge
            if (uploadAge > maxAge) {
                // Check if trace has been accessed recently
                const lastAccess = trace.lastAccessTime?.getTime() ?? trace.uploadTime.getTime();
                const idleTime = now - lastAccess;
                if (idleTime > idleTimeout) {
                    // Trace is old AND idle - safe to clean up
                    tracesToDelete.push(traceId);
                }
                else {
                    // Trace is old but still active - skip cleanup
                    skippedDueToActivity.push(traceId);
                }
            }
        }
        if (skippedDueToActivity.length > 0) {
            console.log(`[TraceProcessorService] Skipping ${skippedDueToActivity.length} active trace(s) (recently accessed)`);
        }
        if (tracesToDelete.length > 0) {
            console.log(`[TraceProcessorService] Cleaning up ${tracesToDelete.length} old and idle trace(s)`);
            for (const traceId of tracesToDelete) {
                await this.deleteTrace(traceId);
            }
        }
        // Note: Don't call TraceProcessorFactory.cleanup() here as it would
        // destroy ALL processors, not just those for deleted traces.
        // The deleteTrace() method already handles processor cleanup for each trace.
    }
    /**
     * Check if a trace is considered active (recently accessed)
     */
    isTraceActive(traceId, idleTimeout = 30 * 60 * 1000) {
        const trace = this.traces.get(traceId);
        if (!trace)
            return false;
        const now = Date.now();
        const lastAccess = trace.lastAccessTime?.getTime() ?? trace.uploadTime.getTime();
        return (now - lastAccess) <= idleTimeout;
    }
}
exports.TraceProcessorService = TraceProcessorService;
function resolveDefaultTraceUploadDir() {
    const configured = process.env.SMARTPERFETTO_TRACE_UPLOAD_DIR?.trim();
    return configured || './uploads/traces';
}
// Singleton instance for sharing across route modules
let _singletonInstance = null;
function getTraceProcessorService() {
    if (!_singletonInstance) {
        _singletonInstance = new TraceProcessorService();
    }
    return _singletonInstance;
}
function setTraceProcessorServiceForTests(service) {
    _singletonInstance = service;
}
//# sourceMappingURL=traceProcessorService.js.map