/**
 * Chat / Agent SSE routes
 *
 * POST /api/chat       — send a message, await full response
 * POST /api/chat/stream — SSE stream of agent events with session persistence
 *
 * Single agent instance per server process (single-user v1).
 */
import { parseFollowups } from '../../agent/followups.js';
import * as access from '../../services/access-control.js';
import { log } from '../../utils/log.js';
export function buildChatContextPrefix(context, user, agent, authService) {
    if (!context)
        return '';
    const workspaceManager = agent.getWorkspaceManager();
    if (context.startsWith('folder:')) {
        const repo = context.slice('folder:'.length);
        if (!access.hasRepoAccess(user, repo, workspaceManager, authService))
            return '';
        return `[Currently viewing folder: ${repo}]\n\nScope your help to this folder unless the user asks otherwise. Use list_entities or search_entities with repoNames: ["${repo}"] to explore its contents.\n\n`;
    }
    const parts = context.split('/');
    if (parts.length < 3) {
        return `[Currently viewing: ${context}]\n\n`;
    }
    const [repo, type, id] = parts;
    if (!access.hasRepoAccess(user, repo, workspaceManager, authService))
        return '';
    try {
        const graph = workspaceManager.getGraph(repo);
        if (graph) {
            const entity = graph.get(type, id);
            const fm = entity.data ?? {};
            const entityContent = entity.document?.content ?? '';
            const fmLines = Object.entries(fm)
                .filter(([k]) => !['entity_type', 'entity_id', 'created_at', 'updated_at', 'created_by', 'updated_by'].includes(k))
                .map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`)
                .join('\n');
            return `[Currently viewing entry: ${context}]\n\nEntity frontmatter:\n${fmLines}\n\nEntity content:\n${entityContent}\n\nAnswer questions about THIS entity. Use the information above.\n\n`;
        }
    }
    catch {
        return `[Currently viewing entry: ${context}]\n\n`;
    }
    return '';
}
export async function registerChatRoutes(fastify, agent, wsHub, authService, memoryService) {
    // POST /api/chat — send message, get full response
    fastify.post('/api/chat', async (req, reply) => {
        const { message } = req.body;
        if (!message || typeof message !== 'string' || !message.trim()) {
            return reply.status(400).send({ error: 'message is required' });
        }
        try {
            const user = req.user ?? null;
            const sessionId = user ? `user:${user.id}:default` : 'default';
            const response = await agent.chatInSession(sessionId, message, { user, userId: user?.id });
            return reply.send(response);
        }
        catch (err) {
            fastify.log.error(err, 'Agent chat error');
            return reply.status(500).send({ error: err.message || 'Agent error' });
        }
    });
    // POST /api/chat/stream — SSE stream of agent events
    fastify.post('/api/chat/stream', async (req, reply) => {
        const { message, context, sessionId, history } = req.body;
        if (!message || typeof message !== 'string' || !message.trim()) {
            return reply.status(400).send({ error: 'message is required' });
        }
        const user = req.user ?? null;
        const resolvedSessionId = sessionId
            ? (user ? `user:${user.id}:${sessionId}` : sessionId)
            : 'default';
        // Set SSE headers
        reply.raw.setHeader('Content-Type', 'text/event-stream');
        reply.raw.setHeader('Cache-Control', 'no-cache');
        reply.raw.setHeader('Connection', 'keep-alive');
        reply.raw.flushHeaders();
        let streamDead = false;
        const sendEvent = (data) => {
            if (streamDead || !reply.raw.writable) {
                streamDead = true;
                return;
            }
            try {
                reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
            }
            catch {
                streamDead = true;
            }
        };
        // Detect client disconnect
        reply.raw.on('close', () => { streamDead = true; });
        reply.raw.on('error', () => { streamDead = true; });
        // Internal-agent identity for activity + entity_change tagging. The web
        // agent runs inside a user session, so the label reads as "<user>'s
        // agent" — distinguishable in the sidebar from external MCP integrations
        // which carry their token name (e.g. "Christian's Cursor").
        const agentLabel = user?.displayName ? `${user.displayName}'s agent` : 'Agent';
        const agentIdentity = {
            kind: 'internal',
            label: agentLabel,
            userId: user?.id != null ? String(user.id) : undefined,
        };
        // Turn-scoped activity tracking. A single chat turn can fan out across
        // multiple folders and intersperse tool calls with LLM "thinking" gaps.
        // We want one continuous chip per touched folder for the whole turn —
        // not a flicker per tool call. So:
        //   1. The first `enter` for a repo broadcasts and adds the repo to a
        //      turn-local set; subsequent enters refresh it (server-side
        //      throttle drops sub-second repeats).
        //   2. Per-tool `leave` events are suppressed — the chip is "pinned"
        //      until the turn ends.
        //   3. A 2s heartbeat keeps the chip alive across multi-second LLM
        //      thinking gaps that would otherwise time out the client's
        //      MIN_VISIBLE floor.
        //   4. The finally block below fires the actual `leave` for every
        //      touched folder when the turn completes (success, error, or
        //      client disconnect).
        const activeRepos = new Set();
        let activityHeartbeat = null;
        function ensureActivityHeartbeat() {
            if (activityHeartbeat)
                return;
            activityHeartbeat = setInterval(() => {
                for (const repo of activeRepos) {
                    wsHub.handleAgentActivity('heartbeat', repo, agentIdentity);
                }
            }, 2_000);
        }
        function stopActivityHeartbeat() {
            if (activityHeartbeat) {
                clearInterval(activityHeartbeat);
                activityHeartbeat = null;
            }
        }
        try {
            const response = await agent.getPool().withAgent(resolvedSessionId, {
                user,
                onHighlight: (ids) => sendEvent({ type: 'highlight', entityIds: ids }),
                onMutation: (m) => {
                    // Stream the event to the requesting user's chat (existing behavior).
                    sendEvent({ type: 'mutation', action: m.action, repo: m.repo, entityType: m.entityType, entityId: m.entityId });
                    // Broadcast to all connected workspace clients so OTHER users
                    // see live updates from this user's agent. Without this, the
                    // web agent's mutations are invisible to peers until they
                    // reload — MCP mutations already broadcast via src/mcp/tools.ts.
                    wsHub.broadcast({
                        type: 'entity_change',
                        action: m.action,
                        repo: m.repo,
                        entityType: m.entityType,
                        entityId: m.entityId,
                        actor: user?.displayName ?? 'Agent',
                        source: 'api',
                        agent: agentIdentity,
                        timestamp: new Date().toISOString(),
                    });
                },
                onAgentActivity: (action, repo) => {
                    if (action === 'enter') {
                        activeRepos.add(repo);
                        wsHub.handleAgentActivity('enter', repo, agentIdentity);
                        ensureActivityHeartbeat();
                    }
                    // Per-tool `leave` is intentionally suppressed — the finally
                    // block at turn end fires a single `leave` per repo. `heartbeat`
                    // doesn't currently fire from tool wrappers, but if it did,
                    // pass through.
                    if (action === 'heartbeat' && activeRepos.has(repo)) {
                        wsHub.handleAgentActivity('heartbeat', repo, agentIdentity);
                    }
                },
            }, async (rawAgent) => {
                // Subscribe to agent events for streaming
                let hasContent = false;
                let needsTurnSep = false;
                const unsubscribe = rawAgent.subscribe((event) => {
                    if (event.type === 'message_update') {
                        const msgEvent = event.assistantMessageEvent;
                        if (msgEvent?.type === 'text_delta') {
                            if (needsTurnSep && hasContent) {
                                sendEvent({ type: 'token', text: '\n\n' });
                            }
                            needsTurnSep = false;
                            hasContent = true;
                            sendEvent({ type: 'token', text: msgEvent.delta });
                        }
                    }
                    else if (event.type === 'message_end') {
                        if (event.message?.role === 'assistant') {
                            needsTurnSep = true;
                            for (const block of event.message.content ?? []) {
                                if (block.type === 'toolCall' && block.name) {
                                    sendEvent({ type: 'tool_start', name: block.name });
                                }
                            }
                        }
                    }
                });
                // Build scope-aware context prefix only after folder access passes.
                const contextPrefix = buildChatContextPrefix(context, user ?? undefined, agent, authService);
                const userPrefix = user ? `[Current user: ${user.displayName} (${user.email}, ${user.role})]\n\n` : '';
                const fullMessage = userPrefix + contextPrefix + message;
                // Send message and await response
                const result = await new Promise((resolve, reject) => {
                    let content = '';
                    let assistantMessageCount = 0;
                    let toolCallCount = 0;
                    const blockTypeCounts = {};
                    let lastStopReason;
                    let lastErrorMessage;
                    const endSub = rawAgent.subscribe((event) => {
                        if (event.type === 'message_end' && event.message?.role === 'assistant') {
                            assistantMessageCount++;
                            const blocks = event.message.content ?? [];
                            for (const b of blocks) {
                                blockTypeCounts[b.type] = (blockTypeCounts[b.type] ?? 0) + 1;
                            }
                            const textBlocks = blocks.filter((b) => b.type === 'text');
                            const toolBlocks = blocks.filter((b) => b.type === 'toolCall');
                            toolCallCount += toolBlocks.length;
                            if (event.message.stopReason)
                                lastStopReason = event.message.stopReason;
                            if (event.message.errorMessage)
                                lastErrorMessage = event.message.errorMessage;
                            // Keep the LAST non-empty text seen, so a follow-up assistant
                            // turn that has only tool calls doesn't wipe out the text that
                            // was produced earlier in the same prompt run.
                            const turnText = textBlocks.map((b) => b.text).join('');
                            if (turnText.length > 0)
                                content = turnText;
                        }
                        else if (event.type === 'agent_end') {
                            endSub();
                            if (!content) {
                                log.warn(`[chat/stream] agent produced no text ` +
                                    `(assistantMessages=${assistantMessageCount}, toolCalls=${toolCallCount}, ` +
                                    `blockTypes=${JSON.stringify(blockTypeCounts)}, ` +
                                    `stopReason=${lastStopReason ?? 'n/a'}, ` +
                                    `errorMessage=${JSON.stringify(lastErrorMessage ?? null)}, ` +
                                    `sessionId=${resolvedSessionId}) prompt="${message.slice(0, 80)}"`);
                            }
                            resolve({ content });
                        }
                    });
                    rawAgent.prompt(fullMessage).catch((err) => { endSub(); reject(err); });
                });
                unsubscribe();
                return result;
            }, { userId: user?.id, fallbackHistory: history });
            // Write brief entry to today's daily memory
            if (memoryService && response.content) {
                try {
                    const snippet = `User: ${message.slice(0, 100)}${message.length > 100 ? '…' : ''}\nAgent: ${response.content.slice(0, 200)}${response.content.length > 200 ? '…' : ''}`;
                    memoryService.saveSessionSummary(snippet, { source: 'web-chat', sessionId }, user?.id);
                }
                catch { /* non-fatal */ }
            }
            // Parse follow-up suggestions from response (shared with CLI transport)
            const { content: parsedContent, suggestions } = parseFollowups(response.content);
            let cleanContent = parsedContent;
            // If the agent finished with no text AND no tool call, surface that
            // explicitly so the UI doesn't look frozen. Users reported "silent
            // failures" that were actually legitimately-empty assistant turns.
            // The `fallback: true` flag lets the client decide what to do \u2014
            // typed prompts render the message, system-initiated silent prompts
            // (auto-greet) drop the placeholder so users don't see a confusing
            // "Could you try rephrasing?" they didn't trigger.
            let fallback = false;
            if (!cleanContent.trim() && !streamDead) {
                cleanContent = '_I didn\u2019t have a response for that. Could you try rephrasing?_';
                fallback = true;
            }
            sendEvent({ type: 'done', message: cleanContent, suggestions, fallback });
        }
        catch (err) {
            // Log to stderr so silent agent failures show up in server logs —
            // the SSE error event goes to the client but was invisible to ops.
            log.error('[chat/stream] agent error:', err?.stack || err?.message || err);
            sendEvent({ type: 'error', message: err.message || 'Agent error' });
        }
        finally {
            // End-of-turn agent activity cleanup. Fires for every folder the
            // agent touched during this turn — including paths through the
            // catch branch above, so a crashed/aborted turn still releases
            // its chips instead of relying on the 90s server TTL.
            stopActivityHeartbeat();
            for (const repo of activeRepos) {
                wsHub.handleAgentActivity('leave', repo, agentIdentity);
            }
            activeRepos.clear();
        }
        reply.raw.end();
    });
    // POST /api/chat/abort — abort the current agent run
    fastify.post('/api/chat/abort', async (_req, reply) => {
        try {
            agent.abort();
            return { success: true };
        }
        catch (err) {
            return reply.status(500).send({ error: err.message });
        }
    });
}
//# sourceMappingURL=chat.js.map