"use strict";
/**
 * Resource Cleanup Module
 * Issue #404: Long-running server resource leak prevention
 *
 * Section 1: MCP orphaned process cleanup (ppid=1 detection)
 * Section 2: globalThis Map periodic cleanup (orphaned entries)
 * Orchestrator: initResourceCleanup / stopResourceCleanup
 *
 * Security:
 * - execFile() only (no exec()), per Issue #393 convention
 * - PID validation: Number.isInteger(pid) && pid > 0
 * - MCP_PROCESS_PATTERNS boundary match
 * - Container environment detection (skip in Docker)
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAX_PS_OUTPUT_BYTES = exports.MCP_PROCESS_PATTERNS = exports.CLEANUP_INTERVAL_MS = void 0;
exports.cleanupOrphanedMcpProcesses = cleanupOrphanedMcpProcesses;
exports.cleanupOrphanedMapEntries = cleanupOrphanedMapEntries;
exports.initResourceCleanup = initResourceCleanup;
exports.stopResourceCleanup = stopResourceCleanup;
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const auto_yes_manager_1 = require("./polling/auto-yes-manager");
const path_validator_1 = require("./security/path-validator");
const schedule_manager_1 = require("./schedule-manager");
const db_instance_1 = require("./db-instance");
const logger_1 = require("../lib/logger");
const logger = (0, logger_1.createLogger)('resource-cleanup');
// =============================================================================
// Constants
// =============================================================================
/** Cleanup interval: 24 hours */
exports.CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
/** Process name patterns for MCP orphaned process detection */
exports.MCP_PROCESS_PATTERNS = ['codex mcp-server', 'playwright-mcp'];
/** Maximum ps output buffer size (1MB) */
exports.MAX_PS_OUTPUT_BYTES = 1 * 1024 * 1024;
// =============================================================================
// Section 1: MCP Orphaned Process Cleanup
// =============================================================================
/**
 * Check if running in a container environment.
 * Detects Docker/container by checking /proc/1/cgroup existence.
 *
 * @returns true if in a container environment
 */
function isContainerEnvironment() {
    try {
        return (0, fs_1.existsSync)('/proc/1/cgroup');
    }
    catch {
        return false;
    }
}
/**
 * Check if a process command matches any MCP process pattern.
 * Uses case-insensitive substring match for reliability.
 *
 * @param command - Process command string from ps output
 * @returns true if command matches an MCP process pattern
 */
function matchesMcpPattern(command) {
    const lowerCommand = command.toLowerCase();
    return exports.MCP_PROCESS_PATTERNS.some(pattern => lowerCommand.includes(pattern.toLowerCase()));
}
/**
 * Clean up orphaned MCP processes (ppid=1).
 * Finds processes whose parent is init (ppid=1) and whose command
 * matches MCP_PROCESS_PATTERNS, then sends SIGTERM.
 *
 * Skips execution in container environments to avoid killing init processes.
 *
 * @returns Result with killed/failed PIDs
 */
async function cleanupOrphanedMcpProcesses() {
    const result = {
        killedPids: [],
        failedPids: [],
        skipped: false,
    };
    // Skip in container environments
    if (isContainerEnvironment()) {
        result.skipped = true;
        result.reason = 'container';
        return result;
    }
    return new Promise((resolve) => {
        (0, child_process_1.execFile)('ps', ['-eo', 'pid,ppid,command'], { maxBuffer: exports.MAX_PS_OUTPUT_BYTES }, (error, stdout) => {
            if (error) {
                logger.warn('failed-to-list-processes:', { error: error instanceof Error ? error.message : String(error) });
                resolve(result);
                return;
            }
            const lines = stdout.split('\n');
            // Skip header line
            for (let i = 1; i < lines.length; i++) {
                const line = lines[i].trim();
                if (!line)
                    continue;
                // Parse: PID  PPID  COMMAND
                const parts = line.split(/\s+/);
                if (parts.length < 3)
                    continue;
                const pid = parseInt(parts[0], 10);
                const ppid = parseInt(parts[1], 10);
                const command = parts.slice(2).join(' ');
                // Validate PID
                if (!Number.isInteger(pid) || pid <= 0)
                    continue;
                if (!Number.isInteger(ppid))
                    continue;
                // Only target orphaned processes (ppid=1) matching MCP patterns
                if (ppid !== 1)
                    continue;
                if (!matchesMcpPattern(command))
                    continue;
                try {
                    process.kill(pid, 'SIGTERM');
                    result.killedPids.push(pid);
                    logger.info('killed-orphaned-mcp-process:pidpid-comma');
                }
                catch (killError) {
                    result.failedPids.push(pid);
                    logger.warn('failed-to-kill-pidpid:', { error: killError instanceof Error ? killError.message : String(killError) });
                }
            }
            if (result.killedPids.length > 0) {
                logger.info('cleaned-up-resultkilledpidslength');
            }
            resolve(result);
        });
    });
}
// =============================================================================
// Section 2: globalThis Map Periodic Cleanup
// =============================================================================
/**
 * Get all valid worktree IDs from the database.
 *
 * @returns Set of valid worktree IDs
 */
