/**
 * Discord adapter — Gateway WebSocket via discord.js (v0.7b).
 *
 * SDK: `discord.js` v14 (npm i discord.js as optionalDependency).
 * Discord's Gateway protocol is non-trivial — heartbeats, resume
 * tokens, sharding, identify backoff, zlib decompression. Rolling our
 * own WebSocket client would be ~500 LOC of fragile protocol code.
 * One big SDK we import a few symbols from is the right tradeoff.
 *
 * Connection: outbound WebSocket via `client.login(token)`. No public
 * webhook URL. Works behind any NAT.
 *
 * Intents: `Guilds`, `GuildMessages`, `MessageContent`, `DirectMessages`
 * — forgetting DirectMessages silently drops DM events, a known
 * newcomer gotcha. MESSAGE CONTENT is a privileged intent that needs
 * to be enabled in the Developer Portal but is exempt for DMs and
 * @-mentions, so personal-bot DM use works regardless.
 *
 * Dynamic import discipline matches [[telegram-adapter]] — non-discord
 * installs pay zero cost.
 */
import { ChatGatewayError, formatChannelId, } from "../gateway.js";
export class DiscordAdapter {
    config;
    platform = "discord";
    client = null;
    handlers = [];
    botUsername = "";
    botId = "";
    constructor(config) {
        this.config = config;
        if (!config.bot_token?.trim()) {
            throw new ChatGatewayError("discord adapter: bot_token is required", "set chat_connections.discord.bot_token in ~/.opensquid/config.json");
        }
    }
    async start() {
        if (this.client)
            return;
        let Client;
        let GatewayIntentBits;
        try {
            const discord = (await import("discord.js"));
            Client = discord.Client;
            GatewayIntentBits = discord.GatewayIntentBits;
        }
        catch (err) {
            // v0.7 audit fix (M5): distinguish not-installed from installed-but-threw.
            const code = err?.code;
            if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
                throw new ChatGatewayError("discord adapter: 'discord.js' SDK not installed", "run `npm install discord.js` (or reinstall opensquid without --omit=optional)");
            }
            throw new ChatGatewayError(`discord adapter: 'discord.js' SDK failed to load: ${err instanceof Error ? err.message : String(err)}`, "the SDK is installed but threw on import — check node version compatibility");
        }
        const client = new Client({
            intents: [
                GatewayIntentBits.Guilds,
                GatewayIntentBits.GuildMessages,
                GatewayIntentBits.MessageContent,
                GatewayIntentBits.DirectMessages,
            ],
        });
        this.client = client;
        // Capture identity once the gateway handshake completes.
        const ready = new Promise((resolve) => {
            client.once("ready", () => {
                const me = client.user;
                if (me) {
                    this.botUsername = me.username;
                    this.botId = me.id;
                }
                resolve();
            });
        });
        client.on("messageCreate", async (m) => {
            if (m.author.bot)
                return; // ignore bot-authored messages (including ourselves)
            // Allowlist enforcement on sender user id.
            if (this.config.allowlist_user_ids &&
                this.config.allowlist_user_ids.length > 0 &&
                !this.config.allowlist_user_ids.includes(m.author.id)) {
                return;
            }
            const normalized = {
                id: m.id,
                platform: "discord",
                channel: formatChannelId("discord", m.channelId),
                sender: m.author.username,
                senderId: m.author.id,
                text: m.content,
                receivedAt: new Date(m.createdTimestamp),
                mentionsBot: client.user ? m.mentions.has(client.user) : false,
            };
            for (const h of this.handlers) {
                try {
                    await h(normalized);
                }
                catch (err) {
                    const msg = err instanceof Error ? err.message : String(err);
                    // eslint-disable-next-line no-console
                    console.error(`[discord adapter] handler error: ${msg}`);
                }
            }
        });
        // login() rejects on bad token; ready resolves after the handshake.
        // v0.7 audit fix (H2): the `MessageContent` intent is privileged and
        // requires explicit enablement in the Discord Developer Portal. Without
        // it, login() throws `DisallowedIntents` with a cryptic message. Catch
        // and rethrow with an actionable hint pointing at the Dev Portal toggle.
        try {
            await client.login(this.config.bot_token);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            if (/disallowed intent|used disallowed/i.test(msg)) {
                // discard the half-built client
                this.client = null;
                throw new ChatGatewayError("discord adapter: bot is requesting privileged intents that aren't enabled", "go to https://discord.com/developers/applications → your app → Bot → Privileged Gateway Intents → toggle ON 'MESSAGE CONTENT INTENT'. DMs and @-mentions are exempt but channel content needs the toggle.");
            }
            this.client = null;
            throw err;
        }
        await ready;
    }
    async shutdown() {
        if (!this.client)
            return;
        try {
            await this.client.destroy();
        }
        catch {
            // best-effort
        }
        this.client = null;
    }
    onMessage(handler) {
        this.handlers.push(handler);
    }
    async send(message) {
        if (!this.client) {
            throw new ChatGatewayError("discord adapter: not started", "call gateway.start() before send()");
        }
        const channelId = nativeChannelIdFromChannel(message.channel);
        const channel = await this.client.channels.fetch(channelId);
        if (!channel) {
            throw new ChatGatewayError(`discord adapter: channel '${channelId}' not found or bot lacks access`);
        }
        const payload = message.replyTo !== undefined
            ? { content: message.text, reply: { messageReference: message.replyTo } }
            : message.text;
        const sent = await channel.send(payload);
        return {
            platform: "discord",
            messageId: sent.id,
            deliveredAt: new Date(sent.createdTimestamp),
        };
    }
    async identity() {
        if (!this.client) {
            throw new ChatGatewayError("discord adapter: not started");
        }
        return { username: this.botUsername, nativeId: this.botId };
    }
}
// ---------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------
function nativeChannelIdFromChannel(channel) {
    const idx = channel.indexOf(":");
    if (idx === -1) {
        throw new ChatGatewayError(`malformed channel id '${channel}'`);
    }
    if (channel.slice(0, idx) !== "discord") {
        throw new ChatGatewayError(`discord adapter received non-discord channel: '${channel}'`);
    }
    return channel.slice(idx + 1);
}
