/**
 * Agent Process Manager
 *
 * Core lifecycle manager for agent processes on the bridge machine.
 * Provides comprehensive process management including creation, discovery,
 * monitoring, termination, and orphan reclamation capabilities.
 */
import { execSync } from 'node:child_process';
import os from 'node:os';
import { createLogger } from '../utils/logger.js';
import { getProcessRegistry } from './process-registry.js';
import { isProcessRunning, findClaudeCodeProcesses, findOrphanProcessByAgentId, terminateProcessTree, waitForProcessExit, checkProcessHealth, } from './process-detector.js';
import { loadPersistedSessions } from './terminal-persistence.js';
const log = createLogger('agent-process-manager');
/**
 * Agent Process Manager
 *
 * Manages the complete lifecycle of agent processes including:
 * - Creation: Register and verify new processes
 * - Discovery: Scan for managed, orphan, and unknown processes
 * - Monitoring: Health checks and heartbeat tracking
 * - Termination: Graceful stop with escalation strategy
 * - Orphan Reclamation: Adopt or terminate orphan processes
 */
export class AgentProcessManager {
    /**
     * Private constructor - use getAgentProcessManager() for singleton access
     */
    constructor() {
        this.registry = getProcessRegistry();
    }
    /**
     * Get the singleton instance
     */
    static getInstance() {
        if (AgentProcessManager.instance === null) {
            AgentProcessManager.instance = new AgentProcessManager();
        }
        return AgentProcessManager.instance;
    }
    // =============================================================================
    // CREATION PHASE
    // =============================================================================
    /**
     * Start a new process and register it in the process registry
     *
     * Flow:
     * 1. Register process with state='creating'
     * 2. Verify it's running
     * 3. Update state to 'running'
     *
     * @param config - Process configuration
     * @throws Error if process registration fails or process is not running
     */
    async startProcess(config) {
        const { agentId, sessionId, pid, mode, workspacePath, command } = config;
        log.info(`[startProcess] Starting process: agentId=${agentId}, pid=${pid}, mode=${mode}`);
        // Step 1: Register with 'creating' state
        await this.registry.register({
            agentId,
            sessionId,
            pid,
            mode,
            state: 'creating',
            workspacePath,
            command,
        });
        // Step 2: Verify process is running
        log.debug(`[startProcess] Verifying process is running: pid=${pid}`);
        const isRunning = isProcessRunning(pid);
        if (!isRunning) {
            log.error(`[startProcess] Process not running after registration: agentId=${agentId}, pid=${pid}`);
            // Mark as terminated since it's not actually running
            await this.registry.markTerminated(agentId);
            throw new Error(`Process ${pid} is not running`);
        }
        // Step 3: Update state to 'running'
        await this.registry.update(agentId, { state: 'running' });
        await this.registry.updateHeartbeat(agentId);
        log.info(`[startProcess] Process started successfully: agentId=${agentId}, pid=${pid}`);
    }
    // =============================================================================
    // DISCOVERY PHASE
    // =============================================================================
    /**
     * Discover all agent processes on the system
     *
     * Scans OS for all agent processes, compares with registry, and returns
     * a list of discovered processes with their management status.
     *
     * @returns Array of discovered processes with management status
     */
    async discoverAllProcesses() {
        log.debug('[discoverAllProcesses] Scanning for all agent processes');
        // Get all processes from OS
        const osProcesses = findClaudeCodeProcesses();
        const managedProcesses = this.registry.getAll();
        const discovered = [];
        // Create a set of managed PIDs for quick lookup
        const managedPids = new Set(managedProcesses.map(p => p.pid));
        const managedAgentIds = new Set(managedProcesses.map(p => p.agentId));
        // Check each OS process
        for (const proc of osProcesses) {
            const isManagedByPid = managedPids.has(proc.pid);
            const isManagedByAgentId = proc.agentId && managedAgentIds.has(proc.agentId);
            if (isManagedByPid || isManagedByAgentId) {
                // Process is managed - find its record
                const record = managedProcesses.find(p => p.pid === proc.pid || p.agentId === proc.agentId);
                discovered.push({
                    ...proc,
                    managementStatus: 'managed',
                    sessionId: record?.sessionId,
                });
            }
            else if (proc.agentId) {
                // Process has agentId but not in registry - it's an orphan
                discovered.push({
                    ...proc,
                    managementStatus: 'orphan',
                });
            }
            else {
                // Unknown agent process
                discovered.push({
                    ...proc,
                    managementStatus: 'unknown',
                });
            }
        }
        // Also check registry processes that might not be in OS scan
        for (const record of managedProcesses) {
            const found = discovered.find(p => p.pid === record.pid);
            if (!found) {
                // Process is in registry but not found in OS scan
                const isAlive = isProcessRunning(record.pid);
                discovered.push({
                    pid: record.pid,
                    command: record.command,
                    agentId: record.agentId,
                    managementStatus: isAlive ? 'managed' : 'orphan',
                    sessionId: record.sessionId,
                });
            }
        }
        log.debug(`[discoverAllProcesses] Discovered ${discovered.length} processes: ` +
            `${discovered.filter(p => p.managementStatus === 'managed').length} managed, ` +
            `${discovered.filter(p => p.managementStatus === 'orphan').length} orphans, ` +
            `${discovered.filter(p => p.managementStatus === 'unknown').length} unknown`);
        return discovered;
    }
    // =============================================================================
    // MONITORING PHASE
    // =============================================================================
    /**
     * Check health of all registered processes
     *
     * Updates heartbeat and health status in registry for each managed process.
     *
     * @returns Map of agentId to health status
     */
    async healthCheckAll() {
        log.debug('[healthCheckAll] Starting health check for all processes');
        const results = new Map();
        const managedProcesses = this.registry.getAll();
        const promises = managedProcesses.map(async (record) => {
            const health = await this.healthCheck(record.agentId);
            results.set(record.agentId, health);
            return health;
        });
        await Promise.all(promises);
        log.debug(`[healthCheckAll] Completed health check for ${managedProcesses.length} processes`);
        return results;
    }
    /**
     * Check health of a single process
     *
     * @param agentId - Agent ID to check
     * @returns Health status of the process
     */
    async healthCheck(agentId) {
        const record = this.registry.get(agentId);
        if (!record) {
            log.warn(`[healthCheck] Process not found in registry: agentId=${agentId}`);
            return {
                pid: -1,
                agentId,
                alive: false,
                cpu: 0,
                memoryMB: 0,
                status: 'dead',
                lastUpdate: new Date(),
            };
        }
        // Check process health
        const health = await checkProcessHealth(record.pid, agentId);
        // Update registry with health info
        const healthStatus = {
            cpu: health.cpu,
            memoryMB: health.memoryMB,
            status: health.status,
        };
        await this.registry.updateHealth(agentId, healthStatus);
        if (health.alive) {
            await this.registry.updateHeartbeat(agentId);
        }
        else if (record.state !== 'terminated' && record.state !== 'orphaned') {
            // Process died unexpectedly - mark as orphaned
            log.warn(`[healthCheck] Process died unexpectedly: agentId=${agentId}, pid=${record.pid}`);
            await this.registry.markOrphaned(agentId);
        }
        return health;
    }
    // =============================================================================
    // TERMINATION PHASE (with escalation strategy)
    // =============================================================================
    /**
     * Stop a process with graceful escalation strategy
     *
     * Escalation levels:
     * Level 1: SIGTERM → wait 3s
     * Level 2: taskkill /T /F (Win) or kill -9 (Unix) → wait 3s
     * Level 3: terminateProcessTree → wait 5s
     * Level 4: mark as "unstoppable", return failure
     *
     * @param agentId - Agent ID to stop
     * @returns Termination result with details
     */
    async stopProcess(agentId) {
        const record = this.registry.get(agentId);
        if (!record) {
            return {
                success: false,
                agentId,
                pid: -1,
                level: 0,
                terminated: 0,
                failed: 0,
                message: `Process not found in registry: ${agentId}`,
            };
        }
        const { pid } = record;
        log.info(`[stopProcess] Stopping process: agentId=${agentId}, pid=${pid}`);
        // Check if already stopped
        if (!isProcessRunning(pid)) {
            log.info(`[stopProcess] Process already stopped: agentId=${agentId}, pid=${pid}`);
            await this.registry.markTerminated(agentId);
            return {
                success: true,
                agentId,
                pid,
                level: 0,
                terminated: 1,
                failed: 0,
                message: 'Process was already stopped',
            };
        }
        // Update state to 'stopping'
        await this.registry.update(agentId, { state: 'stopping' });
        // Level 1: SIGTERM
        log.debug(`[stopProcess] Level 1: Sending SIGTERM to pid=${pid}`);
        const sigtermResult = this.sendSigterm(pid);
        if (sigtermResult) {
            const exited = await waitForProcessExit(pid, 3000);
            if (exited) {
                await this.registry.markTerminated(agentId);
                log.info(`[stopProcess] Process stopped at Level 1: agentId=${agentId}, pid=${pid}`);
                return {
                    success: true,
                    agentId,
                    pid,
                    level: 1,
                    terminated: 1,
                    failed: 0,
                    message: 'Process terminated with SIGTERM',
                };
            }
        }
        // Level 2: Force kill (taskkill /F or kill -9)
        log.debug(`[stopProcess] Level 2: Force killing pid=${pid}`);
        const forceKillResult = this.sendForceKill(pid);
        if (forceKillResult) {
            const exited = await waitForProcessExit(pid, 3000);
            if (exited) {
                await this.registry.markTerminated(agentId);
                log.info(`[stopProcess] Process stopped at Level 2: agentId=${agentId}, pid=${pid}`);
                return {
                    success: true,
                    agentId,
                    pid,
                    level: 2,
                    terminated: 1,
                    failed: 0,
                    message: 'Process terminated with force kill',
                };
            }
        }
        // Level 3: Terminate process tree
        log.debug(`[stopProcess] Level 3: Terminating process tree for pid=${pid}`);
        const treeResult = terminateProcessTree(pid);
        // Wait for process tree to terminate
        const treeExited = await waitForProcessExit(pid, 5000);
        if (treeExited) {
            await this.registry.markTerminated(agentId);
            log.info(`[stopProcess] Process stopped at Level 3: agentId=${agentId}, pid=${pid}, terminated=${treeResult.terminated}, failed=${treeResult.failed}`);
            return {
                success: true,
                agentId,
                pid,
                level: 3,
                terminated: treeResult.terminated,
                failed: treeResult.failed,
                message: `Process tree terminated (${treeResult.terminated} processes)`,
            };
        }
        // Level 4: Process is unstoppable
        log.error(`[stopProcess] Level 4: Process is unstoppable: agentId=${agentId}, pid=${pid}`);
        // Check final state
        const finalStatus = isProcessRunning(pid) ? 'still running' : 'terminated';
        if (finalStatus === 'terminated') {
            await this.registry.markTerminated(agentId);
            return {
                success: true,
                agentId,
                pid,
                level: 4,
                terminated: 1,
                failed: 0,
                message: 'Process eventually terminated after all escalation levels',
            };
        }
        return {
            success: false,
            agentId,
            pid,
            level: 4,
            terminated: treeResult.terminated,
            failed: treeResult.failed + 1,
            message: 'Process could not be terminated - marked as unstoppable',
        };
    }
    /**
     * Send SIGTERM to a process
     */
    sendSigterm(pid) {
        try {
            process.kill(pid, 'SIGTERM');
            return true;
        }
        catch (error) {
            log.debug(`[sendSigterm] Failed to send SIGTERM to ${pid}:`, error);
            return false;
        }
    }
    /**
     * Send force kill (SIGKILL on Unix, taskkill /F on Windows)
     */
    sendForceKill(pid) {
        try {
            if (os.platform() === 'win32') {
                execSync(`taskkill /PID ${pid} /F`, { encoding: 'utf8', timeout: 5000, windowsHide: true });
            }
            else {
                process.kill(pid, 'SIGKILL');
            }
            return true;
        }
        catch (error) {
            log.debug(`[sendForceKill] Failed to force kill ${pid}:`, error);
            return false;
        }
    }
    /**
     * Force kill a process immediately using terminateProcessTree
     *
     * This bypasses the graceful escalation and uses the most aggressive
     * termination method immediately.
     *
     * @param agentId - Agent ID to force kill
     * @returns Termination result
     */
    async forceKill(agentId) {
        const record = this.registry.get(agentId);
        if (!record) {
            return {
                success: false,
                agentId,
                pid: -1,
                level: 0,
                terminated: 0,
                failed: 0,
                message: `Process not found in registry: ${agentId}`,
            };
        }
        const { pid } = record;
        log.info(`[forceKill] Force killing process: agentId=${agentId}, pid=${pid}`);
        // Update state
        await this.registry.update(agentId, { state: 'stopping' });
        // Direct process tree termination
        const result = terminateProcessTree(pid);
        // Wait briefly for termination
        const exited = await waitForProcessExit(pid, 2000);
        if (exited) {
            await this.registry.markTerminated(agentId);
            log.info(`[forceKill] Process terminated: agentId=${agentId}, pid=${pid}`);
            return {
                success: true,
                agentId,
                pid,
                level: 3, // Same level as tree termination
                terminated: result.terminated,
                failed: result.failed,
                message: `Process force killed (${result.terminated} processes terminated)`,
            };
        }
        log.error(`[forceKill] Process could not be force killed: agentId=${agentId}, pid=${pid}`);
        return {
            success: false,
            agentId,
            pid,
            level: 3,
            terminated: result.terminated,
            failed: result.failed + 1,
            message: 'Force kill failed - process may require manual intervention',
        };
    }
    // =============================================================================
    // ORPHAN RECLAMATION
    // =============================================================================
    /**
     * Reclaim an orphan process by agentId
     *
     * Finds an orphan process by agentId, and either:
     * - Adopts it into the registry (if found and not managed)
     * - Terminates it (if adoption fails or process is problematic)
     *
     * @param agentId - Agent ID to find and reclaim
     * @returns Reclamation result
     */
    async reclaimOrphan(agentId) {
        log.info(`[reclaimOrphan] Attempting to reclaim orphan: agentId=${agentId}`);
        // Check if already managed
        if (this.registry.isManaged(agentId)) {
            const record = this.registry.get(agentId);
            if (!record) {
                return { success: false, pid: -1, action: 'failed' };
            }
            log.info(`[reclaimOrphan] Process already managed: agentId=${agentId}, pid=${record.pid}`);
            return {
                success: true,
                pid: record.pid,
                action: 'adopted',
            };
        }
        // Find orphan process
        const orphan = findOrphanProcessByAgentId(agentId);
        if (!orphan) {
            log.warn(`[reclaimOrphan] No orphan process found: agentId=${agentId}`);
            return {
                success: false,
                pid: -1,
                action: 'failed',
            };
        }
        // Verify the process is still running
        if (!isProcessRunning(orphan.pid)) {
            log.warn(`[reclaimOrphan] Orphan process not running: agentId=${agentId}, pid=${orphan.pid}`);
            return {
                success: false,
                pid: orphan.pid,
                action: 'failed',
            };
        }
        // Check if this PID is already managed by a different agent
        if (this.registry.isManagedByPid(orphan.pid)) {
            const existingRecord = this.registry.getByPid(orphan.pid);
            if (!existingRecord) {
                // Race condition - PID was just removed, proceed with adoption inline
                try {
                    const sessions = await loadPersistedSessions();
                    const session = sessions.find(s => s.agentId === agentId);
                    await this.registry.register({
                        agentId,
                        sessionId: session?.sessionId || `reclaimed-${Date.now()}`,
                        pid: orphan.pid,
                        mode: 'sdk',
                        state: 'running',
                        workspacePath: session?.workspacePath || orphan.cwd || '',
                        command: orphan.command || session?.command || '',
                        metadata: { reclaimedAt: new Date().toISOString(), wasOrphan: true },
                    });
                    return { success: true, pid: orphan.pid, action: 'adopted' };
                }
                catch {
                    terminateProcessTree(orphan.pid);
                    return { success: false, pid: orphan.pid, action: 'terminated' };
                }
            }
            log.warn(`[reclaimOrphan] PID already managed by different agent: pid=${orphan.pid}, existingAgentId=${existingRecord.agentId}`);
            // If it's the same agentId, just update - this shouldn't happen but handle it
            if (existingRecord.agentId === agentId) {
                return {
                    success: true,
                    pid: orphan.pid,
                    action: 'adopted',
                };
            }
            // Different agent - this is a conflict, terminate the orphan
            log.warn(`[reclaimOrphan] Terminating conflicting orphan: agentId=${agentId}, pid=${orphan.pid}`);
            terminateProcessTree(orphan.pid);
            return {
                success: true,
                pid: orphan.pid,
                action: 'terminated',
            };
        }
        // Try to adopt the orphan - load persisted sessions for metadata
        const sessions = await loadPersistedSessions();
        const session = sessions.find(s => s.agentId === agentId);
        try {
            await this.registry.register({
                agentId,
                sessionId: session?.sessionId || `reclaimed-${Date.now()}`,
                pid: orphan.pid,
                mode: 'sdk',
                state: 'running',
                workspacePath: session?.workspacePath || orphan.cwd || '',
                command: orphan.command || session?.command || '',
                metadata: {
                    reclaimedAt: new Date().toISOString(),
                    wasOrphan: true,
                },
            });
            log.info(`[reclaimOrphan] Successfully adopted orphan: agentId=${agentId}, pid=${orphan.pid}`);
            return {
                success: true,
                pid: orphan.pid,
                action: 'adopted',
            };
        }
        catch (error) {
            log.error(`[reclaimOrphan] Failed to adopt orphan: agentId=${agentId}, pid=${orphan.pid}`, error);
            // If adoption fails, terminate the process
            terminateProcessTree(orphan.pid);
            return {
                success: false,
                pid: orphan.pid,
                action: 'terminated',
            };
        }
    }
    /**
     * Scan all orphans and attempt to reclaim each
     *
     * @returns Array of reclamation results
     */
    async scanAndReclaimAll() {
        log.info('[scanAndReclaimAll] Scanning for all orphan processes');
        const results = [];
        // Load persisted sessions to find potential orphans
        const sessions = await loadPersistedSessions();
        // Find all agent processes
        const allProcesses = findClaudeCodeProcesses();
        const managedPids = new Set(this.registry.getAll().map(p => p.pid));
        // Identify orphan processes
        const orphans = allProcesses.filter(proc => {
            // Not managed by PID
            if (managedPids.has(proc.pid))
                return false;
            // Has agentId but not managed - this is an orphan
            if (proc.agentId && !this.registry.isManaged(proc.agentId)) {
                return true;
            }
            return false;
        });
        log.info(`[scanAndReclaimAll] Found ${orphans.length} orphan processes`);
        // Try to reclaim each orphan
        for (const orphan of orphans) {
            if (!orphan.agentId) {
                // No agentId - can't reclaim, just terminate
                log.warn(`[scanAndReclaimAll] Orphan without agentId, terminating: pid=${orphan.pid}`);
                terminateProcessTree(orphan.pid);
                results.push({
                    agentId: 'unknown',
                    pid: orphan.pid,
                    action: 'terminated',
                });
                continue;
            }
            const result = await this.reclaimOrphan(orphan.agentId);
            results.push({
                agentId: orphan.agentId,
                pid: result.pid,
                action: result.action,
            });
        }
        // Also check persisted sessions for orphans that might not be in the process list
        for (const session of sessions) {
            if (session.agentId && !this.registry.isManaged(session.agentId)) {
                const orphanByAgentId = findOrphanProcessByAgentId(session.agentId);
                if (orphanByAgentId) {
                    const result = await this.reclaimOrphan(session.agentId);
                    // Avoid duplicates
                    const existing = results.find(r => r.agentId === session.agentId);
                    if (!existing) {
                        results.push({
                            agentId: session.agentId,
                            pid: result.pid,
                            action: result.action,
                        });
                    }
                }
            }
        }
        log.info(`[scanAndReclaimAll] Reclamation complete: ${results.length} processes processed`);
        return results;
    }
    // =============================================================================
    // REGISTRY INTEGRATION
    // =============================================================================
    /**
     * Mark a process as orphaned in the registry
     *
     * @param agentId - Agent ID to mark
     * @returns Whether the operation succeeded
     */
    async markOrphaned(agentId) {
        log.info(`[markOrphaned] Marking process as orphaned: agentId=${agentId}`);
        return await this.registry.markOrphaned(agentId);
    }
    /**
     * Check if a process is managed
     *
     * @param agentId - Agent ID to check
     * @returns Whether the process is managed
     */
    isManaged(agentId) {
        return this.registry.isManaged(agentId);
    }
    /**
     * Get all managed processes
     *
     * @returns Array of all managed process records
     */
    getAllManagedProcesses() {
        return this.registry.getAll();
    }
    /**
     * Get managed processes by state
     *
     * @param state - Process state to filter by
     * @returns Array of process records matching the state
     */
    getManagedProcessesByState(state) {
        return this.registry.getByState(state);
    }
    /**
     * Find processes running on the machine that are NOT in registry
     *
     * @returns Array of unmanaged process info
     */
    async getUnmanagedProcesses() {
        const allProcesses = findClaudeCodeProcesses();
        const managedPids = new Set(this.registry.getAll().map(p => p.pid));
        const managedAgentIds = new Set(this.registry.getAll().map(p => p.agentId));
        return allProcesses.filter(proc => {
            // Not managed by PID
            if (managedPids.has(proc.pid))
                return false;
            // Not managed by agentId
            if (proc.agentId && managedAgentIds.has(proc.agentId))
                return false;
            return true;
        });
    }
    /**
     * Rebuild the process registry from persisted sessions (e.g. on startup)
     *
     * Scans all persisted sessions, checks which processes are still running,
     * and registers them with appropriate state.
     *
     * @param sessions - Array of persisted session data
     */
    async rebuildRegistry(sessions) {
        log.info(`[rebuildRegistry] Rebuilding registry from ${sessions.length} persisted sessions`);
        // Clear existing registry
        await this.registry.clear();
        for (const session of sessions) {
            if (!session.pid) {
                log.debug(`[rebuildRegistry] Skipping session without PID: agentId=${session.agentId}`);
                continue;
            }
            const isRunning = isProcessRunning(session.pid);
            const state = isRunning ? 'running' : 'unknown';
            try {
                await this.registry.register({
                    agentId: session.agentId,
                    sessionId: session.sessionId,
                    pid: session.pid,
                    mode: 'sdk',
                    state,
                    workspacePath: session.workspacePath || '',
                    command: session.command || '',
                    metadata: {
                        rebuiltAt: new Date().toISOString(),
                        wasRunningOnStartup: isRunning,
                    },
                });
                log.debug(`[rebuildRegistry] Registered: agentId=${session.agentId}, pid=${session.pid}, state=${state}`);
            }
            catch (error) {
                log.warn(`[rebuildRegistry] Failed to register: agentId=${session.agentId}`, error);
            }
        }
        const stats = this.registry.getStats();
        log.info(`[rebuildRegistry] Registry rebuilt: total=${stats.total}, running=${stats.running}, unknown=${stats.unknown}`);
    }
    /**
     * Get process statistics
     *
     * @returns Statistics for all managed processes
     */
    getStats() {
        return this.registry.getStats();
    }
    /**
     * Remove a process from registry
     *
     * @param agentId - Agent ID to remove
     * @returns Whether the operation succeeded
     */
    async removeProcess(agentId) {
        log.info(`[removeProcess] Removing process from registry: agentId=${agentId}`);
        return await this.registry.remove(agentId);
    }
    /**
     * Get a single process record
     *
     * @param agentId - Agent ID to get
     * @returns Process record or undefined
     */
    getProcess(agentId) {
        return this.registry.get(agentId);
    }
    /**
     * Get a process record by PID
     *
     * @param pid - Process ID to get
     * @returns Process record or undefined
     */
    getProcessByPid(pid) {
        return this.registry.getByPid(pid);
    }
}
AgentProcessManager.instance = null;
/**
 * Get the AgentProcessManager singleton instance
 *
 * @returns AgentProcessManager instance
 */
export function getAgentProcessManager() {
    return AgentProcessManager.getInstance();
}
