/**
 * @fileoverview Auto-compact, auto-clear, and auto-resume automation for Session.
 *
 * Monitors token counts and triggers /compact or /clear commands when
 * configurable thresholds are reached. Waits for Claude to be idle
 * before sending commands, with retry logic and mutual exclusion
 * (compact and clear never run simultaneously).
 *
 * Also implements auto-resume on usage limit ("token pause" control):
 * when enabled and Claude stops on a usage-limit message ("5-hour limit
 * reached ∙ resets 8pm" and friends — see usage-limit-patterns.ts), a timer
 * is armed for the parsed reset time plus a safety buffer, then Escape
 * (dismisses the rate-limit options dialog if open) and a "continue" prompt
 * are sent so work resumes automatically. If the session is still limited,
 * the fresh limit message re-arms the scheduler — that retry loop is the
 * safety net for clock skew and parse imprecision.
 *
 * @module session-auto-ops
 */
import { EventEmitter } from 'node:events';
import { detectUsageLimitPause } from './usage-limit-patterns.js';
// ============================================================================
// Timing Constants
// ============================================================================
/** Delay for auto-compact/clear retry attempts (2 seconds) */
const AUTO_RETRY_DELAY_MS = 2000;
/** Delay for auto-compact/clear initial check (1 second) */
const AUTO_INITIAL_DELAY_MS = 1000;
/** Cooldown after compact completes before re-enabling (10 seconds) */
const COMPACT_COOLDOWN_MS = 10000;
/** Cooldown after clear completes before re-enabling (5 seconds) */
const CLEAR_COOLDOWN_MS = 5000;
/**
 * Executes an action when the session becomes idle, retrying if currently working.
 *
 * @param action - The async action to execute once idle
 * @param isActive - Returns whether this operation is still active (not cancelled)
 * @param isWorking - Returns whether the session is currently working
 * @param isStopped - Returns whether the session has been stopped
 * @param retryMs - Delay between retry attempts when working
 * @param cooldownMs - Delay after action completes before calling onCooldownDone
 * @param setTimer - Stores the timer reference for cleanup
 * @param onCooldownDone - Called after cooldown to reset state
 */
async function executeWhenIdle(action, isActive, isWorking, isStopped, retryMs, cooldownMs, setTimer, onCooldownDone) {
    if (isStopped())
        return;
    if (!isActive())
        return;
    if (!isWorking()) {
        if (isStopped())
            return;
        await action();
        if (!isStopped()) {
            setTimer(setTimeout(() => {
                if (isStopped())
                    return;
                setTimer(null);
                onCooldownDone();
            }, cooldownMs));
        }
    }
    else {
        if (!isStopped()) {
            setTimer(setTimeout(() => executeWhenIdle(action, isActive, isWorking, isStopped, retryMs, cooldownMs, setTimer, onCooldownDone), retryMs));
        }
    }
}
// ============================================================================
// Auto-resume (usage-limit pause) constants
// ============================================================================
/** Safety buffer after the stated reset time before resuming (2 minutes) */
const RESUME_BUFFER_MS = 2 * 60_000;
/** Minimum delay before an overdue resume fires (lets output settle) */
const RESUME_MIN_DELAY_MS = 5_000;
/** Retry interval when the reset time is stale/past (5 minutes) */
const RESUME_RETRY_MS = 5 * 60_000;
/** Re-detections scheduling within this window of the current schedule are ignored */
const RESUME_DEDUP_TOLERANCE_MS = 90_000;
/** Delay between Escape (dialog dismiss) and the resume prompt */
const RESUME_ESC_DELAY_MS = 600;
/** Prompt sent to resume work after the limit resets */
const RESUME_PROMPT = 'continue';
/** Minimum valid threshold for auto-clear/compact (1000 tokens) */
const MIN_AUTO_THRESHOLD = 1000;
/** Maximum valid threshold for auto-clear/compact (500k tokens) */
const MAX_AUTO_THRESHOLD = 500_000;
/** Default auto-clear threshold when invalid value provided */
const DEFAULT_AUTO_CLEAR_THRESHOLD = 140_000;
/** Default auto-compact threshold when invalid value provided */
const DEFAULT_AUTO_COMPACT_THRESHOLD = 110_000;
/**
 * Events emitted by SessionAutoOps.
 */
