import { logEvent } from '../../security/audit.js';
import { createComment as createCommentService, deleteComment as deleteCommentService, listComments as listCommentsService, reopenComment as reopenCommentService, resolveComment as resolveCommentService, updateComment as updateCommentService, } from '../../realtime/comments.js';
import { createNotification, excerptForNotification, extractMentionedUserIds, listForUser as listNotificationsForUser, markAllRead as markAllNotificationsRead, markRead as markNotificationRead, unreadCount as notificationsUnreadCount, } from '../../realtime/notifications.js';
import { publishNotification } from '../../realtime/notification-bus.js';
import { createNotificationStreamResponse } from '../../realtime/notification-stream.js';
import { guardedDb } from '../guarded-db.js';
import { json, errorResponse, internalError, requireAuth, WRITE_ROLES, ADMIN_ROLES, requireRole, } from '../route-helpers.js';
export function registerCollabRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Comments — Phase 3 / slice 4
    //
    // Threaded, anchored discussion attached to documents. The Yjs gateway
    // already lives on `/realtime/sync`; comments are *not* sent over that
    // wire — they're plain REST so plugins, mobile clients, and notification
    // workers can consume them without speaking the CRDT protocol.
    //
    // Routes (all rewritten paths, no `/api/cms` prefix):
    //   POST   /documents/:documentId/comments      — create top-level / reply
    //   GET    /documents/:documentId/comments      — list (filters: resolved, deleted, pageSize)
    //   PATCH  /comments/:id                        — edit body (author / admin)
    //   POST   /comments/:id/resolve                — resolve thread (write-role / author)
    //   POST   /comments/:id/reopen                 — re-open a resolved thread
    //   DELETE /comments/:id                        — soft-delete (author / admin)
    //
    // The service module in `realtime/comments.ts` owns validation +
    // permissions; these handlers are thin envelopes that map auth → service
    // call → JSON response.
    // ---------------------------------------------------------------------------
    function mapCommentsErrorStatus(error) {
        switch (error.code) {
            case 'validation':
                return 400;
            case 'not_found':
                return 404;
            case 'forbidden':
                return 403;
            case 'conflict':
                return 409;
        }
    }
    function respondCommentsError(error) {
        return errorResponse(error.message, mapCommentsErrorStatus(error));
    }
    function commentsDb() {
        return db();
    }
    function notificationsDb() {
        return db();
    }
    function mapNotificationsErrorStatus(error) {
        if (error.code === 'validation')
            return 400;
        if (error.code === 'forbidden')
            return 403;
        if (error.code === 'not_found')
            return 404;
        return 500;
    }
    function respondNotificationsError(error) {
        return errorResponse(error.message, mapNotificationsErrorStatus(error));
    }
    /**
     * Lookup helper used by the notification triggers below. We need the
     * comment's `userId` (and, for replies, the *parent* comment's userId)
     * so we can decide who to notify. The lookup is best-effort: if the
     * row is missing (e.g. the comment was deleted between create and
     * trigger), we silently skip the notification.
     */
    async function findCommentRecipient(commentId) {
        try {
            const client = db();
            if (!client.documentComment)
                return null;
            return await client.documentComment.findUnique({ where: { id: commentId } });
        }
        catch {
            return null;
        }
    }
    /**
     * Issue notifications for a new comment. Two paths:
     *
     * - **Reply** — the actor replied to someone else's thread → notify
     *   the root comment's author with kind `comment_reply`.
     * - **Mentions** — any `@<userId>` token in the body → notify each
     *   mentioned user (other than the actor) with kind `comment_mention`.
     */
    async function notifyOnCommentCreated(opts) {
        const ndb = notificationsDb();
        if (opts.parentId) {
            const root = await findCommentRecipient(opts.parentId);
            if (root && root.userId && root.userId !== opts.actorId) {
                const result = await createNotification(ndb, {
                    userId: root.userId,
                    documentId: opts.documentId,
                    commentId: opts.commentId,
                    payload: {
                        kind: 'comment_reply',
                        actorId: opts.actorId,
                        actorName: opts.actorName,
                        excerpt: excerptForNotification(opts.body),
                    },
                });
                // Push the new DTO to any active SSE subscribers for this user
                // so the bell badge bumps in real time. We only publish AFTER
                // the DB insert succeeds — otherwise an SSE client would see a
                // ghost notification that doesn't exist on a subsequent
                // refresh. Failure to publish (no subscribers, or bus error)
                // is non-fatal: the poll fallback in the admin UI still
                // converges.
                if (result.ok)
                    publishNotification(result.value);
            }
        }
        for (const userId of extractMentionedUserIds(opts.body)) {
            if (userId === opts.actorId)
                continue;
            const result = await createNotification(ndb, {
                userId,
                documentId: opts.documentId,
                commentId: opts.commentId,
                payload: {
                    kind: 'comment_mention',
                    actorId: opts.actorId,
                    actorName: opts.actorName,
                    excerpt: excerptForNotification(opts.body),
                },
            });
            if (result.ok)
                publishNotification(result.value);
        }
    }
    /** Notify the comment's author when someone else resolves their thread. */
    async function notifyOnCommentResolved(opts) {
        const comment = await findCommentRecipient(opts.commentId);
        if (!comment || !comment.userId || comment.userId === opts.actorId)
            return;
        const result = await createNotification(notificationsDb(), {
            userId: comment.userId,
            commentId: opts.commentId,
            payload: {
                kind: 'comment_resolved',
                actorId: opts.actorId,
                actorName: opts.actorName,
                rootExcerpt: excerptForNotification(comment.body),
            },
        });
        if (result.ok)
            publishNotification(result.value);
    }
    router.post('/documents/:documentId/comments', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            // Posting a comment requires at least write access; comments mutate
            // collaborative state and we don't want anonymous read-only tokens
            // (API keys without write scope) leaving discussion behind.
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            const body = (await request.json());
            const result = await createCommentService(commentsDb(), {
                documentId: params.documentId,
                userId: auth.session.userId,
                body: typeof body.body === 'string' ? body.body : '',
                anchor: body.anchor,
                parentId: typeof body.parentId === 'string' ? body.parentId : null,
            });
            if (!result.ok)
                return respondCommentsError(result.error);
            await logEvent({
                event: 'comment_created',
                userId: auth.session.userId,
                details: {
                    documentId: params.documentId,
                    commentId: result.value.id,
                    parentId: result.value.parentId,
                },
            });
            // Fire-and-forget — a notification failure should never block the
            // comment from being saved. Errors surface via logEvent (TODO:
            // wire a dedicated audit channel once the realtime SSE feed is up).
            void notifyOnCommentCreated({
                commentId: result.value.id,
                body: result.value.body,
                parentId: result.value.parentId,
                documentId: result.value.documentId,
                actorId: auth.session.userId,
                actorName: null,
            }).catch(() => undefined);
            return json({ data: result.value }, 201);
        }
        catch (err) {
            return internalError(err, 'create comment');
        }
    });
    router.get('/documents/:documentId/comments', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            // Reading comments piggy-backs on the same scope as reading the
            // document. There is no comment-specific API-key scope today; we
            // re-use document-read so external integrations can fetch a thread
            // without granting them write access.
            const url = new URL(request.url);
            const includeResolved = url.searchParams.get('includeResolved') === 'true';
            const includeDeleted = url.searchParams.get('includeDeleted') === 'true';
            const pageSizeRaw = url.searchParams.get('pageSize');
            const pageSize = pageSizeRaw === null ? undefined : Number(pageSizeRaw);
            // Soft-deleted comments are admin-only — they hold moderation history
            // that should not leak via a query parameter to a regular editor.
            if (includeDeleted && !ADMIN_ROLES.has(auth.session.role)) {
                return errorResponse('includeDeleted requires admin role', 403);
            }
            const result = await listCommentsService(commentsDb(), {
                documentId: params.documentId,
                includeResolved,
                includeDeleted,
                pageSize,
            });
            if (!result.ok)
                return respondCommentsError(result.error);
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'list comments');
        }
    });
    router.patch('/comments/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const body = (await request.json());
            const result = await updateCommentService(commentsDb(), params.id, { body: typeof body.body === 'string' ? body.body : '' }, { userId: auth.session.userId, isAdmin: ADMIN_ROLES.has(auth.session.role) });
            if (!result.ok)
                return respondCommentsError(result.error);
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'update comment');
        }
    });
    router.post('/comments/:id/resolve', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const result = await resolveCommentService(commentsDb(), params.id, {
                userId: auth.session.userId,
                canResolve: WRITE_ROLES.has(auth.session.role),
            });
            if (!result.ok)
                return respondCommentsError(result.error);
            await logEvent({
                event: 'comment_resolved',
                userId: auth.session.userId,
                details: { commentId: params.id },
            });
            void notifyOnCommentResolved({
                commentId: params.id,
                actorId: auth.session.userId,
                actorName: null,
            }).catch(() => undefined);
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'resolve comment');
        }
    });
    router.post('/comments/:id/reopen', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const result = await reopenCommentService(commentsDb(), params.id, {
                userId: auth.session.userId,
                canResolve: WRITE_ROLES.has(auth.session.role),
            });
            if (!result.ok)
                return respondCommentsError(result.error);
            await logEvent({
                event: 'comment_reopened',
                userId: auth.session.userId,
                details: { commentId: params.id },
            });
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'reopen comment');
        }
    });
    router.delete('/comments/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const result = await deleteCommentService(commentsDb(), params.id, {
                userId: auth.session.userId,
                isAdmin: ADMIN_ROLES.has(auth.session.role),
            });
            if (!result.ok)
                return respondCommentsError(result.error);
            await logEvent({
                event: 'comment_deleted',
                userId: auth.session.userId,
                details: { commentId: params.id },
            });
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'delete comment');
        }
    });
    // ---------------------------------------------------------------------------
    // Notifications — Phase 3 / slice 6
    //
    // Notifications are the read-side of the collaboration event stream:
    // when a comment lifecycle action affects another user (reply,
    // mention, resolve-of-yours) we record a row in `DocumentNotification`
    // and the bell in the admin polls / streams it.
    //
    // Routes:
    //
    //   GET    /notifications              — list current user's feed
    //   GET    /notifications/unread-count — badge counter
    //   POST   /notifications/:id/read     — mark a single notification read
    //   POST   /notifications/read-all     — clear the bell
    //
    // Auth: every route is scoped to `auth.session.userId`; there is no
    // admin override (notifications are personal and never moderated).
    // ---------------------------------------------------------------------------
    router.get('/notifications', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const url = new URL(request.url);
            const includeRead = url.searchParams.get('includeRead') === 'true';
            const pageSizeRaw = url.searchParams.get('pageSize');
            const pageSize = pageSizeRaw === null ? undefined : Number(pageSizeRaw);
            const result = await listNotificationsForUser(notificationsDb(), {
                userId: auth.session.userId,
                includeRead,
                pageSize,
            });
            if (!result.ok)
                return respondNotificationsError(result.error);
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'list notifications');
        }
    });
    router.get('/notifications/unread-count', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const result = await notificationsUnreadCount(notificationsDb(), auth.session.userId);
            if (!result.ok)
                return respondNotificationsError(result.error);
            return json({ data: { count: result.value } });
        }
        catch (err) {
            return internalError(err, 'notifications unread count');
        }
    });
    router.post('/notifications/:id/read', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const result = await markNotificationRead(notificationsDb(), params.id, {
                userId: auth.session.userId,
            });
            if (!result.ok)
                return respondNotificationsError(result.error);
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'mark notification read');
        }
    });
    router.post('/notifications/read-all', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const result = await markAllNotificationsRead(notificationsDb(), auth.session.userId);
            if (!result.ok)
                return respondNotificationsError(result.error);
            return json({ data: result.value });
        }
        catch (err) {
            return internalError(err, 'mark all notifications read');
        }
    });
    // Per-user notification SSE push channel (Phase 7 carry-over). The
    // admin's NotificationBell subscribes via EventSource for real-time
    // delivery of comment_reply / comment_mention / comment_resolved
    // events. The existing GET /notifications and
    // /notifications/unread-count REST endpoints are kept as a
    // poll-fallback path for multi-node deployments where a write on
    // node A doesn't reach SSE subscribers on node B (the in-process
    // bus is single-instance by design — see notification-bus.ts).
    //
    // Auth: same scope as every other /notifications route — the
    // session userId determines which channel the stream subscribes to;
    // there is no admin override.
    router.get('/notifications/stream', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            return createNotificationStreamResponse(auth.session.userId);
        }
        catch (err) {
            return internalError(err, 'notifications stream');
        }
    });
}
//# sourceMappingURL=collab.js.map