/**
 * Messages API routes
 *
 * Folder-scoped messaging endpoints.
 * Auth: checks folder access via authService, admin/owner bypass.
 * @agent mentions trigger async agent invocation in the folder thread.
 */
import * as access from '../../services/access-control.js';
import { log } from '../../utils/log.js';
const AGENT_MENTION_RE = /@agent\b/i;
// Match @kenton-powell with hyphens, but NOT inside email addresses
// (`alice@example.com` must not mention a user with handle `example`).
// Negative lookbehind rejects the @-sign when preceded by an email
// local-part character (alnum / dot / underscore / hyphen). Pre-bridge
// this matched only `[a-z0-9]+` and keyed off a whitespace-stripped
// display name, which produced `@kentonpowell` — disagreeing with both
// auth.db and the home page's slug. The boundary fix closes both gaps.
const USER_MENTION_RE = /(?<![A-Za-z0-9._-])@([a-z0-9][a-z0-9-]*)/gi;
/** Detect @user mentions in content. Returns matched user IDs (excludes sender). */
function detectUserMentions(content, users, senderId) {
    // Match against the canonical `handle` field — single source of truth
    // shared with created_by/updated_by, the user-person bridge, and the
    // home "My Tasks" filter.
    const handles = new Map();
    for (const u of users)
        handles.set(u.handle, u);
    const mentioned = new Set();
    const result = [];
    let match;
    while ((match = USER_MENTION_RE.exec(content)) !== null) {
        const handle = match[1].toLowerCase();
        if (handle === 'agent')
            continue; // skip @agent
        const user = handles.get(handle);
        if (user && user.id !== senderId && !mentioned.has(user.id)) {
            mentioned.add(user.id);
            result.push(user);
        }
    }
    return result;
}
/** Check if the current user can access a specific folder. Delegates to the
 *  shared access-control helper so personal-folder restrictions and
 *  workspace-owner-on-shared-folder semantics match the rest of the API. */
function hasFolderAccess(req, folder, workspaceManager, authService) {
    const user = req.user;
    return access.hasRepoAccess(user, folder, workspaceManager, authService);
}
/**
 * Build conversation context for an @agent invocation.
 *
 * Folder scope includes an entity-count summary of the folder so the agent
 * knows the shape of what it's working in. Workspace scope skips the summary —
 * the per-user briefing is already injected by the orchestrator via `userId`.
 */
