/**
 * Telegram Service
 *
 * Core service for Telegram Bot API integration with long-polling
 * for incoming messages. Manages bot lifecycle, incoming message
 * polling, and outgoing message delivery.
 *
 * @module services/telegram/telegram
 */
import { EventEmitter } from 'events';
import { TELEGRAM_CONSTANTS } from '../../constants.js';
import { LoggerService } from '../core/logger.service.js';
import { formatError } from '../../utils/format-error.js';
/**
 * TelegramService manages the Telegram Bot API connection, incoming
 * message polling via getUpdates, and outgoing message delivery.
 *
 * Events:
 * - 'message' — Emitted when a text message is received
 * - 'error' — Emitted on polling errors
 * - 'connected' — Emitted when polling starts
 * - 'disconnected' — Emitted when polling stops
 *
 * @example
 * ```typescript
 * const service = getTelegramService();
 * await service.initialize({ botToken: 'BOT_TOKEN' });
 * service.on('message', (msg) => console.log(msg.text));
 * service.startPolling();
 * ```
 */
export class TelegramService extends EventEmitter {
    logger;
    config = null;
    botInfo = null;
    polling = false;
    pollTimer = null;
    lastUpdateOffset = 0;
    consecutiveFailures = 0;
    connected = false;
    constructor() {
        super();
        this.logger = LoggerService.getInstance().createComponentLogger('TelegramService');
    }
    /**
     * Initialize the Telegram service by validating the bot token.
     *
     * @param config - Telegram configuration with bot token
     * @throws Error if token is invalid or validation fails
     */
    async initialize(config) {
        if (!config.botToken) {
            throw new Error('Telegram bot token is required');
        }
        // Validate token via getMe
        const resp = await fetch(`${TELEGRAM_CONSTANTS.API_BASE}${config.botToken}/getMe`, {
            signal: AbortSignal.timeout(TELEGRAM_CONSTANTS.FETCH_TIMEOUT_MS),
        });
        if (!resp.ok) {
            throw new Error(`Telegram token validation failed (HTTP ${resp.status})`);
        }
        const data = (await resp.json());
        if (!data.ok || !data.result) {
            throw new Error(data.description || 'Telegram token validation failed');
        }
        this.config = config;
        this.botInfo = { id: data.result.id, username: data.result.username };
        this.logger.info('Telegram bot validated', { username: data.result.username });
    }
    /**
     * Start long-polling for incoming messages.
     * Emits 'message' events for each incoming text message.
     */
    startPolling() {
        if (this.polling) {
            this.logger.warn('Polling already active');
            return;
        }
        if (!this.config) {
            throw new Error('TelegramService not initialized — call initialize() first');
        }
        this.polling = true;
        this.connected = true;
        this.consecutiveFailures = 0;
        this.emit('connected');
        this.logger.info('Polling started');
        this.poll();
    }
    /**
     * Stop polling for incoming messages.
     */
    stopPolling() {
        this.polling = false;
        this.connected = false;
        if (this.pollTimer) {
            clearTimeout(this.pollTimer);
            this.pollTimer = null;
        }
        this.emit('disconnected');
        this.logger.info('Polling stopped');
    }
    /**
     * Send a text message to a Telegram chat.
     *
     * @param chatId - Telegram chat ID
     * @param text - Message content
     * @param replyToMessageId - Optional message ID to reply to
     * @returns The sent message ID
     * @throws Error if service is not initialized or send fails
     */
    async sendMessage(chatId, text, replyToMessageId) {
        if (!this.config) {
            throw new Error('TelegramService not initialized');
        }
        // Truncate message if too long
        const truncatedText = text.length > TELEGRAM_CONSTANTS.MAX_MESSAGE_LENGTH
            ? text.slice(0, TELEGRAM_CONSTANTS.MAX_MESSAGE_LENGTH - 20) + '\n\n...(truncated)'
            : text;
        const body = {
            chat_id: chatId,
            text: truncatedText,
            parse_mode: 'Markdown',
        };
        if (replyToMessageId) {
            body.reply_to_message_id = replyToMessageId;
        }
        const { botToken } = this.config;
        const sendOnce = (payload) => fetch(`${TELEGRAM_CONSTANTS.API_BASE}${botToken}/sendMessage`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
            signal: AbortSignal.timeout(TELEGRAM_CONSTANTS.FETCH_TIMEOUT_MS),
        });
        let resp = await sendOnce(body);
        // Telegram returns HTTP 400 "can't parse entities" when the text contains
        // Markdown its legacy parser can't parse — extremely common with LLM/agent
        // output (e.g. **snake_case_name**, an unbalanced `code` span, or a stray
        // '_' / '*'). Without a fallback the message is silently dropped. Retry once
        // as plain text (no parse_mode) so delivery always succeeds.
        if (!resp.ok && resp.status === 400) {
            const details = await resp.text();
            if (/can't parse entities/i.test(details)) {
                this.logger.warn('Telegram Markdown parse failed — retrying as plain text', {
                    chatId,
                    details: details.slice(0, 200),
                });
                const plainBody = { ...body };
                delete plainBody.parse_mode;
                resp = await sendOnce(plainBody);
            }
            else {
                throw new Error(`Telegram send failed (${resp.status}): ${details}`);
            }
        }
        if (!resp.ok) {
            const details = await resp.text();
            throw new Error(`Telegram send failed (${resp.status}): ${details}`);
        }
        const data = (await resp.json());
        return data.result?.message_id ?? 0;
    }
    /**
     * Check if the service is currently connected and polling.
     *
     * @returns True if polling is active
     */
    isConnected() {
        return this.connected && this.polling;
    }
    /**
     * Get the current service status.
     *
     * @returns Status object with connection state and bot info
     */
    getStatus() {
        return {
            connected: this.isConnected(),
            polling: this.polling,
            botUsername: this.botInfo?.username ?? null,
            botId: this.botInfo?.id ?? null,
            consecutiveFailures: this.consecutiveFailures,
            lastUpdateOffset: this.lastUpdateOffset,
        };
    }
    /**
     * Disconnect the service and stop polling.
     */
    async disconnect() {
        this.stopPolling();
        this.config = null;
        this.botInfo = null;
        this.lastUpdateOffset = 0;
        this.consecutiveFailures = 0;
    }
    /**
     * Get the bot's username.
     *
     * @returns Bot username or null if not initialized
     */
    getBotUsername() {
        return this.botInfo?.username ?? null;
    }
    /**
     * Internal polling loop. Calls getUpdates and schedules next poll.
     */
    async poll() {
        if (!this.polling || !this.config)
            return;
        try {
            const updates = await this.getUpdates();
            this.consecutiveFailures = 0;
            for (const update of updates) {
                this.processUpdate(update);
            }
        }
        catch (error) {
            this.consecutiveFailures++;
            this.logger.error('Polling error', {
                error: formatError(error),
                consecutiveFailures: this.consecutiveFailures,
            });
            this.emit('error', error);
            if (this.consecutiveFailures >= TELEGRAM_CONSTANTS.MAX_CONSECUTIVE_FAILURES) {
                this.logger.error('Max consecutive failures reached — pausing polling');
                this.stopPolling();
                return;
            }
        }
        // Schedule next poll
        if (this.polling) {
            this.pollTimer = setTimeout(() => this.poll(), TELEGRAM_CONSTANTS.POLL_INTERVAL_MS);
        }
    }
    /**
     * Call the Telegram getUpdates API with long polling.
     *
     * @returns Array of updates
     */
    async getUpdates() {
        if (!this.config)
            return [];
        const params = new URLSearchParams({
            timeout: String(TELEGRAM_CONSTANTS.LONG_POLL_TIMEOUT_S),
            allowed_updates: JSON.stringify(['message']),
        });
        if (this.lastUpdateOffset > 0) {
            params.set('offset', String(this.lastUpdateOffset));
        }
        const resp = await fetch(`${TELEGRAM_CONSTANTS.API_BASE}${this.config.botToken}/getUpdates?${params}`, {
            signal: AbortSignal.timeout(TELEGRAM_CONSTANTS.FETCH_TIMEOUT_MS),
        });
        if (!resp.ok) {
            throw new Error(`getUpdates failed (HTTP ${resp.status})`);
        }
        const data = (await resp.json());
        const updates = data.result ?? [];
        // Update offset to acknowledge processed updates
        if (updates.length > 0) {
            const maxId = Math.max(...updates.map((u) => u.update_id));
            this.lastUpdateOffset = maxId + 1;
        }
        return updates;
    }
    /**
     * Process a single Telegram update and emit events.
     *
     * @param update - The update to process
     */
    processUpdate(update) {
        if (!update.message?.text)
            return;
        const msg = update.message;
        // Skip messages from bots
        if (msg.from?.is_bot)
            return;
        // Check allowed users filter
        if (this.config?.allowedUserIds &&
            this.config.allowedUserIds.length > 0 &&
            !this.config.allowedUserIds.includes(String(msg.from?.id))) {
            this.logger.debug('Message from non-allowed user ignored', {
                userId: msg.from?.id,
            });
            return;
        }
        const incoming = {
            chatId: String(msg.chat.id),
            messageId: msg.message_id,
            userId: String(msg.from?.id ?? 0),
            userName: msg.from?.first_name +
                (msg.from?.last_name ? ` ${msg.from.last_name}` : ''),
            text: msg.text,
            timestamp: msg.date,
            replyToMessageId: msg.reply_to_message?.message_id,
        };
        this.logger.debug('Incoming message', {
            chatId: incoming.chatId,
            userId: incoming.userId,
            textLength: incoming.text.length,
        });
        this.emit('message', incoming);
    }
}
/** Singleton instance */
let instance = null;
/**
 * Get the TelegramService singleton.
 *
 * @returns The service instance
 */
export function getTelegramService() {
    if (!instance) {
        instance = new TelegramService();
    }
    return instance;
}
/**
 * Reset the TelegramService singleton (for testing).
 */
export function resetTelegramService() {
    if (instance) {
        instance.stopPolling();
        instance.removeAllListeners();
    }
    instance = null;
}
//# sourceMappingURL=telegram.service.js.map