import { Flags } from '@oclif/core';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import Database from 'better-sqlite3';
import { styles } from '../../lib/styles.js';
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
import { ExecutionStorage } from '../../lib/execution/index.js';
import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, captureTmuxPane, } from '../../lib/execution/session-utils.js';
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
import { visualPadEnd } from '../../lib/string-utils.js';
import { onShutdown } from '../../lib/signal-handler.js';
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
// =============================================================================
// Detection Logic
// =============================================================================
/**
 * Detect agent health state from tmux pane content.
 *
 * Detection patterns (checked in priority order):
 * - '0 tokens' (the down-arrow + 0 tokens pattern) = HUNG
 * - 'esc to interrupt' = WORKING
 * - 'Agent work complete' or similar completion messages = DONE
 * - Idle prompt patterns ($ or ❯ at end of line) = IDLE
 */
function detectState(paneContent) {
    if (!paneContent)
        return 'UNKNOWN';
    // Check last few lines for patterns
    const lines = paneContent.split('\n');
    const lastLines = lines.slice(-10).join('\n');
    // HUNG: stuck API call showing '0 tokens' (the ↓ 0 tokens pattern)
    if (/0 tokens/.test(lastLines)) {
        return 'HUNG';
    }
    // WORKING: agent is actively processing
    if (/esc to interrupt/i.test(lastLines)) {
        return 'WORKING';
    }
    // DONE: agent work is complete
    if (/agent work complete/i.test(lastLines) || /work ready/i.test(lastLines)) {
        return 'DONE';
    }
    // IDLE: shell prompt visible (agent has finished or is waiting)
    // Match common prompt patterns at end of last non-empty line
    const lastNonEmpty = [...lines].reverse().find((l) => l.trim().length > 0) || '';
    if (/[$❯#>]\s*$/.test(lastNonEmpty) || /^\s*\$\s*$/.test(lastNonEmpty)) {
        return 'IDLE';
    }
    return 'UNKNOWN';
}
/**
 * Format elapsed time from a start date to now.
 */
function formatElapsed(startedAt) {
    const ms = Date.now() - startedAt.getTime();
    const totalMinutes = Math.floor(ms / 60000);
    const hours = Math.floor(totalMinutes / 60);
    const minutes = totalMinutes % 60;
    if (hours > 0) {
        return `${hours}h ${minutes}m`;
    }
    return `${minutes}m`;
}
/**
 * Send Escape key to a tmux session to unstick a hung agent.
 */
function sendEscape(sessionId, containerId) {
    try {
        const sendCmd = `tmux send-keys -t "${sessionId}" Escape`;
        if (containerId) {
            execSync(`docker exec ${containerId} bash -c '${sendCmd}'`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000 });
        }
        else {
            execSync(sendCmd, {
                encoding: 'utf-8',
                stdio: ['pipe', 'pipe', 'pipe'],
                timeout: 5000,
            });
        }
        return true;
    }
    catch {
        return false;
    }
}
// =============================================================================
// Command
// =============================================================================
export default class SessionHealth extends PMOCommand {
    static description = 'Check health of running agent sessions and detect/recover hung agents';
    static examples = [
        '<%= config.bin %> session health',
        '<%= config.bin %> session health --fix',
        '<%= config.bin %> session health --watch',
        '<%= config.bin %> session health --watch --interval 3 --threshold 5',
    ];
    static flags = {
        ...pmoBaseFlags,
        fix: Flags.boolean({
            description: 'Send Escape to hung agents to unstick them',
            default: false,
        }),
        watch: Flags.boolean({
            description: 'Continuously monitor agents and auto-recover hung ones',
            default: false,
        }),
        interval: Flags.integer({
            description: 'Watch polling interval in minutes',
            default: 5,
        }),
        threshold: Flags.integer({
            description: 'Minutes an agent must be hung before auto-recovery in watch mode',
            default: 10,
        }),
    };
    getPMOOptions() {
        return { promptIfMultiple: false };
    }
    async execute() {
        const { flags } = await this.parse(SessionHealth);
        const jsonMode = shouldOutputJson(flags);
        if (flags.watch) {
            await this.watchMode(flags.interval, flags.threshold);
        }
        else {
            await this.runHealthCheck(flags.fix, jsonMode, flags);
        }
    }
    /**
     * Single health check pass. Returns the list of agent health infos.
     */
    async runHealthCheck(fix, jsonMode = false, flags = {}) {
        let executionStorage = null;
        let db = null;
        try {
            const workspaceInfo = getWorkspaceInfo();
            const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
            db = new Database(dbPath);
            executionStorage = new ExecutionStorage(db);
        }
        catch {
            if (jsonMode) {
                outputErrorAsJson('NOT_IN_WORKSPACE', 'Not in a workspace. Run from a proletariat HQ directory.', createMetadata('session health', flags));
            }
            this.log('');
            this.log(styles.error('Not in a workspace. Run from a proletariat HQ directory.'));
            this.log('');
            return [];
        }
        try {
            // Get active executions from DB
            const runningExecutions = executionStorage.listExecutions({ status: 'running' });
            const startingExecutions = executionStorage.listExecutions({ status: 'starting' });
            const activeExecutions = [...runningExecutions, ...startingExecutions];
            // Get actual tmux sessions
            const hostTmuxSessions = getHostTmuxSessionNames();
            const containerTmuxSessions = getContainerTmuxSessionMap();
            const allContainerSessions = flattenContainerSessions(containerTmuxSessions);
            // Track matched sessions
            const matchedHostSessions = new Set();
            const matchedContainerSessions = new Set();
            const agents = [];
            // Process DB-tracked executions
            for (const exec of activeExecutions) {
                const isContainer = exec.environment === 'devcontainer';
                let exists = false;
                let containerId;
                let actualSessionId = exec.sessionId;
                // Try to find session if sessionId is NULL
                if (!exec.sessionId) {
                    if (isContainer && exec.containerId) {
                        const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
                        const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
                        if (match) {
                            actualSessionId = match;
                            exists = true;
                            containerId = exec.containerId;
                        }
                    }
                    else {
                        const match = findSessionForExecution(exec.ticketId, exec.agentName, hostTmuxSessions);
                        if (match) {
                            actualSessionId = match;
                            exists = true;
                        }
                    }
                    if (!actualSessionId)
                        continue;
                }
                else {
                    if (isContainer && exec.containerId) {
                        const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
                        exists = containerSessions.includes(exec.sessionId);
                        containerId = exec.containerId;
                    }
                    else {
                        exists = hostTmuxSessions.includes(exec.sessionId);
                    }
                }
                if (exists && actualSessionId) {
                    if (isContainer && containerId) {
                        matchedContainerSessions.add(`${containerId}:${actualSessionId}`);
                    }
                    else {
                        matchedHostSessions.add(actualSessionId);
                    }
                    // Capture pane and detect state
                    const paneContent = captureTmuxPane(actualSessionId, 10, containerId);
                    const state = detectState(paneContent);
                    agents.push({
                        sessionId: actualSessionId,
                        ticketId: exec.ticketId,
                        agentName: exec.agentName,
                        state,
                        environment: isContainer ? 'container' : 'host',
                        containerId,
                        elapsed: formatElapsed(exec.startedAt),
                        paneContent: paneContent || undefined,
                    });
                }
            }
            // Also discover orphan tmux sessions matching prlt pattern
            for (const sessionName of hostTmuxSessions) {
                if (matchedHostSessions.has(sessionName))
                    continue;
                const parsed = parseSessionName(sessionName);
                if (parsed) {
                    const paneContent = captureTmuxPane(sessionName, 10);
                    const state = detectState(paneContent);
                    agents.push({
                        sessionId: sessionName,
                        ticketId: parsed.ticketId,
                        agentName: parsed.agentName,
                        state,
                        environment: 'host',
                        elapsed: '?',
                        paneContent: paneContent || undefined,
                    });
                }
            }
            for (const { sessionName, containerId: cId } of allContainerSessions) {
                if (matchedContainerSessions.has(`${cId}:${sessionName}`))
                    continue;
                const parsed = parseSessionName(sessionName);
                if (parsed) {
                    const paneContent = captureTmuxPane(sessionName, 10, cId);
                    const state = detectState(paneContent);
                    agents.push({
                        sessionId: sessionName,
                        ticketId: parsed.ticketId,
                        agentName: parsed.agentName,
                        state,
                        environment: 'container',
                        containerId: cId,
                        elapsed: '?',
                        paneContent: paneContent || undefined,
                    });
                }
            }
            // JSON mode: output structured data and exit
            if (jsonMode) {
                const hungAgents = agents.filter(a => a.state === 'HUNG');
                const fixResults = [];
                if (fix && hungAgents.length > 0) {
                    for (const agent of hungAgents) {
                        const success = sendEscape(agent.sessionId, agent.containerId);
                        fixResults.push({
                            agentName: agent.agentName,
                            ticketId: agent.ticketId,
                            recovered: success,
                        });
                    }
                }
                outputSuccessAsJson({
                    agents: agents.map(a => ({
                        ticketId: a.ticketId,
                        agentName: a.agentName,
                        state: a.state,
                        environment: a.environment,
                        containerId: a.containerId,
                        sessionId: a.sessionId,
                        elapsed: a.elapsed,
                    })),
                    summary: {
                        total: agents.length,
                        hung: agents.filter(a => a.state === 'HUNG').length,
                        working: agents.filter(a => a.state === 'WORKING').length,
                        done: agents.filter(a => a.state === 'DONE').length,
                        idle: agents.filter(a => a.state === 'IDLE').length,
                        unknown: agents.filter(a => a.state === 'UNKNOWN').length,
                    },
                    ...(fix && fixResults.length > 0 ? { recovered: fixResults } : {}),
                    commands: {
                        fix: 'prlt session health --fix',
                        watch: 'prlt session health --watch',
                    },
                }, createMetadata('session health', flags));
            }
            // Display status table
            this.displayHealthTable(agents);
            // Auto-fix hung agents if --fix
            if (fix) {
                const hungAgents = agents.filter(a => a.state === 'HUNG');
                if (hungAgents.length > 0) {
                    this.log('');
                    this.log(styles.header('Recovering hung agents...'));
                    this.log('');
                    for (const agent of hungAgents) {
                        const success = sendEscape(agent.sessionId, agent.containerId);
                        if (success) {
                            this.log(styles.success(`  Sent Escape to ${agent.agentName} (${agent.ticketId}) - recovered`));
                        }
                        else {
                            this.log(styles.error(`  Failed to send Escape to ${agent.agentName} (${agent.ticketId})`));
                        }
                    }
                    this.log('');
                }
                else {
                    this.log('');
                    this.log(styles.success('No hung agents found.'));
                    this.log('');
                }
            }
            return agents;
        }
        finally {
            db?.close();
        }
    }
    /**
     * Display the health status table.
     */
    displayHealthTable(agents) {
        if (agents.length === 0) {
            this.log('');
            this.log(styles.muted('No active agent sessions found.'));
            this.log('');
            this.log(styles.muted('Start work with: prlt work start <ticket-id>'));
            this.log('');
            return;
        }
        this.log('');
        this.log(styles.header('Agent Health Status'));
        this.log('═'.repeat(95));
        this.log(styles.muted('  ' +
            visualPadEnd('Ticket', 12) +
            visualPadEnd('Agent', 20) +
            visualPadEnd('State', 12) +
            visualPadEnd('Type', 15) +
            visualPadEnd('Elapsed', 10) +
            'Session'));
        this.log('  ' + '─'.repeat(93));
        for (const agent of agents) {
            const typeIcon = agent.environment === 'container' ? '🐳 container' : '💻 host';
            const stateColor = agent.state === 'HUNG' ? styles.error :
                agent.state === 'WORKING' ? styles.success :
                    agent.state === 'DONE' ? styles.info :
                        agent.state === 'IDLE' ? styles.warning :
                            styles.muted;
            const stateIcon = agent.state === 'HUNG' ? '🔴' :
                agent.state === 'WORKING' ? '🟢' :
                    agent.state === 'DONE' ? '✅' :
                        agent.state === 'IDLE' ? '🟡' :
                            '⚪';
            const displaySession = agent.sessionId.length > 24
                ? agent.sessionId.substring(0, 21) + '...'
                : agent.sessionId;
            this.log('  ' +
                visualPadEnd(agent.ticketId, 12) +
                visualPadEnd(agent.agentName, 20) +
                stateColor(visualPadEnd(`${stateIcon} ${agent.state}`, 12)) +
                visualPadEnd(typeIcon, 15) +
                visualPadEnd(agent.elapsed, 10) +
                styles.muted(displaySession));
        }
        this.log('');
        this.log('═'.repeat(95));
        // Summary counts
        const counts = {
            HUNG: agents.filter(a => a.state === 'HUNG').length,
            WORKING: agents.filter(a => a.state === 'WORKING').length,
            DONE: agents.filter(a => a.state === 'DONE').length,
            IDLE: agents.filter(a => a.state === 'IDLE').length,
            UNKNOWN: agents.filter(a => a.state === 'UNKNOWN').length,
        };
        const parts = [];
        if (counts.WORKING > 0)
            parts.push(styles.success(`${counts.WORKING} working`));
        if (counts.HUNG > 0)
            parts.push(styles.error(`${counts.HUNG} hung`));
        if (counts.DONE > 0)
            parts.push(styles.info(`${counts.DONE} done`));
        if (counts.IDLE > 0)
            parts.push(styles.warning(`${counts.IDLE} idle`));
        if (counts.UNKNOWN > 0)
            parts.push(styles.muted(`${counts.UNKNOWN} unknown`));
        this.log(`  ${parts.join('  ')}`);
        this.log('');
        // Show fix hint if there are hung agents
        if (counts.HUNG > 0) {
            this.log(styles.warning(`  ${counts.HUNG} agent(s) appear hung. Run with --fix to send Escape and recover them.`));
            this.log(styles.muted('  prlt session health --fix'));
            this.log('');
        }
    }
    /**
     * Watchdog mode: continuously monitor and auto-recover hung agents.
     */
    async watchMode(intervalMinutes, thresholdMinutes) {
        this.log('');
        this.log(styles.header('Watchdog Mode'));
        this.log(styles.muted(`  Polling every ${intervalMinutes} minute(s)`));
        this.log(styles.muted(`  Auto-recovering agents hung for >${thresholdMinutes} minute(s)`));
        this.log(styles.muted('  Press Ctrl+C to stop'));
        this.log('');
        // Track how long each agent has been in HUNG state (by sessionId)
        const hungSince = new Map();
        const poll = async () => {
            const timestamp = new Date().toLocaleTimeString();
            this.log(styles.muted(`[${timestamp}] Checking agent health...`));
            const agents = await this.runHealthCheck(false);
            // Track hung durations and auto-recover
            const currentHungIds = new Set();
            for (const agent of agents) {
                if (agent.state === 'HUNG') {
                    currentHungIds.add(agent.sessionId);
                    if (!hungSince.has(agent.sessionId)) {
                        hungSince.set(agent.sessionId, Date.now());
                        this.log(styles.warning(`  Detected hung agent: ${agent.agentName} (${agent.ticketId})`));
                    }
                    const hungDurationMs = Date.now() - hungSince.get(agent.sessionId);
                    const hungMinutes = Math.floor(hungDurationMs / 60000);
                    if (hungMinutes >= thresholdMinutes) {
                        this.log(styles.warning(`  Agent ${agent.agentName} hung for ${hungMinutes}m (threshold: ${thresholdMinutes}m) - recovering...`));
                        const success = sendEscape(agent.sessionId, agent.containerId);
                        if (success) {
                            this.log(styles.success(`  Sent Escape to ${agent.agentName} (${agent.ticketId}) - recovered`));
                            hungSince.delete(agent.sessionId);
                        }
                        else {
                            this.log(styles.error(`  Failed to send Escape to ${agent.agentName} (${agent.ticketId})`));
                        }
                    }
                }
            }
            // Clear hung tracking for agents no longer hung
            for (const sessionId of hungSince.keys()) {
                if (!currentHungIds.has(sessionId)) {
                    hungSince.delete(sessionId);
                }
            }
        };
        // Initial check
        await poll();
        // Poll loop
        const intervalMs = intervalMinutes * 60 * 1000;
        await new Promise((resolve) => {
            const timer = setInterval(async () => {
                try {
                    await poll();
                }
                catch (error) {
                    this.log(styles.error(`  Error during health check: ${error}`));
                }
            }, intervalMs);
            // Register cleanup via centralized signal handler
            onShutdown(() => {
                clearInterval(timer);
                resolve();
            });
        });
    }
}
//# sourceMappingURL=health.js.map