/**
 * @fileoverview Base Class for AI Checker Components
 *
 * Provides shared functionality for AI-powered checkers that spawn fresh Claude CLI
 * sessions to analyze terminal output. This base class handles:
 * - Mux session spawning and cleanup
 * - Temp file management for output capture
 * - Polling for completion markers
 * - Cooldown management
 * - Error handling and consecutive error tracking
 * - Event emission for state changes
 *
 * Subclasses implement:
 * - `muxNamePrefix`: Prefix for mux session names (e.g., 'codeman-aicheck-')
 * - `doneMarker`: Completion marker in output file (e.g., '__AICHECK_DONE__')
 * - `tempFilePrefix`: Prefix for temp files (e.g., 'codeman-aicheck')
 * - `logPrefix`: Prefix for log messages (e.g., '[AiIdleChecker]')
 * - `buildPrompt()`: Build the prompt from terminal buffer
 * - `parseVerdict()`: Parse the verdict from AI output
 * - `getPositiveVerdict()`: Return the "positive" verdict that doesn't trigger cooldown
 * - `defaultConfig`: Default configuration values
 *
 * @module ai-checker-base
 */
import { execSync, spawn as childSpawn } from 'node:child_process';
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { EventEmitter } from 'node:events';
import { getAugmentedPath, ANSI_ESCAPE_PATTERN_SIMPLE } from './utils/index.js';
import { AI_CHECK_MAX_BACKOFF_MS } from './config/ai-defaults.js';
import { getErrorMessage } from './types.js';
// ========== Security Validation ==========
/**
 * Validates that a model name is safe for shell use.
 * Model names should only contain alphanumeric characters, hyphens, underscores, and dots.
 */
function isValidModelName(model) {
    if (!model || typeof model !== 'string')
        return false;
    // Allow: alphanumeric, hyphens, underscores, dots, slashes (for model paths like claude/opus-4.5)
    // Max length 100 to prevent abuse
    return /^[a-zA-Z0-9._/-]+$/.test(model) && model.length <= 100;
}
/**
 * Validates that a mux session name is safe for shell use.
 * Names should only contain alphanumeric characters, hyphens, and underscores.
 */
function isValidMuxName(muxName) {
    if (!muxName || typeof muxName !== 'string')
        return false;
    return /^[a-zA-Z0-9_-]+$/.test(muxName) && muxName.length <= 100;
}
// ========== Constants ==========
/** Poll interval for checking temp file completion */
const POLL_INTERVAL_MS = 500;
// ========== AiCheckerBase Class ==========
/**
 * Abstract base class for AI-powered checkers.
 * Handles spawning Claude CLI in a mux session to analyze terminal output.
 *
 * @template V - The verdict type (e.g., 'IDLE' | 'WORKING' | 'ERROR')
 * @template C - The configuration type
 * @template R - The result type
 * @template S - The state type
 */
