"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.PortPool = void 0;
exports.getPortPool = getPortPool;
exports.resetPortPool = resetPortPool;
/**
 * Port Pool Manager
 *
 * Manages a pool of ports for trace_processor_shell HTTP servers.
 * Ensures ports are properly allocated and released.
 */
const events_1 = require("events");
const config_1 = require("../config");
const IS_TEST_ENV = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
class PortPool extends events_1.EventEmitter {
    constructor(minPort = config_1.traceProcessorConfig.portRange.min, maxPort = config_1.traceProcessorConfig.portRange.max) {
        super();
        this.minPort = minPort;
        this.maxPort = maxPort;
        this.availablePorts = new Set();
        this.blockedPorts = new Set();
        this.allocations = new Map();
        this.portToTraceId = new Map();
        // Initialize all ports as available
        for (let port = minPort; port <= maxPort; port++) {
            this.availablePorts.add(port);
        }
        if (!IS_TEST_ENV) {
            console.log(`[PortPool] Initialized with ${this.availablePorts.size} ports (${minPort}-${maxPort})`);
        }
    }
    /**
     * Allocate a port for a trace
     * @param traceId The trace ID requesting a port
     * @returns The allocated port number
     * @throws Error if no ports are available
     */
    allocate(traceId) {
        // Check if this trace already has a port
        const existing = this.allocations.get(traceId);
        if (existing) {
            if (!IS_TEST_ENV) {
                console.log(`[PortPool] Trace ${traceId} already has port ${existing.port}`);
            }
            return existing.port;
        }
        // Get the first available port
        const port = this.getNextAvailablePort();
        if (port === null) {
            throw new Error(`No available ports in pool (${this.minPort}-${this.maxPort}). Active allocations: ${this.allocations.size}`);
        }
        // Mark port as allocated
        this.availablePorts.delete(port);
        const allocation = {
            port,
            traceId,
            allocatedAt: new Date(),
        };
        this.allocations.set(traceId, allocation);
        this.portToTraceId.set(port, traceId);
        if (!IS_TEST_ENV) {
            console.log(`[PortPool] Allocated port ${port} to trace ${traceId} (${this.availablePorts.size} available)`);
        }
        this.emit('allocated', { port, traceId });
        return port;
    }
    /**
     * Release a port back to the pool
     * @param traceId The trace ID releasing the port
     * @returns true if port was released, false if trace had no port
     */
    release(traceId) {
        const allocation = this.allocations.get(traceId);
        if (!allocation) {
            if (!IS_TEST_ENV) {
                console.log(`[PortPool] Trace ${traceId} has no port to release`);
            }
            return false;
        }
        const port = allocation.port;
        // Return port to available pool
        this.allocations.delete(traceId);
        this.portToTraceId.delete(port);
        // If a port is known-bad, keep it blocked even after release.
        if (!this.blockedPorts.has(port)) {
            this.availablePorts.add(port);
        }
        if (!IS_TEST_ENV) {
            console.log(`[PortPool] Released port ${port} from trace ${traceId} (${this.availablePorts.size} available)`);
        }
        this.emit('released', { port, traceId });
        return true;
    }
    /**
     * Release a port by port number (useful when trace ID is unknown)
     * @param port The port number to release
     * @returns true if port was released
     */
    releaseByPort(port) {
        const traceId = this.portToTraceId.get(port);
        if (!traceId) {
            // Port might not be tracked, just add it back to available
            if (port >= this.minPort && port <= this.maxPort && !this.availablePorts.has(port) && !this.blockedPorts.has(port)) {
                this.availablePorts.add(port);
                if (!IS_TEST_ENV) {
                    console.log(`[PortPool] Force-released untracked port ${port}`);
                }
                return true;
            }
            return false;
        }
        return this.release(traceId);
    }
    /**
     * Mark a port as unusable for the remainder of this process.
     * This is used when trace_processor_shell reports "Address already in use".
     */
    blockPort(port) {
        if (port < this.minPort || port > this.maxPort)
            return;
        // If the port is currently allocated, release it first (will try to add back, but we will keep it blocked).
        const traceId = this.portToTraceId.get(port);
        if (traceId) {
            this.release(traceId);
        }
        this.availablePorts.delete(port);
        this.blockedPorts.add(port);
        console.log(`[PortPool] Blocked port ${port} (marked unusable)`);
        this.emit('blocked', { port });
    }
    /**
     * Get the port allocated to a trace
     * @param traceId The trace ID
     * @returns The port number or null if not allocated
     */
    getPort(traceId) {
        const allocation = this.allocations.get(traceId);
        return allocation ? allocation.port : null;
    }
    /**
     * Check if a port is available
     * @param port The port to check
     * @returns true if the port is available
     */
    isAvailable(port) {
        return this.availablePorts.has(port);
    }
    /**
     * Get the next available port
     * @returns The next available port or null if none available
     */
    getNextAvailablePort() {
        if (this.availablePorts.size === 0) {
            return null;
        }
        // Get the smallest available port for predictability
        return Math.min(...this.availablePorts);
    }
    /**
     * Get pool statistics
     */
    getStats() {
        return {
            total: this.maxPort - this.minPort + 1,
            available: this.availablePorts.size,
            allocated: this.allocations.size,
            blocked: this.blockedPorts.size,
            allocations: Array.from(this.allocations.values()),
        };
    }
    /**
     * Release all ports (cleanup)
     */
    releaseAll() {
        const traceIds = Array.from(this.allocations.keys());
        for (const traceId of traceIds) {
            this.release(traceId);
        }
        console.log(`[PortPool] Released all ports`);
    }
    /**
     * Force cleanup stale allocations (ports allocated more than maxAge ago)
     * @param maxAgeMs Maximum age in milliseconds
     * @returns Number of stale allocations cleaned
     */
    cleanupStale(maxAgeMs = config_1.traceProcessorConfig.staleAllocationMaxAgeMs) {
        const now = Date.now();
        let cleaned = 0;
        for (const [traceId, allocation] of this.allocations) {
            const age = now - allocation.allocatedAt.getTime();
            if (age > maxAgeMs) {
                console.log(`[PortPool] Cleaning stale allocation: port ${allocation.port} for trace ${traceId} (age: ${Math.round(age / 1000)}s)`);
                this.release(traceId);
                cleaned++;
            }
        }
        if (cleaned > 0) {
            console.log(`[PortPool] Cleaned ${cleaned} stale allocations`);
        }
        return cleaned;
    }
}
exports.PortPool = PortPool;
// Singleton instance
let portPoolInstance = null;
function getPortPool() {
    if (!portPoolInstance) {
        portPoolInstance = new PortPool();
    }
    return portPoolInstance;
}
function resetPortPool() {
    if (portPoolInstance) {
        portPoolInstance.releaseAll();
    }
    portPoolInstance = new PortPool();
}
//# sourceMappingURL=portPool.js.map