import { resolveAckReaction } from "../../../agents/identity.js";
import { hasControlCommand } from "../../../auto-reply/command-detection.js";
import { shouldHandleTextCommands } from "../../../auto-reply/commands-registry.js";
import { formatInboundEnvelope, formatThreadStarterEnvelope, resolveEnvelopeFormatOptions, } from "../../../auto-reply/envelope.js";
import { buildPendingHistoryContextFromMap, recordPendingHistoryEntryIfEnabled, } from "../../../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
import { buildMentionRegexes, matchesMentionWithExplicit, } from "../../../auto-reply/reply/mentions.js";
import { logVerbose, shouldLogVerbose } from "../../../globals.js";
import { enqueueSystemEvent } from "../../../infra/system-events.js";
import { buildPairingReply } from "../../../pairing/pairing-messages.js";
import { upsertChannelPairingRequest } from "../../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../../routing/resolve-route.js";
import { resolveThreadSessionKeys } from "../../../routing/session-key.js";
import { shouldAckReaction as shouldAckReactionGate, } from "../../../channels/ack-reactions.js";
import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js";
import { resolveConversationLabel } from "../../../channels/conversation-label.js";
import { resolveControlCommandGate } from "../../../channels/command-gating.js";
import { logInboundDrop } from "../../../channels/logging.js";
import { formatAllowlistMatchMeta } from "../../../channels/allowlist-match.js";
import { recordInboundSession } from "../../../channels/session.js";
import { readSessionUpdatedAt, resolveStorePath } from "../../../config/sessions.js";
import { buildUntrustedChannelMetadata } from "../../../security/channel-metadata.js";
import { reactSlackMessage } from "../../actions.js";
import { sendMessageSlack } from "../../send.js";
import { resolveSlackThreadContext } from "../../threading.js";
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js";
import { resolveSlackEffectiveAllowFrom } from "../auth.js";
import { resolveSlackChannelConfig } from "../channel-config.js";
import { stripSlackMentionsForCommandDetection } from "../commands.js";
import { normalizeSlackChannelType } from "../context.js";
import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js";
export async function prepareSlackMessage(params) {
    const { ctx, account, message, opts } = params;
    const cfg = ctx.cfg;
    let channelInfo = {};
    let channelType = message.channel_type;
    if (!channelType || channelType !== "im") {
        channelInfo = await ctx.resolveChannelName(message.channel);
        channelType = channelType ?? channelInfo.type;
    }
    const channelName = channelInfo?.name;
    const resolvedChannelType = normalizeSlackChannelType(channelType, message.channel);
    const isDirectMessage = resolvedChannelType === "im";
    const isGroupDm = resolvedChannelType === "mpim";
    const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group";
    const isRoomish = isRoom || isGroupDm;
    const channelConfig = isRoom
        ? resolveSlackChannelConfig({
            channelId: message.channel,
            channelName,
            channels: ctx.channelsConfig,
            defaultRequireMention: ctx.defaultRequireMention,
        })
        : null;
    const allowBots = channelConfig?.allowBots ??
        account.config?.allowBots ??
        cfg.channels?.slack?.allowBots ??
        false;
    const isBotMessage = Boolean(message.bot_id);
    if (isBotMessage) {
        if (message.user && ctx.botUserId && message.user === ctx.botUserId)
            return null;
        if (!allowBots) {
            logVerbose(`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`);
            return null;
        }
    }
    if (isDirectMessage && !message.user) {
        logVerbose("slack: drop dm message (missing user id)");
        return null;
    }
    const senderId = message.user ?? (isBotMessage ? message.bot_id : undefined);
    if (!senderId) {
        logVerbose("slack: drop message (missing sender id)");
        return null;
    }
    if (!ctx.isChannelAllowed({
        channelId: message.channel,
        channelName,
        channelType: resolvedChannelType,
    })) {
        logVerbose("slack: drop message (channel not allowed)");
        return null;
    }
    const { allowFromLower } = await resolveSlackEffectiveAllowFrom(ctx);
    if (isDirectMessage) {
        const directUserId = message.user;
        if (!directUserId) {
            logVerbose("slack: drop dm message (missing user id)");
            return null;
        }
        if (!ctx.dmEnabled || ctx.dmPolicy === "disabled") {
            logVerbose("slack: drop dm (dms disabled)");
            return null;
        }
        if (ctx.dmPolicy !== "open") {
            const allowMatch = resolveSlackAllowListMatch({
                allowList: allowFromLower,
                id: directUserId,
            });
            const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
            if (!allowMatch.allowed) {
                if (ctx.dmPolicy === "pairing") {
                    const sender = await ctx.resolveUserName(directUserId);
                    const senderName = sender?.name ?? undefined;
                    const { code, created } = await upsertChannelPairingRequest({
                        channel: "slack",
                        id: directUserId,
                        meta: { name: senderName },
                    });
                    if (created) {
                        logVerbose(`slack pairing request sender=${directUserId} name=${senderName ?? "unknown"} (${allowMatchMeta})`);
                        try {
                            await sendMessageSlack(message.channel, buildPairingReply({
                                channel: "slack",
                                idLine: `Your Slack user id: ${directUserId}`,
                                code,
                            }), {
                                token: ctx.botToken,
                                client: ctx.app.client,
                                accountId: account.accountId,
                            });
                        }
                        catch (err) {
                            logVerbose(`slack pairing reply failed for ${message.user}: ${String(err)}`);
                        }
                    }
                }
                else {
                    logVerbose(`Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`);
                }
                return null;
            }
        }
    }
    const route = resolveAgentRoute({
        cfg,
        channel: "slack",
        accountId: account.accountId,
        teamId: ctx.teamId || undefined,
        peer: {
            kind: isDirectMessage ? "dm" : isRoom ? "channel" : "group",
            id: isDirectMessage ? (message.user ?? "unknown") : message.channel,
        },
    });
    const baseSessionKey = route.sessionKey;
    const threadContext = resolveSlackThreadContext({ message, replyToMode: ctx.replyToMode });
    const threadTs = threadContext.incomingThreadTs;
    const isThreadReply = threadContext.isThreadReply;
    const threadKeys = resolveThreadSessionKeys({
        baseSessionKey,
        threadId: isThreadReply ? threadTs : undefined,
        parentSessionKey: isThreadReply && ctx.threadInheritParent ? baseSessionKey : undefined,
    });
    const sessionKey = threadKeys.sessionKey;
    const historyKey = isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
    const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
    const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
    const explicitlyMentioned = Boolean(ctx.botUserId && message.text?.includes(`<@${ctx.botUserId}>`));
    const wasMentioned = opts.wasMentioned ??
        (!isDirectMessage &&
            matchesMentionWithExplicit({
                text: message.text ?? "",
                mentionRegexes,
                explicit: {
                    hasAnyMention,
                    isExplicitlyMentioned: explicitlyMentioned,
                    canResolveExplicit: Boolean(ctx.botUserId),
                },
            }));
    const implicitMention = Boolean(!isDirectMessage &&
        ctx.botUserId &&
        message.thread_ts &&
        message.parent_user_id === ctx.botUserId);
    const sender = message.user ? await ctx.resolveUserName(message.user) : null;
    const senderName = sender?.name ?? message.username?.trim() ?? message.user ?? message.bot_id ?? "unknown";
    const channelUserAuthorized = isRoom
        ? resolveSlackUserAllowed({
            allowList: channelConfig?.users,
            userId: senderId,
            userName: senderName,
        })
        : true;
    if (isRoom && !channelUserAuthorized) {
        logVerbose(`Blocked unauthorized slack sender ${senderId} (not in channel users)`);
        return null;
    }
    const allowTextCommands = shouldHandleTextCommands({
        cfg,
        surface: "slack",
    });
    // Strip Slack mentions (<@U123>) before command detection so "@Labrador /new" is recognized
    const textForCommandDetection = stripSlackMentionsForCommandDetection(message.text ?? "");
    const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg);
    const ownerAuthorized = resolveSlackAllowListMatch({
        allowList: allowFromLower,
        id: senderId,
        name: senderName,
    }).allowed;
    const channelUsersAllowlistConfigured = isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
    const channelCommandAuthorized = isRoom && channelUsersAllowlistConfigured
        ? resolveSlackUserAllowed({
            allowList: channelConfig?.users,
            userId: senderId,
            userName: senderName,
        })
        : false;
    const commandGate = resolveControlCommandGate({
        useAccessGroups: ctx.useAccessGroups,
        authorizers: [
            { configured: allowFromLower.length > 0, allowed: ownerAuthorized },
            { configured: channelUsersAllowlistConfigured, allowed: channelCommandAuthorized },
        ],
        allowTextCommands,
        hasControlCommand: hasControlCommandInMessage,
    });
    const commandAuthorized = commandGate.commandAuthorized;
    if (isRoomish && commandGate.shouldBlock) {
        logInboundDrop({
            log: logVerbose,
            channel: "slack",
            reason: "control command (unauthorized)",
            target: senderId,
        });
        return null;
    }
    const shouldRequireMention = isRoom
        ? (channelConfig?.requireMention ?? ctx.defaultRequireMention)
        : false;
    // Allow "control commands" to bypass mention gating if sender is authorized.
    const canDetectMention = Boolean(ctx.botUserId) || mentionRegexes.length > 0;
    const mentionGate = resolveMentionGatingWithBypass({
        isGroup: isRoom,
        requireMention: Boolean(shouldRequireMention),
        canDetectMention,
        wasMentioned,
        implicitMention,
        hasAnyMention,
        allowTextCommands,
        hasControlCommand: hasControlCommandInMessage,
        commandAuthorized,
    });
    const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
    if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
        ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel message");
        const pendingText = (message.text ?? "").trim();
        const fallbackFile = message.files?.[0]?.name
            ? `[Slack file: ${message.files[0].name}]`
            : message.files?.length
                ? "[Slack file]"
                : "";
        const pendingBody = pendingText || fallbackFile;
        recordPendingHistoryEntryIfEnabled({
            historyMap: ctx.channelHistories,
            historyKey,
            limit: ctx.historyLimit,
            entry: pendingBody
                ? {
                    sender: senderName,
                    body: pendingBody,
                    timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
                    messageId: message.ts,
                }
                : null,
        });
        return null;
    }
    const media = await resolveSlackMedia({
        files: message.files,
        token: ctx.botToken,
        maxBytes: ctx.mediaMaxBytes,
    });
    const rawBody = (message.text ?? "").trim() || media?.placeholder || "";
    if (!rawBody)
        return null;
    const ackReaction = resolveAckReaction(cfg, route.agentId);
    const ackReactionValue = ackReaction ?? "";
    const shouldAckReaction = () => Boolean(ackReaction &&
        shouldAckReactionGate({
            scope: ctx.ackReactionScope,
            isDirect: isDirectMessage,
            isGroup: isRoomish,
            isMentionableGroup: isRoom,
            requireMention: Boolean(shouldRequireMention),
            canDetectMention,
            effectiveWasMentioned,
            shouldBypassMention: mentionGate.shouldBypassMention,
        }));
    const ackReactionMessageTs = message.ts;
    const ackReactionPromise = shouldAckReaction() && ackReactionMessageTs && ackReactionValue
        ? reactSlackMessage(message.channel, ackReactionMessageTs, ackReactionValue, {
            token: ctx.botToken,
            client: ctx.app.client,
        }).then(() => true, (err) => {
            logVerbose(`slack react failed for channel ${message.channel}: ${String(err)}`);
            return false;
        })
        : null;
    const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
    const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
    const inboundLabel = isDirectMessage
        ? `Slack DM from ${senderName}`
        : `Slack message in ${roomLabel} from ${senderName}`;
    const slackFrom = isDirectMessage
        ? `slack:${message.user}`
        : isRoom
            ? `slack:channel:${message.channel}`
            : `slack:group:${message.channel}`;
    enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
        sessionKey,
        contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
    });
    const envelopeFrom = resolveConversationLabel({
        ChatType: isDirectMessage ? "direct" : "channel",
        SenderName: senderName,
        GroupSubject: isRoomish ? roomLabel : undefined,
        From: slackFrom,
    }) ?? (isDirectMessage ? senderName : roomLabel);
    const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`;
    const storePath = resolveStorePath(ctx.cfg.session?.store, {
        agentId: route.agentId,
    });
    const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
    const previousTimestamp = readSessionUpdatedAt({
        storePath,
        sessionKey: route.sessionKey,
    });
    const body = formatInboundEnvelope({
        channel: "Slack",
        from: envelopeFrom,
        timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
        body: textWithId,
        chatType: isDirectMessage ? "direct" : "channel",
        sender: { name: senderName, id: senderId },
        previousTimestamp,
        envelope: envelopeOptions,
    });
    let combinedBody = body;
    if (isRoomish && ctx.historyLimit > 0) {
        combinedBody = buildPendingHistoryContextFromMap({
            historyMap: ctx.channelHistories,
            historyKey,
            limit: ctx.historyLimit,
            currentMessage: combinedBody,
            formatEntry: (entry) => formatInboundEnvelope({
                channel: "Slack",
                from: roomLabel,
                timestamp: entry.timestamp,
                body: `${entry.body}${entry.messageId ? ` [id:${entry.messageId} channel:${message.channel}]` : ""}`,
                chatType: "channel",
                senderLabel: entry.sender,
                envelope: envelopeOptions,
            }),
        });
    }
    const slackTo = isDirectMessage ? `user:${message.user}` : `channel:${message.channel}`;
    const untrustedChannelMetadata = isRoomish
        ? buildUntrustedChannelMetadata({
            source: "slack",
            label: "Slack channel description",
            entries: [channelInfo?.topic, channelInfo?.purpose],
        })
        : undefined;
    const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter((entry) => Boolean(entry));
    const groupSystemPrompt = systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
    let threadStarterBody;
    let threadLabel;
    let threadStarterMedia = null;
    if (isThreadReply && threadTs) {
        const starter = await resolveSlackThreadStarter({
            channelId: message.channel,
            threadTs,
            client: ctx.app.client,
        });
        if (starter?.text) {
            const starterUser = starter.userId ? await ctx.resolveUserName(starter.userId) : null;
            const starterName = starterUser?.name ?? starter.userId ?? "Unknown";
            const starterWithId = `${starter.text}\n[slack message id: ${starter.ts ?? threadTs} channel: ${message.channel}]`;
            threadStarterBody = formatThreadStarterEnvelope({
                channel: "Slack",
                author: starterName,
                timestamp: starter.ts ? Math.round(Number(starter.ts) * 1000) : undefined,
                body: starterWithId,
                envelope: envelopeOptions,
            });
            const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80);
            threadLabel = `Slack thread ${roomLabel}${snippet ? `: ${snippet}` : ""}`;
            // If current message has no files but thread starter does, fetch starter's files
            if (!media && starter.files && starter.files.length > 0) {
                threadStarterMedia = await resolveSlackMedia({
                    files: starter.files,
                    token: ctx.botToken,
                    maxBytes: ctx.mediaMaxBytes,
                });
                if (threadStarterMedia) {
                    logVerbose(`slack: hydrated thread starter file ${threadStarterMedia.placeholder} from root message`);
                }
            }
        }
        else {
            threadLabel = `Slack thread ${roomLabel}`;
        }
    }
    // Use thread starter media if current message has none
    const effectiveMedia = media ?? threadStarterMedia;
    const ctxPayload = finalizeInboundContext({
        Body: combinedBody,
        RawBody: rawBody,
        CommandBody: rawBody,
        From: slackFrom,
        To: slackTo,
        SessionKey: sessionKey,
        AccountId: route.accountId,
        ChatType: isDirectMessage ? "direct" : "channel",
        ConversationLabel: envelopeFrom,
        GroupSubject: isRoomish ? roomLabel : undefined,
        GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
        UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
        SenderName: senderName,
        SenderId: senderId,
        Provider: "slack",
        Surface: "slack",
        MessageSid: message.ts,
        ReplyToId: threadContext.replyToId,
        // Preserve thread context for routed tool notifications.
        MessageThreadId: threadContext.messageThreadId,
        ParentSessionKey: threadKeys.parentSessionKey,
        ThreadStarterBody: threadStarterBody,
        ThreadLabel: threadLabel,
        Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
        WasMentioned: isRoomish ? effectiveWasMentioned : undefined,
        MediaPath: effectiveMedia?.path,
        MediaType: effectiveMedia?.contentType,
        MediaUrl: effectiveMedia?.path,
        CommandAuthorized: commandAuthorized,
        OriginatingChannel: "slack",
        OriginatingTo: slackTo,
    });
    await recordInboundSession({
        storePath,
        sessionKey,
        ctx: ctxPayload,
        updateLastRoute: isDirectMessage
            ? {
                sessionKey: route.mainSessionKey,
                channel: "slack",
                to: `user:${message.user}`,
                accountId: route.accountId,
            }
            : undefined,
        onRecordError: (err) => {
            ctx.logger.warn({
                error: String(err),
                storePath,
                sessionKey,
            }, "failed updating session meta");
        },
    });
    const replyTarget = ctxPayload.To ?? undefined;
    if (!replyTarget)
        return null;
    if (shouldLogVerbose()) {
        logVerbose(`slack inbound: channel=${message.channel} from=${slackFrom} preview="${preview}"`);
    }
    return {
        ctx,
        account,
        message,
        route,
        channelConfig,
        replyTarget,
        ctxPayload,
        isDirectMessage,
        isRoomish,
        historyKey,
        preview,
        ackReactionMessageTs,
        ackReactionValue,
        ackReactionPromise,
    };
}
