/**
 * @fileoverview Ralph Loop - Autonomous task execution engine.
 *
 * Orchestrates autonomous Claude sessions by:
 * - Polling for available tasks from the task queue
 * - Assigning tasks to idle sessions
 * - Monitoring completion and handling failures
 * - Auto-generating follow-up tasks when min duration not reached
 *
 * Key exports:
 * - `RalphLoop` class — the loop engine, extends EventEmitter
 * - `RalphLoopEvents` interface — typed event map
 * - `RalphLoopOptions` interface — configuration options
 *
 * Lifecycle: `start()` → poll loop → `stop()` (when all tasks done + min duration met)
 *
 * @dependencies session-manager (session lifecycle), task-queue (task FIFO),
 *   state-store (persistence), session (PTY execution), task (task model)
 * @consumedby web/server (ralph routes, SSE)
 * @emits started, stopped, taskAssigned, taskCompleted, taskFailed, error
 * @persistence Ralph loop state saved to `~/.codeman/state.json` (ralphLoop key)
 *
 * @module ralph-loop
 */
import { EventEmitter } from 'node:events';
import { getSessionManager } from './session-manager.js';
import { getTaskQueue } from './task-queue.js';
import { getStore } from './state-store.js';
import { getErrorMessage } from './types.js';
/**
 * Autonomous task execution loop.
 *
 * @description
 * Manages the lifecycle of task execution:
 * 1. Start: Begin polling and task assignment
 * 2. Run: Assign tasks to idle sessions, monitor completion
 * 3. Stop: When all tasks done and min duration reached
 *
 * Supports time-aware loops that continue generating tasks
 * until a minimum duration is reached.
 *
 * @extends EventEmitter
 */