export class AiCheckerBase extends EventEmitter {
    config;
    sessionId;
    // State
    _status = 'ready';
    lastVerdict = null;
    lastReasoning = null;
    lastCheckDurationMs = null;
    cooldownEndsAt = null;
    cooldownTimer = null;
    consecutiveErrors = 0;
    totalChecks = 0;
    disabledReason = null;
    // Active check state
    checkMuxName = null;
    checkTempFile = null;
    checkPromptFile = null;
    checkPollTimer = null;
    checkTimeoutTimer = null;
    checkStartTime = 0;
    checkCancelled = false;
    checkResolve = null;
    constructor(sessionId, defaultConfig, config = {}) {
        super();
        this.sessionId = sessionId;
        // Filter out undefined values to prevent overwriting defaults
        const filteredConfig = Object.fromEntries(Object.entries(config).filter(([, v]) => v !== undefined));
        this.config = { ...defaultConfig, ...filteredConfig };
    }
    /** Get the current status */
    get status() {
        return this._status;
    }
    /** Get comprehensive state for UI display */
    getState() {
        return {
            status: this._status,
            lastVerdict: this.lastVerdict,
            lastReasoning: this.lastReasoning,
            lastCheckDurationMs: this.lastCheckDurationMs,
            cooldownEndsAt: this.cooldownEndsAt,
            consecutiveErrors: this.consecutiveErrors,
            totalChecks: this.totalChecks,
            disabledReason: this.disabledReason,
        };
    }
    /** Check if the checker is on cooldown */
    isOnCooldown() {
        if (this.cooldownEndsAt === null)
            return false;
        return Date.now() < this.cooldownEndsAt;
    }
    /** Get remaining cooldown time in ms */
    getCooldownRemainingMs() {
        if (this.cooldownEndsAt === null)
            return 0;
        return Math.max(0, this.cooldownEndsAt - Date.now());
    }
    /**
     * Run an AI check against the provided terminal buffer.
     * Spawns a fresh Claude CLI in a tmux session, captures output to temp file.
     *
     * @param terminalBuffer - Raw terminal output to analyze
     * @returns The verdict result
     */
    async check(terminalBuffer) {
        if (this._status === 'disabled') {
            return this.createErrorResult(`Disabled: ${this.disabledReason}`, 0);
        }
        if (this.isOnCooldown()) {
            return this.createErrorResult('On cooldown', 0);
        }
        if (this._status === 'checking') {
            return this.createErrorResult('Already checking', 0);
        }
        this._status = 'checking';
        this.checkCancelled = false;
        this.checkStartTime = Date.now();
        this.totalChecks++;
        this.emit('checkStarted');
        this.log(`Starting ${this.checkDescription}`);
        try {
            const result = await this.runCheck(terminalBuffer);
            if (this.checkCancelled) {
                return this.createErrorResult('Cancelled', Date.now() - this.checkStartTime);
            }
            this.lastVerdict = result.verdict;
            this.lastReasoning = result.reasoning;
            this.lastCheckDurationMs = result.durationMs;
            if (result.verdict === this.getPositiveVerdict()) {
                this.consecutiveErrors = 0;
                this._status = 'ready';
                this.log(`${this.checkDescription} verdict: ${result.verdict} (${result.durationMs}ms) - ${result.reasoning}`);
            }
            else if (result.verdict === this.getNegativeVerdict()) {
                this.consecutiveErrors = 0;
                this.startCooldown(this.config.cooldownMs);
                this.log(`${this.checkDescription} verdict: ${result.verdict} (${result.durationMs}ms) - ${result.reasoning}`);
            }
            else {
                this.handleError('Unexpected verdict');
            }
            this.emit('checkCompleted', result);
            return result;
        }
        catch (err) {
            const errorMsg = getErrorMessage(err);
            this.handleError(errorMsg);
            const result = this.createErrorResult(errorMsg, Date.now() - this.checkStartTime);
            this.emit('checkFailed', errorMsg);
            return result;
        }
        finally {
            this.cleanupCheck();
        }
    }
    /**
     * Cancel an in-progress check.
     * Kills the mux session and cleans up.
     */
    cancel() {
        if (this._status !== 'checking')
            return;
        this.log(`Cancelling ${this.checkDescription}`);
        this.checkCancelled = true;
        // Clear poll/timeout timers first to prevent race condition where
        // the poll timer fires between setting checkCancelled and cleanup
        this.cleanupCheck();
        // Resolve the pending promise after cleanup
        if (this.checkResolve) {
            this.checkResolve(this.createErrorResult('Cancelled', Date.now() - this.checkStartTime));
            this.checkResolve = null;
        }
        this._status = 'ready';
    }
    /** Reset all state for a new cycle */
    reset() {
        this.cancel();
        this.clearCooldown();
        this.lastVerdict = null;
        this.lastReasoning = null;
        this.lastCheckDurationMs = null;
        this.consecutiveErrors = 0;
        this._status = this.disabledReason ? 'disabled' : 'ready';
    }
    /** Update configuration at runtime */
    updateConfig(config) {
        // Filter out undefined values to prevent overwriting existing config
        const filteredConfig = Object.fromEntries(Object.entries(config).filter(([, v]) => v !== undefined));
        this.config = { ...this.config, ...filteredConfig };
        if (config.enabled === false) {
            this.disable('Disabled by config');
        }
        else if (config.enabled === true && this._status === 'disabled') {
            this.disabledReason = null;
            this._status = 'ready';
        }
    }
    /** Get current config */
    getConfig() {
        return { ...this.config };
    }
    // ========== Private Methods ==========
    async runCheck(terminalBuffer) {
        // Security: Validate model name before use in shell commands
        if (!isValidModelName(this.config.model)) {
            throw new Error(`Invalid model name: ${this.config.model.substring(0, 50)}`);
        }
        // Prepare the terminal buffer (strip ANSI, trim to maxContextChars)
        const stripped = terminalBuffer.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '');
        const trimmed = stripped.length > this.config.maxContextChars ? stripped.slice(-this.config.maxContextChars) : stripped;
        // Build the prompt
        const prompt = this.buildPrompt(trimmed);
        // Generate temp files and mux session name
        const shortId = this.sessionId.slice(0, 8);
        const timestamp = Date.now();
        this.checkTempFile = join(tmpdir(), `${this.tempFilePrefix}-${shortId}-${timestamp}.txt`);
        this.checkPromptFile = join(tmpdir(), `${this.tempFilePrefix}-prompt-${shortId}-${timestamp}.txt`);
        this.checkMuxName = `${this.muxNamePrefix}${shortId}`;
        // Security: Validate mux name before use in shell commands
        if (!isValidMuxName(this.checkMuxName)) {
            throw new Error(`Invalid mux name generated: ${this.checkMuxName.substring(0, 50)}`);
        }
        // Ensure output temp file exists (empty) so we can poll it
        writeFileSync(this.checkTempFile, '');
        // Write prompt to file to avoid E2BIG error (argument list too long)
        // The prompt can be 16KB+ which exceeds shell argument limits
        writeFileSync(this.checkPromptFile, prompt);
        // Build the command - read prompt from file via stdin to avoid argument size limits
        // Quote model name to prevent command injection (model names should be simple alphanumeric but be safe)
        const modelArg = `--model "${this.config.model.replace(/"/g, '\\"')}"`;
        const augmentedPath = getAugmentedPath();
        const claudeCmd = `cat "${this.checkPromptFile}" | claude -p ${modelArg} --output-format text`;
        const fullCmd = `export PATH="${augmentedPath}"; ${claudeCmd} > "${this.checkTempFile}" 2>&1; echo "${this.doneMarker}" >> "${this.checkTempFile}"; rm -f "${this.checkPromptFile}"`;
        // Spawn tmux session
        try {
            // Kill any leftover session with this name first (mux name already validated above)
            try {
                execSync(`tmux kill-session -t "${this.checkMuxName}" 2>/dev/null`, { timeout: 3000 });
            }
            catch {
                // No existing session, that's fine
            }
            const muxProcess = childSpawn('tmux', ['new-session', '-d', '-s', this.checkMuxName, 'bash', '-c', fullCmd], {
                detached: true,
                stdio: 'ignore',
            });
            muxProcess.unref();
        }
        catch (err) {
            throw new Error(`Failed to spawn ${this.checkDescription} tmux session: ${getErrorMessage(err)}`);
        }
        // Poll the temp file for completion
        return new Promise((resolve, reject) => {
            const startTime = this.checkStartTime;
            this.checkResolve = resolve;
            // Guard flag to prevent both poll and timeout from resolving (race condition)
            let resolved = false;
            this.checkPollTimer = setInterval(() => {
                if (this.checkCancelled || resolved) {
                    // Cancel or already resolved - stop polling
                    return;
                }
                try {
                    if (!this.checkTempFile || !existsSync(this.checkTempFile))
                        return;
                    const content = readFileSync(this.checkTempFile, 'utf-8');
                    if (content.includes(this.doneMarker)) {
                        resolved = true; // Mark as resolved first to prevent timeout race
                        const durationMs = Date.now() - startTime;
                        const result = this.parseOutput(content, durationMs);
                        this.checkResolve = null;
                        resolve(result);
                    }
                }
                catch {
                    // File might not be ready yet or was deleted during cleanup, keep polling
                }
            }, POLL_INTERVAL_MS);
            // Set timeout
            this.checkTimeoutTimer = setTimeout(() => {
                if (this._status === 'checking' && !this.checkCancelled && !resolved) {
                    resolved = true; // Mark as resolved first to prevent poll race
                    this.checkResolve = null;
                    reject(new Error(`${this.checkDescription} timed out after ${this.config.checkTimeoutMs}ms`));
                }
            }, this.config.checkTimeoutMs);
        });
    }
    parseOutput(content, durationMs) {
        // Remove the done marker and trim
        const output = content.replace(this.doneMarker, '').trim();
        if (!output) {
            return this.createErrorResult(`Empty output from ${this.checkDescription}`, durationMs);
        }
        // Delegate to subclass for verdict parsing
        const parsed = this.parseVerdict(output);
        if (!parsed) {
            return this.createErrorResult(`Could not parse verdict from: "${output.substring(0, 100)}"`, durationMs);
        }
        return this.createResult(parsed.verdict, parsed.reasoning, durationMs);
    }
    cleanupCheck() {
        // Clear poll timer
        if (this.checkPollTimer) {
            clearInterval(this.checkPollTimer);
            this.checkPollTimer = null;
        }
        // Clear timeout timer
        if (this.checkTimeoutTimer) {
            clearTimeout(this.checkTimeoutTimer);
            this.checkTimeoutTimer = null;
        }
        // Kill the tmux session
        if (this.checkMuxName) {
            const muxName = this.checkMuxName;
            try {
                execSync(`tmux kill-session -t "${muxName}" 2>/dev/null`, { timeout: 2000 });
            }
            catch {
                // Session may already be dead
            }
            this.checkMuxName = null;
        }
        // Delete temp files
        if (this.checkTempFile) {
            try {
                if (existsSync(this.checkTempFile)) {
                    unlinkSync(this.checkTempFile);
                }
            }
            catch {
                // Best effort cleanup
            }
            this.checkTempFile = null;
        }
        if (this.checkPromptFile) {
            try {
                if (existsSync(this.checkPromptFile)) {
                    unlinkSync(this.checkPromptFile);
                }
            }
            catch {
                // Best effort cleanup
            }
            this.checkPromptFile = null;
        }
    }
    handleError(errorMsg) {
        this.consecutiveErrors++;
        this.log(`${this.checkDescription} error (${this.consecutiveErrors}/${this.config.maxConsecutiveErrors}): ${errorMsg}`);
        if (this.consecutiveErrors >= this.config.maxConsecutiveErrors) {
            this.disable(`${this.config.maxConsecutiveErrors} consecutive errors: ${errorMsg}`);
        }
        else {
            // P1-005: Exponential backoff for errors
            // Base cooldown * 2^(consecutiveErrors-1), capped at 5 minutes
            const backoffMultiplier = Math.pow(2, this.consecutiveErrors - 1);
            const backoffCooldownMs = Math.min(this.config.errorCooldownMs * backoffMultiplier, AI_CHECK_MAX_BACKOFF_MS);
            this.log(`Exponential backoff: ${Math.round(backoffCooldownMs / 1000)}s (error #${this.consecutiveErrors})`);
            this.startCooldown(backoffCooldownMs);
        }
    }
    startCooldown(durationMs) {
        this.clearCooldown();
        this.cooldownEndsAt = Date.now() + durationMs;
        this._status = 'cooldown';
        this.emit('cooldownStarted', this.cooldownEndsAt);
        this.log(`Cooldown started: ${Math.round(durationMs / 1000)}s`);
        this.cooldownTimer = setTimeout(() => {
            this.cooldownEndsAt = null;
            this._status = 'ready';
            this.emit('cooldownEnded');
            this.log('Cooldown ended');
        }, durationMs);
    }
    clearCooldown() {
        if (this.cooldownTimer) {
            clearTimeout(this.cooldownTimer);
            this.cooldownTimer = null;
        }
        this.cooldownEndsAt = null;
        if (this._status === 'cooldown') {
            this._status = 'ready';
        }
    }
    disable(reason) {
        this.disabledReason = reason;
        this._status = 'disabled';
        this.clearCooldown();
        this.log(`${this.checkDescription} disabled: ${reason}`);
        this.emit('disabled', reason);
    }
    log(message) {
        this.emit('log', `${this.logPrefix} ${message}`);
    }
}
//# sourceMappingURL=ai-checker-base.js.map