import { ChannelType, Routes } from "discord-api-types/v10";
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { resolveChunkMode } from "../auto-reply/chunk.js";
import { loadConfig } from "../config/config.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { recordChannelActivity } from "../infra/channel-activity.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import { convertMarkdownTables } from "../markdown/tables.js";
import { maxBytesForKind } from "../media/constants.js";
import { extensionForMime } from "../media/mime.js";
import { loadWebMediaRaw } from "../web/media.js";
import { resolveDiscordAccount } from "./accounts.js";
import { buildDiscordSendError, buildDiscordTextChunks, createDiscordClient, normalizeDiscordPollInput, normalizeStickerIds, parseAndResolveRecipient, resolveChannelId, sendDiscordMedia, sendDiscordText, SUPPRESS_NOTIFICATIONS_FLAG, } from "./send.shared.js";
import { ensureOggOpus, getVoiceMessageMetadata, sendDiscordVoiceMessage, } from "./voice-message.js";
/** Discord thread names are capped at 100 characters. */
const DISCORD_THREAD_NAME_LIMIT = 100;
/** Derive a thread title from the first non-empty line of the message text. */
function deriveForumThreadName(text) {
    const firstLine = text
        .split("\n")
        .find((l) => l.trim())
        ?.trim() ?? "";
    return firstLine.slice(0, DISCORD_THREAD_NAME_LIMIT) || new Date().toISOString().slice(0, 16);
}
/** Forum/Media channels cannot receive regular messages; detect them here. */
function isForumLikeType(channelType) {
    return channelType === ChannelType.GuildForum || channelType === ChannelType.GuildMedia;
}
export async function sendMessageDiscord(to, text, opts = {}) {
    const cfg = loadConfig();
    const accountInfo = resolveDiscordAccount({
        cfg,
        accountId: opts.accountId,
    });
    const tableMode = resolveMarkdownTableMode({
        cfg,
        channel: "discord",
        accountId: accountInfo.accountId,
    });
    const chunkMode = resolveChunkMode(cfg, "discord", accountInfo.accountId);
    const textWithTables = convertMarkdownTables(text ?? "", tableMode);
    const { token, rest, request } = createDiscordClient(opts, cfg);
    const recipient = await parseAndResolveRecipient(to, opts.accountId);
    // @ts-ignore -- cherry-pick upstream type mismatch
    // @ts-ignore -- cherry-pick upstream type mismatch
    const { channelId } = await resolveChannelId(rest, recipient, request);
    // Forum/Media channels reject POST /messages; auto-create a thread post instead.
    let channelType;
    try {
        const channel = (await rest.get(Routes.channel(channelId)));
        channelType = channel?.type;
    }
    catch {
        // If we can't fetch the channel, fall through to the normal send path.
    }
    if (isForumLikeType(channelType)) {
        const threadName = deriveForumThreadName(textWithTables);
        const chunks = buildDiscordTextChunks(textWithTables, {
            maxLinesPerMessage: accountInfo.config.maxLinesPerMessage,
            chunkMode,
        });
        const starterContent = chunks[0]?.trim() ? chunks[0] : threadName;
        const starterEmbeds = opts.embeds?.length ? opts.embeds : undefined;
        let threadRes;
        try {
            threadRes = (await request(() => rest.post(Routes.threads(channelId), {
                body: {
                    name: threadName,
                    message: {
                        content: starterContent,
                        ...(starterEmbeds ? { embeds: starterEmbeds } : {}),
                    },
                },
            }), "forum-thread"));
        }
        catch (err) {
            throw await buildDiscordSendError(err, {
                channelId,
                rest,
                token,
                hasMedia: Boolean(opts.mediaUrl),
            });
        }
        const threadId = threadRes.id;
        const messageId = threadRes.message?.id ?? threadId;
        const resultChannelId = threadRes.message?.channel_id ?? threadId;
        const remainingChunks = chunks.slice(1);
        try {
            if (opts.mediaUrl) {
                const [mediaCaption, ...afterMediaChunks] = remainingChunks;
                await sendDiscordMedia(rest, threadId, mediaCaption ?? "", opts.mediaUrl, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, 
                // @ts-ignore -- cherry-pick upstream type mismatch
                chunkMode, 
                // @ts-ignore -- cherry-pick upstream type mismatch
                opts.silent);
                for (const chunk of afterMediaChunks) {
                    await sendDiscordText(rest, threadId, chunk, undefined, request, accountInfo.config.maxLinesPerMessage, 
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    undefined, chunkMode, 
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    opts.silent);
                }
            }
            else {
                for (const chunk of remainingChunks) {
                    await sendDiscordText(rest, threadId, chunk, undefined, request, 
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    accountInfo.config.maxLinesPerMessage, undefined, chunkMode, 
                    // @ts-ignore -- cherry-pick upstream type mismatch
                    opts.silent);
                }
            }
        }
        catch (err) {
            throw await buildDiscordSendError(err, {
                channelId: threadId,
                rest,
                token,
                hasMedia: Boolean(opts.mediaUrl),
            });
        }
        recordChannelActivity({
            channel: "discord",
            accountId: accountInfo.accountId,
            direction: "outbound",
        });
        return {
            messageId: messageId ? String(messageId) : "unknown",
            channelId: String(resultChannelId ?? channelId),
        };
    }
    let result;
    try {
        if (opts.mediaUrl) {
            result = await sendDiscordMedia(rest, channelId, textWithTables, opts.mediaUrl, opts.replyTo, 
            // @ts-ignore -- cherry-pick upstream type mismatch
            request, accountInfo.config.maxLinesPerMessage, opts.embeds, chunkMode, 
            // @ts-ignore -- cherry-pick upstream type mismatch
            opts.silent);
        }
        else {
            result = await sendDiscordText(rest, channelId, textWithTables, 
            // @ts-ignore -- cherry-pick upstream type mismatch
            opts.replyTo, request, accountInfo.config.maxLinesPerMessage, opts.embeds, chunkMode, 
            // @ts-ignore -- cherry-pick upstream type mismatch
            opts.silent);
        }
    }
    catch (err) {
        throw await buildDiscordSendError(err, {
            channelId,
            rest,
            token,
            hasMedia: Boolean(opts.mediaUrl),
        });
    }
    recordChannelActivity({
        channel: "discord",
        accountId: accountInfo.accountId,
        direction: "outbound",
    });
    return {
        messageId: result.id ? String(result.id) : "unknown",
        channelId: String(result.channel_id ?? channelId),
    };
}
export async function sendStickerDiscord(to, 
// @ts-ignore -- cherry-pick upstream type mismatch
stickerIds, opts = {}) {
    const cfg = loadConfig();
    const { rest, request } = createDiscordClient(opts, cfg);
    const recipient = await parseAndResolveRecipient(to, opts.accountId);
    // @ts-ignore -- cherry-pick upstream type mismatch
    const { channelId } = await resolveChannelId(rest, recipient, request);
    const content = opts.content?.trim();
    const stickers = normalizeStickerIds(stickerIds);
    const res = (await request(() => rest.post(Routes.channelMessages(channelId), {
        body: {
            content: content || undefined,
            sticker_ids: stickers,
        },
    }), "sticker"));
    return {
        messageId: res.id ? String(res.id) : "unknown",
        channelId: String(res.channel_id ?? channelId),
    };
}
export async function sendPollDiscord(
// @ts-ignore -- cherry-pick upstream type mismatch
to, poll, 
// @ts-ignore -- cherry-pick upstream type mismatch
opts = {}) {
    const cfg = loadConfig();
    const { rest, request } = createDiscordClient(opts, cfg);
    const recipient = await parseAndResolveRecipient(to, opts.accountId);
    // @ts-ignore -- cherry-pick upstream type mismatch
    const { channelId } = await resolveChannelId(rest, recipient, request);
    const content = opts.content?.trim();
    // @ts-ignore -- cherry-pick upstream type mismatch
    if (poll.durationSeconds !== undefined) {
        throw new Error("Discord polls do not support durationSeconds; use durationHours");
    }
    const payload = normalizeDiscordPollInput(poll);
    const flags = opts.silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined;
    const res = (await request(() => rest.post(Routes.channelMessages(channelId), {
        body: {
            content: content || undefined,
            poll: payload,
            ...(flags ? { flags } : {}),
        },
    }), "poll"));
    return {
        messageId: res.id ? String(res.id) : "unknown",
        channelId: String(res.channel_id ?? channelId),
    };
}
async function materializeVoiceMessageInput(mediaUrl) {
    // Security: reuse the standard media loader so we apply SSRF guards + allowed-local-root checks.
    // Then write to a private temp file so ffmpeg/ffprobe never sees the original URL/path string.
    const media = await loadWebMediaRaw(mediaUrl, maxBytesForKind("audio"));
    const extFromName = media.fileName ? path.extname(media.fileName) : "";
    const extFromMime = media.contentType ? extensionForMime(media.contentType) : "";
    const ext = extFromName || extFromMime || ".bin";
    const tempDir = resolvePreferredOpenClawTmpDir();
    const filePath = path.join(tempDir, `voice-src-${crypto.randomUUID()}${ext}`);
    await fs.writeFile(filePath, media.buffer, { mode: 0o600 });
    return { filePath };
}
/**
 * Send a voice message to Discord.
 *
 * Voice messages are a special Discord feature that displays audio with a waveform
 * visualization. They require OGG/Opus format and cannot include text content.
 *
 * @param to - Recipient (user ID for DM or channel ID)
 * @param audioPath - Path to local audio file (will be converted to OGG/Opus if needed)
 * @param opts - Send options
 */