function getDbWorktreeIds() {
    try {
        const db = (0, db_instance_1.getDbInstance)();
        const rows = db.prepare('SELECT id FROM worktrees').all();
        return new Set(rows.map(r => r.id));
    }
    catch (error) {
        logger.warn('failed-to-query-worktrees-from-db:', { error: error instanceof Error ? error.message : String(error) });
        return new Set();
    }
}
/**
 * Clean up orphaned entries from globalThis Maps.
 * Compares Map keys against DB worktree IDs and removes entries
 * for worktrees that no longer exist.
 *
 * Uses better-sqlite3's synchronous API, so the DB query and Map
 * mutation happen in the same synchronous block (no race conditions).
 *
 * @returns Result with deleted entry IDs
 */
function cleanupOrphanedMapEntries() {
    const result = {
        deletedAutoYesStateIds: [],
        deletedAutoYesPollerIds: [],
        deletedScheduleWorktreeIds: [],
    };
    const validWorktreeIds = getDbWorktreeIds();
    if (validWorktreeIds.size === 0) {
        // If DB query returned nothing, skip cleanup to avoid false positives
        return result;
    }
    // Cleanup autoYesStates (Issue #525: composite keys)
    const autoYesStateKeys = (0, auto_yes_manager_1.getAutoYesStateCompositeKeys)();
    for (const compositeKey of autoYesStateKeys) {
        const worktreeId = (0, auto_yes_manager_1.extractWorktreeId)(compositeKey);
        // [SEC4-MF-001] Validate extracted worktreeId
        if (!(0, path_validator_1.isValidWorktreeId)(worktreeId)) {
            (0, auto_yes_manager_1.deleteAutoYesState)(compositeKey);
            result.deletedAutoYesStateIds.push(compositeKey);
            continue;
        }
        if (!validWorktreeIds.has(worktreeId)) {
            (0, auto_yes_manager_1.deleteAutoYesState)(compositeKey);
            result.deletedAutoYesStateIds.push(compositeKey);
        }
    }
    // Cleanup autoYesPollerStates (Issue #525: composite keys)
    const autoYesPollerKeys = (0, auto_yes_manager_1.getAutoYesPollerCompositeKeys)();
    for (const compositeKey of autoYesPollerKeys) {
        const worktreeId = (0, auto_yes_manager_1.extractWorktreeId)(compositeKey);
        if (!(0, path_validator_1.isValidWorktreeId)(worktreeId) || !validWorktreeIds.has(worktreeId)) {
            (0, auto_yes_manager_1.stopAutoYesPolling)(compositeKey);
            result.deletedAutoYesPollerIds.push(compositeKey);
        }
    }
    // Cleanup schedule manager entries (via encapsulated accessor)
    const scheduleWorktreeIds = (0, schedule_manager_1.getScheduleWorktreeIds)();
    for (const worktreeId of scheduleWorktreeIds) {
        if (!validWorktreeIds.has(worktreeId)) {
            (0, schedule_manager_1.stopScheduleForWorktree)(worktreeId);
            result.deletedScheduleWorktreeIds.push(worktreeId);
        }
    }
    const totalDeleted = result.deletedAutoYesStateIds.length +
        result.deletedAutoYesPollerIds.length +
        result.deletedScheduleWorktreeIds.length;
    if (totalDeleted > 0) {
        logger.info('cleanup:orphaned-map-entries', {
            totalDeleted,
            autoYesStates: result.deletedAutoYesStateIds.length,
            autoYesPollers: result.deletedAutoYesPollerIds.length,
            schedules: result.deletedScheduleWorktreeIds.length,
        });
    }
    return result;
}
// =============================================================================
// Orchestrator
// =============================================================================
/**
 * Run the complete cleanup cycle.
 * Executes both MCP process cleanup and Map entry cleanup.
 */
async function runCleanupCycle() {
    try {
        await cleanupOrphanedMcpProcesses();
    }
    catch (error) {
        logger.warn('mcp-cleanup-error:', { error: error instanceof Error ? error.message : String(error) });
    }
    try {
        cleanupOrphanedMapEntries();
    }
    catch (error) {
        logger.warn('map-cleanup-error:', { error: error instanceof Error ? error.message : String(error) });
    }
}
/**
 * Initialize resource cleanup.
 * Starts a periodic timer that runs the cleanup cycle every CLEANUP_INTERVAL_MS.
 * Prevents duplicate timers via globalThis.__resourceCleanupTimerId check.
 */
function initResourceCleanup() {
    if (globalThis.__resourceCleanupTimerId !== undefined) {
        logger.info('already-initialized-skipping');
        return;
    }
    // Run initial cleanup
    void runCleanupCycle();
    // Start periodic timer
    globalThis.__resourceCleanupTimerId = setInterval(() => {
        void runCleanupCycle();
    }, exports.CLEANUP_INTERVAL_MS);
    logger.info('initialized-interval:24h');
}
/**
 * Stop resource cleanup.
 * Clears the periodic timer.
 */
function stopResourceCleanup() {
    if (globalThis.__resourceCleanupTimerId !== undefined) {
        clearInterval(globalThis.__resourceCleanupTimerId);
        globalThis.__resourceCleanupTimerId = undefined;
        logger.info('stopped');
    }
}
