import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import { assertMediaNotDataUrl, resolveSandboxedMediaSource } from "../../agents/sandbox-paths.js";
import { readNumberParam, readStringArrayParam, readStringParam, } from "../../agents/tools/common.js";
import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js";
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
import { extensionForMime } from "../../media/mime.js";
import { parseSlackTarget } from "../../slack/targets.js";
import { parseTelegramTarget } from "../../telegram/targets.js";
import { isDeliverableMessageChannel, normalizeMessageChannel, } from "../../utils/message-channel.js";
import { loadWebMedia } from "../../web/media.js";
import { listConfiguredMessageChannels, resolveMessageChannelSelection, } from "./channel-selection.js";
import { applyTargetToParams } from "./channel-target.js";
import { actionHasTarget, actionRequiresTarget } from "./message-action-spec.js";
import { applyCrossContextDecoration, buildCrossContextDecoration, enforceCrossContextPolicy, shouldApplyCrossContextMarker, } from "./outbound-policy.js";
import { executePollAction, executeSendAction } from "./outbound-send-service.js";
import { ensureOutboundSessionEntry, resolveOutboundSessionRoute } from "./outbound-session.js";
import { resolveChannelTarget } from "./target-resolver.js";
export function getToolResult(result) {
    return "toolResult" in result ? result.toolResult : undefined;
}
function extractToolPayload(result) {
    if (result.details !== undefined) {
        return result.details;
    }
    const textBlock = Array.isArray(result.content)
        ? result.content.find((block) => block &&
            typeof block === "object" &&
            block.type === "text" &&
            typeof block.text === "string")
        : undefined;
    const text = textBlock?.text;
    if (text) {
        try {
            return JSON.parse(text);
        }
        catch {
            return text;
        }
    }
    return result.content ?? result;
}
function applyCrossContextMessageDecoration({ params, message, decoration, preferEmbeds, }) {
    const applied = applyCrossContextDecoration({
        message,
        decoration,
        preferEmbeds,
    });
    params.message = applied.message;
    if (applied.embeds?.length) {
        params.embeds = applied.embeds;
    }
    return applied.message;
}
async function maybeApplyCrossContextMarker(params) {
    if (!shouldApplyCrossContextMarker(params.action) || !params.toolContext) {
        return params.message;
    }
    const decoration = await buildCrossContextDecoration({
        cfg: params.cfg,
        channel: params.channel,
        target: params.target,
        toolContext: params.toolContext,
        accountId: params.accountId ?? undefined,
    });
    if (!decoration) {
        return params.message;
    }
    return applyCrossContextMessageDecoration({
        params: params.args,
        message: params.message,
        decoration,
        preferEmbeds: params.preferEmbeds,
    });
}
function readBooleanParam(params, key) {
    const raw = params[key];
    if (typeof raw === "boolean") {
        return raw;
    }
    if (typeof raw === "string") {
        const trimmed = raw.trim().toLowerCase();
        if (trimmed === "true") {
            return true;
        }
        if (trimmed === "false") {
            return false;
        }
    }
    return undefined;
}
function resolveSlackAutoThreadId(params) {
    const context = params.toolContext;
    if (!context?.currentThreadTs || !context.currentChannelId) {
        return undefined;
    }
    // Only mirror auto-threading when Slack would reply in the active thread for this channel.
    if (context.replyToMode !== "all" && context.replyToMode !== "first") {
        return undefined;
    }
    const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
    if (!parsedTarget || parsedTarget.kind !== "channel") {
        return undefined;
    }
    if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) {
        return undefined;
    }
    if (context.replyToMode === "first" && context.hasRepliedRef?.value) {
        return undefined;
    }
    return context.currentThreadTs;
}
/**
 * Auto-inject Telegram forum topic thread ID when the message tool targets
 * the same chat the session originated from.  Mirrors the Slack auto-threading
 * pattern so media, buttons, and other tool-sent messages land in the correct
 * topic instead of the General Topic.
 *
 * Unlike Slack, we do not gate on `replyToMode` here: Telegram forum topics
 * are persistent sub-channels (not ephemeral reply threads), so auto-injection
 * should always apply when the target chat matches.
 */
