import { spawn } from 'node:child_process';
import { normalizeClaudeEvent, normalizeCodexEvent } from './agent-normalizers.js';
import { providerCommitMessageDefaultModel, providerDefaultEffort, providerEffortOptions } from './agent-provider-catalog.js';
function splitLines(buffer, chunk) {
    const combined = buffer + chunk.toString('utf8');
    const parts = combined.split(/\r?\n/);
    return { lines: parts.slice(0, -1), rest: parts.at(-1) ?? '' };
}
function isRecord(value) {
    return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function stringifyUnknown(value) {
    if (typeof value === 'string')
        return value;
    if (value == null)
        return '';
    try {
        return JSON.stringify(value, null, 2);
    }
    catch {
        return String(value);
    }
}
function pickEffort(provider, model) {
    return providerEffortOptions(provider, model).find((option) => option.length > 0) ?? providerDefaultEffort(provider, model) ?? 'low';
}
function applyAssistantContent(state, event) {
    if (event.role !== 'assistant' || !event.content)
        return;
    if (event.eventType.includes('delta') || event.eventType.includes('partial')) {
        state.text += event.content;
        return;
    }
    state.text = event.content;
}
class JsonRpcStdioClient {
    process;
    onMessage;
    nextId = 1;
    pending = new Map();
    stdoutBuffer = '';
    closed = false;
    constructor(process, onMessage) {
        this.process = process;
        this.onMessage = onMessage;
        this.process.stdout?.on('data', (chunk) => this.handleStdout(chunk));
        this.process.on('close', () => {
            this.closed = true;
            this.flushStdoutBuffer();
            if (this.pending.size > 0) {
                this.rejectPending(new Error('Codex app-server closed before pending requests completed'));
            }
        });
        this.process.on('error', (error) => this.rejectPending(error instanceof Error ? error : new Error(String(error))));
    }
    request(method, params) {
        if (this.closed)
            return Promise.reject(new Error('Codex app-server is closed'));
        const id = this.nextId++;
        const payload = { method, id, params };
        return new Promise((resolve, reject) => {
            this.pending.set(id, { resolve, reject });
            this.write(payload);
        });
    }
    notify(method, params) {
        if (this.closed)
            return;
        this.write({ method, params });
    }
    write(payload) {
        this.process.stdin?.write(`${JSON.stringify(payload)}\n`);
    }
    handleStdout(chunk) {
        const result = splitLines(this.stdoutBuffer, chunk);
        this.stdoutBuffer = result.rest;
        for (const line of result.lines)
            this.handleLine(line);
    }
    flushStdoutBuffer() {
        const line = this.stdoutBuffer.trim();
        if (!line)
            return;
        this.stdoutBuffer = '';
        this.handleLine(line);
    }
    handleLine(line) {
        if (!line.trim())
            return;
        let parsed;
        try {
            parsed = JSON.parse(line);
        }
        catch {
            this.onMessage({ type: 'text', text: line });
            return;
        }
        if (isRecord(parsed) && typeof parsed.id === 'number' && ('result' in parsed || 'error' in parsed)) {
            const pending = this.pending.get(parsed.id);
            if (pending) {
                this.pending.delete(parsed.id);
                if ('error' in parsed && parsed.error) {
                    pending.reject(new Error(stringifyUnknown(parsed.error)));
                }
                else {
                    pending.resolve(parsed.result);
                }
            }
        }
        if (isRecord(parsed)) {
            this.onMessage(parsed);
        }
        else {
            this.onMessage({ type: 'text', value: parsed });
        }
    }
    rejectPending(error) {
        for (const pending of this.pending.values())
            pending.reject(error);
        this.pending.clear();
    }
}
export function buildCommitMessagePrompt(stagedFiles, stagedDiff) {
    const fileList = stagedFiles.length
        ? stagedFiles
            .map((file) => {
            if (file.status === 'renamed' && file.originalPath) {
                return `- renamed: ${file.originalPath} -> ${file.path}`;
            }
            return `- ${file.status}: ${file.path}`;
        })
            .join('\n')
        : '- (no staged files)';
    const diff = stagedDiff.trim() || '(no diff available)';
    return [
        'You are an expert software engineer writing a git commit message.',
        '',
        'Rules:',
        '- Output ONLY the commit message — no explanation, no markdown, no quotes.',
        '- First line: 50 chars or less, imperative mood, no period.',
        '  Good: "Add rate limiting to auth endpoints"',
        '  Bad: "Added some rate limiting stuff" / "This commit adds..."',
        '- Use a conventional commit prefix when it fits naturally:',
        '  feat, fix, refactor, test, docs, chore, perf, style, ci, build',
        '  Example: "feat: add dark mode toggle"',
        '- Skip the prefix for mixed or unclear changes.',
        '- Body (optional, blank line after subject): explain WHY, not what.',
        '  Wrap at 72 chars. Only include if the change needs context.',
        '- Never mention file names in the subject unless truly essential.',
        '- Focus on the intent and impact, not the mechanics.',
        '',
        'Staged files:',
        fileList,
        '',
        'Staged diff:',
        diff
    ].join('\n');
}
export class CommitMessageGenerator {
    async generate(input) {
        const model = input.model?.trim() || providerCommitMessageDefaultModel(input.provider) || '';
        if (input.provider === 'claude') {
            return this.generateClaude(input.cwd, model, input.prompt);
        }
        return this.generateCodex(input.cwd, model, input.prompt);
    }
    async generateClaude(cwd, model, prompt) {
        const effort = pickEffort('claude', model);
        const child = spawn('claude', [
            '-p',
            '--verbose',
            '--output-format',
            'stream-json',
            '--include-partial-messages',
            '--model',
            model,
            '--effort',
            effort,
            prompt
        ], {
            cwd,
            env: process.env,
            stdio: ['ignore', 'pipe', 'pipe']
        });
        return new Promise((resolve, reject) => {
            let stdoutBuffer = '';
            let stderrBuffer = '';
            const stderrLines = [];
            const state = { text: '' };
            child.stdout.on('data', (chunk) => {
                const result = splitLines(stdoutBuffer, chunk);
                stdoutBuffer = result.rest;
                for (const line of result.lines) {
                    if (!line.trim())
                        continue;
                    let raw;
                    try {
                        const parsed = JSON.parse(line);
                        raw = isRecord(parsed) ? parsed : { type: 'text', text: parsed };
                    }
                    catch {
                        raw = { type: 'text', text: line };
                    }
                    for (const event of normalizeClaudeEvent(raw)) {
                        applyAssistantContent(state, event);
                    }
                }
            });
            child.stderr.on('data', (chunk) => {
                const result = splitLines(stderrBuffer, chunk);
                stderrBuffer = result.rest;
                for (const line of result.lines) {
                    if (line.trim())
                        stderrLines.push(line.trim());
                }
            });
            child.on('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
            child.on('close', (code, signal) => {
                if (stdoutBuffer.trim()) {
                    try {
                        const parsed = JSON.parse(stdoutBuffer.trim());
                        const raw = isRecord(parsed) ? parsed : { type: 'text', text: parsed };
                        for (const event of normalizeClaudeEvent(raw))
                            applyAssistantContent(state, event);
                    }
                    catch {
                        for (const event of normalizeClaudeEvent({ type: 'text', text: stdoutBuffer.trim() })) {
                            applyAssistantContent(state, event);
                        }
                    }
                }
                if (code !== 0 && signal !== 'SIGTERM') {
                    reject(new Error(stderrLines.join('\n') || `claude exited with code ${code ?? 'unknown'}`));
                    return;
                }
                const message = state.text.trim();
                if (!message) {
                    reject(new Error(stderrLines.join('\n') || 'Claude did not return a commit message'));
                    return;
                }
                resolve(message);
            });
        });
    }
    async generateCodex(cwd, model, prompt) {
        const effort = pickEffort('codex', model);
        const child = spawn('codex', ['app-server'], {
            cwd,
            env: process.env,
            stdio: ['pipe', 'pipe', 'pipe']
        });
        return new Promise((resolve, reject) => {
            let stderrBuffer = '';
            const stderrLines = [];
            const state = { text: '' };
            let completed = false;
            let finished = false;
            const fail = (error) => {
                if (finished)
                    return;
                finished = true;
                reject(error);
                child.kill('SIGTERM');
            };
            const client = new JsonRpcStdioClient(child, (raw) => {
                for (const event of normalizeCodexEvent(raw)) {
                    applyAssistantContent(state, event);
                }
                if (raw.method === 'turn/completed') {
                    completed = true;
                    setTimeout(() => {
                        if (!child.killed)
                            child.kill('SIGTERM');
                    }, 100);
                }
            });
            child.stderr.on('data', (chunk) => {
                const result = splitLines(stderrBuffer, chunk);
                stderrBuffer = result.rest;
                for (const line of result.lines) {
                    if (line.trim())
                        stderrLines.push(line.trim());
                }
            });
            child.on('error', (error) => fail(error instanceof Error ? error : new Error(String(error))));
            child.on('close', (code, signal) => {
                if (finished)
                    return;
                finished = true;
                if (code !== 0 && signal !== 'SIGTERM' && !completed) {
                    reject(new Error(stderrLines.join('\n') || `codex exited with code ${code ?? 'unknown'}`));
                    return;
                }
                const message = state.text.trim();
                if (!message) {
                    reject(new Error(stderrLines.join('\n') || 'Codex did not return a commit message'));
                    return;
                }
                resolve(message);
            });
            void client
                .request('initialize', {
                clientInfo: { name: 'fatmux', title: 'fatmux', version: '2026.4.30-1' }
            })
                .then(async () => {
                client.notify('initialized', {});
                const threadResponse = await client.request('thread/start', {
                    cwd,
                    model,
                    approvalPolicy: 'never',
                    sandbox: 'read-only'
                });
                const threadId = (isRecord(threadResponse) && typeof threadResponse.threadId === 'string'
                    ? threadResponse.threadId
                    : isRecord(threadResponse) && isRecord(threadResponse.thread) && typeof threadResponse.thread.id === 'string'
                        ? threadResponse.thread.id
                        : null) ?? null;
                if (!threadId) {
                    throw new Error('Codex thread id not returned by app-server');
                }
                await client.request('turn/start', {
                    threadId,
                    input: [{ type: 'text', text: prompt }],
                    cwd,
                    model,
                    effort,
                    approvalPolicy: 'never',
                    sandboxPolicy: { type: 'readOnly' }
                });
            })
                .catch((error) => fail(error instanceof Error ? error : new Error(String(error))));
        });
    }
}
//# sourceMappingURL=commit-message-generator.js.map