/**
 * @fileoverview Persistent JSON state storage for Codeman.
 *
 * Persists application state with debounced writes (500ms) to prevent excessive disk I/O.
 * State is split into two files:
 * - `~/.codeman/state.json` — main app state (sessions, tasks, config, global stats)
 * - `~/.codeman/state-inner.json` — Ralph loop state per session (changes rapidly)
 *
 * Key exports:
 * - `StateStore` class — singleton store with circuit breaker for save failures
 * - `getStore(filePath?)` — factory/singleton accessor
 *
 * Key methods: `getState()`, `getSessions()`, `setSession()`, `getConfig()`,
 * `setConfig()`, `getGlobalStats()`, `getAggregateStats()`, `getTokenStats()`,
 * `getDailyStats()`, `getRalphState()`, `setRalphState()`, `save()`, `saveNow()`
 *
 * Auto-migrates legacy `~/.claudeman/` → `~/.codeman/` on first load.
 *
 * @dependencies types (AppState, RalphSessionState, GlobalStats, TokenStats),
 *   utils (Debouncer, MAX_SESSION_TOKENS)
 * @consumedby session-manager, ralph-loop, web/server, respawn-controller,
 *   hooks-config, and most subsystems
 *
 * @module state-store
 */
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync, copyFileSync } from 'node:fs';
import { writeFile, rename, unlink, copyFile, access } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { createInitialState, createInitialRalphSessionState, createInitialGlobalStats, } from './types.js';
import { Debouncer, MAX_SESSION_TOKENS } from './utils/index.js';
import { dataPath, CODEMAN_INSTANCE } from './config/instance.js';
/** Debounce delay for batching state writes (ms) */
const SAVE_DEBOUNCE_MS = 500;
/**
 * Persistent JSON state storage with debounced writes.
 *
 * State is automatically loaded on construction and saved with 500ms
 * debouncing to batch rapid updates into single disk writes.
 *
 * @example
 * ```typescript
 * const store = new StateStore();
 *
 * // Read state
 * const sessions = store.getState().sessions;
 *
 * // Modify and save
 * store.getState().sessions[id] = sessionState;
 * store.save();  // Debounced - won't write immediately
 *
 * // Force immediate write
 * store.saveNow();
 * ```
 */
