import { spawn } from 'node:child_process';
import { normalizeClaudeEvent, normalizeCodexEvent } from './agent-normalizers.js';
import { providerAdapter, providerAdapters } from './agent-provider-adapters.js';
import { normalizeProviderEffort, providerDefaultEffort, providerDefaultModel } from './agent-provider-catalog.js';
import { TelegramNotifier } from './telegram-notifier.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) ?? '' };
}
const CODEX_PLAN_PREFIX = 'Plan mode is enabled for this thread. Do not modify files or run mutating commands. Analyze the request and return a clear implementation plan unless the user explicitly asks to leave plan mode.\n\n';
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 stopReasonFromClaudeEvent(raw) {
    if (typeof raw.stop_reason === 'string')
        return raw.stop_reason;
    if (isRecord(raw.delta) && typeof raw.delta.stop_reason === 'string')
        return raw.delta.stop_reason;
    if (isRecord(raw.message) && typeof raw.message.stop_reason === 'string')
        return raw.message.stop_reason;
    return '';
}
function withCodexPlanMode(session, prompt) {
    return session.interactionMode === 'plan' ? `${CODEX_PLAN_PREFIX}${prompt}` : prompt;
}
function codexSandboxMode(permissionMode) {
    if (permissionMode === 'read-only' ||
        permissionMode === 'workspace-write' ||
        permissionMode === 'danger-full-access') {
        return permissionMode;
    }
    if (permissionMode === 'full-auto')
        return 'danger-full-access';
    return null;
}
function codexTurnPermissions(permissionMode) {
    const sandboxMode = codexSandboxMode(permissionMode);
    if (!sandboxMode)
        return {};
    if (sandboxMode === 'danger-full-access') {
        return { approvalPolicy: 'never', sandboxPolicy: { type: 'dangerFullAccess' } };
    }
    return {
        sandboxPolicy: sandboxMode === 'read-only' ? { type: 'readOnly' } : { type: 'workspaceWrite' }
    };
}
function codexThreadPermissions(permissionMode) {
    const sandboxMode = codexSandboxMode(permissionMode);
    if (!sandboxMode)
        return {};
    if (sandboxMode === 'danger-full-access') {
        return { approvalPolicy: 'never', sandbox: 'danger-full-access' };
    }
    return { sandbox: sandboxMode };
}
function threadIdFromValue(value) {
    if (!isRecord(value))
        return null;
    if (typeof value.threadId === 'string')
        return value.threadId;
    if (typeof value.thread_id === 'string')
        return value.thread_id;
    if (isRecord(value.thread) && typeof value.thread.id === 'string')
        return value.thread.id;
    return null;
}
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 buildPromptWithAttachments(prompt, attachments) {
    if (attachments.length === 0)
        return prompt;
    const lines = attachments.map((attachment, index) => {
        const type = attachment.kind === 'project_file' ? 'project file' : 'uploaded file';
        const size = typeof attachment.size === 'number' ? `, ${attachment.size} bytes` : '';
        return `${index + 1}. ${type}: ${attachment.name}${size}\n   path: ${attachment.path}`;
    });
    return [
        'The user attached files for this turn. Treat these filesystem paths as read-only context unless the user asks you to modify them.',
        '',
        ...lines,
        '',
        'User prompt:',
        prompt
    ].join('\n');
}
export function buildAgentCommand(session, cwd, prompt) {
    return providerAdapter(session.provider).buildCommand(session, cwd, prompt);
}
function deriveTitleFromPrompt(prompt) {
    const first = prompt.trimStart().split('\n')[0].trim();
    if (first.length <= 60)
        return first || prompt.slice(0, 60);
    const cut = first.slice(0, 60);
    const lastSpace = cut.lastIndexOf(' ');
    return (lastSpace > 30 ? cut.slice(0, lastSpace) : cut) + '…';
}
export class AgentManager {
    db;
    broadcast;
    extensions;
    running = new Map();
    telegram = new TelegramNotifier();
    constructor(db, broadcast, extensions = null) {
        this.db = db;
        this.broadcast = broadcast;
        this.extensions = extensions;
    }
    setExtensionManager(extensions) {
        this.extensions = extensions;
    }
    async providers() {
        return Promise.all(providerAdapters().map((adapter) => adapter.detect()));
    }
    markSessionRead(sessionId) {
        const session = this.db.getAgentSession(sessionId);
        if (!session)
            throw new Error('Agent session not found');
        const updated = this.db.markAgentSessionRead(sessionId);
        if (!updated)
            throw new Error('Agent session not found');
        if (updated.lastOpenedAt !== session.lastOpenedAt) {
            this.broadcast({ type: 'agent.status', session: updated });
        }
        return updated;
    }
    emitAgentEvent(session, message, event) {
        this.broadcast({
            type: 'agent.event',
            projectId: session.projectId,
            sessionId: session.id,
            message,
            event
        });
    }
    notifyTelegram(kind, session) {
        const project = this.db.getProject(session.projectId);
        const projectName = project?.name || 'Project';
        const settings = this.db.getAppSettings();
        void this.telegram.notify(settings, kind, projectName, session).catch(() => undefined);
    }
    maybeNotifyApprovalWait(session, event) {
        if (session.provider === 'codex') {
            if (event.eventType === 'ExecApprovalRequest') {
                this.notifyTelegram('waiting_for_approval', session);
            }
            return;
        }
        if (session.provider === 'claude' && stopReasonFromClaudeEvent(event.raw) === 'pause_turn') {
            this.notifyTelegram('waiting_for_approval', session);
        }
    }
    send(sessionId, prompt, attachmentIds = []) {
        const session = this.db.getAgentSession(sessionId);
        if (!session)
            throw new Error('Agent session not found');
        const project = this.db.getProject(session.projectId);
        if (!project)
            throw new Error('Project not found');
        const attachments = this.db.getDraftAgentAttachments(sessionId, attachmentIds);
        const providerPrompt = buildPromptWithAttachments(prompt, attachments);
        const userMessage = this.db.addAgentMessage({
            sessionId,
            role: 'user',
            content: prompt,
            eventType: 'user_message',
            metadata: {
                attachments: attachments.map((attachment) => ({
                    id: attachment.id,
                    kind: attachment.kind,
                    name: attachment.name,
                    path: attachment.path,
                    size: attachment.size,
                    mimeType: attachment.mimeType
                }))
            }
        });
        this.db.attachAgentAttachmentsToMessage(sessionId, attachmentIds, userMessage.id);
        this.emitAgentEvent(session, userMessage, { type: 'user_message' });
        if (session.title === 'Claude' || session.title === 'Codex') {
            const derived = deriveTitleFromPrompt(prompt);
            const updated = this.db.updateAgentSession(sessionId, { title: derived });
            if (updated)
                this.broadcast({ type: 'agent.status', session: updated });
        }
        const running = this.running.get(sessionId);
        if (running) {
            running.queue.push({ prompt: providerPrompt });
            const updated = this.db.updateAgentSession(sessionId, { status: 'queued' });
            if (updated)
                this.broadcast({ type: 'agent.status', session: updated });
            return userMessage;
        }
        this.run(session, project.path, providerPrompt);
        return userMessage;
    }
    cancel(sessionId) {
        const running = this.running.get(sessionId);
        if (running) {
            running.queue.length = 0;
            running.process.kill('SIGTERM');
            this.running.delete(sessionId);
        }
        const updated = this.db.updateAgentSession(sessionId, { status: 'cancelled' });
        if (updated)
            this.broadcast({ type: 'agent.status', session: updated });
    }
    updateSettings(sessionId, patch) {
        return this.updateSession(sessionId, patch);
    }
    updateSession(sessionId, patch) {
        const current = this.db.getAgentSession(sessionId);
        if (!current)
            throw new Error('Agent session not found');
        const updatesSettings = Object.hasOwn(patch, 'model') ||
            Object.hasOwn(patch, 'permissionMode') ||
            Object.hasOwn(patch, 'interactionMode') ||
            Object.hasOwn(patch, 'effort');
        if (updatesSettings && (current.status === 'running' || current.status === 'queued')) {
            throw new Error('Cannot update settings while the agent is running');
        }
        const filtered = {};
        if (patch.title !== undefined)
            filtered.title = patch.title.trim() || current.title;
        if (patch.archived !== undefined)
            filtered.archived = patch.archived;
        if (patch.model !== undefined)
            filtered.model = patch.model?.trim() || null;
        if (patch.permissionMode !== undefined)
            filtered.permissionMode = patch.permissionMode?.trim() || null;
        if (patch.interactionMode !== undefined)
            filtered.interactionMode = patch.interactionMode;
        const nextModel = patch.model !== undefined ? patch.model?.trim() || null : current.model;
        const nextEffort = patch.effort !== undefined ? patch.effort : current.effort;
        filtered.effort = nextEffort === null ? null : normalizeProviderEffort(current.provider, nextModel, nextEffort) || null;
        const updated = this.db.updateAgentSession(sessionId, filtered);
        if (!updated)
            throw new Error('Agent session not found');
        if (updatesSettings) {
            this.db.updateAgentProviderSettings(current.provider, {
                model: filtered.model,
                permissionMode: filtered.permissionMode,
                interactionMode: filtered.interactionMode,
                effort: filtered.effort
            });
        }
        this.broadcast({ type: 'agent.status', session: updated });
        return updated;
    }
    buildCommand(session, cwd, prompt) {
        const command = providerAdapter(session.provider).buildCommand(session, cwd, prompt, {
            extensions: this.extensions ?? undefined,
            projectId: session.projectId,
            projectPath: cwd
        });
        if (command.providerSessionId && !session.providerSessionId) {
            this.db.updateAgentSession(session.id, { providerSessionId: command.providerSessionId });
        }
        return command;
    }
    run(session, cwd, prompt) {
        const command = this.buildCommand(session, cwd, prompt);
        if (command.mode === 'app-server' && session.provider === 'codex') {
            this.runCodexAppServer(session, cwd, prompt, command);
            return;
        }
        this.runCli(session, cwd, prompt, command);
    }
    runCli(session, cwd, prompt, command) {
        const updated = this.db.updateAgentSession(session.id, { status: 'running', lastError: null });
        if (updated)
            this.broadcast({ type: 'agent.status', session: updated });
        const child = spawn(command.command, command.args, {
            cwd,
            env: process.env,
            stdio: ['ignore', 'pipe', 'pipe']
        });
        const running = { process: child, queue: [], partialMessages: new Map() };
        this.running.set(session.id, running);
        let stdoutBuffer = '';
        let stderrBuffer = '';
        child.stdout.on('data', (chunk) => {
            const result = splitLines(stdoutBuffer, chunk);
            stdoutBuffer = result.rest;
            for (const line of result.lines) {
                this.handleJsonLine(session, line);
            }
        });
        child.stderr.on('data', (chunk) => {
            const result = splitLines(stderrBuffer, chunk);
            stderrBuffer = result.rest;
            for (const line of result.lines) {
                if (!line.trim())
                    continue;
                const message = this.db.addAgentMessage({
                    sessionId: session.id,
                    role: 'tool',
                    content: line,
                    eventType: 'stderr'
                });
                this.emitAgentEvent(session, message, { type: 'stderr', text: line });
            }
        });
        child.on('error', (error) => {
            this.running.delete(session.id);
            const message = this.db.addAgentMessage({
                sessionId: session.id,
                role: 'error',
                content: error.message,
                eventType: 'process_error'
            });
            const errored = this.db.updateAgentSession(session.id, {
                status: 'error',
                lastError: error.message
            });
            this.emitAgentEvent(session, message, { type: 'process_error' });
            if (errored)
                this.broadcast({ type: 'agent.status', session: errored });
        });
        child.on('close', (code, signal) => {
            if (stdoutBuffer.trim())
                this.handleJsonLine(session, stdoutBuffer.trim());
            if (stderrBuffer.trim()) {
                const message = this.db.addAgentMessage({
                    sessionId: session.id,
                    role: 'tool',
                    content: stderrBuffer.trim(),
                    eventType: 'stderr'
                });
                this.emitAgentEvent(session, message, { type: 'stderr', text: stderrBuffer.trim() });
            }
            const current = this.running.get(session.id);
            this.running.delete(session.id);
            if (code === 0) {
                const idle = this.db.updateAgentSession(session.id, { status: 'idle', lastError: null });
                if (idle)
                    this.broadcast({ type: 'agent.status', session: idle });
                this.notifyTelegram('done', idle ?? session);
            }
            else if (signal !== 'SIGTERM') {
                const content = `${session.provider} exited with code ${code ?? 'unknown'}`;
                const message = this.db.addAgentMessage({
                    sessionId: session.id,
                    role: 'error',
                    content,
                    eventType: 'process_exit'
                });
                const errored = this.db.updateAgentSession(session.id, { status: 'error', lastError: content });
                this.emitAgentEvent(session, message, { type: 'process_exit', code });
                if (errored)
                    this.broadcast({ type: 'agent.status', session: errored });
            }
            const next = current?.queue.shift();
            if (next) {
                const refreshed = this.db.getAgentSession(session.id);
                if (refreshed)
                    this.run(refreshed, cwd, next.prompt);
            }
        });
    }
    runCodexAppServer(session, cwd, prompt, command) {
        const updated = this.db.updateAgentSession(session.id, { status: 'running', lastError: null });
        if (updated)
            this.broadcast({ type: 'agent.status', session: updated });
        const child = spawn(command.command, command.args, {
            cwd,
            env: process.env,
            stdio: ['pipe', 'pipe', 'pipe']
        });
        const running = { process: child, queue: [], partialMessages: new Map() };
        this.running.set(session.id, running);
        let stderrBuffer = '';
        let completed = false;
        let failed = false;
        const reportError = (error) => {
            if (failed)
                return;
            failed = true;
            this.running.delete(session.id);
            const message = this.db.addAgentMessage({
                sessionId: session.id,
                role: 'error',
                content: error.message,
                eventType: 'process_error'
            });
            const errored = this.db.updateAgentSession(session.id, {
                status: 'error',
                lastError: error.message
            });
            this.emitAgentEvent(session, message, { type: 'process_error' });
            if (errored)
                this.broadcast({ type: 'agent.status', session: errored });
        };
        const client = new JsonRpcStdioClient(child, (raw) => {
            this.handleJsonMessage(session, raw);
            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())
                    continue;
                const message = this.db.addAgentMessage({
                    sessionId: session.id,
                    role: 'tool',
                    content: line,
                    eventType: 'stderr'
                });
                this.emitAgentEvent(session, message, { type: 'stderr', text: line });
            }
        });
        child.on('error', (error) => {
            reportError(error instanceof Error ? error : new Error(String(error)));
        });
        child.on('close', (code, signal) => {
            if (stderrBuffer.trim()) {
                const message = this.db.addAgentMessage({
                    sessionId: session.id,
                    role: 'tool',
                    content: stderrBuffer.trim(),
                    eventType: 'stderr'
                });
                this.emitAgentEvent(session, message, { type: 'stderr', text: stderrBuffer.trim() });
            }
            const current = this.running.get(session.id);
            this.running.delete(session.id);
            if (!failed && (code === 0 || (signal === 'SIGTERM' && completed))) {
                const idle = this.db.updateAgentSession(session.id, { status: 'idle', lastError: null });
                if (idle)
                    this.broadcast({ type: 'agent.status', session: idle });
                this.notifyTelegram('done', idle ?? session);
            }
            else if (!failed && signal !== 'SIGTERM') {
                const content = `${session.provider} exited with code ${code ?? 'unknown'}`;
                const message = this.db.addAgentMessage({
                    sessionId: session.id,
                    role: 'error',
                    content,
                    eventType: 'process_exit'
                });
                const errored = this.db.updateAgentSession(session.id, { status: 'error', lastError: content });
                this.emitAgentEvent(session, message, { type: 'process_exit', code });
                if (errored)
                    this.broadcast({ type: 'agent.status', session: errored });
            }
            const next = current?.queue.shift();
            if (next) {
                const refreshed = this.db.getAgentSession(session.id);
                if (refreshed)
                    this.run(refreshed, cwd, next.prompt);
            }
        });
        void client
            .request('initialize', {
            clientInfo: { name: 'fatmux', title: 'fatmux', version: '2026.4.30-1' }
        })
            .then(async () => {
            client.notify('initialized', {});
            await this.startCodexTurn(client, session, cwd, prompt);
        })
            .catch((error) => {
            reportError(error instanceof Error ? error : new Error(String(error)));
            child.kill('SIGTERM');
        });
    }
    async startCodexTurn(client, session, cwd, prompt) {
        const model = session.model?.trim() || providerDefaultModel(session.provider);
        const effort = session.effort?.trim() || providerDefaultEffort(session.provider, model);
        const effectivePrompt = withCodexPlanMode(session, prompt);
        const turnPermissions = codexTurnPermissions(session.permissionMode);
        const threadPermissions = codexThreadPermissions(session.permissionMode);
        const baseThreadParams = {
            cwd,
            model: model || undefined,
            ...threadPermissions
        };
        let threadResponse;
        if (session.providerSessionId) {
            try {
                threadResponse = await client.request('thread/resume', {
                    threadId: session.providerSessionId,
                    ...baseThreadParams
                });
            }
            catch {
                threadResponse = await client.request('thread/start', baseThreadParams);
            }
        }
        else {
            threadResponse = await client.request('thread/start', baseThreadParams);
        }
        const threadId = threadIdFromValue(threadResponse) ?? session.providerSessionId;
        if (!threadId) {
            throw new Error('Codex thread id not returned by app-server');
        }
        if (threadId !== session.providerSessionId) {
            this.db.updateAgentSession(session.id, { providerSessionId: threadId });
        }
        await client.request('turn/start', {
            threadId,
            input: [{ type: 'text', text: effectivePrompt }],
            cwd,
            model: model || undefined,
            effort: effort || undefined,
            ...turnPermissions
        });
    }
    handleJsonLine(session, line) {
        if (!line.trim())
            return;
        let raw;
        try {
            const parsed = JSON.parse(line);
            raw = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : { type: 'text', value: parsed };
        }
        catch {
            const message = this.db.addAgentMessage({
                sessionId: session.id,
                role: 'assistant',
                content: line,
                eventType: 'text'
            });
            this.emitAgentEvent(session, message, { type: 'text' });
            return;
        }
        this.handleJsonMessage(session, raw);
    }
    handleJsonMessage(session, raw) {
        const normalized = session.provider === 'claude' ? normalizeClaudeEvent(raw) : normalizeCodexEvent(raw);
        for (const event of normalized) {
            this.persistAndBroadcast(session, event);
        }
    }
    persistAndBroadcast(session, event) {
        this.db.addAgentEvent(session.id, event.eventType, event.raw);
        if (event.providerSessionId) {
            const updated = this.db.updateAgentSession(session.id, { providerSessionId: event.providerSessionId });
            if (updated)
                this.broadcast({ type: 'agent.status', session: updated });
        }
        this.maybeNotifyApprovalWait(session, event);
        let message;
        if (event.role && event.content) {
            const running = this.running.get(session.id);
            const partial = event.itemId ? running?.partialMessages.get(event.itemId) : null;
            const nextContent = partial && event.eventType.endsWith('/delta')
                ? `${partial.content}${event.content}`
                : partial
                    ? event.content || partial.content
                    : event.content;
            if (partial) {
                const updatedMessage = this.db.updateAgentMessage(partial.messageId, {
                    content: nextContent,
                    eventType: event.eventType,
                    metadata: event.metadata ?? event.raw
                });
                if (updatedMessage) {
                    partial.content = nextContent;
                    message = updatedMessage;
                }
            }
            else {
                message = this.db.addAgentMessage({
                    sessionId: session.id,
                    role: event.role,
                    content: nextContent,
                    eventType: event.eventType,
                    metadata: event.metadata ?? event.raw
                });
                if (event.itemId && running) {
                    running.partialMessages.set(event.itemId, { messageId: message.id, content: nextContent });
                }
            }
        }
        this.emitAgentEvent(session, message, { type: event.eventType, raw: event.raw });
    }
}
//# sourceMappingURL=agent-manager.js.map