/**
 * src/channels/slack/adapter.ts
 *
 * Slack channel adapter — receives messages via Socket Mode (so no public
 * webhook URL needed) and sends replies via chat.postMessage. Modeled on
 * the Telegram long-poll pattern: one persistent connection per tenant,
 * exponential backoff on transient errors, integrates with the same
 * allowlist + brain pipeline as WhatsApp/Telegram.
 *
 * Two tokens needed (both from a Slack app the user creates):
 *   - SLACK_BOT_TOKEN  (xoxb-...) for posting messages
 *   - SLACK_APP_TOKEN  (xapp-...) for Socket Mode connection
 *
 * Why Socket Mode and not webhooks: webhooks require a public HTTPS URL.
 * Device-mode Muassis runs on localhost, so Socket Mode (Slack pulls events
 * via WebSocket) is the correct fit. Same architectural choice OpenClaw
 * made.
 *
 * Minimal-deps implementation: uses `ws` (already in node_modules via
 * Baileys deps) instead of @slack/bolt to keep the bundle small. We
 * implement the bits we need:
 *   1. apps.connections.open → returns wss URL
 *   2. WebSocket → receive events
 *   3. ack frames + envelope_id roundtrip
 *   4. chat.postMessage to reply
 */
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('slack:adapter');
const sessions = new Map();
async function slackCall(token, method, body) {
    const url = `https://slack.com/api/${method}`;
    const init = {
        method: 'POST',
        headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json; charset=utf-8',
        },
    };
    if (body)
        init.body = JSON.stringify(body);
    const resp = await fetch(url, init);
    const data = (await resp.json());
    if (!data.ok) {
        throw new Error(`slack.${method}: ${data.error || 'unknown error'}`);
    }
    return data;
}
export async function startSlackSession(tenantId, botToken, appToken) {
    const existing = sessions.get(tenantId);
    if (existing && existing.status !== 'disconnected' && existing.status !== 'auth_failed')
        return existing;
    const session = {
        tenantId,
        botToken,
        appToken,
        ws: null,
        status: 'connecting',
        botUserId: 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 bot token by calling auth.test
    try {
        const me = await slackCall(botToken, 'auth.test');
        session.botUserId = me.user_id;
        log.info({ tenantId, botUserId: me.user_id, team: me.team }, 'slack bot authenticated ✅');
    }
    catch (err) {
        session.status = 'auth_failed';
        session.lastError = err.message;
        return session;
    }
    void connectSocketMode(session);
    return session;
}
async function connectSocketMode(session) {
    try {
        // Request a temporary wss URL — valid for ~30 seconds
        const { url } = await slackCall(session.appToken, 'apps.connections.open');
        const ws = new WebSocket(url);
        session.ws = ws;
        ws.addEventListener('open', () => {
            session.status = 'connected';
            session.reconnectAttempts = 0;
            log.info({ tenantId: session.tenantId }, 'slack: socket-mode connected');
        });
        ws.addEventListener('message', async (ev) => {
            try {
                const data = typeof ev.data === 'string' ? ev.data : await ev.data.text();
                const env = JSON.parse(data);
                // ACK immediately — Slack requires this within 3s
                if (env.envelope_id) {
                    ws.send(JSON.stringify({ envelope_id: env.envelope_id }));
                }
                // Disconnect notification — gracefully reconnect
                if (env.type === 'disconnect') {
                    log.info({ tenantId: session.tenantId }, 'slack: server requested disconnect — reconnecting');
                    ws.close();
                    return;
                }
                if (env.type !== 'events_api')
                    return;
                const event = env.payload?.event;
                if (!event || event.type !== 'message')
                    return;
                if (event.subtype || event.bot_id || event.user === session.botUserId)
                    return;
                if (!event.text || !event.user || !event.channel)
                    return;
                await handleSlackMessage(session, event.user, event.channel, event.text);
            }
            catch (err) {
                log.warn({ tenantId: session.tenantId, error: err.message }, 'slack: message handling failed');
            }
        });
        ws.addEventListener('close', () => {
            session.status = 'disconnected';
            session.ws = null;
            if (session.reconnectAttempts >= 8) {
                log.warn({ tenantId: session.tenantId }, 'slack: max reconnects reached');
                return;
            }
            const delay = Math.min(3000 * 2 ** session.reconnectAttempts, 300_000);
            session.reconnectAttempts++;
            session.reconnectTimer = setTimeout(() => connectSocketMode(session), delay);
            log.info({ tenantId: session.tenantId, attempt: session.reconnectAttempts, delay }, 'slack: scheduling reconnect');
        });
        ws.addEventListener('error', (err) => {
            session.lastError = err.message || 'websocket error';
            log.warn({ tenantId: session.tenantId, error: session.lastError }, 'slack: ws error');
        });
    }
    catch (err) {
        session.status = 'disconnected';
        session.lastError = err.message;
        log.error({ tenantId: session.tenantId, error: session.lastError }, 'slack: connect failed');
    }
}
async function handleSlackMessage(session, userId, channel, text) {
    session.messagesTotal++;
    session.messagesToday++;
    // Allowlist gate — same pattern as WhatsApp/Telegram
    const { decideInbound } = await import('../../autonomy/allowlist.js');
    const access = await decideInbound(session.tenantId, 'slack', userId, null);
    if (!access.allowed) {
        log.info({ tenantId: session.tenantId, userId, mode: access.policyMode }, 'slack: inbound blocked by allowlist');
        return;
    }
    // Find-or-create customer keyed by Slack user ID
    let customerId;
    const existing = await supabase
        .from('customers').select('id').eq('tenant_id', session.tenantId).eq('phone', `slack:${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: `slack:${userId}`, name: null,
            language: 'ar', first_contact: now, last_contact: now, total_messages: 0, status: 'active',
            metadata: { channel: 'slack', slack_user_id: userId, channel_id: channel },
        });
    }
    // Send typing indicator (Slack uses reactions:write or just delay — we
    // post the reply directly; Slack already shows "thinking" if we take >2s
    // due to message ordering).
    try {
        const brainResp = await processMessage(session.tenantId, customerId, text, { mediaType: 'text', channel: 'whatsapp', customerName: userId }, // brain doesn't yet have 'slack' channel; reuse 'whatsapp' shape
        { enableTools: true, maxToolLoops: 12 });
        await slackCall(session.botToken, 'chat.postMessage', {
            channel,
            text: brainResp.response,
        });
    }
    catch (err) {
        log.error({ tenantId: session.tenantId, error: err.message }, 'slack: reply failed');
        try {
            await slackCall(session.botToken, 'chat.postMessage', {
                channel,
                text: `❌ تعذّر الرد: ${err.message.slice(0, 100)}`,
            });
        }
        catch { /* ignore */ }
    }
}
export async function disconnectSlackSession(tenantId) {
    const session = sessions.get(tenantId);
    if (!session)
        return;
    if (session.reconnectTimer)
        clearTimeout(session.reconnectTimer);
    if (session.ws) {
        try {
            session.ws.close();
        }
        catch { /* ignore */ }
    }
    sessions.delete(tenantId);
    log.info({ tenantId }, 'slack: session disconnected');
}
export function getSlackStatus(tenantId) {
    const s = sessions.get(tenantId);
    if (!s) {
        return { status: 'disconnected', bot_user_id: null, reconnect_attempts: 0, messages_today: 0, messages_total: 0, last_error: null };
    }
    return {
        status: s.status,
        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