/** Maximum consecutive save failures before circuit breaker opens */
const MAX_CONSECUTIVE_FAILURES = 3;
export class StateStore {
    state;
    filePath;
    saveDeb = new Debouncer(SAVE_DEBOUNCE_MS);
    dirty = false;
    dirtySessions = new Set();
    cachedSessionJsons = new Map();
    // Inner state storage (separate from main state to reduce write frequency)
    ralphStates = new Map();
    ralphStatePath;
    ralphStateSaveDeb = new Debouncer(SAVE_DEBOUNCE_MS);
    ralphStateDirty = false;
    // Circuit breaker for save failures (prevents hammering disk on persistent errors)
    consecutiveSaveFailures = 0;
    circuitBreakerOpen = false;
    // Guard against concurrent saveNowAsync() calls (debounce can race with in-flight write)
    _saveInFlight = null;
    constructor(filePath) {
        // Migrate legacy data directory (~/.claudeman → ~/.codeman). Default (prod)
        // instance only — a named instance (e.g. beta) must never touch the shared
        // ~/.codeman / ~/codeman-cases layout, preserving instance isolation.
        if (!filePath && !CODEMAN_INSTANCE) {
            const legacyDir = join(homedir(), '.claudeman');
            const newDir = join(homedir(), '.codeman');
            if (existsSync(legacyDir) && !existsSync(newDir)) {
                console.log(`[state-store] Migrating data directory: ${legacyDir} → ${newDir}`);
                renameSync(legacyDir, newDir);
            }
            const legacyCasesDir = join(homedir(), 'claudeman-cases');
            const newCasesDir = join(homedir(), 'codeman-cases');
            if (existsSync(legacyCasesDir) && !existsSync(newCasesDir)) {
                console.log(`[state-store] Migrating cases directory: ${legacyCasesDir} → ${newCasesDir}`);
                renameSync(legacyCasesDir, newCasesDir);
            }
        }
        this.filePath = filePath || dataPath('state.json');
        this.ralphStatePath = this.filePath.replace('.json', '-inner.json');
        this.state = this.load();
        this.state.config.stateFilePath = this.filePath;
        // Pre-populate session cache for loaded state
        for (const [id, session] of Object.entries(this.state.sessions)) {
            this.cachedSessionJsons.set(id, JSON.stringify(session));
        }
        this.loadRalphStates();
    }
    _mergeWithInitialState(parsed) {
        const initial = createInitialState();
        return {
            ...initial,
            ...parsed,
            sessions: { ...parsed.sessions },
            tasks: { ...parsed.tasks },
            ralphLoop: { ...initial.ralphLoop, ...parsed.ralphLoop },
            config: { ...initial.config, ...parsed.config },
        };
    }
    _resetCircuitBreaker() {
        this.consecutiveSaveFailures = 0;
        if (this.circuitBreakerOpen) {
            console.log('[StateStore] Circuit breaker CLOSED - save succeeded');
            this.circuitBreakerOpen = false;
        }
    }
    ensureDir() {
        const dir = dirname(this.filePath);
        if (!existsSync(dir)) {
            // Use restrictive permissions (0o700) - owner only can read/write/traverse
            // State files may contain sensitive session data
            mkdirSync(dir, { recursive: true, mode: 0o700 });
        }
    }
    load() {
        // Try main file first, then .bak fallback
        for (const path of [this.filePath, this.filePath + '.bak']) {
            try {
                if (existsSync(path)) {
                    const data = readFileSync(path, 'utf-8');
                    const parsed = JSON.parse(data);
                    const result = this._mergeWithInitialState(parsed);
                    if (path !== this.filePath) {
                        console.warn(`[StateStore] Recovered state from backup: ${path}`);
                    }
                    return result;
                }
            }
            catch (err) {
                console.error(`Failed to load state from ${path}:`, err);
            }
        }
        return createInitialState();
    }
    /**
     * Schedules a debounced save.
     * Multiple calls within 500ms are batched into a single disk write.
     * Uses async I/O to avoid blocking the event loop.
     */
    save() {
        this.dirty = true;
        if (this.saveDeb.isPending)
            return; // Already scheduled
        this.saveDeb.schedule(() => {
            this.saveNowAsync().catch((err) => {
                console.error('[StateStore] Async save failed:', err);
            });
        });
    }
    /**
     * Async version of saveNow — used by the debounced save() path.
     * Uses non-blocking fs.promises to avoid blocking the event loop during
     * the debounced write cycle. For synchronous shutdown flush, use saveNow().
     *
     * Guards against concurrent execution: if a save is already in flight,
     * waits for it to complete then re-checks dirty flag before starting another.
     */
    async saveNowAsync() {
        if (this._saveInFlight) {
            await this._saveInFlight;
            // After waiting, re-check if still dirty (the previous save may have handled it)
            if (!this.dirty)
                return;
        }
        this._saveInFlight = this._doSaveAsync();
        try {
            await this._saveInFlight;
        }
        finally {
            this._saveInFlight = null;
        }
    }
    /**
     * Assemble JSON string with incremental per-session caching.
     * Only dirty sessions are re-serialized; clean sessions use cached JSON fragments.
     */
    assembleStateJson() {
        this.updateDirtySessionCache();
        // Build sessions object from cached fragments
        const sessionParts = [];
        for (const [id, session] of Object.entries(this.state.sessions)) {
            let json = this.cachedSessionJsons.get(id);
            if (!json) {
                // Session not in cache (loaded from disk or set via direct state mutation)
                json = JSON.stringify(session);
                this.cachedSessionJsons.set(id, json);
            }
            sessionParts.push(`${JSON.stringify(id)}:${json}`);
        }
        this.pruneStaleCacheEntries();
        return this.buildPartialJson(sessionParts);
    }
    updateDirtySessionCache() {
        // Re-serialize dirty sessions and update cache
        for (const id of this.dirtySessions) {
            const session = this.state.sessions[id];
            if (session) {
                this.cachedSessionJsons.set(id, JSON.stringify(session));
            }
            else {
                this.cachedSessionJsons.delete(id);
            }
        }
        this.dirtySessions.clear();
    }
    pruneStaleCacheEntries() {
        // Prune stale cache entries (sessions removed via direct state mutation)
        if (this.cachedSessionJsons.size > Object.keys(this.state.sessions).length) {
            for (const cachedId of this.cachedSessionJsons.keys()) {
                if (!(cachedId in this.state.sessions)) {
                    this.cachedSessionJsons.delete(cachedId);
                }
            }
        }
    }
    buildPartialJson(sessionParts) {
        // Build final JSON: sessions from cache, everything else re-serialized (tiny)
        const sessionsJson = `{${sessionParts.join(',')}}`;
        // Serialize non-session fields individually (they're small)
        const parts = [
            `"sessions":${sessionsJson}`,
            `"tasks":${JSON.stringify(this.state.tasks)}`,
            `"ralphLoop":${JSON.stringify(this.state.ralphLoop)}`,
            `"config":${JSON.stringify(this.state.config)}`,
        ];
        // Optional fields
        if (this.state.globalStats) {
            parts.push(`"globalStats":${JSON.stringify(this.state.globalStats)}`);
        }
        if (this.state.tokenStats) {
            parts.push(`"tokenStats":${JSON.stringify(this.state.tokenStats)}`);
        }
        return `{${parts.join(',')}}`;
    }
    serializeState() {
        try {
            return this.assembleStateJson();
        }
        catch (assembleErr) {
            // Fallback to full serialization if incremental assembly fails
            console.warn('[StateStore] assembleStateJson failed, falling back to full serialize:', assembleErr);
            this.cachedSessionJsons.clear();
            this.dirtySessions.clear();
            try {
                return JSON.stringify(this.state);
            }
            catch (err) {
                console.error('[StateStore] Failed to serialize state (circular reference or invalid data):', err);
                this.consecutiveSaveFailures++;
                if (this.consecutiveSaveFailures >= MAX_CONSECUTIVE_FAILURES) {
                    console.error('[StateStore] Circuit breaker OPEN - serialization failing repeatedly');
                    this.circuitBreakerOpen = true;
                }
                return null;
            }
        }
    }
    async _doSaveAsync() {
        this.saveDeb.cancel();
        if (!this.dirty) {
            return;
        }
        // Circuit breaker: stop attempting writes after too many failures
        if (this.circuitBreakerOpen) {
            console.warn('[StateStore] Circuit breaker open - skipping save (too many consecutive failures)');
            return;
        }
        this.ensureDir();
        const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
        const backupPath = this.filePath + '.bak';
        // Step 1: Serialize state (validates it's JSON-safe)
        const json = this.serializeState();
        if (json === null)
            return;
        // Clear dirty flag BEFORE async I/O so mutations during write re-set it.
        // The state snapshot is already captured in `json` above.
        this.dirty = false;
        // Step 2: Create backup via file copy (async, no read+parse+write)
        try {
            await access(this.filePath);
            await copyFile(this.filePath, backupPath);
        }
        catch {
            // Backup failed or file doesn't exist yet - continue with write
        }
        // Step 3: Atomic write: write to temp file, then rename (async)
        try {
            await writeFile(tempPath, json, 'utf-8');
            await rename(tempPath, this.filePath);
            this._resetCircuitBreaker();
        }
        catch (err) {
            console.error('[StateStore] Failed to write state file:', err);
            // Re-mark dirty so the data is retried on the next save cycle
            this.dirty = true;
            this.consecutiveSaveFailures++;
            // Try to clean up temp file on error
            try {
                await unlink(tempPath);
            }
            catch {
                // Temp file may not exist
            }
            // Check circuit breaker threshold
            if (this.consecutiveSaveFailures >= MAX_CONSECUTIVE_FAILURES) {
                console.error('[StateStore] Circuit breaker OPEN - writes failing repeatedly');
                this.circuitBreakerOpen = true;
            }
        }
    }
    /**
     * Synchronous immediate write to disk using atomic write pattern.
     * Used by flushAll() during shutdown when async is not appropriate.
     * Prefer saveNowAsync() for normal operation.
     */
    saveNow() {
        this.saveDeb.cancel();
        if (!this.dirty) {
            return;
        }
        if (this.circuitBreakerOpen) {
            console.warn('[StateStore] Circuit breaker open - skipping save (too many consecutive failures)');
            return;
        }
        this.ensureDir();
        const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
        const backupPath = this.filePath + '.bak';
        const json = this.serializeState();
        if (json === null)
            return;
        // Backup via atomic copy (avoids reading entire file into memory)
        try {
            if (existsSync(this.filePath)) {
                copyFileSync(this.filePath, backupPath);
            }
        }
        catch {
            // Backup failed - continue with write
        }
        try {
            writeFileSync(tempPath, json, 'utf-8');
            renameSync(tempPath, this.filePath);
            // Clear dirty flag only AFTER successful write
            this.dirty = false;
            this._resetCircuitBreaker();
        }
        catch (err) {
            console.error('[StateStore] Failed to write state file:', err);
            this.consecutiveSaveFailures++;
            try {
                if (existsSync(tempPath))
                    unlinkSync(tempPath);
            }
            catch {
                /* ignore */
            }
            if (this.consecutiveSaveFailures >= MAX_CONSECUTIVE_FAILURES) {
                console.error('[StateStore] Circuit breaker OPEN - writes failing repeatedly');
                this.circuitBreakerOpen = true;
            }
        }
    }
    /**
     * Attempt to recover state from backup file.
     * Call this if main state file is corrupt.
     */
    recoverFromBackup() {
        const backupPath = this.filePath + '.bak';
        try {
            if (existsSync(backupPath)) {
                const backupContent = readFileSync(backupPath, 'utf-8');
                const parsed = JSON.parse(backupContent);
                this.state = this._mergeWithInitialState(parsed);
                console.log('[StateStore] Successfully recovered state from backup');
                // Reset circuit breaker after successful recovery
                this.circuitBreakerOpen = false;
                this.consecutiveSaveFailures = 0;
                return true;
            }
        }
        catch (err) {
            console.error('[StateStore] Failed to recover from backup:', err);
        }
        return false;
    }
    /**
     * Reset the circuit breaker (for manual intervention).
     */
    resetCircuitBreaker() {
        this.circuitBreakerOpen = false;
        this.consecutiveSaveFailures = 0;
        console.log('[StateStore] Circuit breaker manually reset');
    }
    /** Flushes any pending main state save. Call before shutdown. */
    flush() {
        this.saveNow();
    }
    /** Returns the full application state object. */
    getState() {
        return this.state;
    }
    /** Returns all session states keyed by session ID. */
    getSessions() {
        return this.state.sessions;
    }
    /** Returns a session state by ID, or null if not found. */
    getSession(id) {
        return this.state.sessions[id] ?? null;
    }
    /** Sets a session state and triggers a debounced save. */
    setSession(id, session) {
        this.state.sessions[id] = session;
        this.dirtySessions.add(id);
        this.save();
    }
    /** Removes a session state and triggers a debounced save. */
    removeSession(id) {
        delete this.state.sessions[id];
        this.cachedSessionJsons.delete(id);
        this.dirtySessions.delete(id);
        this.save();
    }
    /**
     * Cleans up stale sessions from state that don't have corresponding active sessions.
     * @param activeSessionIds - Set of currently active session IDs
     * @returns Number of sessions cleaned up
     */
    cleanupStaleSessions(activeSessionIds) {
        const allSessionIds = Object.keys(this.state.sessions);
        const cleaned = [];
        for (const sessionId of allSessionIds) {
            if (!activeSessionIds.has(sessionId)) {
                const name = this.state.sessions[sessionId]?.name;
                cleaned.push({ id: sessionId, name });
                delete this.state.sessions[sessionId];
                this.cachedSessionJsons.delete(sessionId);
                this.dirtySessions.delete(sessionId);
                // Also clean up Ralph state for this session
                this.ralphStates.delete(sessionId);
            }
        }
        if (cleaned.length > 0) {
            console.log(`[StateStore] Cleaned up ${cleaned.length} stale session(s) from state`);
            this.save();
        }
        return { count: cleaned.length, cleaned };
    }
    /** Returns all task states keyed by task ID. */
    getTasks() {
        return this.state.tasks;
    }
    /** Returns a task state by ID, or null if not found. */
    getTask(id) {
        return this.state.tasks[id] ?? null;
    }
    /** Sets a task state and triggers a debounced save. */
    setTask(id, task) {
        this.state.tasks[id] = task;
        this.save();
    }
    /** Removes a task state and triggers a debounced save. */
    removeTask(id) {
        delete this.state.tasks[id];
        this.save();
    }
    /** Returns the Ralph Loop state. */
    getRalphLoopState() {
        return this.state.ralphLoop;
    }
    /** Updates Ralph Loop state (partial merge) and triggers a debounced save. */
    setRalphLoopState(ralphLoop) {
        this.state.ralphLoop = { ...this.state.ralphLoop, ...ralphLoop };
        this.save();
    }
    // ========== Orchestrator Loop State Methods ==========
    /** Returns the orchestrator loop state, or null if never initialized. */
    getOrchestratorState() {
        return this.state.orchestrator ?? null;
    }
    /** Updates orchestrator loop state (partial merge) and triggers a debounced save. */
    setOrchestratorState(orchestrator) {
        if (this.state.orchestrator) {
            this.state.orchestrator = { ...this.state.orchestrator, ...orchestrator };
        }
        else {
            // First initialization — caller must provide full state
            this.state.orchestrator = orchestrator;
        }
        this.save();
    }
    /** Clears orchestrator state and triggers a debounced save. */
    clearOrchestratorState() {
        this.state.orchestrator = undefined;
        this.save();
    }
    /** Returns the application configuration. */
    getConfig() {
        return this.state.config;
    }
    /** Updates configuration (partial merge) and triggers a debounced save. */
    setConfig(config) {
        this.state.config = { ...this.state.config, ...config };
        this.save();
    }
    /** Resets all state to initial values and saves immediately. */
    reset() {
        this.state = createInitialState();
        this.state.config.stateFilePath = this.filePath;
        this.ralphStates.clear();
        this.cachedSessionJsons.clear();
        this.dirtySessions.clear();
        this.saveNow(); // Immediate save for reset operations
        this.saveRalphStatesNow();
    }
    // ========== Global Stats Methods ==========
    /** Returns global stats, creating initial stats if needed. */
    getGlobalStats() {
        if (!this.state.globalStats) {
            this.state.globalStats = createInitialGlobalStats();
        }
        return this.state.globalStats;
    }
    /**
     * Adds tokens and cost to global stats.
     * Call when a session is deleted to preserve its usage in lifetime stats.
     */
    addToGlobalStats(inputTokens, outputTokens, cost) {
        // Sanity check: reject absurdly large values
        if (inputTokens > MAX_SESSION_TOKENS || outputTokens > MAX_SESSION_TOKENS) {
            console.warn(`[StateStore] Rejected absurd global stats: input=${inputTokens}, output=${outputTokens}`);
            return;
        }
        // Reject negative values
        if (inputTokens < 0 || outputTokens < 0 || cost < 0) {
            console.warn(`[StateStore] Rejected negative global stats: input=${inputTokens}, output=${outputTokens}, cost=${cost}`);
            return;
        }
        const stats = this.getGlobalStats();
        stats.totalInputTokens += inputTokens;
        stats.totalOutputTokens += outputTokens;
        stats.totalCost += cost;
        stats.lastUpdatedAt = Date.now();
        this.save();
    }
    /** Increments the total sessions created counter. */
    incrementSessionsCreated() {
        const stats = this.getGlobalStats();
        stats.totalSessionsCreated += 1;
        stats.lastUpdatedAt = Date.now();
        this.save();
    }
    /**
     * Returns aggregate stats combining global (deleted sessions) + active sessions.
     * @param activeSessions Map of active session states
     */
    getAggregateStats(activeSessions) {
        const global = this.getGlobalStats();
        let activeInput = 0;
        let activeOutput = 0;
        let activeCost = 0;
        let activeCount = 0;
        for (const session of Object.values(activeSessions)) {
            activeInput += session.inputTokens ?? 0;
            activeOutput += session.outputTokens ?? 0;
            activeCost += session.totalCost ?? 0;
            activeCount++;
        }
        return {
            totalInputTokens: global.totalInputTokens + activeInput,
            totalOutputTokens: global.totalOutputTokens + activeOutput,
            totalCost: global.totalCost + activeCost,
            totalSessionsCreated: global.totalSessionsCreated,
            activeSessionsCount: activeCount,
        };
    }
    // ========== Token Stats Methods (Daily Tracking) ==========
    /** Maximum days to keep in daily history */
    static MAX_DAILY_HISTORY = 30;
    /**
     * Get or initialize token stats from state.
     */
    getTokenStats() {
        if (!this.state.tokenStats) {
            this.state.tokenStats = {
                daily: [],
                lastUpdated: Date.now(),
            };
        }
        return this.state.tokenStats;
    }
    /**
     * Get today's date string in YYYY-MM-DD format.
     */
    getTodayDateString() {
        const now = new Date();
        return now.toISOString().split('T')[0];
    }
    /**
     * Calculate estimated cost from tokens using Claude Opus pricing.
     * Input: $15/M tokens, Output: $75/M tokens
     */
    calculateEstimatedCost(inputTokens, outputTokens) {
        const inputCost = (inputTokens / 1000000) * 15;
        const outputCost = (outputTokens / 1000000) * 75;
        return inputCost + outputCost;
    }
    // Track unique sessions per day for accurate session count
    dailySessionIds = new Set();
    dailySessionDate = '';
    /**
     * Record token usage for today.
     * Accumulates tokens to today's entry, creating it if needed.
     * @param inputTokens Input tokens to add
     * @param outputTokens Output tokens to add
     * @param sessionId Optional session ID for unique session counting
     */
    recordDailyUsage(inputTokens, outputTokens, sessionId) {
        if (inputTokens <= 0 && outputTokens <= 0)
            return;
        // Sanity check: reject absurdly large values (max 1M tokens per recording)
        // Claude's context window is ~200k, so 1M per recording is already very generous
        const MAX_TOKENS_PER_RECORDING = 1_000_000;
        if (inputTokens > MAX_TOKENS_PER_RECORDING || outputTokens > MAX_TOKENS_PER_RECORDING) {
            console.warn(`[StateStore] Rejected absurd token values: input=${inputTokens}, output=${outputTokens}`);
            return;
        }
        const stats = this.getTokenStats();
        const today = this.getTodayDateString();
        // Reset daily session tracking on date change
        if (this.dailySessionDate !== today) {
            this.dailySessionIds.clear();
            this.dailySessionDate = today;
        }
        // Find or create today's entry
        let todayEntry = stats.daily.find((e) => e.date === today);
        if (!todayEntry) {
            todayEntry = {
                date: today,
                inputTokens: 0,
                outputTokens: 0,
                estimatedCost: 0,
                sessions: 0,
            };
            stats.daily.unshift(todayEntry); // Add to front (most recent first)
        }
        // Accumulate tokens
        todayEntry.inputTokens += inputTokens;
        todayEntry.outputTokens += outputTokens;
        todayEntry.estimatedCost = this.calculateEstimatedCost(todayEntry.inputTokens, todayEntry.outputTokens);
        // Only increment session count for unique sessions
        if (sessionId && !this.dailySessionIds.has(sessionId)) {
            this.dailySessionIds.add(sessionId);
            todayEntry.sessions = this.dailySessionIds.size;
        }
        // Prune old entries (keep last 30 days)
        if (stats.daily.length > StateStore.MAX_DAILY_HISTORY) {
            stats.daily = stats.daily.slice(0, StateStore.MAX_DAILY_HISTORY);
        }
        stats.lastUpdated = Date.now();
        this.save();
    }
    /**
     * Get daily stats for display.
     * @param days Number of days to return (default: 30)
     * @returns Array of daily entries, most recent first
     */
    getDailyStats(days = 30) {
        const stats = this.getTokenStats();
        return stats.daily.slice(0, days);
    }
    // ========== Inner State Methods (Ralph Loop tracking) ==========
    loadRalphStates() {
        try {
            if (existsSync(this.ralphStatePath)) {
                const data = readFileSync(this.ralphStatePath, 'utf-8');
                const parsed = JSON.parse(data);
                for (const [sessionId, state] of Object.entries(parsed)) {
                    this.ralphStates.set(sessionId, state);
                }
            }
        }
        catch (err) {
            console.error('Failed to load inner states:', err);
        }
    }
    // Debounced save for inner states
    saveRalphStates() {
        this.ralphStateDirty = true;
        if (this.ralphStateSaveDeb.isPending)
            return; // Already scheduled
        this.ralphStateSaveDeb.schedule(() => {
            this.saveRalphStatesNow();
        });
    }
    /**
     * Immediate save for inner states using atomic write pattern.
     * Writes to temp file first, then renames to prevent corruption on crash.
     */
    saveRalphStatesNow() {
        this.ralphStateSaveDeb.cancel();
        if (!this.ralphStateDirty) {
            return;
        }
        // Clear dirty flag only on success to enable retry on failure
        this.ensureDir();
        const data = Object.fromEntries(this.ralphStates);
        // Atomic write: write to temp file, then rename (atomic on POSIX)
        const tempPath = this.ralphStatePath + '.tmp';
        let json;
        try {
            json = JSON.stringify(data);
        }
        catch (err) {
            console.error('[StateStore] Failed to serialize Ralph state (circular reference or invalid data):', err);
            // Keep dirty flag true for retry - don't throw, let caller continue
            return;
        }
        try {
            writeFileSync(tempPath, json, 'utf-8');
            renameSync(tempPath, this.ralphStatePath);
            // Success - clear dirty flag
            this.ralphStateDirty = false;
        }
        catch (err) {
            console.error('[StateStore] Failed to write Ralph state file:', err);
            // Keep dirty flag true for retry on next save
            // Try to clean up temp file on error
            try {
                if (existsSync(tempPath)) {
                    unlinkSync(tempPath);
                }
            }
            catch (cleanupErr) {
                console.warn('[StateStore] Failed to cleanup temp file during Ralph state save error:', cleanupErr);
            }
            // Don't throw - let caller continue, retry on next save
        }
    }
    /** Returns inner state for a session, or null if not found. */
    getRalphState(sessionId) {
        return this.ralphStates.get(sessionId) ?? null;
    }
    /** Sets inner state for a session and triggers a debounced save. */
    setRalphState(sessionId, state) {
        this.ralphStates.set(sessionId, state);
        this.saveRalphStates();
    }
    /**
     * Updates inner state for a session (partial merge).
     * Creates initial state if none exists.
     * @returns The updated inner state.
     */
    updateRalphState(sessionId, updates) {
        let state = this.ralphStates.get(sessionId);
        if (!state) {
            state = createInitialRalphSessionState(sessionId);
        }
        state = { ...state, ...updates, lastUpdated: Date.now() };
        this.ralphStates.set(sessionId, state);
        this.saveRalphStates();
        return state;
    }
    /** Removes inner state for a session and triggers a debounced save. */
    removeRalphState(sessionId) {
        if (this.ralphStates.has(sessionId)) {
            this.ralphStates.delete(sessionId);
            this.saveRalphStates();
        }
    }
    /** Returns a copy of all inner states as a Map. */
    getAllRalphStates() {
        return new Map(this.ralphStates);
    }
    /** Flushes all pending saves (main and inner state). Call before shutdown. */
    flushAll() {
        // Save both states, catching errors to ensure both are attempted
        let mainError = null;
        let ralphError = null;
        try {
            this.saveNow();
        }
        catch (err) {
            mainError = err;
            console.error('[StateStore] Error flushing main state:', err);
        }
        try {
            this.saveRalphStatesNow();
        }
        catch (err) {
            ralphError = err;
            console.error('[StateStore] Error flushing Ralph state:', err);
        }
        // Log summary if any errors occurred
        if (mainError || ralphError) {
            console.warn('[StateStore] flushAll completed with errors - some state may not be persisted');
        }
    }
}
// Singleton instance
let storeInstance = null;
/**
 * Gets or creates the singleton StateStore instance.
 * @param filePath Optional custom file path (only used on first call).
 */
export function getStore(filePath) {
    if (!storeInstance) {
        storeInstance = new StateStore(filePath);
    }
    return storeInstance;
}
//# sourceMappingURL=state-store.js.map