/**
 * src/channels/telegram/adapter.ts
 *
 * Telegram Bot API integration via long-polling. Zero user setup beyond
 * pasting a bot token from @BotFather. Once running, inbound DMs to the bot
 * route through the same agent loop as WhatsApp + web chat.
 *
 * Why long-polling instead of webhooks: webhooks require a public HTTPS URL
 * the bot can reach. Device-mode Muassis runs on localhost — long-polling
 * is the zero-config path. (For control-plane SaaS we can swap to webhooks
 * later; the adapter contract stays the same.)
 *
 * Operational notes:
 *   • Token comes from MUASSIS_TELEGRAM_BOT_TOKEN env var (or per-tenant
 *     setting in control-plane mode — TODO).
 *   • One bot = one Telegram-side identity. Customers DM the bot's @username.
 *   • Bot sees /start automatically when a new user clicks "Start" — we
 *     greet them with the agent's welcome message.
 *   • No groups for now (B2B SMB use case is 1:1). Group support later.
 */
import { randomUUID } from 'node:crypto';
import { createChildLogger } from '../../utils/logger.js';
import { supabase } from '../../utils/supabase.js';
import { processMessage } from '../../agent/brain.js';
import { learnFromConversation } from '../../knowledge/unified.js';
const log = createChildLogger('telegram:adapter');
const sessions = new Map();
const TELEGRAM_API = 'https://api.telegram.org';
const POLL_TIMEOUT_SEC = 25;
async function tgCall(token, method, params = {}, signal) {
    const url = `${TELEGRAM_API}/bot${token}/${method}`;
    const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
        signal,
    });
    const json = (await res.json());
    if (!json.ok) {
        throw new Error(`Telegram ${method} failed: ${json.description ?? 'unknown error'} (code ${json.error_code ?? '?'})`);
    }
    return json.result;
}
/**
 * (v2.5.5) Upload a local file to Telegram and post as media. Picks the
 * right method by mime type: image → sendPhoto, video → sendVideo,
 * audio → sendAudio, anything else → sendDocument.
 */
async function tgSendFile(token, chatId, filePath, mimeType) {
    const { readFile } = await import('node:fs/promises');
    const buf = await readFile(filePath);
    const fileName = filePath.split('/').pop() || 'attachment';
    const blob = new Blob([buf], { type: mimeType || 'application/octet-stream' });
    let method;
    let fileField;
    if (mimeType?.startsWith('image/')) {
        method = 'sendPhoto';
        fileField = 'photo';
    }
    else if (mimeType?.startsWith('video/')) {
        method = 'sendVideo';
        fileField = 'video';
    }
    else if (mimeType?.startsWith('audio/')) {
        method = 'sendAudio';
        fileField = 'audio';
    }
    else {
        method = 'sendDocument';
        fileField = 'document';
    }
    const form = new FormData();
    form.append('chat_id', String(chatId));
    form.append(fileField, blob, fileName);
    const url = `${TELEGRAM_API}/bot${token}/${method}`;
    const res = await fetch(url, { method: 'POST', body: form });
    const json = (await res.json());
    if (!json.ok) {
        throw new Error(`Telegram ${method} failed: ${json.description ?? 'unknown error'}`);
    }
}
export async function startTelegramSession(tenantId, token) {
    const existing = sessions.get(tenantId);
    if (existing && existing.status !== 'disconnected' && existing.status !== 'auth_failed') {
        return existing;
    }
    // Stop any old session
    if (existing?.pollAbort) {
        try {
            existing.pollAbort.abort();
        }
        catch { /* ignore */ }
    }
    const session = {
        tenantId,
        token,
        status: 'connecting',
        botUsername: null,
        botId: null,
        lastUpdateId: 0,
        pollAbort: null,
        startedAt: Date.now(),
        reconnectAttempts: existing?.reconnectAttempts ?? 0,
        messagesToday: existing?.messagesToday ?? 0,
        messagesTotal: existing?.messagesTotal ?? 0,
        lastMessagePreview: existing?.lastMessagePreview ?? null,
        lastMessageAt: existing?.lastMessageAt ?? null,
        lastError: null,
    };
    sessions.set(tenantId, session);
    // Validate token + identify bot
    try {
        const me = await tgCall(token, 'getMe');
        session.botId = me.id;
        session.botUsername = me.username ?? null;
        session.status = 'connected';
        log.info({ tenantId, botUsername: session.botUsername, botId: session.botId }, 'telegram bot authenticated ✅');
    }
    catch (err) {
        session.status = 'auth_failed';
        log.error({ tenantId, error: err.message }, 'telegram getMe failed — bad token?');
        return session;
    }
    // Kick off long-poll loop in the background
    void pollLoop(session);
    return session;
}
async function pollLoop(session) {
    while (sessions.get(session.tenantId) === session && session.status === 'connected') {
        const ctrl = new AbortController();
        session.pollAbort = ctrl;
        try {
            const updates = await tgCall(session.token, 'getUpdates', {
                offset: session.lastUpdateId + 1,
                timeout: POLL_TIMEOUT_SEC,
                allowed_updates: ['message', 'edited_message', 'callback_query'],
            }, ctrl.signal);
            for (const u of updates) {
                session.lastUpdateId = Math.max(session.lastUpdateId, u.update_id);
                try {
                    if (u.message)
                        await handleMessage(session, u.message);
                    else if (u.edited_message)
                        await handleMessage(session, u.edited_message, /*edited=*/ true);
                }
                catch (err) {
                    log.error({ tenantId: session.tenantId, error: err.message }, 'inbound telegram update failed');
                }
            }
        }
        catch (err) {
            const msg = err.message;
            if (msg.includes('aborted')) {
                log.info({ tenantId: session.tenantId }, 'telegram poll aborted');
                return;
            }
            session.lastError = msg;
            session.reconnectAttempts++;
            // Exponential backoff capped at 5m, matching the WA pattern. Telegram
            // long-poll errors are usually transient (network blips, 502 from
            // api.telegram.org) — backing off instead of hammering keeps us
            // within Bot API rate limits.
            const backoffMs = Math.min(5_000 * Math.pow(2, session.reconnectAttempts - 1), 300_000);
            log.warn({ tenantId: session.tenantId, error: msg, attempt: session.reconnectAttempts, backoffMs }, 'telegram poll error — backing off');
            await new Promise(r => setTimeout(r, backoffMs));
        }
    }
}
/**
 * Show a live "typing…" indicator while the brain works. Telegram clears the
 * chat action after ~5s, so we refresh every 4s until stop() is called. Mirrors
 * the WhatsApp 'composing' presence so all channels feel responsive.
 */