function resolveTelegramAutoThreadId(params) {
    const context = params.toolContext;
    if (!context?.currentThreadTs || !context.currentChannelId) {
        return undefined;
    }
    // Use parseTelegramTarget to extract canonical chatId from both sides,
    // mirroring how Slack uses parseSlackTarget. This handles format variations
    // like `telegram:group:123:topic:456` vs `telegram:123`.
    const parsedTo = parseTelegramTarget(params.to);
    const parsedChannel = parseTelegramTarget(context.currentChannelId);
    if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) {
        return undefined;
    }
    return context.currentThreadTs;
}
function resolveAttachmentMaxBytes(params) {
    const fallback = params.cfg.agents?.defaults?.mediaMaxMb;
    if (params.channel !== "bluebubbles") {
        return typeof fallback === "number" ? fallback * 1024 * 1024 : undefined;
    }
    const accountId = typeof params.accountId === "string" ? params.accountId.trim() : "";
    const channelCfg = params.cfg.channels?.bluebubbles;
    const channelObj = channelCfg && typeof channelCfg === "object"
        ? channelCfg
        : undefined;
    const channelMediaMax = typeof channelObj?.mediaMaxMb === "number" ? channelObj.mediaMaxMb : undefined;
    const accountsObj = channelObj?.accounts && typeof channelObj.accounts === "object"
        ? channelObj.accounts
        : undefined;
    const accountCfg = accountId && accountsObj ? accountsObj[accountId] : undefined;
    const accountMediaMax = accountCfg && typeof accountCfg === "object"
        ? accountCfg.mediaMaxMb
        : undefined;
    const limitMb = (typeof accountMediaMax === "number" ? accountMediaMax : undefined) ??
        channelMediaMax ??
        params.cfg.agents?.defaults?.mediaMaxMb;
    return typeof limitMb === "number" ? limitMb * 1024 * 1024 : undefined;
}
function inferAttachmentFilename(params) {
    const mediaHint = params.mediaHint?.trim();
    if (mediaHint) {
        try {
            if (mediaHint.startsWith("file://")) {
                const filePath = fileURLToPath(mediaHint);
                const base = path.basename(filePath);
                if (base) {
                    return base;
                }
            }
            else if (/^https?:\/\//i.test(mediaHint)) {
                const url = new URL(mediaHint);
                const base = path.basename(url.pathname);
                if (base) {
                    return base;
                }
            }
            else {
                const base = path.basename(mediaHint);
                if (base) {
                    return base;
                }
            }
        }
        catch {
            // fall through to content-type based default
        }
    }
    const ext = params.contentType ? extensionForMime(params.contentType) : undefined;
    return ext ? `attachment${ext}` : "attachment";
}
function normalizeBase64Payload(params) {
    if (!params.base64) {
        return { base64: params.base64, contentType: params.contentType };
    }
    const match = /^data:([^;]+);base64,(.*)$/i.exec(params.base64.trim());
    if (!match) {
        return { base64: params.base64, contentType: params.contentType };
    }
    const [, mime, payload] = match;
    return {
        base64: payload,
        contentType: params.contentType ?? mime,
    };
}
async function normalizeSandboxMediaParams(params) {
    const sandboxRoot = params.sandboxRoot?.trim();
    const mediaKeys = ["media", "path", "filePath"];
    for (const key of mediaKeys) {
        const raw = readStringParam(params.args, key, { trim: false });
        if (!raw) {
            continue;
        }
        assertMediaNotDataUrl(raw);
        if (!sandboxRoot) {
            continue;
        }
        const normalized = await resolveSandboxedMediaSource({ media: raw, sandboxRoot });
        if (normalized !== raw) {
            params.args[key] = normalized;
        }
    }
}
async function normalizeSandboxMediaList(params) {
    const sandboxRoot = params.sandboxRoot?.trim();
    const normalized = [];
    const seen = new Set();
    for (const value of params.values) {
        const raw = value?.trim();
        if (!raw) {
            continue;
        }
        assertMediaNotDataUrl(raw);
        const resolved = sandboxRoot
            ? await resolveSandboxedMediaSource({ media: raw, sandboxRoot })
            : raw;
        if (seen.has(resolved)) {
            continue;
        }
        seen.add(resolved);
        normalized.push(resolved);
    }
    return normalized;
}
async function hydrateSetGroupIconParams(params) {
    if (params.action !== "setGroupIcon") {
        return;
    }
    const mediaHint = readStringParam(params.args, "media", { trim: false });
    const fileHint = readStringParam(params.args, "path", { trim: false }) ??
        readStringParam(params.args, "filePath", { trim: false });
    const contentTypeParam = readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType");
    const rawBuffer = readStringParam(params.args, "buffer", { trim: false });
    const normalized = normalizeBase64Payload({
        base64: rawBuffer,
        contentType: contentTypeParam ?? undefined,
    });
    if (normalized.base64 !== rawBuffer && normalized.base64) {
        params.args.buffer = normalized.base64;
        if (normalized.contentType && !contentTypeParam) {
            params.args.contentType = normalized.contentType;
        }
    }
    const filename = readStringParam(params.args, "filename");
    const mediaSource = mediaHint ?? fileHint;
    if (!params.dryRun && !readStringParam(params.args, "buffer", { trim: false }) && mediaSource) {
        const maxBytes = resolveAttachmentMaxBytes({
            cfg: params.cfg,
            channel: params.channel,
            accountId: params.accountId,
        });
        const media = await loadWebMedia(mediaSource, maxBytes);
        params.args.buffer = media.buffer.toString("base64");
        if (!contentTypeParam && media.contentType) {
            params.args.contentType = media.contentType;
        }
        if (!filename) {
            params.args.filename = inferAttachmentFilename({
                mediaHint: media.fileName ?? mediaSource,
                contentType: media.contentType ?? contentTypeParam ?? undefined,
            });
        }
    }
    else if (!filename) {
        params.args.filename = inferAttachmentFilename({
            mediaHint: mediaSource,
            contentType: contentTypeParam ?? undefined,
        });
    }
}
async function hydrateSendAttachmentParams(params) {
    if (params.action !== "sendAttachment") {
        return;
    }
    const mediaHint = readStringParam(params.args, "media", { trim: false });
    const fileHint = readStringParam(params.args, "path", { trim: false }) ??
        readStringParam(params.args, "filePath", { trim: false });
    const contentTypeParam = readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType");
    const caption = readStringParam(params.args, "caption", { allowEmpty: true })?.trim();
    const message = readStringParam(params.args, "message", { allowEmpty: true })?.trim();
    if (!caption && message) {
        params.args.caption = message;
    }
    const rawBuffer = readStringParam(params.args, "buffer", { trim: false });
    const normalized = normalizeBase64Payload({
        base64: rawBuffer,
        contentType: contentTypeParam ?? undefined,
    });
    if (normalized.base64 !== rawBuffer && normalized.base64) {
        params.args.buffer = normalized.base64;
        if (normalized.contentType && !contentTypeParam) {
            params.args.contentType = normalized.contentType;
        }
    }
    const filename = readStringParam(params.args, "filename");
    const mediaSource = mediaHint ?? fileHint;
    if (!params.dryRun && !readStringParam(params.args, "buffer", { trim: false }) && mediaSource) {
        const maxBytes = resolveAttachmentMaxBytes({
            cfg: params.cfg,
            channel: params.channel,
            accountId: params.accountId,
        });
        const media = await loadWebMedia(mediaSource, maxBytes);
        params.args.buffer = media.buffer.toString("base64");
        if (!contentTypeParam && media.contentType) {
            params.args.contentType = media.contentType;
        }
        if (!filename) {
            params.args.filename = inferAttachmentFilename({
                mediaHint: media.fileName ?? mediaSource,
                contentType: media.contentType ?? contentTypeParam ?? undefined,
            });
        }
    }
    else if (!filename) {
        params.args.filename = inferAttachmentFilename({
            mediaHint: mediaSource,
            contentType: contentTypeParam ?? undefined,
        });
    }
}
function parseButtonsParam(params) {
    const raw = params.buttons;
    if (typeof raw !== "string") {
        return;
    }
    const trimmed = raw.trim();
    if (!trimmed) {
        delete params.buttons;
        return;
    }
    try {
        params.buttons = JSON.parse(trimmed);
    }
    catch {
        throw new Error("--buttons must be valid JSON");
    }
}
function parseCardParam(params) {
    const raw = params.card;
    if (typeof raw !== "string") {
        return;
    }
    const trimmed = raw.trim();
    if (!trimmed) {
        delete params.card;
        return;
    }
    try {
        params.card = JSON.parse(trimmed);
    }
    catch {
        throw new Error("--card must be valid JSON");
    }
}
async function resolveChannel(cfg, params) {
    const channelHint = readStringParam(params, "channel");
    const selection = await resolveMessageChannelSelection({
        cfg,
        channel: channelHint,
    });
    return selection.channel;
}
async function resolveActionTarget(params) {
    let resolvedTarget;
    const toRaw = typeof params.args.to === "string" ? params.args.to.trim() : "";
    if (toRaw) {
        const resolved = await resolveChannelTarget({
            cfg: params.cfg,
            channel: params.channel,
            input: toRaw,
            accountId: params.accountId ?? undefined,
        });
        if (resolved.ok) {
            params.args.to = resolved.target.to;
            resolvedTarget = resolved.target;
        }
        else {
            throw resolved.error;
        }
    }
    const channelIdRaw = typeof params.args.channelId === "string" ? params.args.channelId.trim() : "";
    if (channelIdRaw) {
        const resolved = await resolveChannelTarget({
            cfg: params.cfg,
            channel: params.channel,
            input: channelIdRaw,
            accountId: params.accountId ?? undefined,
            preferredKind: "group",
        });
        if (resolved.ok) {
            if (resolved.target.kind === "user") {
                throw new Error(`Channel id "${channelIdRaw}" resolved to a user target.`);
            }
            params.args.channelId = resolved.target.to.replace(/^(channel|group):/i, "");
        }
        else {
            throw resolved.error;
        }
    }
    return resolvedTarget;
}
function resolveGateway(input) {
    if (!input.gateway) {
        return undefined;
    }
    return {
        url: input.gateway.url,
        token: input.gateway.token,
        timeoutMs: input.gateway.timeoutMs,
        clientName: input.gateway.clientName,
        clientDisplayName: input.gateway.clientDisplayName,
        mode: input.gateway.mode,
    };
}
async function handleBroadcastAction(input, params) {
    throwIfAborted(input.abortSignal);
    const broadcastEnabled = input.cfg.tools?.message?.broadcast?.enabled !== false;
    if (!broadcastEnabled) {
        throw new Error("Broadcast is disabled. Set tools.message.broadcast.enabled to true.");
    }
    const rawTargets = readStringArrayParam(params, "targets", { required: true }) ?? [];
    if (rawTargets.length === 0) {
        throw new Error("Broadcast requires at least one target in --targets.");
    }
    const channelHint = readStringParam(params, "channel");
    const configured = await listConfiguredMessageChannels(input.cfg);
    if (configured.length === 0) {
        throw new Error("Broadcast requires at least one configured channel.");
    }
    const targetChannels = channelHint && channelHint.trim().toLowerCase() !== "all"
        ? [await resolveChannel(input.cfg, { channel: channelHint })]
        : configured;
    const results = [];
    const isAbortError = (err) => err instanceof Error && err.name === "AbortError";
    for (const targetChannel of targetChannels) {
        throwIfAborted(input.abortSignal);
        for (const target of rawTargets) {
            throwIfAborted(input.abortSignal);
            try {
                const resolved = await resolveChannelTarget({
                    cfg: input.cfg,
                    channel: targetChannel,
                    input: target,
                });
                if (!resolved.ok) {
                    throw resolved.error;
                }
                const sendResult = await runMessageAction({
                    ...input,
                    action: "send",
                    params: {
                        ...params,
                        channel: targetChannel,
                        target: resolved.target.to,
                    },
                });
                results.push({
                    channel: targetChannel,
                    to: resolved.target.to,
                    ok: true,
                    result: sendResult.kind === "send" ? sendResult.sendResult : undefined,
                });
            }
            catch (err) {
                if (isAbortError(err)) {
                    throw err;
                }
                results.push({
                    channel: targetChannel,
                    to: target,
                    ok: false,
                    error: err instanceof Error ? err.message : String(err),
                });
            }
        }
    }
    return {
        kind: "broadcast",
        channel: targetChannels[0] ?? "discord",
        action: "broadcast",
        handledBy: input.dryRun ? "dry-run" : "core",
        payload: { results },
        dryRun: Boolean(input.dryRun),
    };
}
function throwIfAborted(abortSignal) {
    if (abortSignal?.aborted) {
        const err = new Error("Message send aborted");
        err.name = "AbortError";
        throw err;
    }
}
async function handleSendAction(ctx) {
    const { cfg, params, channel, accountId, dryRun, gateway, input, agentId, resolvedTarget, abortSignal, } = ctx;
    throwIfAborted(abortSignal);
    const action = "send";
    const to = readStringParam(params, "to", { required: true });
    // Support media, path, and filePath parameters for attachments
    const mediaHint = readStringParam(params, "media", { trim: false }) ??
        readStringParam(params, "path", { trim: false }) ??
        readStringParam(params, "filePath", { trim: false });
    const hasCard = params.card != null && typeof params.card === "object";
    const caption = readStringParam(params, "caption", { allowEmpty: true }) ?? "";
    let message = readStringParam(params, "message", {
        required: !mediaHint && !hasCard,
        allowEmpty: true,
    }) ?? "";
    if (message.includes("\\n")) {
        message = message.replaceAll("\\n", "\n");
    }
    if (!message.trim() && caption.trim()) {
        message = caption;
    }
    const parsed = parseReplyDirectives(message);
    const mergedMediaUrls = [];
    const seenMedia = new Set();
    const pushMedia = (value) => {
        const trimmed = value?.trim();
        if (!trimmed) {
            return;
        }
        if (seenMedia.has(trimmed)) {
            return;
        }
        seenMedia.add(trimmed);
        mergedMediaUrls.push(trimmed);
    };
    pushMedia(mediaHint);
    for (const url of parsed.mediaUrls ?? []) {
        pushMedia(url);
    }
    pushMedia(parsed.mediaUrl);
    const normalizedMediaUrls = await normalizeSandboxMediaList({
        values: mergedMediaUrls,
        sandboxRoot: input.sandboxRoot,
    });
    mergedMediaUrls.length = 0;
    mergedMediaUrls.push(...normalizedMediaUrls);
    message = parsed.text;
    params.message = message;
    if (!params.replyTo && parsed.replyToId) {
        params.replyTo = parsed.replyToId;
    }
    if (!params.media) {
        // Use path/filePath if media not set, then fall back to parsed directives
        params.media = mergedMediaUrls[0] || undefined;
    }
    message = await maybeApplyCrossContextMarker({
        cfg,
        channel,
        action,
        target: to,
        toolContext: input.toolContext,
        accountId,
        args: params,
        message,
        preferEmbeds: true,
    });
    const mediaUrl = readStringParam(params, "media", { trim: false });
    if (channel === "whatsapp") {
        message = message.replace(/^(?:[ \t]*\r?\n)+/, "");
        if (!message.trim()) {
            message = "";
        }
    }
    if (!message.trim() && !mediaUrl && mergedMediaUrls.length === 0 && !hasCard) {
        throw new Error("send requires text or media");
    }
    params.message = message;
    const gifPlayback = readBooleanParam(params, "gifPlayback") ?? false;
    const bestEffort = readBooleanParam(params, "bestEffort");
    const replyToId = readStringParam(params, "replyTo");
    const threadId = readStringParam(params, "threadId");
    // Slack auto-threading can inject threadTs without explicit params; mirror to that session key.
    const slackAutoThreadId = channel === "slack" && !replyToId && !threadId
        ? resolveSlackAutoThreadId({ to, toolContext: input.toolContext })
        : undefined;
    // Telegram forum topic auto-threading: inject threadId so media/buttons land in the correct topic.
    const telegramAutoThreadId = channel === "telegram" && !threadId
        ? resolveTelegramAutoThreadId({ to, toolContext: input.toolContext })
        : undefined;
    const resolvedThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
    // Write auto-resolved threadId back into params so downstream dispatch
    // (plugin `readStringParam(params, "threadId")`) picks it up.
    if (resolvedThreadId && !params.threadId) {
        params.threadId = resolvedThreadId;
    }
    // Skip outbound session mirroring when the agent is replying to the same
    // channel/chat it received the inbound message from.  The tool result already
    // records the send in the agent's running session, so creating a separate
    // outbound session entry would only produce a confusing duplicate session
    // in the web UI.  This also avoids Feishu oc_ prefix ambiguity (used for
    // both group and p2p chats).
    const isReplyToSameChat = input.toolContext?.currentChannelProvider === channel &&
        !!input.toolContext?.currentChannelId &&
        input.toolContext.currentChannelId.trim().toLowerCase() === to.trim().toLowerCase();
    const outboundRoute = agentId && !dryRun && !isReplyToSameChat
        ? await resolveOutboundSessionRoute({
            cfg,
            channel,
            agentId,
            accountId,
            target: to,
            resolvedTarget,
            replyToId,
            threadId: resolvedThreadId,
        })
        : null;
    if (outboundRoute && agentId && !dryRun) {
        await ensureOutboundSessionEntry({
            cfg,
            agentId,
            channel,
            accountId,
            route: outboundRoute,
        });
    }
    const mirrorMediaUrls = mergedMediaUrls.length > 0 ? mergedMediaUrls : mediaUrl ? [mediaUrl] : undefined;
    throwIfAborted(abortSignal);
    const send = await executeSendAction({
        ctx: {
            cfg,
            channel,
            params,
            accountId: accountId ?? undefined,
            gateway,
            toolContext: input.toolContext,
            deps: input.deps,
            dryRun,
            mirror: outboundRoute && !dryRun
                ? {
                    sessionKey: outboundRoute.sessionKey,
                    agentId,
                    text: message,
                    mediaUrls: mirrorMediaUrls,
                }
                : undefined,
            abortSignal,
        },
        to,
        message,
        mediaUrl: mediaUrl || undefined,
        mediaUrls: mergedMediaUrls.length ? mergedMediaUrls : undefined,
        gifPlayback,
        bestEffort: bestEffort ?? undefined,
    });
    return {
        kind: "send",
        channel,
        action,
        to,
        handledBy: send.handledBy,
        payload: send.payload,
        toolResult: send.toolResult,
        sendResult: send.sendResult,
        dryRun,
    };
}
async function handlePollAction(ctx) {
    const { cfg, params, channel, accountId, dryRun, gateway, input, abortSignal } = ctx;
    throwIfAborted(abortSignal);
    const action = "poll";
    const to = readStringParam(params, "to", { required: true });
    const question = readStringParam(params, "pollQuestion", {
        required: true,
    });
    const options = readStringArrayParam(params, "pollOption", { required: true }) ?? [];
    if (options.length < 2) {
        throw new Error("pollOption requires at least two values");
    }
    const allowMultiselect = readBooleanParam(params, "pollMulti") ?? false;
    const durationHours = readNumberParam(params, "pollDurationHours", {
        integer: true,
    });
    const maxSelections = allowMultiselect ? Math.max(2, options.length) : 1;
    const base = typeof params.message === "string" ? params.message : "";
    await maybeApplyCrossContextMarker({
        cfg,
        channel,
        action,
        target: to,
        toolContext: input.toolContext,
        accountId,
        args: params,
        message: base,
        preferEmbeds: true,
    });
    const poll = await executePollAction({
        ctx: {
            cfg,
            channel,
            params,
            accountId: accountId ?? undefined,
            gateway,
            toolContext: input.toolContext,
            dryRun,
        },
        to,
        question,
        options,
        maxSelections,
        durationHours: durationHours ?? undefined,
    });
    return {
        kind: "poll",
        channel,
        action,
        to,
        handledBy: poll.handledBy,
        payload: poll.payload,
        toolResult: poll.toolResult,
        pollResult: poll.pollResult,
        dryRun,
    };
}
async function handlePluginAction(ctx) {
    const { cfg, params, channel, accountId, dryRun, gateway, input, abortSignal } = ctx;
    throwIfAborted(abortSignal);
    const action = input.action;
    if (dryRun) {
        return {
            kind: "action",
            channel,
            action,
            handledBy: "dry-run",
            payload: { ok: true, dryRun: true, channel, action },
            dryRun: true,
        };
    }
    const handled = await dispatchChannelMessageAction({
        channel,
        action,
        cfg,
        params,
        accountId: accountId ?? undefined,
        gateway,
        toolContext: input.toolContext,
        dryRun,
    });
    if (!handled) {
        throw new Error(`Message action ${action} not supported for channel ${channel}.`);
    }
    return {
        kind: "action",
        channel,
        action,
        handledBy: "plugin",
        payload: extractToolPayload(handled),
        toolResult: handled,
        dryRun,
    };
}
export async function runMessageAction(input) {
    const cfg = input.cfg;
    const params = { ...input.params };
    const resolvedAgentId = input.agentId ??
        (input.sessionKey
            ? resolveSessionAgentId({ sessionKey: input.sessionKey, config: cfg })
            : undefined);
    parseButtonsParam(params);
    parseCardParam(params);
    const action = input.action;
    if (action === "broadcast") {
        return handleBroadcastAction(input, params);
    }
    const explicitTarget = typeof params.target === "string" ? params.target.trim() : "";
    const hasLegacyTarget = (typeof params.to === "string" && params.to.trim().length > 0) ||
        (typeof params.channelId === "string" && params.channelId.trim().length > 0);
    if (explicitTarget && hasLegacyTarget) {
        delete params.to;
        delete params.channelId;
    }
    if (!explicitTarget &&
        !hasLegacyTarget &&
        actionRequiresTarget(action) &&
        !actionHasTarget(action, params)) {
        const inferredTarget = input.toolContext?.currentChannelId?.trim();
        if (inferredTarget) {
            params.target = inferredTarget;
        }
    }
    if (!explicitTarget && actionRequiresTarget(action) && hasLegacyTarget) {
        const legacyTo = typeof params.to === "string" ? params.to.trim() : "";
        const legacyChannelId = typeof params.channelId === "string" ? params.channelId.trim() : "";
        const legacyTarget = legacyTo || legacyChannelId;
        if (legacyTarget) {
            params.target = legacyTarget;
            delete params.to;
            delete params.channelId;
        }
    }
    const explicitChannel = typeof params.channel === "string" ? params.channel.trim() : "";
    if (!explicitChannel) {
        const inferredChannel = normalizeMessageChannel(input.toolContext?.currentChannelProvider);
        if (inferredChannel && isDeliverableMessageChannel(inferredChannel)) {
            params.channel = inferredChannel;
        }
    }
    applyTargetToParams({ action, args: params });
    if (actionRequiresTarget(action)) {
        if (!actionHasTarget(action, params)) {
            throw new Error(`Action ${action} requires a target.`);
        }
    }
    const channel = await resolveChannel(cfg, params);
    const accountId = readStringParam(params, "accountId") ?? input.defaultAccountId;
    if (accountId) {
        params.accountId = accountId;
    }
    const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun"));
    await normalizeSandboxMediaParams({
        args: params,
        sandboxRoot: input.sandboxRoot,
    });
    await hydrateSendAttachmentParams({
        cfg,
        channel,
        accountId,
        args: params,
        action,
        dryRun,
    });
    await hydrateSetGroupIconParams({
        cfg,
        channel,
        accountId,
        args: params,
        action,
        dryRun,
    });
    const resolvedTarget = await resolveActionTarget({
        cfg,
        channel,
        action,
        args: params,
        accountId,
    });
    enforceCrossContextPolicy({
        channel,
        action,
        args: params,
        toolContext: input.toolContext,
        cfg,
    });
    const gateway = resolveGateway(input);
    if (action === "send") {
        return handleSendAction({
            cfg,
            params,
            channel,
            accountId,
            dryRun,
            gateway,
            input,
            agentId: resolvedAgentId,
            resolvedTarget,
            abortSignal: input.abortSignal,
        });
    }
    if (action === "poll") {
        return handlePollAction({
            cfg,
            params,
            channel,
            accountId,
            dryRun,
            gateway,
            input,
            abortSignal: input.abortSignal,
        });
    }
    return handlePluginAction({
        cfg,
        params,
        channel,
        accountId,
        dryRun,
        gateway,
        input,
        abortSignal: input.abortSignal,
    });
}
