/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
import { detectRuntimes } from '../services/runtime-detector.js';
import { buildContext } from '../services/context-builder.js';
import { filterOutput } from '../services/output-filter.js';
import { generateSessionId, spawnChat, getSession, getSessionHistory, touchSession, kill, } from '../services/chat-process-manager.js';
import { createLocalLlmSession, getLocalLlmSession, abortLocalLlmSession } from '../services/local-llm-adapter.js';
import { listConversations, loadConversation, saveConversation, deleteConversation, isValidConversationId, } from '../../services/chat-history.js';
import { getWorkspaceCwd } from '../middleware/workspace.js';
import { readManifest, getEntryFile } from '../../services/context-vault.js';
const TEXT_MIME_PREFIXES = ['text/', 'application/json', 'application/x-yaml', 'application/yaml'];
const IMAGE_MIME_PREFIXES = ['image/'];
/**
 * Parses vault references from the message (format: `[attached: name (vault:path)]`)
 * and resolves them to actual file content for the context builder.
 */
function resolveVaultAttachments(workspace, message) {
    const vaultRefPattern = /\[attached:\s*[^\]]*\(vault:([^)]+)\)\]/g;
    const refs = [];
    let match;
    while ((match = vaultRefPattern.exec(message)) !== null) {
        refs.push(match[1]);
    }
    if (refs.length === 0)
        return [];
    const manifest = readManifest(workspace);
    const results = [];
    for (const ref of refs) {
        // Try to find the entry by matching folder/filename pattern
        const entry = manifest.entries.find((e) => {
            const entryPath = `${e.folder}/${e.filename}`;
            return entryPath === ref || e.filename === ref || ref.endsWith(e.filename);
        });
        if (!entry)
            continue;
        const filePath = getEntryFile(workspace, entry.id);
        if (!filePath)
            continue;
        const isText = TEXT_MIME_PREFIXES.some((p) => entry.mimeType.startsWith(p));
        const isImage = IMAGE_MIME_PREFIXES.some((p) => entry.mimeType.startsWith(p));
        if (isText) {
            try {
                const content = readFileSync(filePath, 'utf-8');
                results.push({ path: `${entry.folder}/${entry.filename}`, content, tags: entry.tags });
            }
            catch {
                // Skip unreadable files
            }
        }
        else if (isImage) {
            // For images, include base64 content so the LLM can reference it
            try {
                const buf = readFileSync(filePath);
                const b64 = buf.toString('base64');
                const content = `[Image: ${entry.filename} (${entry.mimeType}, ${entry.size} bytes)]\nbase64: ${b64}`;
                results.push({ path: `${entry.folder}/${entry.filename}`, content, tags: entry.tags });
            }
            catch {
                // Skip unreadable files
            }
        }
    }
    return results;
}
function readLocalLlmConfig(workspace) {
    const configPath = join(workspace, '.setup-agents', 'config.json');
    if (!existsSync(configPath))
        return null;
    try {
        const raw = readFileSync(configPath, 'utf8');
        const config = JSON.parse(raw);
        if (!config.localLlm?.endpoint || !config.localLlm.model)
            return null;
        return {
            endpoint: config.localLlm.endpoint,
            model: config.localLlm.model,
            provider: config.localLlm.provider,
        };
    }
    catch {
        return null;
    }
}
function handleAssistantBlock(block, write) {
    if (block.type === 'text' && block.text) {
        write('token', JSON.stringify({ content: block.text }));
    }
    else if (block.type === 'tool_use') {
        write('activity', JSON.stringify({ type: 'tool_use', tool: block.name }));
        const input = block.input ?? {};
        const summary = block.name === 'Bash' ? String(input.command ?? '') : String(input.file_path ?? input.description ?? '');
        write('tool_call', JSON.stringify({ id: block.id, tool: block.name, input: summary.slice(0, 500) }));
    }
    else if (block.type === 'thinking') {
        write('activity', JSON.stringify({ type: 'thinking' }));
    }
}
// eslint-disable-next-line complexity
function processStreamLine(line, write) {
    if (!line.trim())
        return;
    try {
        const evt = JSON.parse(line);
        if (evt.type === 'assistant' && evt.message?.content) {
            for (const block of evt.message.content)
                handleAssistantBlock(block, write);
        }
        else if (evt.type === 'user' && evt.message?.content) {
            for (const block of evt.message.content) {
                if (block.type === 'tool_result' && block.id) {
                    write('tool_result', JSON.stringify({ id: block.id, output: String(evt.tool_use_result?.stdout ?? '').slice(0, 2000) }));
                    write('activity', JSON.stringify(null));
                }
            }
        }
        else if (evt.type === 'system' && evt.subtype === 'thinking_tokens') {
            write('activity', JSON.stringify({ type: 'thinking' }));
        }
    }
    catch {
        write('token', JSON.stringify({ content: line }));
    }
}
export function chatRoute(cwd) {
    const app = new Hono();
    // GET /api/v1/chat/runtimes — returns detected AI runtimes
    app.get('/runtimes', async (c) => {
        const workspace = getWorkspaceCwd(c, cwd);
        const result = await detectRuntimes(workspace);
        return c.json(result);
    });
    // POST /api/v1/chat/send — start or continue a chat session
    app.post('/send', async (c) => {
        const body = await c.req.json();
        if (!body.message || body.message.trim().length === 0) {
            return c.json({ error: 'Message is required' }, 400);
        }
        const workspace = body.workspace ?? getWorkspaceCwd(c, cwd);
        const runtime = body.runtime ?? 'claude';
        const sessionId = body.sessionId ?? generateSessionId();
        // Resolve vault attachments from message references
        const vaultFiles = resolveVaultAttachments(workspace, body.message);
        const contextOpts = vaultFiles.length > 0 ? { vaultFiles } : undefined;
        // Handle local LLM runtime
        if (runtime === 'local') {
            const config = readLocalLlmConfig(workspace);
            if (!config) {
                return c.json({ error: 'Local LLM not configured. Add localLlm to .setup-agents/config.json' }, 400);
            }
            const { context } = buildContext(workspace, body.message, contextOpts);
            createLocalLlmSession(sessionId, body.message, config, context);
            return c.json({ sessionId });
        }
        // Validate runtime is available
        const runtimes = await detectRuntimes(workspace);
        const selectedRuntime = runtimes.runtimes.find((r) => r.id === runtime);
        if (!selectedRuntime?.available) {
            return c.json({ error: `Runtime "${runtime}" is not available` }, 400);
        }
        const { context } = buildContext(workspace, body.message, contextOpts);
        spawnChat(sessionId, body.message, workspace, runtime, context, body.model, body.chatMode);
        return c.json({ sessionId });
    });
    // GET /api/v1/chat/stream?session=<id> — SSE stream for a chat session
    app.get('/stream', (c) => {
        const sessionId = c.req.query('session');
        if (!sessionId) {
            return c.json({ error: 'session query parameter is required' }, 400);
        }
        // Check local LLM sessions first
        const localSession = getLocalLlmSession(sessionId);
        if (localSession) {
            return streamSSE(c, async (stream) => {
                let closed = false;
                const safeWrite = async (event, data) => {
                    if (closed)
                        return;
                    try {
                        await stream.writeSSE({ data, event });
                    }
                    catch {
                        closed = true;
                    }
                };
                // If already completed or errored before client connected
                if (localSession.status === 'done') {
                    if (localSession.outputBuffer.length > 0) {
                        const filtered = filterOutput(localSession.outputBuffer);
                        await safeWrite('token', JSON.stringify({ content: filtered.content }));
                        if (filtered.redacted) {
                            await safeWrite('security', JSON.stringify({ findings: filtered.findings }));
                        }
                    }
                    await safeWrite('done', JSON.stringify({ exitCode: 0 }));
                    return;
                }
                if (localSession.status === 'error') {
                    await safeWrite('error', JSON.stringify({ message: 'Local LLM session failed', exitCode: 1 }));
                    return;
                }
                // Stream in progress — attach callbacks
                await new Promise((resolve) => {
                    localSession.onToken = (content) => {
                        void safeWrite('token', JSON.stringify({ content }));
                    };
                    localSession.onDone = () => {
                        const filtered = filterOutput(localSession.outputBuffer);
                        const securityWrite = filtered.redacted
                            ? safeWrite('security', JSON.stringify({ findings: filtered.findings }))
                            : Promise.resolve();
                        void securityWrite.then(() => safeWrite('done', JSON.stringify({ exitCode: 0 }))).then(resolve);
                    };
                    localSession.onError = (message) => {
                        void safeWrite('error', JSON.stringify({ message, exitCode: 1 })).then(resolve);
                    };
                    stream.onAbort(() => {
                        closed = true;
                        abortLocalLlmSession(sessionId);
                        resolve();
                    });
                });
            });
        }
        // Fall through to CLI session
        const session = getSession(sessionId);
        if (!session) {
            return c.json({ error: 'Session not found' }, 404);
        }
        // Process already finished before client connected — return buffered output immediately
        if (session.process.exitCode !== null) {
            const exitCode = session.process.exitCode;
            return streamSSE(c, async (stream) => {
                if (session.outputBuffer.length > 0) {
                    const filtered = filterOutput(session.outputBuffer);
                    await stream.writeSSE({ data: JSON.stringify({ content: filtered.content }), event: 'token' });
                    if (filtered.redacted) {
                        await stream.writeSSE({ data: JSON.stringify({ findings: filtered.findings }), event: 'security' });
                    }
                }
                if (exitCode === 0) {
                    await stream.writeSSE({ data: JSON.stringify({ exitCode: 0 }), event: 'done' });
                }
                else {
                    await stream.writeSSE({
                        data: JSON.stringify({ message: `Process exited with code ${String(exitCode)}`, exitCode }),
                        event: 'error',
                    });
                }
            });
        }
        return streamSSE(c, async (stream) => {
            const proc = session.process;
            let closed = false;
            const safeWrite = async (event, data) => {
                if (closed)
                    return;
                try {
                    await stream.writeSSE({ data, event });
                }
                catch {
                    closed = true;
                }
            };
            const keepalive = setInterval(() => {
                touchSession(sessionId);
                void safeWrite('ping', '');
            }, 15_000);
            let lineBuf = '';
            proc.stdout?.on('data', (chunk) => {
                touchSession(sessionId);
                lineBuf += chunk.toString('utf8');
                const lines = lineBuf.split('\n');
                lineBuf = lines.pop() ?? '';
                const fireWrite = (event, data) => {
                    void safeWrite(event, data);
                };
                for (const line of lines) {
                    processStreamLine(line, fireWrite);
                }
            });
            let stderrBuffer = '';
            proc.stderr?.on('data', (chunk) => {
                stderrBuffer += chunk.toString('utf8');
            });
            // Wait for process to exit
            await new Promise((resolve) => {
                proc.on('close', (exitCode) => {
                    clearInterval(keepalive);
                    setTimeout(() => {
                        // Post-completion deterministic filter on full output
                        const filtered = filterOutput(session.outputBuffer);
                        const securityWrite = filtered.redacted
                            ? safeWrite('security', JSON.stringify({ findings: filtered.findings }))
                            : Promise.resolve();
                        if (exitCode === 0) {
                            void securityWrite.then(() => safeWrite('done', JSON.stringify({ exitCode: 0 }))).then(resolve);
                        }
                        else {
                            const errMsg = stderrBuffer.trim() || `Process exited with code ${String(exitCode)}`;
                            void securityWrite
                                .then(() => safeWrite('error', JSON.stringify({ message: errMsg, exitCode })))
                                .then(resolve);
                        }
                    }, 100);
                });
                stream.onAbort(() => {
                    clearInterval(keepalive);
                    closed = true;
                    if (proc.exitCode === null) {
                        kill(sessionId);
                    }
                    resolve();
                });
            });
        });
    });
    // GET /api/v1/chat/session/:id/history — get conversation history for session
    app.get('/session/:id/history', (c) => {
        const sessionId = c.req.param('id');
        // Check local sessions first
        const localSession = getLocalLlmSession(sessionId);
        if (localSession) {
            return c.json({ history: localSession.history });
        }
        const history = getSessionHistory(sessionId);
        return c.json({ history });
    });
    // POST /api/v1/chat/end — end a chat session
    app.post('/end', async (c) => {
        const body = await c.req.json();
        if (!body.sessionId) {
            return c.json({ error: 'sessionId is required' }, 400);
        }
        // Try local session first
        if (abortLocalLlmSession(body.sessionId)) {
            return c.json({ ok: true });
        }
        const killed = kill(body.sessionId);
        if (!killed) {
            return c.json({ error: 'Session not found' }, 404);
        }
        return c.json({ ok: true });
    });
    // ─── Chat History Endpoints ──────────────────────────────────────────────
    // GET /api/v1/chat/history — list conversations (metadata only)
    app.get('/history', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const conversations = listConversations(dir);
        return c.json({ conversations });
    });
    // GET /api/v1/chat/history/:id — load full conversation with messages
    app.get('/history/:id', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const id = c.req.param('id');
        if (!isValidConversationId(id)) {
            return c.json({ error: 'Invalid conversation id' }, 400);
        }
        const conversation = loadConversation(dir, id);
        if (!conversation) {
            return c.json({ error: 'Conversation not found' }, 404);
        }
        return c.json(conversation);
    });
    // POST /api/v1/chat/history/:id/save — save or update a conversation
    app.post('/history/:id/save', async (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const id = c.req.param('id');
        if (!isValidConversationId(id)) {
            return c.json({ error: 'Invalid conversation id' }, 400);
        }
        const body = await c.req.json();
        if (!body.title || !body.messages) {
            return c.json({ error: 'title and messages are required' }, 400);
        }
        const conversation = saveConversation(dir, id, body.title, body.messages);
        return c.json(conversation);
    });
    // DELETE /api/v1/chat/history/:id — delete a conversation
    app.delete('/history/:id', (c) => {
        const dir = getWorkspaceCwd(c, cwd);
        const id = c.req.param('id');
        if (!isValidConversationId(id)) {
            return c.json({ error: 'Invalid conversation id' }, 400);
        }
        const deleted = deleteConversation(dir, id);
        if (!deleted) {
            return c.json({ error: 'Conversation not found' }, 404);
        }
        return c.json({ ok: true });
    });
    return app;
}
//# sourceMappingURL=chat.js.map