import { agentIntegrationBindingTokens, agentIntegrationChatBindings, agentIntegrationInboundAudit, agentIntegrationInboundDedup, agentIntegrationOutboundMessages, agentIntegrationUserBindings, agentIntegrations, chatContextLinks, chatConversations, chatGenerations, chatMessages, organizationMemberships, } from "@rudderhq/db";
import { formatMessengerTitle } from "@rudderhq/shared";
import { and, desc, eq, inArray, isNull, or } from "drizzle-orm";
import { createHash, randomBytes } from "node:crypto";
import { chatAgentRunService } from "../../chat-agent-runs.js";
import { cancelActiveChatGeneration, getActiveChatGeneration } from "../../chat-generation-locks.js";
import { chatTitleGenerationService } from "../../chat-title-generation.js";
import { chatService } from "../../chats.js";
import { issueService } from "../../issues.js";
import { productIntelligenceService } from "../../product-intelligence.js";
const BINDING_TOKEN_TTL_MS = 15 * 60 * 1000;
function isUniqueViolation(error) {
    return error.code === "23505";
}
function createBindingToken() {
    return `rudder_feishu_${randomBytes(24).toString("hex")}`;
}
function hashToken(token) {
    return createHash("sha256").update(token).digest("hex");
}
function integrationStatus(value) {
    if (value === "active" || value === "revoked" || value === "error")
        return value;
    return "error";
}
function chatTitle(event) {
    return formatMessengerTitle(event.body) ?? "New chat";
}
function feishuQuickCommandMessage(command, event, details = {}) {
    return {
        source: "agent_integration",
        provider: event.provider,
        command,
        integrationId: details.integrationId,
        externalChatId: event.chatId,
        externalChatType: event.chatType,
        externalMessageId: event.messageId,
        externalEventId: event.eventId,
        externalSenderOpenId: event.senderOpenId,
        externalSenderUnionId: event.senderUnionId,
        externalParentMessageId: event.parentMessageId ?? null,
        ...details,
    };
}
async function createFeishuChatConversation(client, input) {
    const [conversation] = await client
        .insert(chatConversations)
        .values({
        orgId: input.orgId,
        title: "New chat",
        summary: null,
        preferredAgentId: input.agentId,
        issueCreationMode: "manual_approval",
        planMode: false,
        createdByUserId: input.userId,
    })
        .returning();
    if (!conversation)
        throw new Error("Failed to create Feishu chat conversation");
    await client
        .insert(chatContextLinks)
        .values({
        orgId: input.orgId,
        conversationId: conversation.id,
        entityType: "agent",
        entityId: input.agentId,
        metadata: { source: "agent_integration", provider: input.provider },
    })
        .onConflictDoNothing();
    return conversation;
}
export function createFeishuInboundDispatcherDbDeps(db, options = {}) {
    const chats = chatService(db);
    const issues = issueService(db);
    const chatRuns = chatAgentRunService(db);
    const chatTitles = chatTitleGenerationService({
        chats,
        productIntelligence: options.productIntelligence ?? productIntelligenceService(db),
    });
    const deps = {
        resolveActiveIntegration: async (event) => {
            const conditions = [
                eq(agentIntegrations.provider, event.provider),
                eq(agentIntegrations.externalAppId, event.appId),
                eq(agentIntegrations.status, "active"),
            ];
            if (options.orgId) {
                conditions.push(eq(agentIntegrations.orgId, options.orgId));
            }
            if (event.botOpenId) {
                conditions.push(or(isNull(agentIntegrations.externalBotOpenId), eq(agentIntegrations.externalBotOpenId, event.botOpenId)));
            }
            const rows = await db
                .select()
                .from(agentIntegrations)
                .where(and(...conditions));
            const exactBotMatches = event.botOpenId
                ? rows.filter((row) => row.externalBotOpenId === event.botOpenId)
                : [];
            const candidates = exactBotMatches.length > 0 ? exactBotMatches : rows;
            if (candidates.length !== 1)
                return null;
            const row = candidates[0];
            if (!row)
                return null;
            return {
                id: row.id,
                orgId: row.orgId,
                agentId: row.agentId,
                provider: row.provider,
                status: integrationStatus(row.status),
            };
        },
        auditDrop: async (input) => {
            await db.insert(agentIntegrationInboundAudit).values({
                orgId: input.orgId,
                integrationId: input.integrationId,
                provider: input.provider,
                externalChatId: input.externalChatId,
                externalChatType: input.externalChatType,
                externalEventId: input.externalEventId,
                externalMessageId: input.externalMessageId,
                senderOpenId: input.senderOpenId,
                dropReason: input.dropReason,
                bodyPersisted: false,
                metadata: input.metadata ?? null,
            });
        },
        resolveUserBinding: async (integration, event) => {
            const identityCondition = event.senderUnionId
                ? or(eq(agentIntegrationUserBindings.externalOpenId, event.senderOpenId), eq(agentIntegrationUserBindings.externalUnionId, event.senderUnionId))
                : eq(agentIntegrationUserBindings.externalOpenId, event.senderOpenId);
            if (!identityCondition)
                return null;
            const row = await db
                .select()
                .from(agentIntegrationUserBindings)
                .where(and(eq(agentIntegrationUserBindings.integrationId, integration.id), identityCondition, isNull(agentIntegrationUserBindings.revokedAt)))
                .then((rows) => rows[0] ?? null);
            if (!row)
                return null;
            const membership = await db
                .select({ id: organizationMemberships.id })
                .from(organizationMemberships)
                .where(and(eq(organizationMemberships.orgId, integration.orgId), eq(organizationMemberships.principalType, "user"), eq(organizationMemberships.principalId, row.userId), eq(organizationMemberships.status, "active")))
                .then((rows) => rows[0] ?? null);
            return {
                userId: row.userId,
                orgMember: Boolean(membership),
            };
        },
        mintBindingToken: async (integration, event) => {
            const token = createBindingToken();
            const expiresAt = new Date(Date.now() + BINDING_TOKEN_TTL_MS);
            await db.insert(agentIntegrationBindingTokens).values({
                orgId: integration.orgId,
                integrationId: integration.id,
                tokenHash: hashToken(token),
                externalOpenId: event.senderOpenId,
                externalUnionId: event.senderUnionId,
                expiresAt,
            });
            return { token, expiresAt };
        },
        tryInsertDedup: async (integration, event) => {
            try {
                await db.insert(agentIntegrationInboundDedup).values({
                    orgId: integration.orgId,
                    integrationId: integration.id,
                    provider: integration.provider,
                    externalMessageId: event.messageId,
                    externalEventId: event.eventId,
                    receivedAt: event.receivedAt ?? new Date(),
                });
                return true;
            }
            catch (error) {
                if (isUniqueViolation(error))
                    return false;
                throw error;
            }
        },
        ensureChatBinding: async (integration, binding, event) => {
            const existing = await db
                .select({ conversationId: agentIntegrationChatBindings.conversationId })
                .from(agentIntegrationChatBindings)
                .where(and(eq(agentIntegrationChatBindings.integrationId, integration.id), eq(agentIntegrationChatBindings.externalChatId, event.chatId)))
                .then((rows) => rows[0] ?? null);
            if (existing)
                return existing;
            const initialTitle = chatTitle(event);
            const conversation = await chats.create(integration.orgId, {
                title: initialTitle,
                summary: null,
                preferredAgentId: integration.agentId,
                issueCreationMode: "manual_approval",
                planMode: false,
                createdByUserId: binding.userId,
                contextLinks: [
                    {
                        entityType: "agent",
                        entityId: integration.agentId,
                        metadata: { source: "agent_integration", provider: integration.provider },
                    },
                ],
            });
            if (!conversation)
                throw new Error("Failed to create Feishu chat conversation");
            try {
                const inserted = await db
                    .insert(agentIntegrationChatBindings)
                    .values({
                    orgId: integration.orgId,
                    integrationId: integration.id,
                    conversationId: conversation.id,
                    externalChatId: event.chatId,
                    externalChatType: event.chatType,
                })
                    .returning({ conversationId: agentIntegrationChatBindings.conversationId })
                    .then((rows) => rows[0]);
                if (inserted)
                    return { ...inserted, created: true, initialTitle };
            }
            catch (error) {
                if (!isUniqueViolation(error))
                    throw error;
            }
            const raced = await db
                .select({ conversationId: agentIntegrationChatBindings.conversationId })
                .from(agentIntegrationChatBindings)
                .where(and(eq(agentIntegrationChatBindings.integrationId, integration.id), eq(agentIntegrationChatBindings.externalChatId, event.chatId)))
                .then((rows) => rows[0] ?? null);
            if (!raced)
                throw new Error("Failed to resolve Feishu chat binding after conflict");
            return raced;
        },
        handleQuickCommand: async (integration, binding, chat, command, event) => {
            if (command.kind === "new") {
                const nextConversation = await db.transaction(async (tx) => {
                    const now = new Date();
                    const [lockedBinding] = await tx
                        .update(agentIntegrationChatBindings)
                        .set({ updatedAt: now })
                        .where(and(eq(agentIntegrationChatBindings.orgId, integration.orgId), eq(agentIntegrationChatBindings.integrationId, integration.id), eq(agentIntegrationChatBindings.externalChatId, event.chatId)))
                        .returning({
                        id: agentIntegrationChatBindings.id,
                        conversationId: agentIntegrationChatBindings.conversationId,
                    });
                    if (!lockedBinding)
                        throw new Error("Failed to lock Feishu chat binding for /new quick command");
                    const previousConversationId = lockedBinding.conversationId;
                    await tx.insert(chatMessages).values({
                        orgId: integration.orgId,
                        conversationId: previousConversationId,
                        role: "system",
                        kind: "system_event",
                        body: "New Feishu session started.",
                        structuredPayload: feishuQuickCommandMessage("new", event, {
                            integrationId: integration.id,
                            previousConversationId,
                        }),
                    });
                    const conversation = await createFeishuChatConversation(tx, {
                        orgId: integration.orgId,
                        agentId: integration.agentId,
                        userId: binding.userId,
                        provider: integration.provider,
                    });
                    const [updatedBinding] = await tx
                        .update(agentIntegrationChatBindings)
                        .set({
                        conversationId: conversation.id,
                        externalChatType: event.chatType,
                        updatedAt: new Date(),
                    })
                        .where(and(eq(agentIntegrationChatBindings.orgId, integration.orgId), eq(agentIntegrationChatBindings.id, lockedBinding.id), eq(agentIntegrationChatBindings.conversationId, previousConversationId)))
                        .returning({ conversationId: agentIntegrationChatBindings.conversationId });
                    if (!updatedBinding)
                        throw new Error("Failed to switch Feishu chat binding for /new quick command");
                    return conversation;
                });
                return {
                    command: "new",
                    conversationId: nextConversation.id,
                    chatMessageId: null,
                    runId: null,
                    text: "New session started.",
                };
            }
            const active = getActiveChatGeneration(chat.conversationId);
            const latestActiveGeneration = active
                ? null
                : await db
                    .select()
                    .from(chatGenerations)
                    .where(and(eq(chatGenerations.conversationId, chat.conversationId), inArray(chatGenerations.status, ["active", "tool_busy", "closing"])))
                    .orderBy(desc(chatGenerations.startedAt), desc(chatGenerations.createdAt))
                    .limit(1)
                    .then((rows) => rows[0] ?? null);
            const stopped = cancelActiveChatGeneration(chat.conversationId);
            const generationId = active?.generationId ?? null;
            const staleGenerationId = stopped ? null : latestActiveGeneration?.id ?? null;
            if (generationId) {
                const now = new Date();
                await db
                    .update(chatGenerations)
                    .set({
                    status: "stopped",
                    terminalReason: "stopped",
                    completedAt: now,
                    updatedAt: now,
                })
                    .where(eq(chatGenerations.id, generationId));
            }
            const ack = await chats.addMessage(chat.conversationId, {
                orgId: integration.orgId,
                role: "system",
                kind: "system_event",
                body: stopped ? "Feishu session stop requested." : "No active Feishu reply to stop.",
                structuredPayload: feishuQuickCommandMessage("stop", event, {
                    integrationId: integration.id,
                    generationId,
                    staleGenerationId,
                    stopped,
                }),
            });
            return {
                command: "stop",
                conversationId: chat.conversationId,
                chatMessageId: ack.id,
                runId: null,
                text: stopped ? "Stop requested." : "No active reply to stop.",
            };
        },
        appendInboundMessage: async (integration, binding, chat, event) => {
            const message = await chats.addMessage(chat.conversationId, {
                orgId: integration.orgId,
                role: "user",
                kind: "message",
                body: event.body,
                structuredPayload: {
                    source: "agent_integration",
                    provider: integration.provider,
                    integrationId: integration.id,
                    externalChatId: event.chatId,
                    externalChatType: event.chatType,
                    externalMessageId: event.messageId,
                    externalEventId: event.eventId,
                    externalSenderOpenId: event.senderOpenId,
                    externalSenderUnionId: event.senderUnionId,
                    externalParentMessageId: event.parentMessageId ?? null,
                },
            });
            if (options.startTitleGeneration !== false) {
                const conversation = await chats.getById(chat.conversationId);
                if (conversation) {
                    chatTitles.startAutomaticGeneration(conversation, message, {
                        expectedCurrentTitle: chat.initialTitle,
                    });
                }
            }
            return { chatMessageId: message.id };
        },
        createIssueFromCommand: async (integration, binding, chat, message, command, event) => {
            const issue = await issues.create(integration.orgId, {
                title: command.title,
                description: command.body,
                status: "todo",
                priority: "medium",
                assigneeAgentId: integration.agentId,
                createdByUserId: binding.userId,
                originKind: "agent_integration",
                originId: `${integration.provider}:${event.messageId}`,
            });
            await db
                .update(chatConversations)
                .set({ primaryIssueId: issue.id, updatedAt: new Date() })
                .where(eq(chatConversations.id, chat.conversationId));
            await chats.addContextLink(chat.conversationId, integration.orgId, {
                entityType: "issue",
                entityId: issue.id,
                metadata: {
                    source: "agent_integration",
                    provider: integration.provider,
                    chatMessageId: message.chatMessageId,
                    externalMessageId: event.messageId,
                },
            });
            return { issueId: issue.id };
        },
    };
    if (options.enqueueAgentRun !== false) {
        deps.enqueueAgentRun = async (integration, _binding, chat, message, _event, issue) => {
            const conversation = await chats.getById(chat.conversationId);
            if (!conversation)
                throw new Error("Feishu chat conversation not found");
            const run = await chatRuns.createRun({
                conversation,
                agentId: integration.agentId,
                triggerDetail: "chat_assistant_reply_stream",
                userMessageId: message.chatMessageId,
                linkedIssueIds: issue ? [issue.issueId] : [],
                linkedProjectId: null,
                sourceMetadata: {
                    source: "agent_integration",
                    provider: integration.provider,
                    integrationId: integration.id,
                    externalChatId: _event.chatId,
                    externalChatType: _event.chatType,
                    externalMessageId: _event.messageId,
                    externalEventId: _event.eventId,
                },
            });
            return { runId: run.id };
        };
    }
    if (options.createOutboundPlaceholder !== false) {
        deps.createOutboundPlaceholder = async (integration, chat, event, message, issue, run) => {
            await db.insert(agentIntegrationOutboundMessages).values({
                orgId: integration.orgId,
                integrationId: integration.id,
                conversationId: chat.conversationId,
                chatMessageId: message.chatMessageId,
                issueId: issue?.issueId ?? null,
                runId: run?.runId ?? null,
                externalChatId: event.chatId,
                status: "pending",
            });
        };
    }
    return deps;
}
//# sourceMappingURL=inbound-dispatcher-db.js.map