/**
 * src/channels/discord/adapter.ts
 *
 * Discord bot adapter — receives messages via Gateway WebSocket (the same
 * mechanism the official discord.js uses, just hand-rolled to keep deps
 * small). Handles:
 *   1. IDENTIFY handshake with intents (GUILDS + GUILD_MESSAGES + MESSAGE_CONTENT + DIRECT_MESSAGES)
 *   2. Heartbeat ack at the rate the server requests
 *   3. MESSAGE_CREATE events → route through Muassis brain
 *   4. Reply via the REST API (POST /channels/{id}/messages)
 *   5. RESUME on disconnect with the last session_id + sequence
 *
 * Token: from a Discord application + bot user (Developer Portal). Requires
 * "MESSAGE CONTENT INTENT" enabled in the app's Bot tab.
 */
import { createChildLogger } from '../../utils/logger.js';
import { processMessage } from '../../agent/brain.js';
import { supabase } from '../../utils/supabase.js';
import { randomUUID } from 'node:crypto';
const log = createChildLogger('discord:adapter');
const DISCORD_API = 'https://discord.com/api/v10';
const DISCORD_GATEWAY = 'wss://gateway.discord.gg/?v=10&encoding=json';
// Intents: GUILDS(1) + GUILD_MESSAGES(512) + MESSAGE_CONTENT(32768) + DIRECT_MESSAGES(4096) = 37377
const INTENTS = 1 | 512 | 32768 | 4096;
const sessions = new Map();
async function discordCall(token, path, init = {}) {
    const resp = await fetch(`${DISCORD_API}${path}`, {
        ...init,
        headers: {
            Authorization: `Bot ${token}`,
            'Content-Type': 'application/json',
            ...(init.headers || {}),
        },
    });
    if (!resp.ok) {
        const text = await resp.text().catch(() => '');
        throw new Error(`discord ${path}: HTTP ${resp.status} ${text.slice(0, 200)}`);
    }
    return (await resp.json());
}
export async function startDiscordSession(tenantId, token) {
    const existing = sessions.get(tenantId);
    if (existing && existing.status !== 'disconnected' && existing.status !== 'auth_failed')
        return existing;
    const session = {
        tenantId, token,
        ws: null, status: 'connecting',
        botUserId: null, botUsername: null,
        sessionId: null, resumeUrl: null, sequence: null,
        heartbeatTimer: null,
        reconnectAttempts: existing?.reconnectAttempts ?? 0,
        reconnectTimer: null,
        messagesToday: existing?.messagesToday ?? 0, messagesTotal: existing?.messagesTotal ?? 0,
        lastError: null,
        startedAt: Date.now(),
    };
    sessions.set(tenantId, session);
    // Validate token via /users/@me
    try {
        const me = await discordCall(token, '/users/@me');
        session.botUserId = me.id;
        session.botUsername = me.username;
        log.info({ tenantId, botUserId: me.id, username: me.username }, 'discord: bot authenticated ✅');
    }
    catch (err) {
        session.status = 'auth_failed';
        session.lastError = err.message;
        return session;
    }
    connectGateway(session);
    return session;
}
function connectGateway(session) {
    const url = session.resumeUrl ?? DISCORD_GATEWAY;
    const ws = new WebSocket(url);
    session.ws = ws;
    ws.addEventListener('message', async (ev) => {
        try {
            const data = typeof ev.data === 'string' ? ev.data : await ev.data.text();
            const payload = JSON.parse(data);
            if (payload.s !== undefined && payload.s !== null)
                session.sequence = payload.s;
            switch (payload.op) {
                case 10: { // HELLO — start heartbeat
                    const interval = payload.d?.heartbeat_interval ?? 41250;
                    if (session.heartbeatTimer)
                        clearInterval(session.heartbeatTimer);
                    session.heartbeatTimer = setInterval(() => {
                        try {
                            ws.send(JSON.stringify({ op: 1, d: session.sequence }));
                        }
                        catch { /* ignore */ }
                    }, interval);
                    // IDENTIFY or RESUME
                    if (session.sessionId && session.sequence !== null) {
                        ws.send(JSON.stringify({ op: 6, d: { token: session.token, session_id: session.sessionId, seq: session.sequence } }));
                    }
                    else {
                        ws.send(JSON.stringify({
                            op: 2,
                            d: {
                                token: session.token,
                                intents: INTENTS,
                                properties: { os: 'linux', browser: 'al-moasis', device: 'al-moasis' },
                            },
                        }));
                    }
                    break;
                }
                case 0: { // DISPATCH
                    if (payload.t === 'READY') {
                        session.status = 'connected';
                        session.reconnectAttempts = 0;
                        session.sessionId = payload.d.session_id;
                        session.resumeUrl = payload.d.resume_gateway_url ? `${payload.d.resume_gateway_url}/?v=10&encoding=json` : null;
                        log.info({ tenantId: session.tenantId, sessionId: session.sessionId }, 'discord: READY');
                    }
                    else if (payload.t === 'RESUMED') {
                        session.status = 'connected';
                        log.info({ tenantId: session.tenantId }, 'discord: session resumed');
                    }
                    else if (payload.t === 'MESSAGE_CREATE') {
                        const msg = payload.d;
                        if (!msg.author?.bot && msg.author?.id !== session.botUserId && msg.content) {
                            await handleDiscordMessage(session, msg.author.id, msg.author.username, msg.channel_id, msg.content);
                        }
                    }
                    break;
                }
                case 1:
                    ws.send(JSON.stringify({ op: 1, d: session.sequence }));
                    break; // server-requested heartbeat
                case 7:
                    ws.close(4000);
                    break; // RECONNECT
                case 9: { // INVALID_SESSION
                    session.sessionId = null;
                    session.sequence = null;
                    setTimeout(() => connectGateway(session), 1500);
                    break;
                }
                case 11: break; // HEARTBEAT_ACK
            }
        }
        catch (err) {
            log.warn({ tenantId: session.tenantId, error: err.message }, 'discord: payload handling failed');
        }
    });
    ws.addEventListener('close', (ev) => {
        if (session.heartbeatTimer)
            clearInterval(session.heartbeatTimer);
        session.heartbeatTimer = null;
        session.status = 'disconnected';
        session.ws = null;
        log.warn({ tenantId: session.tenantId, code: ev.code }, 'discord: ws closed');
        if (session.reconnectAttempts >= 8)
            return;
        const delay = Math.min(3000 * 2 ** session.reconnectAttempts, 300_000);
        session.reconnectAttempts++;
        session.reconnectTimer = setTimeout(() => connectGateway(session), delay);
    });
    ws.addEventListener('error', (err) => {
        session.lastError = err.message || 'gateway error';
        log.warn({ tenantId: session.tenantId, error: session.lastError }, 'discord: ws error');
    });
}
async function handleDiscordMessage(session, userId, username, channelId, text) {
    session.messagesTotal++;
    session.messagesToday++;
    const { decideInbound } = await import('../../autonomy/allowlist.js');
    const access = await decideInbound(session.tenantId, 'discord', userId, username);
    if (!access.allowed) {
        log.info({ tenantId: session.tenantId, userId, mode: access.policyMode }, 'discord: inbound blocked');
        return;
    }
    let customerId;
    const existing = await supabase
        .from('customers').select('id').eq('tenant_id', session.tenantId).eq('phone', `discord:${userId}`).maybeSingle();
    if (existing.data) {
        customerId = existing.data.id;
    }
    else {
        customerId = randomUUID();
        const now = new Date().toISOString();
        await supabase.from('customers').insert({
            id: customerId, tenant_id: session.tenantId, phone: `discord:${userId}`, name: username,
            language: 'ar', first_contact: now, last_contact: now, total_messages: 0, status: 'active',
            metadata: { channel: 'discord', discord_user_id: userId, channel_id: channelId },
        });
    }
    // Type indicator
    try {
        await discordCall(session.token, `/channels/${channelId}/typing`, { method: 'POST' });
    }
    catch { /* ignore */ }
    try {
        const brainResp = await processMessage(session.tenantId, customerId, text, { mediaType: 'text', channel: 'whatsapp', customerName: username }, { enableTools: true, maxToolLoops: 12 });
        // Discord limits messages to 2000 chars
        const out = brainResp.response.length > 1900 ? brainResp.response.slice(0, 1900) + '…' : brainResp.response;
        await discordCall(session.token, `/channels/${channelId}/messages`, {
            method: 'POST',
            body: JSON.stringify({ content: out }),
        });
    }
    catch (err) {
        log.error({ tenantId: session.tenantId, error: err.message }, 'discord: reply failed');
    }
}
export async function disconnectDiscordSession(tenantId) {
    const session = sessions.get(tenantId);
    if (!session)
        return;
    if (session.reconnectTimer)
        clearTimeout(session.reconnectTimer);
    if (session.heartbeatTimer)
        clearInterval(session.heartbeatTimer);
    if (session.ws) {
        try {
            session.ws.close();
        }
        catch { /* ignore */ }
    }
    sessions.delete(tenantId);
    log.info({ tenantId }, 'discord: session disconnected');
}
export function getDiscordStatus(tenantId) {
    const s = sessions.get(tenantId);
    if (!s) {
        return { status: 'disconnected', bot_username: null, bot_user_id: null, reconnect_attempts: 0, messages_today: 0, messages_total: 0, last_error: null };
    }
    return {
        status: s.status,
        bot_username: s.botUsername,
        bot_user_id: s.botUserId,
        reconnect_attempts: s.reconnectAttempts,
        messages_today: s.messagesToday,
        messages_total: s.messagesTotal,
        last_error: s.lastError,
    };
}
//# sourceMappingURL=adapter.js.map