"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InitStateManager = void 0;
const events_1 = require("events");
const eventStore_1 = require("../utils/eventStore");
const log_1 = require("../services/log");
/**
 * InitStateManager - Manages init hook lifecycle with persistence and replay.
 *
 * Uses EventStore abstraction for state management:
 * - In-memory Map for active init hooks (via EventStore)
 * - Disk persistence to init-status.json for replay across page reloads
 * - EventEmitter for streaming events to AgentSession
 * - Permanent storage (never auto-deleted, unlike stream partials)
 *
 * Key differences from StreamManager:
 * - Simpler state machine (running → success/error, no abort)
 * - No throttling (init hooks emit discrete lines, not streaming tokens)
 * - Permanent persistence (init logs kept forever as workspace metadata)
 *
 * Lifecycle:
 * 1. startInit() - Create in-memory state, emit init-start, create completion promise
 * 2. appendOutput() - Accumulate lines, emit init-output
 * 3. endInit() - Finalize state, write to disk, emit init-end, resolve promise
 * 4. State remains in memory until cleared or process restart
 * 5. replayInit() - Re-emit events from in-memory or disk state (via EventStore)
 *
 * Waiting: Tools use waitForInit() which returns a promise that resolves when
 * init completes. This promise is stored in initPromises map and resolved by
 * endInit(). No event listeners needed, eliminating race conditions.
 */