export async function sendVoiceMessageDiscord(to, audioPath, opts = {}) {
    const { filePath: localInputPath } = await materializeVoiceMessageInput(audioPath);
    let oggPath = null;
    let oggCleanup = false;
    let token;
    let rest;
    let channelId;
    try {
        const cfg = loadConfig();
        // @ts-ignore -- cherry-pick upstream type mismatch
        const accountInfo = resolveDiscordAccount({
            cfg,
            accountId: opts.accountId,
        });
        const client = createDiscordClient(opts, cfg);
        token = client.token;
        rest = client.rest;
        const request = client.request;
        const recipient = await parseAndResolveRecipient(to, opts.accountId);
        // @ts-ignore -- cherry-pick upstream type mismatch
        channelId = (await resolveChannelId(rest, recipient, request)).channelId;
        // Convert to OGG/Opus if needed
        const ogg = await ensureOggOpus(localInputPath);
        oggPath = ogg.path;
        oggCleanup = ogg.cleanup;
        // Get voice message metadata (duration and waveform)
        const metadata = await getVoiceMessageMetadata(oggPath);
        // Read the audio file
        const audioBuffer = await fs.readFile(oggPath);
        // Send the voice message
        const result = await sendDiscordVoiceMessage(rest, channelId, audioBuffer, metadata, opts.replyTo, request, opts.silent);
        recordChannelActivity({
            channel: "discord",
            accountId: accountInfo.accountId,
            direction: "outbound",
        });
        return {
            messageId: result.id ? String(result.id) : "unknown",
            channelId: String(result.channel_id ?? channelId),
        };
    }
    catch (err) {
        if (channelId && rest && token) {
            throw await buildDiscordSendError(err, {
                channelId,
                rest,
                token,
                hasMedia: true,
            });
        }
        throw err;
    }
    finally {
        // Clean up temporary OGG file if we created one
        if (oggCleanup && oggPath) {
            try {
                await fs.unlink(oggPath);
            }
            catch {
                // Ignore cleanup errors
            }
        }
        try {
            await fs.unlink(localInputPath);
        }
        catch {
            // Ignore cleanup errors
        }
    }
}
