import { randomUUID } from "node:crypto";
import { Router } from "express";
import multer from "multer";
import { addChatMessageSchema, createChatConversationSchema, updateChatConversationSchema, } from "@rudderhq/shared";
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
import { forbidden, HttpError, unauthorized } from "../errors.js";
import { observeExecutionEvent, updateExecutionObservation, updateExecutionTraceIO, withExecutionObservation, } from "../langfuse.js";
import { emitExecutionTranscriptTree } from "../langfuse-transcript.js";
import { validate } from "../middleware/validate.js";
import { logger } from "../middleware/logger.js";
import { ChatAssistantStreamError, chatAssistantService, } from "../services/chat-assistant.js";
import { claimChatGeneration, hasActiveChatGeneration } from "../services/chat-generation-locks.js";
import { accessService, agentService, chatService, operatorProfileService, organizationService, goalService, issueService, logActivity, projectService, } from "../services/index.js";
import { summarizeRuntimeSkillsForTrace } from "../services/runtime-trace-metadata.js";
import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js";
import { registerChatStreamRoutes } from "./chats.stream-routes.js";
export function chatRoutes(db, storage) {
    const router = Router();
    const svc = chatService(db);
    const organizationsSvc = organizationService(db);
    const issuesSvc = issueService(db);
    const projectsSvc = projectService(db);
    const agentsSvc = agentService(db);
    const goalsSvc = goalService(db);
    const access = accessService(db);
    const assistantSvc = chatAssistantService(db, storage);
    const operatorProfiles = operatorProfileService(db);
    const upload = multer({
        storage: multer.memoryStorage(),
        limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
    });
    const messageUpload = multer({
        storage: multer.memoryStorage(),
        limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 10 },
    });
    async function runSingleFileUpload(req, res) {
        await new Promise((resolve, reject) => {
            upload.single("file")(req, res, (err) => {
                if (err)
                    reject(err);
                else
                    resolve();
            });
        });
    }
    async function runMessageFileUpload(req, res) {
        await new Promise((resolve, reject) => {
            messageUpload.array("files", 10)(req, res, (err) => {
                if (err)
                    reject(err);
                else
                    resolve();
            });
        });
    }
    function isMultipartRequest(req) {
        return (req.headers["content-type"] ?? "").toLowerCase().startsWith("multipart/form-data");
    }
    function uploadedMessageFiles(req) {
        const files = req.files;
        const list = Array.isArray(files) ? files : [];
        return list.filter((file) => typeof file === "object" &&
            file !== null &&
            Buffer.isBuffer(file.buffer));
    }
    function validateUploadedMessageFiles(files) {
        for (const file of files) {
            const contentType = (file.mimetype || "").toLowerCase();
            if (!isAllowedContentType(contentType)) {
                return `Unsupported attachment type: ${contentType || "unknown"}`;
            }
            if (file.buffer.length <= 0) {
                return "Attachment is empty";
            }
        }
        return null;
    }
    async function assertConversationAccess(req, conversationId) {
        const conversation = await svc.getById(conversationId);
        if (!conversation)
            return null;
        assertCompanyAccess(req, conversation.orgId);
        return conversation;
    }
    function boardUserId(req) {
        assertBoard(req);
        return req.actor.userId ?? "local-board";
    }
    function canCreateAgentsLegacy(agent) {
        if (agent.role === "ceo")
            return true;
        if (!agent.permissions || typeof agent.permissions !== "object")
            return false;
        return Boolean(agent.permissions.canCreateAgents);
    }
    async function assertCanAssignTasks(req, orgId) {
        assertCompanyAccess(req, orgId);
        if (req.actor.type === "board") {
            if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)
                return;
            const allowed = await access.canUser(orgId, req.actor.userId, "tasks:assign");
            if (!allowed)
                throw forbidden("Missing permission: tasks:assign");
            return;
        }
        if (req.actor.type === "agent") {
            if (!req.actor.agentId)
                throw forbidden("Agent authentication required");
            const allowedByGrant = await access.hasPermission(orgId, "agent", req.actor.agentId, "tasks:assign");
            if (allowedByGrant)
                return;
            const actorAgent = await agentsSvc.getById(req.actor.agentId);
            if (actorAgent && actorAgent.orgId === orgId && canCreateAgentsLegacy(actorAgent))
                return;
            throw forbidden("Missing permission: tasks:assign");
        }
        throw unauthorized();
    }
    function buildChatObservabilityContext(conversation, input) {
        return {
            surface: input.surface ?? "chat_action",
            rootExecutionId: input.rootExecutionId,
            orgId: conversation.orgId,
            agentId: conversation.preferredAgentId ?? null,
            issueId: input.issueId ?? conversation.primaryIssueId ?? null,
            sessionKey: conversation.id,
            runtime: input.runtime ?? null,
            trigger: input.trigger,
            status: input.status ?? null,
            metadata: {
                conversationId: conversation.id,
                ...(input.metadata ?? {}),
            },
        };
    }
    async function withChatObservation(context, input, fn) {
        let executionError = null;
        try {
            return await withExecutionObservation(context, input, async (observation) => {
                try {
                    return await fn(observation);
                }
                catch (error) {
                    executionError = error;
                    throw error;
                }
            });
        }
        catch (error) {
            if (executionError && error === executionError) {
                throw error;
            }
            logger.warn({
                rootExecutionId: context.rootExecutionId,
                trigger: context.trigger,
                err: error instanceof Error ? error.message : String(error),
            }, "Failed to emit Langfuse chat observation");
            return fn(null);
        }
    }
    async function emitChatObservationEvent(context, input) {
        try {
            await observeExecutionEvent(context, input);
        }
        catch (error) {
            logger.warn({
                rootExecutionId: context.rootExecutionId,
                eventName: input.name,
                err: error instanceof Error ? error.message : String(error),
            }, "Failed to emit Langfuse chat event");
        }
    }
    function summarizeChatObservationMessages(messages) {
        const proposalMessage = messages.find((message) => message.kind === "issue_proposal" || message.kind === "operation_proposal");
        const systemEventMessage = messages.find((message) => message.kind === "system_event");
        const systemPayload = systemEventMessage?.structuredPayload && typeof systemEventMessage.structuredPayload === "object"
            ? systemEventMessage.structuredPayload
            : null;
        return {
            createdMessageIds: messages.map((message) => message.id),
            assistantKind: proposalMessage?.kind ?? messages.find((message) => message.role === "assistant")?.kind ?? null,
            approvalId: proposalMessage?.approvalId ?? null,
            issueId: typeof systemPayload?.issueId === "string" ? systemPayload.issueId : null,
            issueIdentifier: typeof systemPayload?.issueIdentifier === "string" ? systemPayload.issueIdentifier : null,
            eventType: typeof systemPayload?.eventType === "string" ? systemPayload.eventType : null,
        };
    }
    function modelTurnInputFromInvocationMeta(invocationMeta) {
        return typeof invocationMeta.prompt === "string" && invocationMeta.prompt.trim().length > 0
            ? invocationMeta.prompt
            : undefined;
    }
    function buildChatTraceInput(input, invocationMeta) {
        return {
            conversationId: input.conversationId,
            body: input.body,
            userMessageId: input.userMessageId,
            instruction: typeof invocationMeta?.prompt === "string" && invocationMeta.prompt.trim().length > 0
                ? invocationMeta.prompt
                : null,
            promptMetrics: invocationMeta?.promptMetrics ?? null,
        };
    }
    function mergeChatInvocationTraceMetadata(context, invocationMeta) {
        context.metadata = {
            ...(context.metadata ?? {}),
            runtimeAgentType: invocationMeta.agentRuntimeType,
            runtimeCommand: invocationMeta.command,
            runtimeCwd: invocationMeta.cwd ?? null,
            runtimeCommandNotes: invocationMeta.commandNotes ?? [],
            runtimePromptMetrics: invocationMeta.promptMetrics ?? null,
            runtimePromptCaptured: typeof invocationMeta.prompt === "string" && invocationMeta.prompt.length > 0,
            ...(Array.isArray(invocationMeta.loadedSkills)
                ? summarizeRuntimeSkillsForTrace(invocationMeta.loadedSkills)
                : {}),
        };
    }
    async function logChatMessagesAdded(conversation, messages, actor) {
        await Promise.all(messages.map((message) => logActivity(db, {
            orgId: conversation.orgId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId ?? null,
            runId: actor.runId ?? null,
            action: "chat.message_added",
            entityType: "chat",
            entityId: conversation.id,
            details: {
                messageId: message.id,
                role: message.role,
                kind: message.kind,
                status: message.status,
                preview: message.body.slice(0, 280),
            },
        })));
    }
    async function assertContextLinksBelongToCompany(orgId, contextLinks) {
        for (const link of contextLinks) {
            if (link.entityType === "issue") {
                const issue = await issuesSvc.getById(link.entityId);
                if (!issue || issue.orgId !== orgId) {
                    throw new HttpError(422, "Issue context must belong to the same organization");
                }
                continue;
            }
            if (link.entityType === "project") {
                const project = await projectsSvc.getById(link.entityId);
                if (!project || project.orgId !== orgId) {
                    throw new HttpError(422, "Project context must belong to the same organization");
                }
                continue;
            }
            const agent = await agentsSvc.getById(link.entityId);
            if (!agent || agent.orgId !== orgId) {
                throw new HttpError(422, "Agent context must belong to the same organization");
            }
        }
    }
    function turnContextFromUserMessage(userMessage) {
        if (!userMessage.chatTurnId) {
            throw new Error("User message missing chat turn id");
        }
        return { chatTurnId: userMessage.chatTurnId, turnVariant: userMessage.turnVariant };
    }
    async function addUserMessage(conversation, body, actor, editUserMessageId) {
        const userMessage = await svc.addUserChatMessage(conversation.id, conversation.orgId, body, editUserMessageId ?? null);
        await logActivity(db, {
            orgId: conversation.orgId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            runId: actor.runId,
            action: "chat.message_added",
            entityType: "chat",
            entityId: conversation.id,
            details: {
                messageId: userMessage.id,
                role: "user",
                kind: "message",
                editUserMessageId: editUserMessageId ?? null,
            },
        });
        return userMessage;
    }
    async function attachFilesToUserMessage(conversation, messageId, files, actor) {
        const attachments = [];
        for (const file of files) {
            const contentType = (file.mimetype || "").toLowerCase();
            if (!isAllowedContentType(contentType)) {
                throw new HttpError(422, `Unsupported attachment type: ${contentType || "unknown"}`);
            }
            if (file.buffer.length <= 0) {
                throw new HttpError(422, "Attachment is empty");
            }
            const stored = await storage.putFile({
                orgId: conversation.orgId,
                namespace: `chats/${conversation.id}`,
                originalFilename: file.originalname || null,
                contentType,
                body: file.buffer,
            });
            const attachment = await svc.createAttachment({
                orgId: conversation.orgId,
                conversationId: conversation.id,
                messageId,
                provider: stored.provider,
                objectKey: stored.objectKey,
                contentType: stored.contentType,
                byteSize: stored.byteSize,
                sha256: stored.sha256,
                originalFilename: stored.originalFilename,
                createdByAgentId: actor.agentId,
                createdByUserId: actor.actorType === "user" ? actor.actorId : null,
            });
            attachments.push(attachment);
            await logActivity(db, {
                orgId: conversation.orgId,
                actorType: actor.actorType,
                actorId: actor.actorId,
                agentId: actor.agentId,
                runId: actor.runId,
                action: "chat.attachment_added",
                entityType: "chat",
                entityId: conversation.id,
                details: {
                    attachmentId: attachment.id,
                    messageId: attachment.messageId,
                    originalFilename: attachment.originalFilename,
                    contentType: attachment.contentType,
                },
            });
        }
        return attachments;
    }
    async function loadAssistantInput(conversation, actor) {
        const freshConversation = await svc.getById(conversation.id);
        const hydratedConversation = await assistantSvc.enrichConversation((freshConversation ?? conversation));
        const rawMessages = await svc.listMessages(conversation.id);
        const freshMessages = rawMessages.filter((m) => !m.supersededAt);
        const operatorProfile = actor.actorType === "user"
            ? await operatorProfiles.get(actor.actorId)
            : null;
        const issueLabels = await issuesSvc.listLabels(conversation.orgId);
        return {
            conversation: hydratedConversation,
            messages: freshMessages,
            contextLinks: (hydratedConversation.contextLinks ?? conversation.contextLinks),
            issueLabels,
            operatorProfile,
        };
    }
    function chatReplyingAgentId(conversation) {
        return conversation?.chatRuntime?.runtimeAgentId ?? conversation?.preferredAgentId ?? null;
    }
    function proposedIssuePayload(structuredPayload) {
        if (!structuredPayload)
            return structuredPayload ?? null;
        return structuredPayload.issueProposal
            && typeof structuredPayload.issueProposal === "object"
            && !Array.isArray(structuredPayload.issueProposal)
            && structuredPayload.issueProposal !== null
            ? structuredPayload.issueProposal
            : structuredPayload;
    }
    function proposalAssignsOrReviewsIssue(proposal) {
        if (!proposal)
            return false;
        return Boolean((typeof proposal.assigneeAgentId === "string" && proposal.assigneeAgentId.trim().length > 0)
            || (typeof proposal.assigneeUserId === "string" && proposal.assigneeUserId.trim().length > 0)
            || (typeof proposal.reviewerAgentId === "string" && proposal.reviewerAgentId.trim().length > 0)
            || (typeof proposal.reviewerUserId === "string" && proposal.reviewerUserId.trim().length > 0));
    }
    async function proposedIssuePayloadForConversion(conversationId, input) {
        if (input.proposal)
            return proposedIssuePayload(input.proposal);
        if (input.messageId) {
            const message = await svc.getMessage(conversationId, input.messageId);
            return proposedIssuePayload(message?.structuredPayload ?? null);
        }
        const messages = await svc.listMessages(conversationId);
        const message = [...messages].reverse().find((entry) => entry.kind === "issue_proposal");
        return proposedIssuePayload(message?.structuredPayload ?? null);
    }
    async function assertCanConvertIssueProposal(req, conversation, input) {
        const proposal = await proposedIssuePayloadForConversion(conversation.id, input);
        if (proposalAssignsOrReviewsIssue(proposal)) {
            await assertCanAssignTasks(req, conversation.orgId);
        }
    }
    async function chatIssueProposalNeedsOperatorLabelSelection(orgId, proposedByAgentId, proposal) {
        if (!proposedByAgentId)
            return false;
        const labelIds = Array.isArray(proposal?.labelIds)
            ? proposal.labelIds.filter((value) => typeof value === "string" && value.trim().length > 0)
            : [];
        if (labelIds.length > 0)
            return false;
        const labels = await issuesSvc.listLabels(orgId);
        return labels.length >= 5;
    }
    function proposedPlanDocumentPayload(structuredPayload) {
        if (!structuredPayload)
            return null;
        const rawDocument = structuredPayload.planDocument
            && typeof structuredPayload.planDocument === "object"
            && !Array.isArray(structuredPayload.planDocument)
            ? structuredPayload.planDocument
            : structuredPayload.plan && typeof structuredPayload.plan === "object" && !Array.isArray(structuredPayload.plan)
                ? structuredPayload.plan
                : null;
        return rawDocument ? rawDocument : null;
    }
    async function persistAssistantReply(req, conversation, actor, assistantReply, turnContext, transcript = [], replyingAgentId = assistantReply.replyingAgentId ?? chatReplyingAgentId(conversation), existingMessageId) {
        const createdMessages = [];
        const { chatTurnId, turnVariant } = turnContext;
        const attachGeneratedFiles = async (message, generatedAttachments) => {
            if (!generatedAttachments || generatedAttachments.length === 0)
                return message;
            const attachments = [];
            for (const generated of generatedAttachments) {
                if (generated.body.length > MAX_ATTACHMENT_BYTES) {
                    throw new ChatAssistantStreamError(`Generated attachment exceeds ${MAX_ATTACHMENT_BYTES} bytes`, assistantReply.body, generatedAttachments);
                }
                const stored = await storage.putFile({
                    orgId: conversation.orgId,
                    namespace: `chats/${conversation.id}/generated`,
                    originalFilename: generated.originalFilename,
                    contentType: generated.contentType,
                    body: generated.body,
                });
                const attachment = await svc.createAttachment({
                    orgId: conversation.orgId,
                    conversationId: conversation.id,
                    messageId: message.id,
                    provider: stored.provider,
                    objectKey: stored.objectKey,
                    contentType: stored.contentType,
                    byteSize: stored.byteSize,
                    sha256: stored.sha256,
                    originalFilename: stored.originalFilename,
                    createdByAgentId: replyingAgentId,
                    createdByUserId: null,
                });
                attachments.push(attachment);
            }
            return {
                ...message,
                attachments: [...(message.attachments ?? []), ...attachments],
            };
        };
        const saveAssistantMessage = async (input) => {
            if (existingMessageId) {
                const updated = await svc.updateMessage(conversation.id, existingMessageId, {
                    kind: input.kind,
                    status: "completed",
                    body: input.body,
                    structuredPayload: input.structuredPayload ?? null,
                    transcript,
                    approvalId: input.approvalId ?? null,
                    replyingAgentId,
                });
                if (updated)
                    return updated;
            }
            return svc.addMessage(conversation.id, {
                orgId: conversation.orgId,
                role: "assistant",
                kind: input.kind,
                body: input.body,
                structuredPayload: input.structuredPayload ?? null,
                transcript,
                approvalId: input.approvalId ?? null,
                replyingAgentId,
                chatTurnId,
                turnVariant,
            });
        };
        if (assistantReply.kind === "issue_proposal") {
            const issueProposalStructuredPayload = assistantReply.structuredPayload ?? null;
            const proposalPayload = proposedIssuePayload(issueProposalStructuredPayload);
            const needsOperatorLabelSelection = await chatIssueProposalNeedsOperatorLabelSelection(conversation.orgId, replyingAgentId, proposalPayload);
            const shouldAutoCreateIssue = !needsOperatorLabelSelection
                && !conversation.planMode
                && conversation.issueCreationMode === "auto_create";
            if (shouldAutoCreateIssue) {
                const proposalMessage = await saveAssistantMessage({
                    kind: "issue_proposal",
                    body: assistantReply.body,
                    structuredPayload: issueProposalStructuredPayload,
                });
                createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
                await assertCanConvertIssueProposal(req, conversation, {
                    proposal: issueProposalStructuredPayload,
                });
                const issue = await svc.convertToIssue(conversation.id, {
                    actorUserId: actor.actorType === "user" ? actor.actorId : null,
                    createdByAgentId: replyingAgentId,
                    messageId: proposalMessage.id,
                });
                const systemMessage = await svc.addMessage(conversation.id, {
                    orgId: conversation.orgId,
                    role: "system",
                    kind: "system_event",
                    body: `Created issue ${issue.identifier ?? issue.id} from this chat conversation.`,
                    structuredPayload: {
                        eventType: "issue_created",
                        issueId: issue.id,
                        issueIdentifier: issue.identifier,
                    },
                    chatTurnId,
                    turnVariant,
                });
                createdMessages.push(systemMessage);
                await logActivity(db, {
                    orgId: conversation.orgId,
                    actorType: "system",
                    actorId: "chat-assistant",
                    action: "chat.issue_converted",
                    entityType: "chat",
                    entityId: conversation.id,
                    details: {
                        issueId: issue.id,
                        issueIdentifier: issue.identifier,
                        source: "auto_create",
                    },
                });
                return createdMessages;
            }
            const planDocument = proposedPlanDocumentPayload(issueProposalStructuredPayload);
            const approval = await svc.createProposalApproval(conversation.orgId, {
                type: "chat_issue_creation",
                requestedByUserId: actor.actorType === "user" ? actor.actorId : null,
                payload: {
                    chatConversationId: conversation.id,
                    proposedByAgentId: replyingAgentId,
                    proposedIssue: proposalPayload,
                    ...(planDocument ? { planDocument } : {}),
                },
            });
            const proposalMessage = await saveAssistantMessage({
                kind: "issue_proposal",
                body: assistantReply.body,
                structuredPayload: issueProposalStructuredPayload,
                approvalId: approval.id,
            });
            createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
            return createdMessages;
        }
        if (assistantReply.kind === "operation_proposal") {
            const approval = await svc.createProposalApproval(conversation.orgId, {
                type: "chat_operation",
                requestedByUserId: actor.actorType === "user" ? actor.actorId : null,
                payload: {
                    chatConversationId: conversation.id,
                    operationProposal: assistantReply.structuredPayload &&
                        typeof assistantReply.structuredPayload.operationProposal === "object" &&
                        assistantReply.structuredPayload.operationProposal !== null
                        ? assistantReply.structuredPayload.operationProposal
                        : assistantReply.structuredPayload,
                },
            });
            const proposalMessage = await saveAssistantMessage({
                kind: "operation_proposal",
                body: assistantReply.body,
                structuredPayload: {
                    ...(assistantReply.structuredPayload ?? {}),
                    operationProposalState: {
                        status: "pending",
                        decisionNote: null,
                        decidedByUserId: null,
                        decidedAt: null,
                    },
                },
                approvalId: approval.id,
            });
            createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
            return createdMessages;
        }
        if (assistantReply.kind === "ask_user") {
            const assistantMessage = await saveAssistantMessage({
                kind: "ask_user",
                body: assistantReply.body,
                structuredPayload: assistantReply.structuredPayload,
            });
            createdMessages.push(await attachGeneratedFiles(assistantMessage, assistantReply.generatedAttachments));
            return createdMessages;
        }
        const assistantMessage = await saveAssistantMessage({
            kind: "message",
            body: assistantReply.body,
            structuredPayload: assistantReply.structuredPayload,
        });
        createdMessages.push(await attachGeneratedFiles(assistantMessage, assistantReply.generatedAttachments));
        return createdMessages;
    }
    async function attachGeneratedFilesToPartialMessage(conversation, message, generatedAttachments, replyingAgentId) {
        if (!message || !generatedAttachments || generatedAttachments.length === 0)
            return message;
        const attachments = [];
        for (const generated of generatedAttachments) {
            if (generated.body.length > MAX_ATTACHMENT_BYTES)
                continue;
            const stored = await storage.putFile({
                orgId: conversation.orgId,
                namespace: `chats/${conversation.id}/generated`,
                originalFilename: generated.originalFilename,
                contentType: generated.contentType,
                body: generated.body,
            });
            const attachment = await svc.createAttachment({
                orgId: conversation.orgId,
                conversationId: conversation.id,
                messageId: message.id,
                provider: stored.provider,
                objectKey: stored.objectKey,
                contentType: stored.contentType,
                byteSize: stored.byteSize,
                sha256: stored.sha256,
                originalFilename: stored.originalFilename,
                createdByAgentId: replyingAgentId,
                createdByUserId: null,
            });
            attachments.push(attachment);
        }
        return {
            ...message,
            attachments: [...(message.attachments ?? []), ...attachments],
        };
    }
    async function persistPartialAssistantMessage(conversation, body, status, turnContext, transcript = [], replyingAgentId = chatReplyingAgentId(conversation), existingMessageId) {
        const trimmed = body.trim();
        const fallbackBody = status === "stopped"
            ? "Chat run stopped before a final reply. Continue the conversation to resume from the preserved context."
            : "Chat run failed before a final reply. Continue the conversation to resume from the preserved context.";
        const durableBody = trimmed || (transcript.length > 0 ? fallbackBody : "");
        if (!durableBody)
            return null;
        const chatTurnId = turnContext?.chatTurnId ?? randomUUID();
        const turnVariant = turnContext?.turnVariant ?? 0;
        if (existingMessageId) {
            const updated = await svc.updateMessage(conversation.id, existingMessageId, {
                kind: "message",
                status,
                body: durableBody,
                transcript,
                replyingAgentId,
            });
            if (updated)
                return updated;
        }
        const message = await svc.addMessage(conversation.id, {
            orgId: conversation.orgId,
            role: "assistant",
            kind: "message",
            status,
            body: durableBody,
            transcript,
            replyingAgentId,
            chatTurnId,
            turnVariant,
        });
        return message;
    }
    function writeStreamEvent(res, event) {
        if (res.writableEnded || res.destroyed)
            return false;
        res.write(`${JSON.stringify(event)}\n`);
        return true;
    }
    router.get("/orgs/:orgId/chats", async (req, res) => {
        const orgId = req.params.orgId;
        assertCompanyAccess(req, orgId);
        const statusParam = typeof req.query.status === "string" ? req.query.status : "active";
        const status = statusParam === "resolved" || statusParam === "archived" || statusParam === "all"
            ? statusParam
            : "active";
        const q = typeof req.query.q === "string" ? req.query.q : undefined;
        const userId = req.actor.type === "board" ? (req.actor.userId ?? "local-board") : null;
        const conversations = await svc.list(orgId, { status, q }, userId);
        res.json(await assistantSvc.enrichConversations(conversations));
    });
    router.post("/orgs/:orgId/chats", validate(createChatConversationSchema), async (req, res) => {
        const orgId = req.params.orgId;
        assertCompanyAccess(req, orgId);
        const organization = await organizationsSvc.getById(orgId);
        if (!organization) {
            res.status(404).json({ error: "Organization not found" });
            return;
        }
        const contextLinks = req.body.contextLinks ?? [];
        await assertContextLinksBelongToCompany(orgId, contextLinks);
        if (req.body.preferredAgentId) {
            const agent = await agentsSvc.getById(req.body.preferredAgentId);
            if (!agent || agent.orgId !== orgId) {
                res.status(422).json({ error: "Preferred agent must belong to the same organization" });
                return;
            }
        }
        const actor = getActorInfo(req);
        const conversation = await svc.create(orgId, {
            title: req.body.title,
            summary: req.body.summary ?? null,
            preferredAgentId: req.body.preferredAgentId ?? null,
            issueCreationMode: req.body.issueCreationMode ?? organization.defaultChatIssueCreationMode,
            planMode: req.body.planMode ?? false,
            createdByUserId: actor.actorType === "user" ? actor.actorId : null,
            contextLinks,
        });
        await logActivity(db, {
            orgId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            runId: actor.runId,
            action: "chat.created",
            entityType: "chat",
            entityId: conversation?.id ?? "unknown",
            details: {
                title: conversation?.title ?? "New chat",
                contextLinkCount: contextLinks.length,
                contextLinks: contextLinks.map((link) => ({
                    entityType: link.entityType,
                    entityId: link.entityId,
                })),
            },
        });
        res.status(201).json(await assistantSvc.enrichConversation(conversation));
    });
    router.get("/chats/:id", async (req, res) => {
        const conversation = await assertConversationAccess(req, req.params.id);
        if (!conversation) {
            res.status(404).json({ error: "Chat conversation not found" });
            return;
        }
        const userId = req.actor.type === "board" ? (req.actor.userId ?? "local-board") : null;
        const refreshed = await svc.getById(conversation.id, userId);
        res.json(await assistantSvc.enrichConversation(refreshed));
    });
    router.patch("/chats/:id", validate(updateChatConversationSchema), async (req, res) => {
        const existing = await assertConversationAccess(req, req.params.id);
        if (!existing) {
            res.status(404).json({ error: "Chat conversation not found" });
            return;
        }
        if (req.body.primaryIssueId) {
            const issue = await issuesSvc.getById(req.body.primaryIssueId);
            if (!issue || issue.orgId !== existing.orgId) {
                res.status(422).json({ error: "Primary issue must belong to the same organization" });
                return;
            }
        }
        if (req.body.preferredAgentId) {
            const agent = await agentsSvc.getById(req.body.preferredAgentId);
            if (!agent || agent.orgId !== existing.orgId) {
                res.status(422).json({ error: "Preferred agent must belong to the same organization" });
                return;
            }
        }
        if (req.body.routedAgentId) {
            const agent = await agentsSvc.getById(req.body.routedAgentId);
            if (!agent || agent.orgId !== existing.orgId) {
                res.status(422).json({ error: "Routed agent must belong to the same organization" });
                return;
            }
        }
        const updated = await svc.update(existing.id, {
            ...req.body,
            resolvedAt: req.body.resolvedAt ? new Date(req.body.resolvedAt) : req.body.resolvedAt,
        });
        const actor = getActorInfo(req);
        await logActivity(db, {
            orgId: existing.orgId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            runId: actor.runId,
            action: "chat.updated",
            entityType: "chat",
            entityId: existing.id,
            details: req.body,
        });
        res.json(updated ? await assistantSvc.enrichConversation(updated) : null);
    });
    router.get("/chats/:id/messages", async (req, res) => {
        const conversation = await assertConversationAccess(req, req.params.id);
        if (!conversation) {
            res.status(404).json({ error: "Chat conversation not found" });
            return;
        }
        if (!hasActiveChatGeneration(conversation.id)) {
            await svc.markInterruptedStreamingMessages(conversation.id);
        }
        const messages = await svc.listMessages(conversation.id);
        res.json(messages);
    });
    router.post("/chats/:id/messages", validate(addChatMessageSchema), async (req, res) => {
        const conversation = await assertConversationAccess(req, req.params.id);
        if (!conversation) {
            res.status(404).json({ error: "Chat conversation not found" });
            return;
        }
        const assistantAvailability = await assistantSvc.getChatAssistantAvailability(conversation);
        if (!assistantAvailability.available) {
            res.status(503).json({ error: assistantAvailability.error });
            return;
        }
        const releaseGeneration = claimChatGeneration(conversation.id);
        if (!releaseGeneration) {
            res.status(409).json({ error: "A chat reply is already being generated for this conversation" });
            return;
        }
        const actor = getActorInfo(req);
        let chatObservation = null;
        try {
            const userMessage = await addUserMessage(conversation, req.body.body, actor, req.body.editUserMessageId ?? null);
            const turnContext = turnContextFromUserMessage(userMessage);
            chatObservation = buildChatObservabilityContext(conversation, {
                surface: "chat_turn",
                rootExecutionId: turnContext.chatTurnId,
                trigger: "assistant_reply",
                runtime: assistantAvailability.agentRuntimeType ?? null,
                metadata: {
                    stream: false,
                    userMessageId: userMessage.id,
                    editUserMessageId: req.body.editUserMessageId ?? null,
                },
            });
            const traceInputBase = {
                conversationId: conversation.id,
                body: req.body.body,
                userMessageId: userMessage.id,
            };
            let currentChatTraceInput = buildChatTraceInput(traceInputBase);
            const persistedAssistantMessages = await withChatObservation(chatObservation, {
                name: "chat_turn",
                asType: "agent",
                input: currentChatTraceInput,
            }, async (observation) => {
                const assistantInput = await loadAssistantInput(conversation, actor);
                const transcript = [];
                const observedTranscript = [];
                let modelTurnInput;
                let fallbackOutput = null;
                let finalChatOutput = null;
                let finalChatStatus = "completed";
                try {
                    const streamed = await assistantSvc.streamChatAssistantReply({
                        ...assistantInput,
                        onInvocationMeta: async (meta) => {
                            modelTurnInput = modelTurnInputFromInvocationMeta(meta);
                            currentChatTraceInput = buildChatTraceInput(traceInputBase, meta);
                            mergeChatInvocationTraceMetadata(chatObservation, meta);
                            updateExecutionObservation(observation, chatObservation, {
                                input: currentChatTraceInput,
                            });
                            updateExecutionTraceIO(observation, { input: currentChatTraceInput });
                        },
                        onTranscriptEntry: async (entry) => {
                            transcript.push(entry);
                        },
                        onObservedTranscriptEntry: async (entry) => {
                            observedTranscript.push(entry);
                        },
                    });
                    fallbackOutput = streamed.partialBody;
                    if (streamed.outcome !== "completed") {
                        finalChatStatus = "failed";
                        throw new Error("Chat assistant reply was stopped before completion");
                    }
                    const created = await persistAssistantReply(req, assistantInput.conversation, actor, streamed.reply, turnContext, transcript, streamed.replyingAgentId);
                    finalChatOutput = streamed.reply.body;
                    await logChatMessagesAdded(assistantInput.conversation, created, {
                        actorType: "system",
                        actorId: "chat-assistant",
                        agentId: streamed.replyingAgentId,
                    });
                    const summary = summarizeChatObservationMessages(created);
                    await emitChatObservationEvent(chatObservation, {
                        name: "chat.reply.persisted",
                        metadata: {
                            transcriptEntries: transcript.length,
                            observedTranscriptEntries: observedTranscript.length,
                            ...summary,
                        },
                    });
                    return created;
                }
                catch (error) {
                    if (error instanceof ChatAssistantStreamError) {
                        fallbackOutput = error.partialBody;
                    }
                    finalChatStatus = "failed";
                    throw error;
                }
                finally {
                    try {
                        const transcriptStats = emitExecutionTranscriptTree({
                            context: chatObservation,
                            parentObservation: observation,
                            transcript: observedTranscript,
                            initialTurnInput: modelTurnInput,
                            fallbackResult: fallbackOutput
                                ? {
                                    output: fallbackOutput,
                                }
                                : null,
                        });
                        finalChatOutput = finalChatOutput ?? transcriptStats.finalOutput ?? fallbackOutput ?? null;
                    }
                    catch (error) {
                        logger.warn({
                            rootExecutionId: chatObservation.rootExecutionId,
                            err: error instanceof Error ? error.message : String(error),
                        }, "Failed to export chat transcript tree to Langfuse");
                    }
                    updateExecutionObservation(observation, {
                        ...chatObservation,
                        status: finalChatStatus,
                    }, {
                        input: currentChatTraceInput,
                        output: finalChatOutput,
                        level: finalChatStatus === "failed" ? "ERROR" : "DEFAULT",
                        statusMessage: finalChatStatus,
                    });
                    updateExecutionTraceIO(observation, {
                        input: currentChatTraceInput,
                        output: finalChatOutput,
                    });
                }
            });
            const createdMessages = [userMessage, ...persistedAssistantMessages];
            res.status(201).json({ messages: createdMessages });
        }
        catch (err) {
            if (chatObservation) {
                await emitChatObservationEvent(chatObservation, {
                    name: "chat.reply.failed",
                    level: "ERROR",
                    metadata: {
                        error: err instanceof Error ? err.message : String(err),
                    },
                    statusMessage: err instanceof Error ? err.message : "chat_reply_failed",
                });
            }
            logger.warn({ err, conversationId: conversation.id }, "chat assistant reply failed");
            if (err instanceof HttpError) {
                throw err;
            }
            res.status(502).json({
                error: err instanceof Error ? err.message : "Chat assistant failed to respond",
            });
        }
        finally {
            releaseGeneration();
        }
    });
    registerChatStreamRoutes({
        router,
        db,
        storage,
        svc,
        assistantSvc,
        agentsSvc,
        issuesSvc,
        projectsSvc,
        goalsSvc,
        access,
        operatorProfiles,
        assertConversationAccess,
        boardUserId,
        assertCanAssignTasks,
        runSingleFileUpload,
        runMessageFileUpload,
        isMultipartRequest,
        uploadedMessageFiles,
        validateUploadedMessageFiles,
        buildChatObservabilityContext,
        withChatObservation,
        emitChatObservationEvent,
        summarizeChatObservationMessages,
        modelTurnInputFromInvocationMeta,
        buildChatTraceInput,
        mergeChatInvocationTraceMetadata,
        logChatMessagesAdded,
        assertContextLinksBelongToCompany,
        turnContextFromUserMessage,
        addUserMessage,
        attachFilesToUserMessage,
        loadAssistantInput,
        chatReplyingAgentId,
        assertCanConvertIssueProposal,
        persistAssistantReply,
        attachGeneratedFilesToPartialMessage,
        persistPartialAssistantMessage,
        writeStreamEvent,
    });
    return router;
}
//# sourceMappingURL=chats.js.map