class InitStateManager extends events_1.EventEmitter {
    store;
    /**
     * Promise-based completion tracking for running inits.
     * Each running init has a promise that resolves when endInit() is called.
     * Multiple tools can await the same promise without race conditions.
     */
    initPromises = new Map();
    constructor(config) {
        super();
        this.store = new eventStore_1.EventStore(config, "init-status.json", (state) => this.serializeInitEvents(state), (event) => this.emit(event.type, event), "InitStateManager");
    }
    /**
     * Serialize InitHookState into array of events for replay.
     * Used by EventStore.replay() to reconstruct the event stream.
     */
    serializeInitEvents(state) {
        const events = [];
        const workspaceId = state.workspaceId ?? "unknown";
        // Emit init-start
        events.push({
            type: "init-start",
            workspaceId,
            hookPath: state.hookPath,
            timestamp: state.startTime,
        });
        // Emit init-output for each accumulated line with original timestamps
        for (const timedLine of state.lines) {
            events.push({
                type: "init-output",
                workspaceId,
                line: timedLine.line,
                isError: timedLine.isError,
                timestamp: timedLine.timestamp, // Use original timestamp for replay
            });
        }
        // Emit init-end (only if completed)
        if (state.exitCode !== null) {
            events.push({
                type: "init-end",
                workspaceId,
                exitCode: state.exitCode,
                timestamp: state.endTime ?? state.startTime,
            });
        }
        return events;
    }
    /**
     * Start tracking a new init hook execution.
     * Creates in-memory state, completion promise, and emits init-start event.
     */
    startInit(workspaceId, hookPath) {
        const startTime = Date.now();
        const state = {
            status: "running",
            hookPath,
            startTime,
            lines: [],
            exitCode: null,
            endTime: null,
        };
        this.store.setState(workspaceId, state);
        // Create completion promise for this init
        // This allows multiple tools to await the same init without event listeners
        let resolve;
        let reject;
        const promise = new Promise((res, rej) => {
            resolve = res;
            reject = rej;
        });
        this.initPromises.set(workspaceId, {
            promise,
            resolve: resolve,
            reject: reject,
        });
        log_1.log.debug(`Init hook started for workspace ${workspaceId}: ${hookPath}`);
        // Emit init-start event
        this.emit("init-start", {
            type: "init-start",
            workspaceId,
            hookPath,
            timestamp: startTime,
        });
    }
    /**
     * Append output line from init hook.
     * Accumulates in state and emits init-output event.
     */
    appendOutput(workspaceId, line, isError) {
        const state = this.store.getState(workspaceId);
        if (!state) {
            log_1.log.error(`appendOutput called for workspace ${workspaceId} with no active init state`);
            return;
        }
        const timestamp = Date.now();
        // Store line with isError flag and timestamp
        state.lines.push({ line, isError, timestamp });
        // Emit init-output event
        this.emit("init-output", {
            type: "init-output",
            workspaceId,
            line,
            isError,
            timestamp,
        });
    }
    /**
     * Finalize init hook execution.
     * Updates state, persists to disk, emits init-end event, and resolves completion promise.
     */
    async endInit(workspaceId, exitCode) {
        const state = this.store.getState(workspaceId);
        if (!state) {
            log_1.log.error(`endInit called for workspace ${workspaceId} with no active init state`);
            return;
        }
        const endTime = Date.now();
        state.status = exitCode === 0 ? "success" : "error";
        state.exitCode = exitCode;
        state.endTime = endTime;
        // Persist to disk (fire-and-forget, errors logged internally by EventStore)
        await this.store.persist(workspaceId, state);
        log_1.log.info(`Init hook ${state.status} for workspace ${workspaceId} (exit code ${exitCode}, duration ${endTime - state.startTime}ms)`);
        // Emit init-end event
        this.emit("init-end", {
            type: "init-end",
            workspaceId,
            exitCode,
            timestamp: endTime,
        });
        // Resolve completion promise for waiting tools
        const promiseEntry = this.initPromises.get(workspaceId);
        if (promiseEntry) {
            promiseEntry.resolve();
            this.initPromises.delete(workspaceId);
        }
        // Keep state in memory for replay (unlike streams which delete immediately)
    }
    /**
     * Get current in-memory init state for a workspace.
     * Returns undefined if no init state exists.
     */
    getInitState(workspaceId) {
        return this.store.getState(workspaceId);
    }
    /**
     * Read persisted init status from disk.
     * Returns null if no status file exists.
     */
    async readInitStatus(workspaceId) {
        return this.store.readPersisted(workspaceId);
    }
    /**
     * Replay init events for a workspace.
     * Delegates to EventStore.replay() which:
     * 1. Checks in-memory state first, then falls back to disk
     * 2. Serializes state into events via serializeInitEvents()
     * 3. Emits events (init-start, init-output*, init-end)
     *
     * This is called during AgentSession.emitHistoricalEvents() to ensure
     * init state is visible after page reloads.
     */
    async replayInit(workspaceId) {
        // Pass workspaceId as context for serialization
        await this.store.replay(workspaceId, { workspaceId });
    }
    /**
     * Delete persisted init status from disk.
     * Useful for testing or manual cleanup.
     * Does NOT clear in-memory state (for active replay).
     */
    async deleteInitStatus(workspaceId) {
        await this.store.deletePersisted(workspaceId);
    }
    /**
     * Clear in-memory state for a workspace.
     * Useful for testing or cleanup after workspace deletion.
     * Does NOT delete disk file (use deleteInitStatus for that).
     *
     * Also cancels any running init promises to prevent orphaned waiters.
     */
    clearInMemoryState(workspaceId) {
        this.store.deleteState(workspaceId);
        // Cancel any running init promise for this workspace
        const promiseEntry = this.initPromises.get(workspaceId);
        if (promiseEntry) {
            promiseEntry.reject(new Error(`Workspace ${workspaceId} was deleted`));
            this.initPromises.delete(workspaceId);
        }
    }
    /**
     * Wait for workspace initialization to complete.
     * Used by tools (bash, file_*) to ensure files are ready before executing.
     *
     * Behavior:
     * - No init state: Returns immediately (init not needed or backwards compat)
     * - Init succeeded/failed: Returns immediately (tools proceed regardless of init outcome)
     * - Init running: Waits for completion promise (up to 5 minutes, then proceeds anyway)
     *
     * This method NEVER throws - tools should always proceed. If init fails or times out,
     * the tool will either succeed (if init wasn't critical) or fail with its own error
     * (e.g., file not found). This provides better error messages than blocking on init.
     *
     * Promise-based approach eliminates race conditions:
     * - Multiple tools share the same promise (no duplicate listeners)
     * - No event cleanup needed (promise auto-resolves once)
     * - Timeout races handled by Promise.race()
     *
     * @param workspaceId Workspace ID to wait for
     */
    async waitForInit(workspaceId) {
        const state = this.getInitState(workspaceId);
        // No init state - proceed immediately (backwards compat or init not needed)
        if (!state) {
            return;
        }
        // Init already completed (success or failure) - proceed immediately
        // Tools should work regardless of init outcome
        if (state.status !== "running") {
            return;
        }
        // Init is running - wait for completion promise with timeout
        const promiseEntry = this.initPromises.get(workspaceId);
        if (!promiseEntry) {
            // State says running but no promise exists (shouldn't happen, but handle gracefully)
            log_1.log.error(`Init state is running for ${workspaceId} but no promise found, proceeding`);
            return;
        }
        const INIT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
        const timeoutPromise = new Promise((resolve) => {
            setTimeout(() => {
                log_1.log.error(`Init timeout for ${workspaceId} after 5 minutes - tools will proceed anyway. ` +
                    `Init will continue in background.`);
                resolve(); // Resolve, don't reject - tools should proceed
            }, INIT_TIMEOUT_MS);
        });
        // Race between completion and timeout
        // Both resolve (no rejection), so tools always proceed
        try {
            await Promise.race([promiseEntry.promise, timeoutPromise]);
        }
        catch (error) {
            // Init promise was rejected (e.g., workspace deleted)
            // Log and proceed anyway - let the tool fail with its own error if needed
            const errorMsg = error instanceof Error ? error.message : String(error);
            log_1.log.error(`Init wait interrupted for ${workspaceId}: ${errorMsg} - proceeding anyway`);
        }
    }
}
exports.InitStateManager = InitStateManager;
//# sourceMappingURL=initStateManager.js.map