import { randomBytes } from 'node:crypto';
import qrcode from 'qrcode-terminal';
import { Telegraf } from 'telegraf';
import { YagrSessionAgent } from '../agent.js';
import { YagrConfigService } from '../config/yagr-config-service.js';
import { YagrN8nConfigService } from '../config/n8n-config-service.js';
import { resolveLanguageModelConfig } from '../llm/create-language-model.js';
import { YagrSetupApplicationService } from '../setup/application-services.js';
import { mapPhaseEventToUserVisibleUpdate, mapStateEventToUserVisibleUpdate, mapToolEventToUserVisibleUpdate, } from '../runtime/user-visible-updates.js';
import { buildWorkflowBannerHtml, extractWorkflowEmbed, markdownToTelegramHtml, escapeHtml, } from './format-message.js';
const TELEGRAM_MESSAGE_LIMIT = 4096;
function formatTelegramProgressHtml(update) {
    const title = escapeHtml(update.title);
    const detail = update.detail ? escapeHtml(update.detail) : '';
    return detail ? `<b>${title}</b>\n${detail}` : `<b>${title}</b>`;
}
export function createOnboardingToken() {
    return randomBytes(18).toString('base64url');
}
export function buildTelegramDeepLink(botUsername, onboardingToken) {
    return `https://t.me/${botUsername}?start=${onboardingToken}`;
}
export function upsertLinkedChat(chats, nextChat) {
    const chatId = String(nextChat.chatId);
    const existing = chats.find((entry) => String(entry.chatId) === chatId);
    if (!existing) {
        return [...chats, { ...nextChat, chatId }];
    }
    return chats.map((entry) => (String(entry.chatId) === chatId
        ? { ...entry, ...nextChat, chatId }
        : entry));
}
export function removeLinkedChat(chats, chatId) {
    return chats.filter((entry) => String(entry.chatId) !== String(chatId));
}
export function splitTelegramMessage(text, limit = TELEGRAM_MESSAGE_LIMIT) {
    const normalized = text.trim();
    if (!normalized) {
        return [];
    }
    const chunks = [];
    let remaining = normalized;
    while (remaining.length > limit) {
        const candidate = remaining.slice(0, limit);
        const splitAt = Math.max(candidate.lastIndexOf('\n\n'), candidate.lastIndexOf('\n'), candidate.lastIndexOf(' '));
        const cut = splitAt > Math.floor(limit * 0.6) ? splitAt : limit;
        chunks.push(remaining.slice(0, cut).trim());
        remaining = remaining.slice(cut).trim();
    }
    if (remaining) {
        chunks.push(remaining);
    }
    return chunks;
}
function buildTelegramTokenInstructions() {
    return [
        '1) Open Telegram and chat with @BotFather',
        '2) Run /newbot (or /mybots)',
        '3) Copy the token (looks like 123456:ABC...)',
        'Yagr stores this token during setup.',
        'Docs: https://core.telegram.org/bots#how-do-i-create-a-bot',
    ].join('\n');
}
function formatLinkedChatCount(count) {
    return count === 1 ? '1 linked chat' : `${count} linked chats`;
}
function formatRequiredActions(actions) {
    if (actions.length === 0) {
        return '';
    }
    const blocking = actions.filter((action) => action.blocking !== false);
    const followUp = actions.filter((action) => action.blocking === false);
    const lines = [];
    if (blocking.length > 0) {
        lines.push('Required actions:');
        lines.push(...blocking.map((action) => `- ${action.title}: ${action.message}`));
        lines.push('Use /approve to resume if the request can be approved.');
    }
    if (followUp.length > 0) {
        if (lines.length > 0) {
            lines.push('');
        }
        lines.push('Next steps:');
        lines.push(...followUp.map((action) => `- ${action.title}: ${action.message}`));
    }
    return lines.join('\n');
}
export async function resolveTelegramBotIdentity(botToken) {
    const bot = new Telegraf(botToken);
    const me = await bot.telegram.getMe();
    if (!me.username) {
        throw new Error('Telegram bot username is missing. Configure the bot with BotFather first.');
    }
    return {
        username: me.username,
        firstName: me.first_name,
    };
}
export async function setupTelegramGateway(configService = new YagrConfigService()) {
    const currentToken = configService.getTelegramBotToken() ?? '';
    const setupService = new YagrSetupApplicationService(configService, new YagrN8nConfigService(), {
        resolveTelegramIdentity: resolveTelegramBotIdentity,
        createOnboardingToken,
    });
    if (!currentToken) {
        process.stdout.write(`\nTo create a Telegram bot token:\n${buildTelegramTokenInstructions()}\n`);
    }
    const readline = await import('node:readline/promises');
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    let botToken;
    try {
        if (currentToken) {
            const answer = await rl.question(`Reuse saved token? [Y/n] `);
            botToken = answer.trim().toLowerCase() === 'n' ? await rl.question('Telegram bot token: ') : currentToken;
        }
        else {
            botToken = await rl.question('Telegram bot token: ');
        }
    }
    finally {
        rl.close();
    }
    if (!botToken.includes(':'))
        throw new Error('Invalid Telegram bot token format.');
    process.stdout.write('Verifying token...\n');
    const identity = await setupService.configureTelegram(botToken);
    const nextConfig = configService.getLocalConfig();
    const deepLink = buildTelegramDeepLink(nextConfig.telegram?.botUsername ?? identity.username, nextConfig.telegram?.onboardingToken ?? createOnboardingToken());
    process.stdout.write(`\nTelegram bot ready: @${identity.username}\nOnboarding link: ${deepLink}\n`);
    qrcode.generate(deepLink, { small: true });
    process.stdout.write('Gateway saved. Start with `yagr gateway start`.\n');
}
export function showTelegramOnboarding(configService = new YagrConfigService()) {
    const status = new YagrSetupApplicationService(configService, new YagrN8nConfigService()).getTelegramStatus();
    if (!status.configured || !status.botUsername || !status.deepLink) {
        throw new Error('Telegram is not configured. Run `yagr telegram setup` first.');
    }
    process.stdout.write([
        '',
        `Bot: @${status.botUsername}`,
        `Onboarding link: ${status.deepLink}`,
        `Linked chats: ${formatLinkedChatCount(status.linkedChats.length)}`,
        '',
        'Scan the QR or open the link, then press Start in Telegram.',
        '',
    ].join('\n'));
    qrcode.generate(status.deepLink, { small: true });
}
export function getTelegramGatewayStatus(configService = new YagrConfigService()) {
    return new YagrSetupApplicationService(configService, new YagrN8nConfigService()).getTelegramStatus();
}
export function resetTelegramGateway(configService = new YagrConfigService()) {
    new YagrSetupApplicationService(configService, new YagrN8nConfigService()).resetTelegram();
}
export function createTelegramGatewayRuntime(engineResolver, options = {}, configService = new YagrConfigService()) {
    const setupService = new YagrSetupApplicationService(configService, new YagrN8nConfigService());
    const { status, botToken, onboardingToken } = setupService.getTelegramRuntimeConfig(options.botToken);
    if (!botToken || !status.botUsername || !onboardingToken) {
        throw new Error('Telegram is not configured. Run `yagr telegram setup` first.');
    }
    const linkedCount = status.linkedChats.length;
    return {
        gateway: new TelegramGateway(engineResolver, options, configService, botToken, onboardingToken),
        startupMessages: [
            `Yagr Telegram gateway listening as @${status.botUsername}. ${formatLinkedChatCount(linkedCount)}.`,
            linkedCount === 0
                ? `No linked chats. Onboarding link: ${status.deepLink}`
                : 'Telegram transport is ready. The current orchestrator connection will be resolved on first message.',
        ],
        onboardingLink: status.deepLink && linkedCount === 0 ? status.deepLink : undefined,
    };
}
class TelegramGateway {
    engineResolver;
    options;
    configService;
    onboardingToken;
    bot;
    agents = new Map();
    runningChats = new Set();
    pendingApprovals = new Map();
    enginePromise;
    stopped = false;
    setupService;
    constructor(engineResolver, options, configService, botToken, onboardingToken) {
        this.engineResolver = engineResolver;
        this.options = options;
        this.configService = configService;
        this.onboardingToken = onboardingToken;
        this.bot = new Telegraf(botToken);
        this.setupService = new YagrSetupApplicationService(configService, new YagrN8nConfigService());
    }
    buildDeepLink() {
        const botUsername = this.setupService.getTelegramStatus().botUsername ?? '';
        return buildTelegramDeepLink(botUsername, this.onboardingToken);
    }
    async start() {
        this.bot.start(async (ctx) => {
            const payload = typeof ctx.payload === 'string' ? ctx.payload.trim() : '';
            const chatId = String(ctx.chat.id);
            if (payload !== this.onboardingToken) {
                const deepLink = this.buildDeepLink();
                await ctx.reply(`This link is invalid or expired. Click the link below to link this chat, then press Start:\n${deepLink}`);
                return;
            }
            this.linkChat({
                chatId,
                userId: String(ctx.from?.id ?? ''),
                username: ctx.from?.username,
                firstName: ctx.from?.first_name,
                linkedAt: new Date().toISOString(),
                lastSeenAt: new Date().toISOString(),
            });
            await ctx.reply('Yagr is now linked to this chat. You can talk to me directly here.');
        });
        this.bot.command('status', async (ctx) => {
            const chatId = String(ctx.chat.id);
            if (!this.isLinkedChat(chatId)) {
                const deepLink = this.buildDeepLink();
                await ctx.reply(`This chat is not linked. Click the link below, then press Start:\n${deepLink}`);
                return;
            }
            const linkedChats = this.setupService.getLinkedTelegramChats();
            await ctx.reply(`Telegram gateway is active. ${formatLinkedChatCount(linkedChats.length)}.`);
        });
        this.bot.command('pending', async (ctx) => {
            const chatId = String(ctx.chat.id);
            const actions = this.pendingApprovals.get(chatId) ?? [];
            if (actions.length === 0) {
                await ctx.reply('No actions pending.');
                return;
            }
            await ctx.reply(formatRequiredActions(actions));
        });
        this.bot.command('approve', async (ctx) => {
            const chatId = String(ctx.chat.id);
            if (!this.isLinkedChat(chatId)) {
                await ctx.reply('This chat is not linked.');
                return;
            }
            const actions = this.pendingApprovals.get(chatId) ?? [];
            if (actions.length === 0) {
                await ctx.reply('No approvable actions pending.');
                return;
            }
            await this.executeRun(chatId, 'Permission granted. Continue the current task and execute the previously blocked step now.', actions, ctx.reply.bind(ctx));
        });
        this.bot.command('link', async (ctx) => {
            if (this.isLinkedChat(String(ctx.chat.id))) {
                await ctx.reply('This chat is already linked to Yagr. You can talk to me directly.');
                return;
            }
            const deepLink = this.buildDeepLink();
            await ctx.reply(`To link this chat, click the link below, then press Start:\n${deepLink}`);
        });
        const clearSession = async (chatId, reply) => {
            this.resetChatSession(chatId);
            await reply('Yagr conversation reset for this chat.');
        };
        this.bot.command('reset', async (ctx) => {
            await clearSession(String(ctx.chat.id), ctx.reply.bind(ctx));
        });
        this.bot.command('unlink', async (ctx) => {
            const chatId = String(ctx.chat.id);
            if (!this.isLinkedChat(chatId)) {
                const deepLink = this.buildDeepLink();
                await ctx.reply(`This chat is not linked. Click the link below to link it:\n${deepLink}`);
                return;
            }
            this.unlinkChat(chatId);
            this.agents.delete(chatId);
            this.pendingApprovals.delete(chatId);
            await ctx.reply('Chat unlinked. Use the onboarding link or QR code again to reconnect.');
        });
        this.bot.on('text', async (ctx) => {
            const chatId = String(ctx.chat.id);
            const text = ctx.message.text.trim();
            if (!text || text.startsWith('/')) {
                return;
            }
            if (ctx.chat.type !== 'private') {
                await ctx.reply('Telegram currently supports private chats only.');
                return;
            }
            if (!this.isLinkedChat(chatId)) {
                const deepLink = this.buildDeepLink();
                await ctx.reply(`This chat is not linked yet. Click the link below, then press Start:\n${deepLink}`);
                return;
            }
            this.touchChat(chatId, ctx.from?.id, ctx.from?.username, ctx.from?.first_name);
            await this.executeRun(chatId, text, [], ctx.reply.bind(ctx));
        });
        // bot.launch() never resolves while running — start it without awaiting
        // so the caller can proceed (print banner, wait for SIGINT, etc.)
        this.bot.launch({ dropPendingUpdates: true }).catch((err) => {
            if (!this.stopped) {
                process.stderr.write(`Telegram gateway error: ${err instanceof Error ? err.message : String(err)}\n`);
            }
        });
        // Give Telegraf a moment to connect and throw early if the token is invalid
        await new Promise((resolve, reject) => {
            const timeout = setTimeout(resolve, 1500);
            this.bot.telegram.getMe().then(() => {
                clearTimeout(timeout);
                resolve();
            }).catch((err) => {
                clearTimeout(timeout);
                reject(err);
            });
        });
    }
    async stop() {
        if (this.stopped) {
            return;
        }
        this.stopped = true;
        this.bot.stop('Yagr Telegram gateway stopping');
    }
    async reply(chatId, message) {
        const parts = splitTelegramMessage(message);
        for (const part of parts) {
            await this.bot.telegram.sendMessage(Number(chatId), part);
        }
    }
    async sendHtml(chatId, html) {
        const parts = splitTelegramMessage(html);
        for (const part of parts) {
            try {
                await this.bot.telegram.sendMessage(Number(chatId), part, { parse_mode: 'HTML' });
            }
            catch {
                // Fallback to plain text if HTML parsing fails
                await this.bot.telegram.sendMessage(Number(chatId), part);
            }
        }
    }
    linkChat(chat) {
        this.setupService.linkTelegramChat(chat);
    }
    unlinkChat(chatId) {
        this.setupService.unlinkTelegramChat(chatId);
    }
    touchChat(chatId, userId, username, firstName) {
        this.setupService.touchTelegramChat(chatId, userId, username, firstName);
    }
    isLinkedChat(chatId) {
        return this.setupService.isTelegramChatLinked(chatId);
    }
    async getEngine() {
        this.enginePromise ??= this.engineResolver();
        return await this.enginePromise;
    }
    async getAgent(chatId) {
        const existing = this.agents.get(chatId);
        if (existing) {
            return existing;
        }
        const next = new YagrSessionAgent(await this.getEngine());
        this.agents.set(chatId, next);
        return next;
    }
    resetChatSession(chatId) {
        this.agents.delete(chatId);
        this.pendingApprovals.delete(chatId);
    }
    async executeRun(chatId, prompt, satisfiedRequiredActions, reply) {
        if (this.runningChats.has(chatId)) {
            await reply('A run is already in progress for this chat. Wait for it to finish before sending another request.');
            return;
        }
        this.runningChats.add(chatId);
        try {
            await reply('Yagr is working...');
            let lastProgressKey = '';
            const sendProgressUpdate = async (update) => {
                if (!update || update.dedupeKey === lastProgressKey) {
                    return;
                }
                lastProgressKey = update.dedupeKey;
                await this.sendHtml(chatId, formatTelegramProgressHtml(update));
            };
            const embeds = [];
            const resolvedConfig = resolveLanguageModelConfig({}, this.configService);
            const result = await (await this.getAgent(chatId)).run(prompt, {
                ...this.options,
                provider: resolvedConfig.provider,
                model: resolvedConfig.model,
                apiKey: resolvedConfig.apiKey,
                baseUrl: resolvedConfig.baseUrl,
                display: undefined,
                satisfiedRequiredActionIds: satisfiedRequiredActions.map((action) => action.id),
                onPhaseChange: async (event) => {
                    await sendProgressUpdate(mapPhaseEventToUserVisibleUpdate(event));
                    await this.options.onPhaseChange?.(event);
                },
                onStateChange: async (event) => {
                    await sendProgressUpdate(mapStateEventToUserVisibleUpdate(event));
                    await this.options.onStateChange?.(event);
                },
                onToolEvent: async (event) => {
                    const embed = extractWorkflowEmbed(event);
                    if (embed)
                        embeds.push(embed);
                    await sendProgressUpdate(mapToolEventToUserVisibleUpdate(event));
                    await this.options.onToolEvent?.(event);
                },
            });
            if (result.requiredActions.length > 0) {
                this.pendingApprovals.set(chatId, result.requiredActions);
            }
            else {
                this.pendingApprovals.delete(chatId);
            }
            const htmlSections = [markdownToTelegramHtml(result.text.trim())];
            const workflowBanner = buildWorkflowBannerHtml(embeds);
            if (workflowBanner) {
                htmlSections.push(workflowBanner);
            }
            const requiredActionsText = formatRequiredActions(result.requiredActions);
            if (requiredActionsText) {
                htmlSections.push(escapeHtml(requiredActionsText));
            }
            const htmlMessage = htmlSections.filter(Boolean).join('\n\n');
            if (!htmlMessage) {
                await reply('Run finished, but no text response was produced.');
                return;
            }
            await this.sendHtml(chatId, htmlMessage);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            await reply(`Run failed: ${message}`);
        }
        finally {
            this.runningChats.delete(chatId);
        }
    }
}
export async function runTelegramGateway(engineResolver, options = {}, configService = new YagrConfigService()) {
    const runtime = createTelegramGatewayRuntime(engineResolver, options, configService);
    for (const line of runtime.startupMessages) {
        process.stdout.write(`${line}\n`);
    }
    if (runtime.onboardingLink) {
        process.stdout.write(`Onboarding link: ${runtime.onboardingLink}\n`);
        qrcode.generate(runtime.onboardingLink, { small: true });
    }
    await runtime.gateway.start();
    await new Promise((resolve) => {
        const stop = async () => {
            await runtime.gateway.stop();
            resolve();
        };
        process.once('SIGINT', () => {
            void stop();
        });
        process.once('SIGTERM', () => {
            void stop();
        });
    });
}
//# sourceMappingURL=telegram.js.map