/**
 * cursor.ts — Cursor headless invoker.
 *
 * Attempts to spawn: cursor-agent --headless --prompt "<prompt>"
 *
 * Falls through to pull mode (isAvailable → false) if the cursor-agent
 * binary is not in PATH.  Cursor does not ship a stable headless CLI as of
 * v0.42; this adapter is forward-compatible — once Cursor publishes
 * cursor-agent with headless support this invoker activates automatically.
 *
 * Output format (JSON frames on stdout, newline-delimited):
 *   { type: 'progress',    message, step, total }
 *   { type: 'edit',        file, diff }
 *   { type: 'token_usage', input_tokens, output_tokens }
 *   { type: 'done',        exit_code, summary? }
 *
 * Plain text lines (non-JSON) are emitted as stdout events.
 */
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { buildEnv, spawnAgent, readLines } from './base.js';
const execFileAsync = promisify(execFile);
/** Rough chars-to-tokens estimate (GPT-4 class: ~4 chars/token). */
function charsToTokens(n) { return Math.round(n / 4); }
export class CursorInvoker {
    id = 'cursor';
    label = 'Cursor';
    _version = undefined;
    async isAvailable() {
        return (await this.getVersion()) !== null;
    }
    async getVersion() {
        if (this._version !== undefined)
            return this._version;
        try {
            // cursor-agent is the headless cursor CLI (not yet in all distros).
            // Try --version; fall back to checking cursor --version (desktop IDE).
            const { stdout } = await execFileAsync('cursor-agent', ['--version'], { timeout: 5_000 });
            this._version = stdout.trim() || 'unknown';
            return this._version;
        }
        catch {
            // cursor-agent not in PATH → fall through to pull mode
            this._version = null;
            return null;
        }
    }
    async dispatch(req) {
        const prompt = buildPrompt(req);
        const env = buildEnv(req);
        const timeoutMs = req.timeoutMs ?? 600_000;
        const spawned = spawnAgent('cursor-agent', ['--headless', '--prompt', prompt], { cwd: req.workingDir, env, timeoutMs });
        const { process: child, pid, cancel } = spawned;
        let promptTokens = 0, completionTokens = 0, exitCode = 0;
        const filesChanged = [];
        let summary = '';
        let totalChars = 0;
        const t0 = Date.now();
        let _resolveResult;
        const result = new Promise(r => { _resolveResult = r; });
        async function* eventStream() {
            let step = 0;
            if (child.stdout) {
                for await (const line of readLines(child.stdout)) {
                    totalChars += line.length;
                    // Try JSON frame first
                    if (line.startsWith('{')) {
                        let frame;
                        try {
                            frame = JSON.parse(line);
                        }
                        catch { /* fall through to text */ }
                        if (frame) {
                            switch (frame.type) {
                                case 'progress':
                                    step = frame.step ?? step + 1;
                                    yield { kind: 'progress', step, total: frame.total ?? 0, message: String(frame.message ?? '') };
                                    continue;
                                case 'edit': {
                                    const filePath = String(frame.file ?? '');
                                    if (filePath && !filesChanged.includes(filePath))
                                        filesChanged.push(filePath);
                                    yield {
                                        kind: 'file-change',
                                        path: filePath,
                                        before: '',
                                        after: String(frame.diff ?? frame.content ?? ''),
                                    };
                                    continue;
                                }
                                case 'tool_call':
                                    yield { kind: 'tool-call', toolName: String(frame.tool ?? 'unknown'), args: frame.args };
                                    continue;
                                case 'tool_result':
                                    yield { kind: 'tool-result', toolName: String(frame.tool ?? 'unknown'), result: frame.result };
                                    continue;
                                case 'token_usage':
                                    promptTokens = frame.input_tokens ?? frame.prompt_tokens ?? promptTokens;
                                    completionTokens = frame.output_tokens ?? frame.completion_tokens ?? completionTokens;
                                    yield { kind: 'token-usage', prompt: promptTokens, completion: completionTokens };
                                    continue;
                                case 'done':
                                    exitCode = frame.exit_code ?? 0;
                                    summary = String(frame.summary ?? '');
                                    continue;
                                default:
                                    break;
                            }
                        }
                    }
                    // Plain text line
                    if (line.trim()) {
                        yield { kind: 'stdout', chunk: line };
                        step++;
                        yield { kind: 'progress', step, total: 0, message: line.slice(0, 80) };
                    }
                }
            }
            if (child.stderr) {
                for await (const line of readLines(child.stderr)) {
                    if (line.trim())
                        yield { kind: 'stderr', chunk: line };
                }
            }
            exitCode = await new Promise(resolve => {
                if (child.exitCode !== null) {
                    resolve(child.exitCode);
                    return;
                }
                child.once('exit', code => resolve(code ?? 0));
            });
            // If no token-usage frames were emitted, estimate from char count
            if (promptTokens === 0 && totalChars > 0) {
                promptTokens = charsToTokens(totalChars);
            }
            const costUsd = Math.round(((promptTokens * 4 + completionTokens * 16) / 1_000_000) * 1000) / 1000;
            _resolveResult({
                success: exitCode === 0,
                exitCode,
                tokensUsed: { prompt: promptTokens, completion: completionTokens },
                costUsd,
                filesChanged,
                diff: '',
                summary: summary || `cursor run completed in ${Date.now() - t0}ms${filesChanged.length ? `, changed ${filesChanged.length} file(s)` : ''}`,
                memoriesFormed: 0,
                durationMs: Date.now() - t0,
            });
            yield { kind: 'done', exitCode };
        }
        return { runId: req.runId, pid, events: eventStream(), cancel, result };
    }
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function buildPrompt(req) {
    const { goal, contextPack: cp } = req;
    const memLines = cp.memories.slice(0, 6).map(m => `[${m.tier}] ${m.content}`).join('\n');
    const fileList = cp.files.slice(0, 15).join(', ');
    return [
        goal,
        cp.memories.length ? `Context:\n${memLines}` : '',
        cp.files.length ? `Key files: ${fileList}` : '',
    ].filter(Boolean).join('\n\n');
}
