/**
 * Cline Executor Service
 *
 * Infrastructure implementation of IAgentExecutor for the Cline CLI agent.
 * Executes prompts via the `cline` CLI subprocess in headless mode with
 * JSON output for structured parsing.
 *
 * Cline is a multi-provider agentic coding assistant that runs a full
 * agentic loop with file system access, terminal execution, and tool use.
 * It supports multiple LLM backends (Anthropic, OpenRouter, Together AI,
 * Ollama, etc.) configured via `cline auth`.
 *
 * Uses constructor dependency injection for the spawn function
 * to enable testability without mocking node:child_process directly.
 */
import { getCurrentPhase, getLogPrefix } from '../../feature-agent/log-context.js';
/** Features supported by Cline CLI */
const SUPPORTED_FEATURES = new Set(['streaming', 'system-prompt']);
/**
 * Executor service for the Cline agentic coding assistant.
 * Uses subprocess spawning to interact with the `cline` CLI in headless mode.
 */
export class ClineExecutorService {
    spawn;
    agentType = 'cline';
    /** When true, suppresses debug logging (set per-call via options.silent) */
    silent = false;
    constructor(spawn) {
        this.spawn = spawn;
    }
    /** Debug logging — writes to stdout so it appears in the worker log file */
    log(message) {
        if (this.silent)
            return;
        const ts = new Date().toISOString();
        process.stdout.write(`[${ts}] ${getCurrentPhase()}${getLogPrefix()}${message}\n`);
    }
    supportsFeature(feature) {
        return SUPPORTED_FEATURES.has(feature);
    }
    async execute(prompt, options) {
        this.silent = options?.silent ?? false;
        const args = this.buildArgs(prompt, options);
        const spawnOpts = this.buildSpawnOptions(options);
        this.log(`Spawning: cline ${args.map((a) => (a.length > 80 ? `${a.slice(0, 77)}...` : a)).join(' ')}`);
        this.log(`Spawn cwd: ${spawnOpts.cwd ?? '(inherited)'}`);
        const proc = this.spawn('cline', args, spawnOpts);
        this.log(`Subprocess PID: ${proc.pid ?? 'undefined (spawn may have failed)'}`);
        this.log(`Prompt length: ${prompt.length} chars`);
        return new Promise((resolve, reject) => {
            let lineBuffer = '';
            let stderr = '';
            let timedOut = false;
            let timeoutId;
            // Accumulated result text from JSON events
            let resultText = '';
            if (options?.timeout) {
                timeoutId = setTimeout(() => {
                    timedOut = true;
                    proc.kill();
                }, options.timeout);
            }
            const processLine = (line) => {
                this.logStreamEvent(line);
                try {
                    const parsed = JSON.parse(line);
                    // Cline JSON output has { type: "say"|"ask", text: "...", ts: ..., partial: bool }
                    if (parsed.type === 'say' && typeof parsed.text === 'string' && !parsed.partial) {
                        resultText += parsed.text;
                    }
                }
                catch {
                    // Non-JSON line — accumulate as raw text
                    if (line.trim()) {
                        resultText += line;
                    }
                }
            };
            proc.stdout?.on('data', (chunk) => {
                lineBuffer += chunk.toString();
                const lines = lineBuffer.split('\n');
                lineBuffer = lines.pop() ?? '';
                for (const line of lines) {
                    const trimmed = line.trim();
                    if (trimmed)
                        processLine(trimmed);
                }
            });
            proc.stderr?.on('data', (chunk) => {
                const data = chunk.toString();
                stderr += data;
                this.log(`stderr: ${data.trimEnd()}`);
            });
            proc.on('error', (error) => {
                this.log(`Process error event: ${error.message}`);
                if (timeoutId)
                    clearTimeout(timeoutId);
                if (error.code === 'ENOENT') {
                    reject(new Error('Cline CLI ("cline") not found. Please install it: npm install -g cline'));
                }
                else {
                    reject(error);
                }
            });
            proc.on('close', (code) => {
                // Flush remaining buffer
                if (lineBuffer.trim())
                    processLine(lineBuffer.trim());
                this.log(`Process closed with code ${code}, result=${resultText.length} chars`);
                if (timeoutId)
                    clearTimeout(timeoutId);
                if (timedOut) {
                    reject(new Error('Agent execution timed out'));
                    return;
                }
                if (code !== 0 && code !== null) {
                    reject(new Error(stderr.trim() || `Process exited with code ${code}`));
                    return;
                }
                resolve({ result: resultText });
            });
        });
    }
    async *executeStream(prompt, options) {
        this.silent = options?.silent ?? false;
        const args = this.buildArgs(prompt, options);
        const spawnOpts = this.buildSpawnOptions(options);
        const proc = this.spawn('cline', args, spawnOpts);
        let lineBuffer = '';
        let stderr = '';
        const queue = [];
        let resolve = null;
        let error = null;
        function enqueue(event) {
            queue.push(event);
            if (resolve) {
                resolve();
                resolve = null;
            }
        }
        function waitForItem() {
            if (queue.length > 0)
                return Promise.resolve();
            return new Promise((r) => {
                resolve = r;
            });
        }
        proc.stdout?.on('data', (chunk) => {
            lineBuffer += chunk.toString();
            const lines = lineBuffer.split('\n');
            lineBuffer = lines.pop() ?? '';
            for (const line of lines) {
                const trimmed = line.trim();
                if (!trimmed)
                    continue;
                const event = this.parseStreamLine(trimmed);
                if (event) {
                    enqueue(event);
                }
            }
        });
        proc.stderr?.on('data', (chunk) => {
            stderr += chunk.toString();
        });
        proc.on('error', (err) => {
            error = err;
            enqueue(null);
        });
        proc.on('close', (code) => {
            if (lineBuffer.trim()) {
                const event = this.parseStreamLine(lineBuffer.trim());
                if (event)
                    enqueue(event);
            }
            if (code !== 0 && code !== null && stderr.trim()) {
                enqueue({
                    type: 'error',
                    content: stderr.trim(),
                    timestamp: new Date(),
                });
            }
            enqueue(null);
        });
        while (true) {
            await waitForItem();
            const item = queue.shift();
            if (item === null || item === undefined) {
                if (error !== null) {
                    yield {
                        type: 'error',
                        content: error.message,
                        timestamp: new Date(),
                    };
                }
                return;
            }
            yield item;
        }
    }
    /**
     * Log a Cline JSON line as a human-readable event in the worker log.
     */
    logStreamEvent(line) {
        try {
            const parsed = JSON.parse(line);
            if (parsed.type === 'say' && parsed.text) {
                const preview = parsed.text.length > 200 ? `${parsed.text.slice(0, 197)}...` : parsed.text;
                this.log(`[text] ${preview.replace(/\n/g, ' ')}`);
                return;
            }
            if (parsed.type === 'ask') {
                this.log(`[ask] ${(parsed.text ?? '').slice(0, 200).replace(/\n/g, ' ')}`);
                return;
            }
        }
        catch {
            if (line.length > 0) {
                this.log(`[raw] ${line}`);
            }
        }
    }
    /**
     * Build CLI arguments for cline headless execution.
     *
     * Uses `-y` for fully autonomous execution (no approval prompts)
     * and `--json` for machine-readable newline-delimited JSON output.
     */
    buildArgs(prompt, options) {
        const args = ['-y', '--json'];
        if (options?.model)
            args.push('--model', options.model);
        if (options?.cwd)
            args.push('--cwd', options.cwd);
        if (options?.timeout)
            args.push('--timeout', String(Math.ceil(options.timeout / 1000)));
        // The prompt is the last positional argument
        args.push(prompt);
        return args;
    }
    buildSpawnOptions(options) {
        const spawnOpts = {};
        if (options?.cwd)
            spawnOpts.cwd = options.cwd;
        // Explicitly pipe stdio so streams are available even when parent disconnects
        spawnOpts.stdio = ['pipe', 'pipe', 'pipe'];
        if (process.platform === 'win32') {
            spawnOpts.windowsHide = true;
        }
        // Strip CLAUDECODE env var to prevent nested session errors
        const { CLAUDECODE: _, ...cleanEnv } = process.env;
        spawnOpts.env = cleanEnv;
        return spawnOpts;
    }
    parseStreamLine(line) {
        try {
            const parsed = JSON.parse(line);
            // Cline JSON format: { type: "say"|"ask", text: "...", ts: ..., partial: bool }
            if (parsed.type === 'say' && typeof parsed.text === 'string') {
                if (parsed.partial) {
                    return {
                        type: 'progress',
                        content: parsed.text,
                        timestamp: new Date(),
                    };
                }
                return {
                    type: 'result',
                    content: parsed.text,
                    timestamp: new Date(),
                };
            }
            if (parsed.type === 'ask') {
                return {
                    type: 'progress',
                    content: parsed.text ?? '',
                    timestamp: new Date(),
                };
            }
            if (parsed.type === 'error') {
                return {
                    type: 'error',
                    content: parsed.text ?? parsed.message ?? '',
                    timestamp: new Date(),
                };
            }
            // Generic content
            if (parsed.text || parsed.message) {
                const rawContent = parsed.text ?? parsed.message;
                const content = typeof rawContent === 'string' ? rawContent : JSON.stringify(rawContent);
                return {
                    type: 'progress',
                    content,
                    timestamp: new Date(),
                };
            }
            return null;
        }
        catch {
            return {
                type: 'progress',
                content: line,
                timestamp: new Date(),
            };
        }
    }
}