function buildMessageContext(messageService, workspaceManager, scope, folder) {
    const recent = (scope === 'workspace'
        ? messageService.getWorkspaceMessages({ limit: 10 })
        : messageService.getMessages(folder, { limit: 10 })).reverse();
    const transcript = recent.map(m => {
        const role = m.role === 'agent' ? 'Agent' : m.userDisplayName;
        return `${role}: ${m.content}`;
    }).join('\n\n');
    if (scope === 'workspace') {
        return `You are responding in the workspace-wide chat on the home page. ` +
            `Everyone in the workspace can see your response. Be concise and helpful — ` +
            `your personalised briefing and memory have already been loaded for context.\n\n` +
            `Recent conversation:\n${transcript}`;
    }
    let entitySummary = '';
    try {
        const graph = workspaceManager.getGraph(folder);
        const entities = graph.search({});
        if (entities.length > 0) {
            const types = new Map();
            for (const e of entities)
                types.set(e.entityType, (types.get(e.entityType) ?? 0) + 1);
            entitySummary = `\n\nThis folder contains ${entities.length} entries: ${[...types.entries()].map(([t, c]) => `${c} ${t}`).join(', ')}.`;
        }
    }
    catch { /* folder may not have a graph */ }
    return `You are responding in the "${folder}" folder message thread. ` +
        `Other team members can see your response. Be concise and helpful.\n\n` +
        `Recent conversation:\n${transcript}${entitySummary}`;
}
export async function registerMessagesApiRoutes(fastify, messageService, authService, wsHub, workspaceManager, agent, heartbeatService) {
    /**
     * Invoke the agent asynchronously in response to an @agent mention.
     * Unified across workspace and folder scopes. The agent runs as the
     * mentioning user so their briefing + memory are loaded via userId.
     */
    function invokeAgent(scope, folder, mentioner) {
        if (!agent)
            return;
        const context = buildMessageContext(messageService, workspaceManager, scope, folder);
        const sessionId = scope === 'workspace' ? 'workspace:messages' : `folder:${folder}`;
        const userId = mentioner?.id;
        const label = scope === 'workspace' ? 'workspace' : folder;
        const thinkingEvent = scope === 'workspace'
            ? { type: 'workspace_agent_thinking' }
            : { type: 'folder_agent_thinking', repo: folder };
        (async () => {
            wsHub.broadcast({ ...thinkingEvent, thinking: true, timestamp: new Date().toISOString() });
            try {
                const response = await agent.chatInSession(sessionId, context, {
                    user: mentioner ?? null,
                    userId,
                    fallbackHistory: [{ role: 'user', content: context }],
                });
                if (response.content?.trim()) {
                    const trimmed = response.content.trim();
                    if (scope === 'workspace') {
                        const agentMsg = messageService.postWorkspaceMessage(0, 'Agent', 'agent', trimmed);
                        wsHub.broadcast({
                            type: 'workspace_message',
                            message: agentMsg,
                            timestamp: new Date().toISOString(),
                        });
                    }
                    else {
                        const agentMsg = messageService.postMessage(folder, 0, 'Agent', 'agent', trimmed);
                        wsHub.broadcast({
                            type: 'folder_message',
                            repo: folder,
                            message: agentMsg,
                            timestamp: new Date().toISOString(),
                        });
                    }
                }
            }
            catch (err) {
                log.warn(`[messages] @agent invocation failed in ${label}:`, err.message);
            }
            finally {
                wsHub.broadcast({ ...thinkingEvent, thinking: false, timestamp: new Date().toISOString() });
            }
        })();
    }
    /**
     * Detect @user mentions in a message, set the mentions list, create
     * notifications, and broadcast per-user events. Shared by workspace and
     * folder POST handlers.
     */
    function detectAndNotifyMentions(message, senderName, scope) {
        const allUsers = authService.listUsers();
        const mentionedUsers = detectUserMentions(message.content, allUsers, message.userId);
        if (mentionedUsers.length === 0)
            return;
        messageService.setMentions(message.id, mentionedUsers.map(u => u.id));
        const preview = message.content.trim().slice(0, 200);
        const now = new Date().toISOString();
        const folder = scope === 'workspace' ? null : message.folder;
        for (const u of mentionedUsers) {
            const notificationId = messageService.createNotification(u.id, folder, message.id, senderName, preview);
            wsHub.broadcastToUser(String(u.id), {
                type: 'user_mention',
                userId: u.id,
                notificationId,
                folder,
                messageId: message.id,
                mentionerName: senderName,
                messagePreview: preview,
                timestamp: now,
            });
        }
    }
    // ── Workspace-wide chat (home page) ──
    // Static paths register ahead of the parametric `/api/messages/:folder`
    // via find-my-way, so no ambiguity with a folder literally named "workspace".
    // GET /api/messages/workspace — paginated workspace message history
    fastify.get('/api/messages/workspace', async (req, reply) => {
        const before = req.query.before ? parseInt(req.query.before, 10) : undefined;
        const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
        const messages = messageService.getWorkspaceMessages({ before, limit });
        return reply.send({ messages });
    });
    // POST /api/messages/workspace — send a workspace message
    fastify.post('/api/messages/workspace', async (req, reply) => {
        const user = req.user;
        const { content, replyToMessageId } = req.body || {};
        if (!content || typeof content !== 'string' || !content.trim()) {
            return reply.status(400).send({ error: 'Message content is required' });
        }
        const userId = user?.id ?? 0;
        const displayName = user?.displayName ?? 'API';
        const message = messageService.postWorkspaceMessage(userId, displayName, 'user', content.trim(), typeof replyToMessageId === 'number' ? replyToMessageId : undefined);
        wsHub.broadcast({
            type: 'workspace_message',
            message,
            timestamp: new Date().toISOString(),
        });
        // Invoke agent: explicit @agent mention OR reply-to an agent message
        let shouldInvokeAgent = AGENT_MENTION_RE.test(content);
        if (!shouldInvokeAgent && typeof replyToMessageId === 'number') {
            const replyTarget = messageService.getMessageById(replyToMessageId);
            if (replyTarget?.role === 'agent') {
                shouldInvokeAgent = true;
            }
        }
        if (shouldInvokeAgent) {
            invokeAgent('workspace', null, user ?? null);
        }
        detectAndNotifyMentions(message, displayName, 'workspace');
        // Track for heartbeat conversation analysis
        heartbeatService?.trackWorkspaceMessage(message.id);
        return reply.status(201).send({ message });
    });
    // POST /api/messages/workspace/read — mark workspace messages as read
    fastify.post('/api/messages/workspace/read', async (req, reply) => {
        const user = req.user;
        if (!user)
            return reply.status(401).send({ error: 'Authentication required' });
        const { messageId } = req.body || {};
        if (typeof messageId !== 'number' || messageId < 1) {
            return reply.status(400).send({ error: 'Valid messageId is required' });
        }
        messageService.markWorkspaceRead(user.id, messageId);
        return reply.send({ ok: true });
    });
    // GET /api/messages/:folder — paginated message history
    fastify.get('/api/messages/:folder', async (req, reply) => {
        const { folder } = req.params;
        // Verify folder exists
        if (!workspaceManager.getRepoConfig(folder)) {
            return reply.status(404).send({ error: `Folder "${folder}" not found` });
        }
        if (!hasFolderAccess(req, folder, workspaceManager, authService)) {
            return reply.status(403).send({ error: 'Access denied' });
        }
        const before = req.query.before ? parseInt(req.query.before, 10) : undefined;
        const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
        const messages = messageService.getMessages(folder, { before, limit });
        return reply.send({ messages });
    });
    // POST /api/messages/:folder — send a message
    fastify.post('/api/messages/:folder', async (req, reply) => {
        const { folder } = req.params;
        const user = req.user;
        if (!workspaceManager.getRepoConfig(folder)) {
            return reply.status(404).send({ error: `Folder "${folder}" not found` });
        }
        if (!hasFolderAccess(req, folder, workspaceManager, authService)) {
            return reply.status(403).send({ error: 'Access denied' });
        }
        const { content, replyToMessageId } = req.body || {};
        if (!content || typeof content !== 'string' || !content.trim()) {
            return reply.status(400).send({ error: 'Message content is required' });
        }
        const userId = user?.id ?? 0;
        const displayName = user?.displayName ?? 'API';
        const message = messageService.postMessage(folder, userId, displayName, 'user', content.trim(), typeof replyToMessageId === 'number' ? replyToMessageId : undefined);
        // Broadcast via WebSocket
        wsHub.broadcast({
            type: 'folder_message',
            repo: folder,
            message,
            timestamp: new Date().toISOString(),
        });
        // Invoke agent: explicit @agent mention OR reply-to an agent message
        let shouldInvokeAgent = AGENT_MENTION_RE.test(content);
        if (!shouldInvokeAgent && typeof replyToMessageId === 'number') {
            const replyTarget = messageService.getMessageById(replyToMessageId);
            if (replyTarget?.role === 'agent') {
                shouldInvokeAgent = true;
            }
        }
        if (shouldInvokeAgent) {
            invokeAgent('folder', folder, user ?? null);
        }
        detectAndNotifyMentions(message, displayName, 'folder');
        // Track for heartbeat conversation analysis
        heartbeatService?.trackMessage(folder, message.id);
        return reply.status(201).send({ message });
    });
    // POST /api/messages/:folder/capture — create entity from conversation suggestion
    fastify.post('/api/messages/:folder/capture', async (req, reply) => {
        const { folder } = req.params;
        const user = req.user;
        if (!workspaceManager.getRepoConfig(folder)) {
            return reply.status(404).send({ error: `Folder "${folder}" not found` });
        }
        if (!hasFolderAccess(req, folder, workspaceManager, authService)) {
            return reply.status(403).send({ error: 'Access denied' });
        }
        if (!agent) {
            return reply.status(503).send({ error: 'Agent not available' });
        }
        const { entityType, entityId, messageId } = req.body || {};
        if (!entityType || !entityId) {
            return reply.status(400).send({ error: 'entityType and entityId are required' });
        }
        // Build context from recent conversation
        const context = buildMessageContext(messageService, workspaceManager, 'folder', folder);
        const sessionId = `capture:${folder}:${Date.now()}`;
        // Broadcast "capturing" state — replace marker so other clients show progress
        if (typeof messageId === 'number') {
            const msg = messageService.getMessageById(messageId);
            if (msg) {
                const marker = `[capture:${entityType}:${entityId}]`;
                const capturingMarker = `[capturing:${entityType}:${entityId}]`;
                if (msg.content.includes(marker)) {
                    const updatedContent = msg.content.replace(marker, capturingMarker);
                    messageService.updateMessageContent(messageId, updatedContent);
                    wsHub.broadcast({
                        type: 'message_update',
                        repo: folder,
                        messageId,
                        content: updatedContent,
                        timestamp: new Date().toISOString(),
                    });
                }
            }
        }
        // Invoke agent to create the entity
        try {
            const today = new Date().toISOString().slice(0, 10);
            const prompt = `Call the create_entity tool now with repo="${folder}", entityType="${entityType}", entityId="${entityId}". Populate the data and content fields based on the conversation context above. Today's date is ${today} — use it for the date field.`;
            const response = await agent.chatInSession(sessionId, prompt, {
                user: user ?? null,
                fallbackHistory: [{ role: 'user', content: context }],
            });
            // Verify the entity was actually created before marking as captured
            const graph = workspaceManager.getGraph(folder);
            let entityExists = false;
            try {
                graph.get(entityType, entityId);
                entityExists = true;
            }
            catch { /* entity doesn't exist */ }
            if (!entityExists) {
                log.warn(`[messages] Capture agent ran but entity ${entityType}/${entityId} was not created in ${folder}. Response: ${response.content?.slice(0, 200)}`);
                // Revert capturing marker back to capture
                if (typeof messageId === 'number') {
                    const msg = messageService.getMessageById(messageId);
                    if (msg) {
                        const capturingMarker = `[capturing:${entityType}:${entityId}]`;
                        const captureMarker = `[capture:${entityType}:${entityId}]`;
                        if (msg.content.includes(capturingMarker)) {
                            const reverted = msg.content.replace(capturingMarker, captureMarker);
                            messageService.updateMessageContent(messageId, reverted);
                            wsHub.broadcast({
                                type: 'message_update',
                                repo: folder,
                                messageId,
                                content: reverted,
                                timestamp: new Date().toISOString(),
                            });
                        }
                    }
                }
                return reply.status(500).send({ error: 'Agent did not create the entity' });
            }
            // Mark this capture item as done in the suggestion message
            if (typeof messageId === 'number') {
                const msg = messageService.getMessageById(messageId);
                if (msg) {
                    // May be [capture:...] or [capturing:...] depending on race
                    const capturingMarker = `[capturing:${entityType}:${entityId}]`;
                    const captureMarker = `[capture:${entityType}:${entityId}]`;
                    const replacement = `[captured:${entityType}:${entityId}]`;
                    let updatedContent = msg.content;
                    if (updatedContent.includes(capturingMarker)) {
                        updatedContent = updatedContent.replace(capturingMarker, replacement);
                    }
                    else if (updatedContent.includes(captureMarker)) {
                        updatedContent = updatedContent.replace(captureMarker, replacement);
                    }
                    if (updatedContent !== msg.content) {
                        messageService.updateMessageContent(messageId, updatedContent);
                        wsHub.broadcast({
                            type: 'message_update',
                            repo: folder,
                            messageId,
                            content: updatedContent,
                            timestamp: new Date().toISOString(),
                        });
                    }
                }
            }
            // Broadcast entity change so all clients refresh their sidebar/graph
            wsHub.broadcast({
                type: 'entity_change',
                action: 'created',
                repo: folder,
                entityType,
                entityId,
                actor: 'Agent',
                source: 'api',
                timestamp: new Date().toISOString(),
            });
            return reply.send({ ok: true, entityType, entityId });
        }
        catch (err) {
            log.warn(`[messages] Capture failed in ${folder}:`, err.message);
            return reply.status(500).send({ error: 'Failed to capture entity' });
        }
    });
    /**
     * Undo an auto-capture within the 10-minute window. Deletes the entity and
     * rewrites the message marker from [auto-captured:...] to [auto-capture-undone:...].
     * The undo window is enforced by the UI; the server enforces the entity delete
     * and only requires the marker currently be in the auto-captured state.
     */
    async function undoCapture(args) {
        const { repo, entityType, entityId, messageId, actorUser } = args;
        try {
            const graph = workspaceManager.getGraph(repo);
            try {
                graph.get(entityType, entityId);
            }
            catch {
                return { error: `Entity ${entityType}/${entityId} does not exist in ${repo}`, status: 404 };
            }
            const gitUser = actorUser
                ? { id: String(actorUser.id), name: actorUser.displayName, email: actorUser.email }
                : { id: '0', name: 'Agent', email: 'agent@studiograph.local' };
            await graph.delete(entityType, entityId, `chore: undo auto-capture of ${entityType}/${entityId}`, gitUser);
        }
        catch (err) {
            log.warn(`[messages] Undo-capture delete failed for ${repo}/${entityType}/${entityId}:`, err.message);
            return { error: 'Failed to delete entity', status: 500 };
        }
        // Rewrite marker on the agent message (best-effort; UI also listens for entity_change).
        // Handles both 3-part [auto-captured:type:id] (folder scope) and
        // 4-part [auto-captured:type:id:repo] (workspace scope, routed to folder).
        if (typeof messageId === 'number') {
            const msg = messageService.getMessageById(messageId);
            if (msg) {
                const short = `[auto-captured:${entityType}:${entityId}]`;
                const long = `[auto-captured:${entityType}:${entityId}:${repo}]`;
                const shortAfter = `[auto-capture-undone:${entityType}:${entityId}]`;
                const longAfter = `[auto-capture-undone:${entityType}:${entityId}:${repo}]`;
                let updated = null;
                if (msg.content.includes(long))
                    updated = msg.content.replace(long, longAfter);
                else if (msg.content.includes(short))
                    updated = msg.content.replace(short, shortAfter);
                if (updated && updated !== msg.content) {
                    messageService.updateMessageContent(messageId, updated);
                    wsHub.broadcast({
                        type: 'message_update',
                        repo,
                        messageId,
                        content: updated,
                        timestamp: new Date().toISOString(),
                    });
                }
            }
        }
        wsHub.broadcast({
            type: 'entity_change',
            action: 'deleted',
            repo,
            entityType,
            entityId,
            actor: actorUser?.displayName ?? 'Agent',
            source: 'api',
            timestamp: new Date().toISOString(),
        });
        return { ok: true };
    }
    // POST /api/messages/:folder/undo-capture — reverse an auto-capture
    fastify.post('/api/messages/:folder/undo-capture', async (req, reply) => {
        const { folder } = req.params;
        const user = req.user;
        if (!workspaceManager.getRepoConfig(folder)) {
            return reply.status(404).send({ error: `Folder "${folder}" not found` });
        }
        if (!hasFolderAccess(req, folder, workspaceManager, authService)) {
            return reply.status(403).send({ error: 'Access denied' });
        }
        const { entityType, entityId, messageId } = req.body || {};
        if (!entityType || !entityId) {
            return reply.status(400).send({ error: 'entityType and entityId are required' });
        }
        const result = await undoCapture({
            repo: folder,
            entityType,
            entityId,
            messageId,
            actorUser: user ?? null,
        });
        if ('error' in result)
            return reply.status(result.status).send({ error: result.error });
        return reply.send(result);
    });
    // POST /api/messages/workspace/undo-capture — reverse an auto-capture made in workspace chat
    // (The entity lives in whichever folder the agent routed it into; the client passes `repo`.)
    fastify.post('/api/messages/workspace/undo-capture', async (req, reply) => {
        const user = req.user;
        const { repo, entityType, entityId, messageId } = req.body || {};
        if (!repo || !entityType || !entityId) {
            return reply.status(400).send({ error: 'repo, entityType, entityId are required' });
        }
        if (!workspaceManager.getRepoConfig(repo)) {
            return reply.status(404).send({ error: `Folder "${repo}" not found` });
        }
        if (!hasFolderAccess(req, repo, workspaceManager, authService)) {
            return reply.status(403).send({ error: 'Access denied' });
        }
        const result = await undoCapture({
            repo,
            entityType,
            entityId,
            messageId,
            actorUser: user ?? null,
        });
        if ('error' in result)
            return reply.status(result.status).send({ error: result.error });
        return reply.send(result);
    });
    // POST /api/messages/:folder/read — mark messages as read
    fastify.post('/api/messages/:folder/read', async (req, reply) => {
        const { folder } = req.params;
        const user = req.user;
        if (!user) {
            return reply.status(401).send({ error: 'Authentication required' });
        }
        if (!hasFolderAccess(req, folder, workspaceManager, authService)) {
            return reply.status(403).send({ error: 'Access denied' });
        }
        const { messageId } = req.body || {};
        if (typeof messageId !== 'number' || messageId < 1) {
            return reply.status(400).send({ error: 'Valid messageId is required' });
        }
        messageService.markRead(user.id, folder, messageId);
        return reply.send({ ok: true });
    });
    // GET /api/messages/unread — all unread counts for the current user
    fastify.get('/api/messages/unread', async (req, reply) => {
        const user = req.user;
        if (!user) {
            return reply.send({ unread: {}, workspace: 0 });
        }
        // Folders the user can access (excludes personal folders of others
        // even when a stale folder_access row exists).
        const folders = access.getAccessibleRepos(user, workspaceManager, authService).map(r => r.name);
        const unread = messageService.getUnreadCounts(user.id, folders);
        const workspace = messageService.getWorkspaceUnreadCount(user.id);
        return reply.send({ unread, workspace });
    });
    // ── Notification endpoints ──
    // GET /api/notifications — get notifications for the current user
    fastify.get('/api/notifications', async (req, reply) => {
        const user = req.user;
        if (!user)
            return reply.status(401).send({ error: 'Authentication required' });
        const unreadOnly = req.query.unreadOnly === 'true';
        const notifications = messageService.getNotifications(user.id, { unreadOnly, limit: 50 });
        const unreadCount = messageService.getUnreadNotificationCount(user.id);
        return reply.send({ notifications, unreadCount });
    });
    // POST /api/notifications/:id/read — mark a single notification as read
    fastify.post('/api/notifications/:id/read', async (req, reply) => {
        const user = req.user;
        if (!user)
            return reply.status(401).send({ error: 'Authentication required' });
        messageService.markNotificationRead(parseInt(req.params.id, 10), user.id);
        return reply.send({ ok: true });
    });
    // POST /api/notifications/read-all — mark all notifications as read
    fastify.post('/api/notifications/read-all', async (req, reply) => {
        const user = req.user;
        if (!user)
            return reply.status(401).send({ error: 'Authentication required' });
        messageService.markAllNotificationsRead(user.id);
        return reply.send({ ok: true });
    });
    // GET /api/users — list all workspace users (for @mention autocomplete).
    // Surfaces the canonical `handle` field so the autocomplete inserts the
    // same string the mention regex matches on. Pre-bridge this returned a
    // display-name-derived slug that disagreed with auth.db; that's gone.
    fastify.get('/api/users', async (_req, reply) => {
        const users = authService.listUsers().map(u => ({
            id: u.id,
            displayName: u.displayName,
            handle: u.handle,
        }));
        return reply.send({ users });
    });
}
//# sourceMappingURL=messages-api.js.map