export class RalphLoop extends EventEmitter {
    sessionManager;
    taskQueue;
    store;
    pollIntervalMs;
    minDurationMs;
    autoGenerateTasks;
    loopTimer = null;
    _status = 'stopped';
    startedAt = null;
    tasksCompleted = 0;
    tasksGenerated = 0;
    /** Bound event handlers for cleanup (prevents memory leaks) */
    sessionEventHandlers = null;
    constructor(options = {}) {
        super();
        this.sessionManager = getSessionManager();
        this.taskQueue = getTaskQueue();
        this.store = getStore();
        const config = this.store.getConfig();
        this.pollIntervalMs = options.pollIntervalMs ?? config.pollIntervalMs;
        this.minDurationMs = options.minDurationMs ?? null;
        this.autoGenerateTasks = options.autoGenerateTasks ?? true;
        // Load state from store
        const savedState = this.store.getRalphLoopState();
        if (savedState.status === 'running') {
            // If we crashed while running, reset to stopped
            this._status = 'stopped';
            this.store.setRalphLoopState({ status: 'stopped' });
            // Reset orphaned in_progress tasks back to pending
            for (const task of this.taskQueue.getRunningTasks()) {
                task.reset();
                this.taskQueue.updateTask(task);
            }
        }
        this.setupEventHandlers();
    }
    setupEventHandlers() {
        // Store bound handlers for later cleanup
        this.sessionEventHandlers = {
            completion: (sessionId, phrase) => {
                this.handleSessionCompletion(sessionId, phrase);
            },
            error: (sessionId, error) => {
                this.handleSessionError(sessionId, error);
            },
            stopped: (sessionId) => {
                this.handleSessionStopped(sessionId);
            },
            taskError: (sessionId, taskId, error) => {
                this.handleSessionTaskError(sessionId, taskId, error);
            },
        };
        this.sessionManager.on('sessionCompletion', this.sessionEventHandlers.completion);
        this.sessionManager.on('sessionError', this.sessionEventHandlers.error);
        this.sessionManager.on('sessionStopped', this.sessionEventHandlers.stopped);
        this.sessionManager.on('sessionTaskError', this.sessionEventHandlers.taskError);
    }
    /** Remove event listeners to prevent memory leaks */
    cleanupEventHandlers() {
        if (this.sessionEventHandlers) {
            this.sessionManager.off('sessionCompletion', this.sessionEventHandlers.completion);
            this.sessionManager.off('sessionError', this.sessionEventHandlers.error);
            this.sessionManager.off('sessionStopped', this.sessionEventHandlers.stopped);
            this.sessionManager.off('sessionTaskError', this.sessionEventHandlers.taskError);
            this.sessionEventHandlers = null;
        }
    }
    get status() {
        return this._status;
    }
    isRunning() {
        return this._status === 'running';
    }
    getElapsedMs() {
        if (!this.startedAt) {
            return 0;
        }
        return Date.now() - this.startedAt;
    }
    getElapsedHours() {
        return this.getElapsedMs() / (1000 * 60 * 60);
    }
    isMinDurationReached() {
        if (!this.minDurationMs) {
            return true;
        }
        return this.getElapsedMs() >= this.minDurationMs;
    }
    getStats() {
        const taskCounts = this.taskQueue.getCount();
        return {
            status: this._status,
            elapsedMs: this.getElapsedMs(),
            elapsedHours: this.getElapsedHours(),
            minDurationMs: this.minDurationMs,
            minDurationReached: this.isMinDurationReached(),
            tasksCompleted: this.tasksCompleted,
            tasksGenerated: this.tasksGenerated,
            ...taskCounts,
            activeSessions: this.sessionManager.getSessionCount(),
            idleSessions: this.sessionManager.getIdleSessions().length,
            busySessions: this.sessionManager.getBusySessions().length,
        };
    }
    /** Starts the task execution loop. */
    async start() {
        if (this._status === 'running') {
            return;
        }
        this._status = 'running';
        this.startedAt = Date.now();
        this.tasksCompleted = 0;
        this.tasksGenerated = 0;
        // Re-setup event handlers if they were cleaned up during stop()
        if (!this.sessionEventHandlers) {
            this.setupEventHandlers();
        }
        this.store.setRalphLoopState({
            status: 'running',
            startedAt: this.startedAt,
            minDurationMs: this.minDurationMs,
            tasksCompleted: 0,
            tasksGenerated: 0,
        });
        this.emit('started');
        this.runLoop();
    }
    /** Stops the task execution loop. */
    stop() {
        if (this._status === 'stopped') {
            return;
        }
        this._status = 'stopped';
        if (this.loopTimer) {
            clearTimeout(this.loopTimer);
            this.loopTimer = null;
        }
        // Clean up event handlers to prevent memory leaks
        this.cleanupEventHandlers();
        this.store.setRalphLoopState({
            status: 'stopped',
            lastCheckAt: Date.now(),
        });
        this.emit('stopped');
    }
    /** Pauses the loop (can be resumed). */
    pause() {
        if (this._status !== 'running') {
            return;
        }
        this._status = 'paused';
        if (this.loopTimer) {
            clearTimeout(this.loopTimer);
            this.loopTimer = null;
        }
        this.store.setRalphLoopState({ status: 'paused' });
    }
    /** Resumes a paused loop. */
    resume() {
        if (this._status !== 'paused') {
            return;
        }
        this._status = 'running';
        this.store.setRalphLoopState({ status: 'running' });
        this.runLoop();
    }
    runLoop() {
        if (this._status !== 'running') {
            return;
        }
        this.tick()
            .catch((err) => {
            // Only emit if still running (stop() may have been called during tick())
            if (this._status === 'running') {
                this.emit('error', err);
            }
        })
            .finally(() => {
            // Guard: only reschedule if still running AND no timer is pending
            // (prevents race where stop() clears timer between our check and setTimeout)
            if (this._status === 'running' && this.loopTimer === null) {
                this.loopTimer = setTimeout(() => this.runLoop(), this.pollIntervalMs);
            }
        });
    }
    async tick() {
        this.store.setRalphLoopState({ lastCheckAt: Date.now() });
        // Run sequentially: timeouts first so timed-out tasks are cleaned up
        // before assignTasks() picks new work (prevents race where both
        // mutate the same task concurrently)
        await this.checkTimeouts();
        await this.assignTasks();
        // Check if we should auto-generate tasks (depends on assignment results)
        if (this.autoGenerateTasks && this.shouldGenerateTasks()) {
            await this.generateFollowUpTasks();
        }
        // Check if we're done
        if (this.shouldStop()) {
            this.stop();
        }
    }
    async assignTasks() {
        const idleSessions = this.sessionManager.getIdleSessions();
        for (const session of idleSessions) {
            const task = this.taskQueue.next();
            if (!task) {
                break;
            }
            try {
                await this.assignTaskToSession(task, session);
            }
            catch (err) {
                console.error(`[RalphLoop] Failed to assign task ${task.id} to session ${session.id}:`, err);
            }
        }
    }
    async assignTaskToSession(task, session) {
        try {
            task.assign(session.id);
            session.assignTask(task.id);
            this.taskQueue.updateTask(task);
            // Send the prompt to the session
            await session.sendInput(task.prompt);
            this.emit('taskAssigned', task.id, session.id);
        }
        catch (err) {
            task.fail(getErrorMessage(err));
            session.clearTask();
            this.taskQueue.updateTask(task);
            this.emit('taskFailed', task.id, getErrorMessage(err));
        }
    }
    async checkTimeouts() {
        for (const task of this.taskQueue.getRunningTasks()) {
            if (task.isTimedOut()) {
                task.fail('Task timed out');
                this.taskQueue.updateTask(task);
                const session = this.sessionManager.getSession(task.assignedSessionId);
                if (session) {
                    session.clearTask();
                }
                this.emit('taskFailed', task.id, 'Task timed out');
            }
        }
    }
    handleSessionCompletion(sessionId, phrase) {
        const session = this.sessionManager.getSession(sessionId);
        if (!session) {
            return;
        }
        const taskId = session.currentTaskId;
        if (!taskId) {
            return;
        }
        const task = this.taskQueue.getTask(taskId);
        if (!task) {
            return;
        }
        // Append output and check for completion
        task.appendOutput(session.getOutput());
        if (task.checkCompletion(session.getOutput()) || phrase) {
            task.complete();
            this.taskQueue.updateTask(task);
            session.clearTask();
            this.tasksCompleted++;
            this.store.setRalphLoopState({ tasksCompleted: this.tasksCompleted });
            this.emit('taskCompleted', task.id);
        }
    }
    handleSessionError(sessionId, error) {
        const session = this.sessionManager.getSession(sessionId);
        if (!session) {
            return;
        }
        const taskId = session.currentTaskId;
        if (!taskId) {
            return;
        }
        const task = this.taskQueue.getTask(taskId);
        if (!task) {
            return;
        }
        task.setError(error);
        // Don't fail the task immediately on stderr - some tools write to stderr normally
    }
    handleSessionStopped(sessionId) {
        const task = this.taskQueue.getRunningTaskForSession(sessionId);
        if (task) {
            task.fail('Session stopped unexpectedly');
            this.taskQueue.updateTask(task);
            this.emit('taskFailed', task.id, 'Session stopped unexpectedly');
        }
    }
    handleSessionTaskError(_sessionId, taskId, error) {
        const task = this.taskQueue.getTask(taskId);
        if (!task) {
            return;
        }
        task.fail(error);
        this.taskQueue.updateTask(task);
        this.emit('taskFailed', task.id, error);
    }
    shouldGenerateTasks() {
        // Generate tasks if:
        // 1. No pending tasks
        // 2. Min duration not reached
        // 3. We have idle sessions
        const counts = this.taskQueue.getCount();
        return counts.pending === 0 && !this.isMinDurationReached() && this.sessionManager.getIdleSessions().length > 0;
    }
    async generateFollowUpTasks() {
        // This is a placeholder for auto-generating follow-up tasks
        // In a real implementation, this could:
        // - Analyze completed tasks to find optimization opportunities
        // - Generate tasks for code cleanup, tests, documentation
        // - Use Claude to suggest improvements
        const suggestions = [
            'Review and optimize recently changed code',
            'Add tests for uncovered code paths',
            'Update documentation for changed APIs',
            'Check for security vulnerabilities',
            'Run linting and fix any issues',
        ];
        // Only generate one task at a time
        const suggestion = suggestions[this.tasksGenerated % suggestions.length];
        const defaultDir = process.cwd();
        this.taskQueue.addTask({
            prompt: suggestion,
            workingDir: defaultDir,
            priority: -1, // Lower priority than user-added tasks
        });
        this.tasksGenerated++;
        this.store.setRalphLoopState({ tasksGenerated: this.tasksGenerated });
    }
    shouldStop() {
        const counts = this.taskQueue.getCount();
        // Don't stop if there are pending or running tasks
        if (counts.pending > 0 || counts.running > 0) {
            return false;
        }
        // Don't stop if min duration not reached and auto-generate is on
        if (!this.isMinDurationReached() && this.autoGenerateTasks) {
            return false;
        }
        // All tasks done and conditions met
        return true;
    }
    /** Sets the minimum duration in hours before the loop can stop. */
    setMinDuration(hours) {
        this.minDurationMs = hours * 60 * 60 * 1000;
        this.store.setRalphLoopState({ minDurationMs: this.minDurationMs });
    }
    /**
     * Destroys the loop and cleans up all resources.
     * Use this for complete cleanup (e.g., in tests or before creating a new instance).
     * After calling destroy(), the instance should not be reused.
     */
    destroy() {
        this.stop();
        this.cleanupEventHandlers();
        this.removeAllListeners();
    }
}
// Singleton instance
let loopInstance = null;
/** Gets or creates the singleton RalphLoop instance. */
export function getRalphLoop(options) {
    if (!loopInstance) {
        loopInstance = new RalphLoop(options);
    }
    return loopInstance;
}
/** Destroys the singleton instance. Use in tests or for cleanup. */
function destroyRalphLoop() {
    if (loopInstance) {
        loopInstance.destroy();
        loopInstance = null;
    }
}
// Ensure cleanup on process exit (prevents orphaned event handlers and circular references)
process.on('exit', () => {
    destroyRalphLoop();
});
//# sourceMappingURL=ralph-loop.js.map