function startTyping(session, chatId) {
    let stopped = false;
    const ping = () => {
        if (stopped)
            return;
        tgCall(session.token, 'sendChatAction', { chat_id: chatId, action: 'typing' }).catch(() => { });
    };
    ping();
    const iv = setInterval(ping, 4000);
    return { stop: () => { stopped = true; clearInterval(iv); } };
}
async function handleMessage(session, m, edited = false) {
    // Skip non-private chats for now (B2B 1:1 focus)
    if (m.chat.type !== 'private')
        return;
    // Skip bot-to-bot
    if (m.from?.is_bot)
        return;
    // Skip own messages
    if (session.botId != null && m.from?.id === session.botId)
        return;
    const text = m.text || m.caption || '';
    if (!text.trim())
        return;
    const tgUserId = m.from?.id ?? m.chat.id;
    const fullName = [m.from?.first_name, m.from?.last_name].filter(Boolean).join(' ') || m.chat.first_name || 'Telegram User';
    const username = m.from?.username ?? null;
    // Allowlist gate (same logic as WhatsApp). Unknown senders are escalated
    // when inbound_access=ask, dropped when =never, auto-allowed when =auto.
    {
        const { decideInbound } = await import('../../autonomy/allowlist.js');
        const access = await decideInbound(session.tenantId, 'telegram', String(tgUserId), fullName);
        if (!access.allowed) {
            if (access.policyMode === 'ask') {
                const { escalate } = await import('../../autonomy/escalation.js');
                await escalate({
                    tenantId: session.tenantId,
                    title: `Unknown Telegram sender: ${fullName}${username ? ` (@${username})` : ''}`,
                    context: `Inbound message: "${text.slice(0, 200)}"`,
                    recommendation: `Authorize this Telegram user if you recognize them.`,
                    counterargument: `If unknown, declining keeps the bot from leaking info.`,
                    alternatives: [
                        'Approve once (one-time reply)',
                        'Approve and remember (auto-allow future)',
                        'Decline and block',
                    ],
                    riskLevel: 'medium',
                });
            }
            log.info({ tenantId: session.tenantId, tgUserId, mode: access.policyMode, reason: access.reason }, 'inbound blocked by allowlist');
            return;
        }
    }
    // Find or create the customer
    const customer = await ensureTelegramCustomer(session.tenantId, tgUserId, fullName, username);
    // Greet on /start (Telegram bot UX convention)
    if (text.trim() === '/start') {
        await sendTelegramMessage(session.tenantId, m.chat.id, '👋 Hi! I am the Muassis AI Employee. How can I help you today?');
        return;
    }
    // Telemetry update mirrors WhatsApp adapter behavior
    session.messagesTotal++;
    const nowDay = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Riyadh' });
    const lastDay = session.lastMessageAt
        ? new Date(session.lastMessageAt).toLocaleDateString('en-CA', { timeZone: 'Asia/Riyadh' })
        : null;
    if (nowDay !== lastDay)
        session.messagesToday = 1;
    else
        session.messagesToday++;
    session.lastMessageAt = Date.now();
    session.lastMessagePreview = text.slice(0, 80);
    // (C2) Pull the last ~10 messages BEFORE inserting the inbound. Same shape
    // as the web-chat + WhatsApp paths so the brain has consistent context
    // across all 3 channels. Without this, every Telegram turn was stateless.
    const recentResult = await supabase
        .from('messages')
        .select('direction, content')
        .eq('tenant_id', session.tenantId)
        .eq('customer_id', customer.id)
        .order('created_at', { ascending: false })
        .limit(10);
    const recentMessages = (recentResult.data ?? [])
        .reverse()
        .filter((r) => r.content && r.content.trim().length > 0)
        .map((r) => ({
        role: (r.direction === 'inbound' ? 'user' : 'assistant'),
        content: r.content,
    }));
    // Log inbound
    await logInbound(session.tenantId, customer.id, text);
    // (v2.5.5) Auto-attach artifacts on Telegram too — same UX rule as
    // WhatsApp: don't say "saved to your desktop", just send the file.
    const artifacts = [];
    const seenArtifacts = new Set();
    // Live "typing…" indicator for the whole brain+send turn.
    const typing = startTyping(session, m.chat.id);
    // Run through brain
    const brainResp = await processMessage(session.tenantId, customer.id, text, {
        mediaType: 'text',
        channel: 'telegram',
        customerName: fullName,
        recentMessages,
    }, {
        budgetAware: true,
        enableTools: true,
        maxToolLoops: 12,
        onArtifact: (a) => {
            if (!seenArtifacts.has(a.path)) {
                seenArtifacts.add(a.path);
                artifacts.push(a);
            }
        },
    });
    // Parse delivery directives so the agent can react / silent / ack /
    // delay on Telegram, same as WhatsApp. Telegram uses message reactions
    // via the setMessageReaction Bot API method.
    const { parseDirectives } = await import('../../agent/directives.js');
    const directive = parseDirectives(brainResp.response);
    const finalText = directive.bodyText || brainResp.response;
    if (directive.delaySec > 0) {
        await new Promise(r => setTimeout(r, directive.delaySec * 1000));
    }
    // Reply
    try {
        if (directive.mode === 'silent') {
            log.info({ tenantId: session.tenantId, chatId: m.chat.id }, 'directive=silent — suppressing telegram reply');
        }
        else if ((directive.mode === 'react' || directive.mode === 'ack') && directive.reactionEmoji) {
            try {
                await tgCall(session.token, 'setMessageReaction', {
                    chat_id: m.chat.id,
                    message_id: m.message_id,
                    reaction: [{ type: 'emoji', emoji: directive.reactionEmoji }],
                });
            }
            catch (err) {
                log.debug({ err: err.message }, 'telegram react directive failed');
            }
            if (directive.mode === 'ack' && finalText) {
                await tgCall(session.token, 'sendMessage', {
                    chat_id: m.chat.id,
                    text: finalText,
                    parse_mode: undefined,
                });
            }
        }
        else {
            await tgCall(session.token, 'sendMessage', {
                chat_id: m.chat.id,
                text: finalText,
                parse_mode: undefined,
            });
        }
        // (v2.5.5) Attach every artifact the brain produced this turn so the
        // user actually SEES the work, not just a sentence about it.
        for (const a of artifacts) {
            try {
                await tgSendFile(session.token, m.chat.id, a.path, a.mimeType);
                log.info({ tenantId: session.tenantId, file: a.path, mime: a.mimeType }, 'telegram: artifact attached');
            }
            catch (err) {
                log.warn({ err: err.message, file: a.path }, 'telegram: artifact attach failed');
            }
        }
        await logOutbound({
            tenantId: session.tenantId,
            customerId: customer.id,
            content: brainResp.response,
            modelUsed: brainResp.model_used,
            tokensInput: brainResp.tokens.input,
            tokensOutput: brainResp.tokens.output,
            costUsd: brainResp.cost,
            responseTimeMs: brainResp.latencyMs,
        });
    }
    catch (err) {
        log.error({ tenantId: session.tenantId, error: err.message }, 'telegram sendMessage failed');
    }
    finally {
        typing.stop();
    }
    // (C5) Await raw-stub write so next turn sees N's entities.
    await learnFromConversation(session.tenantId, customer.id, fullName, `User: ${text}\nMuassis: ${brainResp.response}`).catch((err) => log.debug({ err: err.message }, 'Telegram learn-from-conversation outer failed'));
}
async function ensureTelegramCustomer(tenantId, tgUserId, name, username) {
    // Synthetic phone field reserved for tg-<id> so the customers table can
    // hold all channels in one place. Keeps WhatsApp / Telegram / web side-by-side.
    const phone = `tg-${tgUserId}`;
    const existing = await supabase
        .from('customers')
        .select('id')
        .eq('tenant_id', tenantId)
        .eq('phone', phone)
        .maybeSingle();
    if (existing.data)
        return existing.data;
    const id = randomUUID();
    const now = new Date().toISOString();
    await supabase.from('customers').insert({
        id,
        tenant_id: tenantId,
        phone,
        name,
        language: 'ar',
        first_contact: now,
        last_contact: now,
        total_messages: 0,
        status: 'active',
        metadata: { channel: 'telegram', telegram_user_id: tgUserId, telegram_username: username },
    });
    return { id };
}
async function logInbound(tenantId, customerId, content) {
    await supabase.from('messages').insert({
        id: randomUUID(),
        tenant_id: tenantId,
        customer_id: customerId,
        direction: 'inbound',
        type: 'text',
        content,
        delivery_status: 'delivered',
        created_at: new Date().toISOString(),
    });
    await supabase
        .from('customers')
        .update({ last_contact: new Date().toISOString() })
        .eq('id', customerId);
}
async function logOutbound(p) {
    await supabase.from('messages').insert({
        id: randomUUID(),
        tenant_id: p.tenantId,
        customer_id: p.customerId,
        direction: 'outbound',
        type: 'text',
        content: p.content,
        model_used: p.modelUsed ?? null,
        tokens_input: p.tokensInput ?? 0,
        tokens_output: p.tokensOutput ?? 0,
        cost_usd: p.costUsd ?? 0,
        response_time_ms: p.responseTimeMs ?? null,
        delivery_status: 'sent',
        created_at: new Date().toISOString(),
    });
}
export function getTelegramStatus(tenantId) {
    const s = sessions.get(tenantId);
    if (!s) {
        return {
            status: 'disconnected', bot_username: null, bot_id: null, invite_link: null, started_at: null,
            reconnect_attempts: 0, messages_today: 0, messages_total: 0,
            last_message_preview: null, last_message_at: null, last_error: null,
        };
    }
    return {
        status: s.status,
        bot_username: s.botUsername,
        bot_id: s.botId,
        invite_link: s.botUsername ? `https://t.me/${s.botUsername}` : null,
        started_at: s.startedAt,
        reconnect_attempts: s.reconnectAttempts,
        messages_today: s.messagesToday,
        messages_total: s.messagesTotal,
        last_message_preview: s.lastMessagePreview,
        last_message_at: s.lastMessageAt,
        last_error: s.lastError,
    };
}
export async function disconnectTelegramSession(tenantId) {
    const s = sessions.get(tenantId);
    if (!s)
        return;
    if (s.pollAbort) {
        try {
            s.pollAbort.abort();
        }
        catch { /* ignore */ }
    }
    s.status = 'disconnected';
    sessions.delete(tenantId);
    log.info({ tenantId }, 'telegram session stopped');
}
export async function sendTelegramMessage(tenantId, chatId, text) {
    const s = sessions.get(tenantId);
    if (!s || s.status !== 'connected') {
        throw new Error('Telegram session not connected');
    }
    await tgCall(s.token, 'sendMessage', { chat_id: chatId, text });
}
// Async file delivery (subagents / scheduled tasks). Mirrors the live-turn
// artifact attach so background-created files reach the Telegram chat too.
export async function sendTelegramFile(tenantId, chatId, filePath, mimeType) {
    const s = sessions.get(tenantId);
    if (!s || s.status !== 'connected') {
        throw new Error('Telegram session not connected');
    }
    await tgSendFile(s.token, chatId, filePath, mimeType);
}
//# sourceMappingURL=adapter.js.map