/**
 * Manages auto-compact and auto-clear automation for a Session.
 *
 * When enabled, monitors token counts after each update and triggers
 * /compact or /clear commands when thresholds are exceeded. Ensures
 * mutual exclusion between compact and clear operations.
 */
export class SessionAutoOps extends EventEmitter {
    // Auto-compact state
    _autoCompactThreshold;
    _autoCompactEnabled = false;
    _autoCompactPrompt = '';
    _isCompacting = false;
    _autoCompactTimer = null;
    // Auto-clear state
    _autoClearThreshold;
    _autoClearEnabled = false;
    _isClearing = false;
    _autoClearTimer = null;
    // Auto-resume (usage-limit pause) state
    _autoResumeEnabled = false;
    _autoResumeTimer = null;
    /** Esc→continue gap timer; detections must NOT cancel a resume in flight */
    _resumeFollowupTimer = null;
    /** When the scheduled resume fires (epoch ms), null when not armed */
    _autoResumeAt = null;
    _limitPaused = false;
    _resumeAttempts = 0;
    callbacks;
    constructor(callbacks, config) {
        super();
        this.callbacks = callbacks;
        this._autoCompactThreshold = config?.compactThreshold ?? DEFAULT_AUTO_COMPACT_THRESHOLD;
        this._autoClearThreshold = config?.clearThreshold ?? DEFAULT_AUTO_CLEAR_THRESHOLD;
    }
    // ============================================================================
    // Auto-compact getters/setters
    // ============================================================================
    get autoCompactThreshold() {
        return this._autoCompactThreshold;
    }
    get autoCompactEnabled() {
        return this._autoCompactEnabled;
    }
    get autoCompactPrompt() {
        return this._autoCompactPrompt;
    }
    get isCompacting() {
        return this._isCompacting;
    }
    setAutoCompact(enabled, threshold, prompt) {
        this._autoCompactEnabled = enabled;
        if (threshold !== undefined) {
            if (threshold < MIN_AUTO_THRESHOLD || threshold > MAX_AUTO_THRESHOLD) {
                console.warn(`[SessionAutoOps ${this.callbacks.getSessionId()}] Invalid autoCompact threshold ${threshold}, must be between ${MIN_AUTO_THRESHOLD} and ${MAX_AUTO_THRESHOLD}. Using default ${DEFAULT_AUTO_COMPACT_THRESHOLD}.`);
                this._autoCompactThreshold = DEFAULT_AUTO_COMPACT_THRESHOLD;
            }
            else {
                this._autoCompactThreshold = threshold;
            }
        }
        if (prompt !== undefined) {
            this._autoCompactPrompt = prompt;
        }
    }
    // ============================================================================
    // Auto-clear getters/setters
    // ============================================================================
    get autoClearThreshold() {
        return this._autoClearThreshold;
    }
    get autoClearEnabled() {
        return this._autoClearEnabled;
    }
    get isClearing() {
        return this._isClearing;
    }
    setAutoClear(enabled, threshold) {
        this._autoClearEnabled = enabled;
        if (threshold !== undefined) {
            if (threshold < MIN_AUTO_THRESHOLD || threshold > MAX_AUTO_THRESHOLD) {
                console.warn(`[SessionAutoOps ${this.callbacks.getSessionId()}] Invalid autoClear threshold ${threshold}, must be between ${MIN_AUTO_THRESHOLD} and ${MAX_AUTO_THRESHOLD}. Using default ${DEFAULT_AUTO_CLEAR_THRESHOLD}.`);
                this._autoClearThreshold = DEFAULT_AUTO_CLEAR_THRESHOLD;
            }
            else {
                this._autoClearThreshold = threshold;
            }
        }
    }
    // ============================================================================
    // Auto-resume (usage-limit pause) — getters/setters
    // ============================================================================
    get autoResumeEnabled() {
        return this._autoResumeEnabled;
    }
    /** When the scheduled resume fires (epoch ms), or null when not armed. */
    get autoResumeAt() {
        return this._autoResumeAt;
    }
    /** True while the session is believed to be paused on a usage limit. */
    get isLimitPaused() {
        return this._limitPaused;
    }
    setAutoResume(enabled) {
        this._autoResumeEnabled = enabled;
        if (!enabled) {
            this._cancelAutoResume('disabled');
        }
    }
    /**
     * Restore auto-resume state after a Codeman restart. A persisted pending
     * schedule is re-armed; an overdue one fires shortly after boot (the limit
     * footer won't reprint on its own, so without this the pause would stall).
     */
    restoreAutoResume(enabled, resumeAt) {
        this._autoResumeEnabled = enabled;
        if (!enabled || !resumeAt)
            return;
        const now = Date.now();
        this._scheduleResume(Math.max(resumeAt, now + RESUME_MIN_DELAY_MS), resumeAt, 'restored');
    }
    // ============================================================================
    // Auto-resume — detection and scheduling
    // ============================================================================
    /**
     * Scan cleaned terminal output for a usage-limit pause message and (re)arm
     * the resume schedule. Called from the session's throttled parser path.
     */
    processCleanData(cleanData) {
        if (!this._autoResumeEnabled || this.callbacks.isStopped())
            return;
        // A resume is in flight (Esc sent, continue pending): output from our own
        // Escape can redraw the stale limit footer — don't let it re-arm and
        // cancel the continue. Fresh evidence arrives after the prompt is sent.
        if (this._resumeFollowupTimer)
            return;
        const detection = detectUsageLimitPause(cleanData);
        if (!detection)
            return;
        const now = Date.now();
        const overdue = detection.resetAt <= now;
        const fireAt = overdue
            ? now + RESUME_RETRY_MS // stale reset time → gentle retry loop
            : Math.max(detection.resetAt + RESUME_BUFFER_MS, now + RESUME_MIN_DELAY_MS);
        if (this._autoResumeTimer && this._autoResumeAt !== null) {
            // Already armed: the footer redraws constantly, so ignore re-detections
            // that land on (or later than) the current schedule. Only an EARLIER
            // parsed time replaces it — an overdue retry never preempts a real one.
            if (overdue || fireAt >= this._autoResumeAt - RESUME_DEDUP_TOLERANCE_MS)
                return;
        }
        this._scheduleResume(fireAt, detection.resetAt, detection.matched);
    }
    /**
     * Claude started working — the limit is lifted (or the user resumed
     * manually), so any pending auto-resume is obsolete.
     */
    notifyWorking() {
        this._resumeAttempts = 0;
        if (!this._limitPaused && !this._autoResumeTimer && !this._resumeFollowupTimer)
            return;
        this._cancelAutoResume('working');
    }
    _scheduleResume(fireAt, resetAt, matched) {
        if (this._autoResumeTimer) {
            clearTimeout(this._autoResumeTimer);
            this._autoResumeTimer = null;
        }
        this._limitPaused = true;
        this._autoResumeAt = fireAt;
        const delay = Math.max(fireAt - Date.now(), 0);
        console.log(`[SessionAutoOps ${this.callbacks.getSessionId()}] Usage-limit pause detected ("${matched.slice(0, 60)}"), auto-resume in ${Math.round(delay / 60000)}min`);
        this._autoResumeTimer = setTimeout(() => void this._fireResume(), delay);
        this.emit('limitPauseScheduled', { resetAt, resumeAt: fireAt, matched });
    }
    async _fireResume() {
        this._autoResumeTimer = null;
        if (!this._autoResumeEnabled || this.callbacks.isStopped())
            return;
        if (this.callbacks.isWorking()) {
            // Session resumed on its own (or via the user) — nothing to do.
            this._cancelAutoResume('working');
            return;
        }
        this._resumeAttempts++;
        const attempt = this._resumeAttempts;
        this._limitPaused = false; // optimistic: a fresh limit message re-arms us
        this._autoResumeAt = null;
        // Escape first: dismisses the rate-limit options dialog if Claude opened
        // one (harmless at an idle prompt), then the resume prompt after a beat.
        await this.callbacks.writeCommand('\x1b');
        this._resumeFollowupTimer = setTimeout(() => {
            this._resumeFollowupTimer = null;
            if (this.callbacks.isStopped())
                return;
            void this.callbacks.writeCommand(`${RESUME_PROMPT}\r`);
            this.emit('limitResume', { attempt });
        }, RESUME_ESC_DELAY_MS);
    }
    _cancelAutoResume(reason) {
        const wasArmed = this._autoResumeTimer !== null || this._resumeFollowupTimer !== null || this._limitPaused;
        if (this._autoResumeTimer) {
            clearTimeout(this._autoResumeTimer);
            this._autoResumeTimer = null;
        }
        if (this._resumeFollowupTimer) {
            clearTimeout(this._resumeFollowupTimer);
            this._resumeFollowupTimer = null;
        }
        this._limitPaused = false;
        this._autoResumeAt = null;
        if (wasArmed && reason !== 'stopped') {
            this.emit('limitResumeCancelled', { reason });
        }
    }
    // ============================================================================
    // Threshold checks
    // ============================================================================
    /**
     * Check if auto-compact should be triggered based on current token count.
     * Called after token count updates.
     */
    checkAutoCompact() {
        if (this.callbacks.isStopped())
            return;
        if (!this._autoCompactEnabled || this._isCompacting || this._isClearing)
            return;
        const totalTokens = this.callbacks.getTotalTokens();
        if (totalTokens >= this._autoCompactThreshold) {
            this._isCompacting = true;
            console.log(`[SessionAutoOps] Auto-compact triggered: ${totalTokens} tokens >= ${this._autoCompactThreshold} threshold`);
            const action = async () => {
                const compactCmd = this._autoCompactPrompt ? `/compact ${this._autoCompactPrompt}\r` : '/compact\r';
                await this.callbacks.writeCommand(compactCmd);
                this.emit('autoCompact', {
                    tokens: totalTokens,
                    threshold: this._autoCompactThreshold,
                    prompt: this._autoCompactPrompt || undefined,
                });
            };
            if (!this.callbacks.isStopped()) {
                this._autoCompactTimer = setTimeout(() => executeWhenIdle(action, () => this._isCompacting, () => this.callbacks.isWorking(), () => this.callbacks.isStopped(), AUTO_RETRY_DELAY_MS, COMPACT_COOLDOWN_MS, (timer) => {
                    this._autoCompactTimer = timer;
                }, () => {
                    this._isCompacting = false;
                }), AUTO_INITIAL_DELAY_MS);
            }
        }
    }
    /**
     * Check if auto-clear should be triggered based on current token count.
     * Called after token count updates.
     */
    checkAutoClear() {
        if (this.callbacks.isStopped())
            return;
        if (!this._autoClearEnabled || this._isClearing || this._isCompacting)
            return;
        const totalTokens = this.callbacks.getTotalTokens();
        if (totalTokens >= this._autoClearThreshold) {
            this._isClearing = true;
            console.log(`[SessionAutoOps] Auto-clear triggered: ${totalTokens} tokens >= ${this._autoClearThreshold} threshold`);
            const action = async () => {
                await this.callbacks.writeCommand('/clear\r');
                this.emit('autoClear', { tokens: totalTokens, threshold: this._autoClearThreshold });
            };
            if (!this.callbacks.isStopped()) {
                this._autoClearTimer = setTimeout(() => executeWhenIdle(action, () => this._isClearing, () => this.callbacks.isWorking(), () => this.callbacks.isStopped(), AUTO_RETRY_DELAY_MS, CLEAR_COOLDOWN_MS, (timer) => {
                    this._autoClearTimer = timer;
                }, () => {
                    this._isClearing = false;
                }), AUTO_INITIAL_DELAY_MS);
            }
        }
    }
    // ============================================================================
    // Cleanup
    // ============================================================================
    /**
     * Clear all timers and reset state. Called when the session stops.
     */
    destroy() {
        if (this._autoCompactTimer) {
            clearTimeout(this._autoCompactTimer);
            this._autoCompactTimer = null;
        }
        this._isCompacting = false;
        if (this._autoClearTimer) {
            clearTimeout(this._autoClearTimer);
            this._autoClearTimer = null;
        }
        this._isClearing = false;
        this._cancelAutoResume('stopped');
    }
}
//# sourceMappingURL=session-auto-ops.js.map