/**
 * claude-code.ts — Claude Code headless invoker.
 *
 * Spawns:  claude -p "<prompt>" --output-format stream-json --verbose
 * Parses:  NDJSON stream — each line is a JSON object with a "type" field.
 *
 * Recognized frame types:
 *   { type: 'assistant', message: { content: [...] } }
 *   { type: 'tool_use', name, input }
 *   { type: 'tool_result', tool_use_id, content }
 *   { type: 'usage', input_tokens, output_tokens }
 *   { type: 'result', subtype: 'success'|'error', ... }
 */
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { buildEnv, spawnAgent, readLines } from './base.js';
const execFileAsync = promisify(execFile);
const MIN_MAJOR = 1; // minimum supported claude CLI major version
export class ClaudeCodeInvoker {
    id = 'claude-code';
    label = 'Claude Code';
    _version = undefined; // undefined = not checked yet
    async isAvailable() {
        return (await this.getVersion()) !== null;
    }
    async getVersion() {
        if (this._version !== undefined)
            return this._version;
        try {
            const { stdout } = await execFileAsync('claude', ['--version'], { timeout: 5_000 });
            const match = stdout.trim().match(/(\d+)\.\d+/);
            if (!match) {
                this._version = null;
                return null;
            }
            if (parseInt(match[1], 10) < MIN_MAJOR) {
                this._version = null;
                return null;
            }
            this._version = stdout.trim();
            return this._version;
        }
        catch {
            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('claude', ['-p', prompt, '--output-format', 'stream-json', '--verbose'], {
            cwd: req.workingDir,
            env,
            timeoutMs,
        });
        const { process: child, pid, cancel } = spawned;
        // Accumulate state for the final DispatchResult
        let promptTokens = 0, completionTokens = 0;
        let exitCode = 0;
        const filesChanged = [];
        const t0 = Date.now();
        // result resolves from inside the generator when it's done (single-drain design).
        let _resolveResult;
        const result = new Promise(r => { _resolveResult = r; });
        // Single event async-generator — the caller is the sole consumer.
        // result promise resolves immediately before the 'done' event is yielded.
        async function* eventStream() {
            let step = 0;
            // Parse stdout as NDJSON
            if (child.stdout) {
                for await (const line of readLines(child.stdout)) {
                    let frame;
                    try {
                        frame = JSON.parse(line);
                    }
                    catch {
                        yield { kind: 'stdout', chunk: line };
                        continue;
                    }
                    switch (frame.type) {
                        case 'assistant': {
                            const content = frame.message?.content ?? [];
                            const text = content
                                .filter((b) => b.type === 'text')
                                .map((b) => b.text)
                                .join('');
                            if (text)
                                yield { kind: 'stdout', chunk: text };
                            // Extract tool_use blocks nested inside the assistant message
                            for (const block of content) {
                                if (block.type === 'tool_use') {
                                    yield { kind: 'tool-call', toolName: block.name ?? 'unknown', args: block.input };
                                }
                            }
                            step++;
                            yield { kind: 'progress', step, total: 0, message: `step ${step}` };
                            break;
                        }
                        case 'tool_use':
                            // Also handle top-level tool_use frames (some claude versions emit these)
                            yield { kind: 'tool-call', toolName: frame.name, args: frame.input };
                            break;
                        case 'tool_result':
                            yield { kind: 'tool-result', toolName: frame.tool_use_id ?? 'unknown', result: frame.content };
                            break;
                        case 'usage':
                            promptTokens += frame.input_tokens ?? 0;
                            completionTokens += frame.output_tokens ?? 0;
                            yield { kind: 'token-usage', prompt: promptTokens, completion: completionTokens };
                            break;
                        case 'result':
                            if (frame.subtype === 'error') {
                                yield { kind: 'error', error: frame.error ?? 'claude-code error' };
                            }
                            break;
                        default:
                            // Unrecognized frame — surface as raw stdout for debugging
                            yield { kind: 'stdout', chunk: line };
                    }
                }
            }
            // Drain stderr
            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));
            });
            // Resolve the result promise before yielding 'done' so callers can await both.
            _resolveResult({
                success: exitCode === 0,
                exitCode,
                tokensUsed: { prompt: promptTokens, completion: completionTokens },
                costUsd: estimateCost(promptTokens, completionTokens),
                filesChanged,
                diff: '',
                summary: `claude-code run completed in ${Date.now() - t0}ms`,
                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 memBlock = cp.memories.length
        ? `\n\n## Relevant memories\n${cp.memories.slice(0, 8).map(m => `- [${m.tier}] ${m.content}`).join('\n')}`
        : '';
    const fileBlock = cp.files.length
        ? `\n\n## Key files\n${cp.files.slice(0, 20).map(f => `- ${f}`).join('\n')}`
        : '';
    const crBlock = cp.crGraphContext
        ? `\n\n## Structural context\n${cp.crGraphContext.slice(0, 1200)}`
        : '';
    return `${goal}${memBlock}${fileBlock}${crBlock}\n\nRepo: ${cp.repoName}. Work inside ${cp.repoRoot}.`;
}
/** Rough USD estimate using Sonnet-class blended pricing (~$4/M in, $16/M out). */
function estimateCost(prompt, completion) {
    return Math.round(((prompt * 4 + completion * 16) / 1_000_000) * 1000) / 1000;
}
