/**
 * Persistent CLI session manager — eliminates cold-start delay by keeping
 * the Claude CLI process alive between messages.
 *
 * Instead of spawning a new CLI process per message (~5s cold start + MCP
 * handshake), we spawn once with `--input-format stream-json` and feed
 * messages via stdin. The CLI maintains conversation context internally.
 *
 * KEY LEARNINGS (Claude Code CLI v2.1.76):
 *
 * 1. Stdin message format (NDJSON):
 *    {"type":"user","message":{"role":"user","content":"..."},"parent_tool_use_id":null}
 *
 * 2. The CLI stays alive between messages when stdin is kept open.
 *    Each message gets its own system/init → stream_events → result cycle.
 *
 * 3. Turn boundary: the `result` event marks the end of a CLI turn, NOT
 *    `message_stop` from stream_event.
 *
 * 4. The CLI uses NDJSON for MCP stdio transport (not Content-Length framing).
 */
import { randomUUID } from 'node:crypto';
import { spawn as nodeSpawn } from 'node:child_process';
import { StreamJsonParser } from './streaming.js';
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
export class CliSession {
    options;
    sessionId = randomUUID();
    child = null;
    alive = false;
    stderrBuf = '';
    stdoutBuffer = '';
    activeTurn = null;
    idleTimer = null;
    cleanupFn = null;
    parser;
    log;
    spawnFn;
    idleTimeout;
    constructor(options) {
        this.options = options;
        this.log = options.logger;
        this.spawnFn = options.spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, { ...opts, stdio: opts.stdio }));
        this.idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT_MS;
        // Parser delegates to pushEvent which routes to activeTurn
        this.parser = new StreamJsonParser((event) => this.pushEvent(event));
    }
    get ready() {
        return this.alive;
    }
    /**
     * Inject a mock child process for testing.
     * @internal — test only
     */
    _injectForTest(child) {
        this.child = child;
        this.alive = true;
        this.stderrBuf = '';
        this.stdoutBuffer = '';
        child.stdout.on('data', (chunk) => {
            this.onStdoutData(chunk.toString());
        });
        child.on('exit', () => this.markDead());
        child.on('error', () => this.markDead());
    }
    /**
     * Spawn the CLI process. Must be called before send().
     */
    async spawn() {
        const { binPath, cwd, env, model, mcpConfigPath } = this.options;
        const args = [
            '-p',
            '--input-format',
            'stream-json',
            '--output-format',
            'stream-json',
            '--include-partial-messages',
            '--verbose',
            '--dangerously-skip-permissions',
            '--permission-mode',
            'bypassPermissions',
            '--setting-sources',
            'user,local',
            '--model',
            model,
        ];
        if (mcpConfigPath) {
            args.push('--mcp-config', mcpConfigPath, '--strict-mcp-config');
        }
        const spawnResult = this.spawnFn(binPath, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: env ?? process.env });
        const child = 'child' in spawnResult ? spawnResult.child : spawnResult;
        const cleanup = 'cleanup' in spawnResult ? spawnResult.cleanup : undefined;
        this.child = child;
        this.cleanupFn = cleanup ?? null;
        this.alive = true;
        this.stderrBuf = '';
        this.stdoutBuffer = '';
        child.stderr.on('data', (chunk) => {
            this.stderrBuf += chunk.toString();
        });
        child.stdout.on('data', (chunk) => {
            this.onStdoutData(chunk.toString());
        });
        child.on('exit', (code) => {
            this.log?.info('CLI session process exited', { sessionId: this.sessionId, exitCode: code });
            this.markDead();
        });
        child.on('error', (err) => {
            this.log?.error('CLI session process error', { sessionId: this.sessionId, err });
            this.markDead();
        });
        this.resetIdleTimer();
        this.log?.info('CLI session spawned', { sessionId: this.sessionId, cwd, model });
    }
    /**
     * Send a user message and stream back events.
     * Auto-respawns if the process has died.
     */
    async *send(userMessage, systemPromptPrefix) {
        if (!this.alive) {
            this.log?.info('CLI session dead, respawning', { sessionId: this.sessionId });
            await this.spawn();
        }
        this.resetIdleTimer();
        this.parser.reset();
        const content = systemPromptPrefix ? `${systemPromptPrefix}\n\n${userMessage}` : userMessage;
        // Write NDJSON message to stdin
        const ndjsonMessage = JSON.stringify({
            type: 'user',
            message: { role: 'user', content },
            parent_tool_use_id: null,
        }) + '\n';
        const turn = { resolve: () => { }, events: [], done: false };
        this.activeTurn = turn;
        // Track whether this turn saw a 'result' event (definitive turn end)
        let sawResult = false;
        // Wrap pushEvent to detect result-driven message_stop as turn end
        const originalPush = this.pushEvent.bind(this);
        this.parser = new StreamJsonParser((event) => {
            // The result event emits message_stop — but in session mode,
            // we need to detect it as the turn boundary
            if (event.type === 'message_stop' && !sawResult) {
                // This is a stream_event message_stop (API turn), not CLI turn end.
                // Push it but don't complete the turn.
                originalPush(event);
                return;
            }
            originalPush(event);
        });
        // Override parser feed to detect result events for turn completion
        const baseFeed = this.parser.feed.bind(this.parser);
        this.parser.feed = (line) => {
            // Check if this line is a result event before parsing
            try {
                let parsed = JSON.parse(line);
                if (parsed.type === 'stream_event' && parsed.event)
                    parsed = parsed.event;
                if (parsed.type === 'result') {
                    sawResult = true;
                }
            }
            catch {
                // Not JSON, let parser handle it
            }
            baseFeed(line);
            if (sawResult) {
                this.completeTurn();
            }
        };
        try {
            this.child.stdin.write(ndjsonMessage, (err) => {
                if (err) {
                    this.log?.error('stdin write error', { sessionId: this.sessionId, err });
                    this.markDead();
                    turn.done = true;
                    turn.resolve();
                }
            });
        }
        catch (err) {
            this.log?.error('stdin write exception', { sessionId: this.sessionId, err });
            this.markDead();
            throw new Error('CLI session stdin write failed');
        }
        // Yield events as they arrive
        while (!turn.done || turn.events.length > 0) {
            if (turn.events.length > 0) {
                yield turn.events.shift();
            }
            else {
                await new Promise((r) => {
                    turn.resolve = r;
                    setTimeout(r, 50);
                });
            }
        }
        // Yield any remaining events
        while (turn.events.length > 0) {
            yield turn.events.shift();
        }
        this.activeTurn = null;
        this.resetIdleTimer();
    }
    /**
     * Kill the CLI process.
     */
    kill() {
        this.clearIdleTimer();
        if (this.child && this.alive) {
            this.log?.info('Killing CLI session', { sessionId: this.sessionId });
            this.child.kill('SIGTERM');
            setTimeout(() => {
                if (this.child && !this.child.killed) {
                    this.child.kill('SIGKILL');
                }
            }, 2000);
        }
        this.markDead();
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    pushEvent(event) {
        if (!this.activeTurn)
            return;
        this.activeTurn.events.push(event);
        this.activeTurn.resolve();
    }
    completeTurn() {
        if (!this.activeTurn)
            return;
        this.activeTurn.done = true;
        this.activeTurn.resolve();
    }
    markDead() {
        this.alive = false;
        this.cleanupFn?.();
        this.cleanupFn = null;
        if (this.activeTurn && !this.activeTurn.done) {
            if (!this.activeTurn.events.some((e) => e.type === 'message_stop')) {
                this.activeTurn.events.push({ type: 'message_stop', finishReason: 'error' });
            }
            this.activeTurn.done = true;
            this.activeTurn.resolve();
        }
    }
    onStdoutData(data) {
        this.stdoutBuffer += data;
        const lines = this.stdoutBuffer.split('\n');
        this.stdoutBuffer = lines.pop() || '';
        for (const line of lines) {
            this.parser.feed(line);
        }
    }
    resetIdleTimer() {
        this.clearIdleTimer();
        this.idleTimer = setTimeout(() => {
            this.log?.info('CLI session idle timeout, killing', { sessionId: this.sessionId });
            this.kill();
        }, this.idleTimeout);
    }
    clearIdleTimer() {
        if (this.idleTimer) {
            clearTimeout(this.idleTimer);
            this.idleTimer = null;
        }
    }
}
// ---------------------------------------------------------------------------
// Session manager — cache keyed by identifier
// ---------------------------------------------------------------------------
const sessions = new Map();
/**
 * Get an existing session or create a new one.
 */
export function getOrCreateCliSession(key, options) {
    const existing = sessions.get(key);
    if (existing && existing.ready) {
        return existing;
    }
    // Kill stale session if present
    if (existing) {
        existing.kill();
        sessions.delete(key);
    }
    const session = new CliSession(options);
    sessions.set(key, session);
    return session;
}
/**
 * Kill a specific session.
 */
export function killCliSession(key) {
    const session = sessions.get(key);
    if (session) {
        session.kill();
        sessions.delete(key);
    }
}
/**
 * Kill all CLI sessions (for shutdown).
 */
export function killAllCliSessions() {
    for (const [, session] of sessions) {
        session.kill();
    }
    sessions.clear();
}
//# sourceMappingURL=cli-session.js.map