"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserbotClient = void 0;
const telegram_1 = require("telegram");
const index_js_1 = require("telegram/sessions/index.js");
const index_js_2 = require("telegram/events/index.js");
const index_js_3 = require("telegram/tl/index.js");
const uploads_js_1 = require("telegram/client/uploads.js");
const logger_js_1 = require("../../utils/logger.js");
const config_js_1 = require("../../utils/config.js");
const path_1 = require("path");
const promises_1 = require("fs/promises");
const log = (0, logger_js_1.createModuleLogger)('userbot-client');
class UserbotClient {
    client;
    characterId;
    phoneNumber;
    messageHandler;
    connected = false;
    session;
    peerCache = new Map();
    constructor(cfg) {
        this.characterId = cfg.characterId;
        this.phoneNumber = cfg.phoneNumber;
        this.session = new index_js_1.StringSession(cfg.session || '');
        const apiId = cfg.telegramApiId ?? config_js_1.config.telegramApiId;
        const apiHash = cfg.telegramApiHash ?? config_js_1.config.telegramApiHash;
        this.client = new telegram_1.TelegramClient(this.session, apiId, apiHash, {
            connectionRetries: 5,
            baseLogger: undefined,
        });
    }
    onMessage(handler) {
        this.messageHandler = handler;
    }
    /**
     * Start the client. If no session exists, requires interactive login.
     * Returns the session string to persist for future logins.
     */
    async start(codeCallback, passwordCallback) {
        log.info({ characterId: this.characterId, phone: this.phoneNumber }, 'Starting userbot client');
        await this.client.start({
            phoneNumber: this.phoneNumber,
            phoneCode: codeCallback,
            password: passwordCallback,
            onError: (err) => {
                log.error({ characterId: this.characterId, error: err.message }, 'Login error');
            },
        });
        this.connected = true;
        const sessionString = this.client.session.save();
        log.info({ characterId: this.characterId }, 'Userbot client connected');
        // Register message handler
        this.client.addEventHandler(async (event) => {
            await this.handleNewMessage(event);
        }, new index_js_2.NewMessage({ incoming: true }));
        log.info({ characterId: this.characterId }, 'Message handler registered');
        return sessionString;
    }
    /**
     * Reconnect using saved session (no code needed)
     */
    async reconnect() {
        log.info({ characterId: this.characterId }, 'Reconnecting userbot with saved session');
        await this.client.connect();
        this.connected = true;
        // Register message handler
        this.client.addEventHandler(async (event) => {
            await this.handleNewMessage(event);
        }, new index_js_2.NewMessage({ incoming: true }));
        log.info({ characterId: this.characterId }, 'Userbot reconnected');
    }
    /**
     * Create a Telegram group via GramJS.
     * If supergroup is true, creates a megagroup (supergroup) via channels.CreateChannel.
     * Otherwise creates a normal group via messages.CreateChat.
     * Returns the chatId and an invite link.
     */
    async createGroup(title, description, supergroup) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        log.info({ characterId: this.characterId, title, supergroup }, 'Creating Telegram group');
        if (supergroup) {
            // Create a supergroup (megagroup) via channels.CreateChannel with megagroup flag
            const result = await this.client.invoke(new index_js_3.Api.channels.CreateChannel({
                title,
                about: description || '',
                megagroup: true,
            }));
            const updates = result;
            const channel = updates.chats?.[0];
            if (!channel)
                throw new Error('Failed to create supergroup: no chat returned');
            const chatId = channel.id.valueOf();
            // Export invite link
            let inviteLink;
            try {
                const exportResult = await this.client.invoke(new index_js_3.Api.messages.ExportChatInvite({
                    peer: channel,
                }));
                if (exportResult instanceof index_js_3.Api.ChatInviteExported) {
                    inviteLink = exportResult.link;
                }
            }
            catch (err) {
                log.warn({ characterId: this.characterId, chatId, error: err.message }, 'Failed to export invite link for supergroup');
            }
            log.info({ characterId: this.characterId, chatId, inviteLink }, 'Supergroup created');
            return { chatId, inviteLink };
        }
        else {
            // Create a normal group via messages.CreateChat
            const result = await this.client.invoke(new index_js_3.Api.messages.CreateChat({
                users: [],
                title,
            }));
            // messages.CreateChat may return Updates, InvitedUsers, or other wrappers
            const resultAny = result;
            const chats = resultAny.chats || resultAny.updates?.chats || [];
            const chat = chats[0];
            if (!chat) {
                log.warn({ characterId: this.characterId, title, resultType: result?.className }, 'CreateChat returned unexpected structure, group may still be created');
                throw new Error('Failed to create group: no chat returned');
            }
            const chatId = chat.id.valueOf();
            // Export invite link
            let inviteLink;
            try {
                const exportResult = await this.client.invoke(new index_js_3.Api.messages.ExportChatInvite({
                    peer: chat,
                }));
                if (exportResult instanceof index_js_3.Api.ChatInviteExported) {
                    inviteLink = exportResult.link;
                }
            }
            catch (err) {
                log.warn({ characterId: this.characterId, chatId, error: err.message }, 'Failed to export invite link for group');
            }
            log.info({ characterId: this.characterId, chatId, inviteLink }, 'Group created');
            return { chatId, inviteLink };
        }
    }
    /**
     * Create a Telegram channel (broadcast) via GramJS.
     * Returns the chatId and an invite link.
     */
    async createChannel(title, description) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        log.info({ characterId: this.characterId, title }, 'Creating Telegram channel');
        const result = await this.client.invoke(new index_js_3.Api.channels.CreateChannel({
            title,
            about: description || '',
            broadcast: true,
        }));
        const updates = result;
        const channel = updates.chats?.[0];
        if (!channel)
            throw new Error('Failed to create channel: no chat returned');
        const chatId = channel.id.valueOf();
        // Export invite link
        let inviteLink;
        try {
            const exportResult = await this.client.invoke(new index_js_3.Api.messages.ExportChatInvite({
                peer: channel,
            }));
            if (exportResult instanceof index_js_3.Api.ChatInviteExported) {
                inviteLink = exportResult.link;
            }
        }
        catch (err) {
            log.warn({ characterId: this.characterId, chatId, error: err.message }, 'Failed to export invite link for channel');
        }
        log.info({ characterId: this.characterId, chatId, inviteLink }, 'Channel created');
        return { chatId, inviteLink };
    }
    /**
     * Resolve a chatId to a proper InputPeer that GramJS can use.
     * Telegram channels/supergroups use -100 prefix internally.
     * GramJS needs the full marked ID to resolve them.
     */
    async resolvePeer(chatId) {
        const cacheKey = String(chatId);
        if (this.peerCache.has(cacheKey)) {
            return this.peerCache.get(cacheKey);
        }
        const numId = Number(chatId);
        // Try multiple ID formats — Telegram uses different ID schemes:
        // - Channels/supergroups: -100XXXXXXXXXX (marked ID)
        // - Basic groups: -XXXXXXXXXX
        // - Users: XXXXXXXXXX (positive)
        const idsToTry = [
            BigInt(numId), // raw as-is
            BigInt(-numId), // negative (basic group)
            BigInt(`-100${Math.abs(numId)}`),
        ];
        for (const tryId of idsToTry) {
            try {
                const entity = await this.client.getEntity(tryId);
                const inputPeer = await this.client.getInputEntity(entity);
                log.info({
                    characterId: this.characterId,
                    chatId,
                    resolvedAs: inputPeer.className,
                    triedId: tryId.toString(),
                }, 'Peer resolved successfully');
                this.peerCache.set(cacheKey, inputPeer);
                return inputPeer;
            }
            catch {
                // Try next format
            }
        }
        log.error({ characterId: this.characterId, chatId }, 'Could not resolve peer with any ID format');
        throw new Error(`Chat ID ${chatId} çözümlenemedi. Kanal/grup Telegram'da mevcut olmayabilir veya userbot bu kanala erişemiyor olabilir.`);
    }
    /**
     * Resolve an image URL/path to a Buffer.
     * Handles both local /static/ paths and remote URLs.
     */
    async resolveImageToBuffer(imageUrl) {
        if (imageUrl.startsWith('/static/')) {
            // Local file — resolve to disk path
            const relativePath = imageUrl.replace('/static/', '');
            const filePath = (0, path_1.resolve)(config_js_1.config.dataDir, relativePath);
            const { readFile } = await import('fs/promises');
            const buffer = await readFile(filePath);
            return buffer;
        }
        else if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
            // Remote URL — fetch
            const response = await fetch(imageUrl);
            if (!response.ok)
                throw new Error(`Failed to fetch image: ${response.status}`);
            const arrayBuffer = await response.arrayBuffer();
            return Buffer.from(arrayBuffer);
        }
        else {
            // Try as local file path
            const filePath = (0, path_1.resolve)(config_js_1.config.dataDir, imageUrl);
            const { readFile } = await import('fs/promises');
            const buffer = await readFile(filePath);
            return buffer;
        }
    }
    /**
     * Send paid media (Stars-locked photo) to a channel via GramJS.
     * Users must pay the specified amount of Stars to view the media.
     */
    async sendPaidMedia(chatId, starAmount, imageUrl, caption) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        if (!chatId || chatId === 0)
            throw new Error('Geçersiz chat ID');
        if (starAmount < 1 || starAmount > 25000)
            throw new Error('Stars miktarı 1-25000 arasında olmalıdır');
        log.info({ characterId: this.characterId, chatId, starAmount, imageUrl }, 'Sending paid media');
        // Resolve peer properly (channel needs InputPeerChannel with accessHash)
        const peer = await this.resolvePeer(chatId);
        // Resolve image to buffer (handles both local /static/ paths and remote URLs)
        const buffer = await this.resolveImageToBuffer(imageUrl);
        const filename = `paid_${Date.now()}.jpg`;
        // Upload the file first
        const uploadedFile = await this.client.uploadFile({
            file: new uploads_js_1.CustomFile(filename, buffer.length, '', buffer),
            workers: 1,
        });
        // Send as paid media using inputMediaPaidMedia
        await this.client.invoke(new index_js_3.Api.messages.SendMedia({
            peer,
            media: new index_js_3.Api.InputMediaPaidMedia({
                starsAmount: BigInt(starAmount),
                extendedMedia: [
                    new index_js_3.Api.InputMediaUploadedPhoto({
                        file: uploadedFile,
                    }),
                ],
            }),
            message: caption || '',
            randomId: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
        }));
        log.info({ characterId: this.characterId, chatId, starAmount }, 'Paid media sent successfully');
    }
    /**
     * Create a subscription invite link for a channel.
     * Users must pay the specified Stars amount monthly to access the channel.
     */
    async createSubscriptionInvite(chatId, starAmount) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        if (!chatId || chatId === 0)
            throw new Error('Geçersiz chat ID');
        if (starAmount < 1)
            throw new Error('Stars miktarı en az 1 olmalıdır');
        log.info({ characterId: this.characterId, chatId, starAmount }, 'Creating subscription invite link');
        const MONTHLY_PERIOD = 30 * 24 * 60 * 60; // 30 days in seconds
        const result = await this.client.invoke(new index_js_3.Api.messages.ExportChatInvite({
            peer: await this.resolvePeer(chatId),
            subscriptionPricing: new index_js_3.Api.StarsSubscriptionPricing({
                period: MONTHLY_PERIOD,
                amount: BigInt(starAmount),
            }),
        }));
        if (result instanceof index_js_3.Api.ChatInviteExported && result.link) {
            log.info({ characterId: this.characterId, chatId, starAmount, link: result.link }, 'Subscription invite link created');
            return result.link;
        }
        throw new Error('Failed to create subscription invite link');
    }
    /**
     * Get available Telegram Star Gifts catalog.
     * Returns the list of gifts with their sticker info, price, and availability.
     */
    async getStarGifts() {
        if (!this.connected)
            throw new Error('Userbot not connected');
        log.info({ characterId: this.characterId }, 'Fetching available star gifts');
        const result = await this.client.invoke(new index_js_3.Api.payments.GetStarGifts({ hash: 0 }));
        if ('gifts' in result && Array.isArray(result.gifts)) {
            return result.gifts.map((gift) => ({
                id: gift.id?.toString() || '',
                stars: Number(gift.stars || 0),
                limited: !!gift.limited,
                soldOut: !!gift.soldOut,
                availabilityRemains: gift.availabilityRemains,
                availabilityTotal: gift.availabilityTotal,
                convertStars: Number(gift.convertStars || 0),
                stickerId: gift.sticker?.id?.toString(),
            }));
        }
        return [];
    }
    /**
     * Send a Star Gift to a Telegram user.
     * Uses the payment flow: getPaymentForm + sendStarsForm with inputInvoiceStarGift.
     */
    async sendGift(userId, giftId, message) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        if (!userId)
            throw new Error('Geçersiz kullanıcı ID');
        if (!giftId)
            throw new Error('Geçersiz hediye ID');
        log.info({ characterId: this.characterId, userId, giftId }, 'Sending star gift to user');
        // Resolve the user to get a proper InputPeerUser with accessHash.
        // GramJS MUST have the accessHash or it will throw "Could not find input entity".
        const userIdStr = String(userId).replace(/^@/, '');
        let inputPeer;
        try {
            // getInputEntity returns the InputPeer form directly and caches it
            const entity = await this.client.getEntity(userIdStr);
            inputPeer = await this.client.getInputEntity(entity);
            log.info({
                characterId: this.characterId,
                resolvedClass: inputPeer.className,
                resolvedId: inputPeer.userId?.toString(),
                hasAccessHash: !!inputPeer.accessHash,
            }, 'User resolved to InputPeer');
        }
        catch (err) {
            log.error({ characterId: this.characterId, userId: userIdStr, error: err.message }, 'Failed to resolve user');
            throw new Error(`Kullanıcı "${userIdStr}" çözümlenemedi. @username formatında girin (örn: @kullaniciadi).`);
        }
        // Build the invoice
        const invoiceArgs = {
            peer: inputPeer,
            giftId: BigInt(giftId),
        };
        if (message) {
            invoiceArgs.message = new index_js_3.Api.TextWithEntities({ text: message, entities: [] });
        }
        const invoice = new index_js_3.Api.InputInvoiceStarGift(invoiceArgs);
        // Step 1: Get payment form
        const paymentForm = await this.client.invoke(new index_js_3.Api.payments.GetPaymentForm({ invoice }));
        const formId = paymentForm.formId;
        if (!formId)
            throw new Error('Payment form alınamadı');
        // Step 2: Complete the payment via SendStarsForm
        await this.client.invoke(new index_js_3.Api.payments.SendStarsForm({ formId, invoice }));
        log.info({ characterId: this.characterId, userId, giftId }, 'Star gift sent successfully');
    }
    async sendMessage(chatId, text, imageUrl) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        if (!chatId || chatId === 0)
            throw new Error('Geçersiz chat ID: grup/kanal Telegram chat ID değeri 0 veya boş');
        // Resolve peer properly
        const peer = await this.resolvePeer(chatId);
        if (imageUrl) {
            // Download image and send as file with caption
            try {
                const buffer = await this.resolveImageToBuffer(imageUrl);
                const filename = `publish_${Date.now()}.jpg`;
                log.info({ characterId: this.characterId, chatId, imageUrl }, 'Sending message with image');
                await this.client.sendFile(peer, {
                    file: new uploads_js_1.CustomFile(filename, buffer.length, '', buffer),
                    caption: text,
                });
            }
            catch (err) {
                log.error({ characterId: this.characterId, chatId, error: err.message }, 'Failed to send image, falling back to text');
                await this.client.sendMessage(peer, { message: text });
            }
        }
        else {
            await this.client.sendMessage(peer, { message: text });
        }
    }
    async sendPhoto(chatId, filePath) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        log.info({ characterId: this.characterId, chatId }, 'Sending photo from file');
        const peer = await this.resolvePeer(chatId);
        await this.client.sendFile(peer, { file: filePath });
    }
    async sendPhotoFromBuffer(chatId, buffer, filename) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        log.info({ characterId: this.characterId, chatId, filename, size: buffer.length }, 'Sending photo from buffer');
        const peer = await this.resolvePeer(chatId);
        await this.client.sendFile(peer, {
            file: new uploads_js_1.CustomFile(filename, buffer.length, '', buffer),
        });
    }
    async sendTyping(chatId) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        try {
            const peer = await this.resolvePeer(chatId);
            await this.client.invoke(new index_js_3.Api.messages.SetTyping({
                peer,
                action: new index_js_3.Api.SendMessageTypingAction(),
            }));
        }
        catch (err) {
            log.warn({ error: err.message }, 'Failed to send typing action');
        }
    }
    /**
     * Update Telegram profile: first name, last name, about (bio).
     */
    async updateProfile(opts) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        await this.client.invoke(new index_js_3.Api.account.UpdateProfile({
            firstName: opts.firstName,
            lastName: opts.lastName,
            about: opts.about,
        }));
        log.info({ characterId: this.characterId, ...opts }, 'Profile updated');
    }
    /**
     * Update a channel/group title and about text.
     */
    async updateChatInfo(chatId, opts) {
        if (!this.connected)
            throw new Error('Userbot not connected');
        const peer = await this.resolvePeer(chatId);
        if (opts.title) {
            await this.client.invoke(new index_js_3.Api.channels.EditTitle({ channel: peer, title: opts.title }));
        }
        if (opts.about !== undefined) {
            await this.client.invoke(new index_js_3.Api.messages.EditChatAbout({ peer, about: opts.about }));
        }
        log.info({ characterId: this.characterId, chatId, ...opts }, 'Chat info updated');
    }
    /**
     * Update a channel/group profile photo from a Buffer.
     */
    async updateChatPhoto(chatId, imageBuffer, filename = 'photo.jpg') {
        if (!this.connected)
            throw new Error('Userbot not connected');
        const peer = await this.resolvePeer(chatId);
        const file = new uploads_js_1.CustomFile(filename, imageBuffer.length, '', imageBuffer);
        const uploaded = await this.client.uploadFile({ file, workers: 1 });
        await this.client.invoke(new index_js_3.Api.channels.EditPhoto({
            channel: peer,
            photo: new index_js_3.Api.InputChatUploadedPhoto({ file: uploaded }),
        }));
        log.info({ characterId: this.characterId, chatId }, 'Chat photo updated');
    }
    /**
     * Update Telegram profile photo from a Buffer.
     */
    async updateProfilePhoto(imageBuffer, filename = 'profile.jpg') {
        if (!this.connected)
            throw new Error('Userbot not connected');
        const file = new uploads_js_1.CustomFile(filename, imageBuffer.length, '', imageBuffer);
        const uploaded = await this.client.uploadFile({ file, workers: 1 });
        await this.client.invoke(new index_js_3.Api.photos.UploadProfilePhoto({
            file: uploaded,
        }));
        log.info({ characterId: this.characterId }, 'Profile photo updated');
    }
    async disconnect() {
        if (this.connected) {
            await this.client.disconnect();
            this.connected = false;
            log.info({ characterId: this.characterId }, 'Userbot disconnected');
        }
    }
    isConnected() {
        return this.connected;
    }
    getSessionString() {
        return this.client.session.save();
    }
    async handleNewMessage(event) {
        if (!this.messageHandler)
            return;
        const message = event.message;
        const text = message.text || '';
        const hasPhoto = !!(message.photo || (message.media && message.media.photo));
        // Skip messages with neither text nor photo
        if (!text && !hasPhoto)
            return;
        // Only handle private messages
        const isPrivate = message.isPrivate;
        if (!isPrivate)
            return;
        const senderId = message.senderId?.valueOf();
        if (!senderId)
            return;
        // Don't respond to own messages
        const me = await this.client.getMe();
        if (senderId === me.id?.valueOf())
            return;
        let senderName = '';
        try {
            const sender = await message.getSender();
            if (sender && 'firstName' in sender) {
                senderName = sender.firstName || '';
            }
        }
        catch { /* ignore */ }
        const chatId = message.chatId?.valueOf();
        if (!chatId)
            return;
        // Download incoming photo if present
        let photoInfo;
        if (hasPhoto) {
            try {
                const photoBuffer = await this.client.downloadMedia(message, {});
                if (photoBuffer && photoBuffer.length > 0) {
                    const photoFilename = `incoming_${senderId}_${Date.now()}.jpg`;
                    const photoDir = (0, path_1.resolve)(config_js_1.config.dataDir, 'characters', this.characterId, 'incoming-photos');
                    await (0, promises_1.mkdir)(photoDir, { recursive: true });
                    const diskPath = (0, path_1.resolve)(photoDir, photoFilename);
                    await (0, promises_1.writeFile)(diskPath, photoBuffer);
                    const servePath = `/static/characters/${this.characterId}/incoming-photos/${photoFilename}`;
                    photoInfo = { imagePath: servePath };
                    log.info({ characterId: this.characterId, senderId, photoSize: photoBuffer.length, path: servePath }, 'Incoming photo saved to disk');
                }
            }
            catch (err) {
                log.warn({ characterId: this.characterId, senderId, error: err.message }, 'Failed to download incoming photo');
            }
        }
        log.info({ characterId: this.characterId, senderId, senderName, textLen: text.length, hasPhoto }, 'Incoming private message');
        try {
            const response = await this.messageHandler(this.characterId, Number(senderId), senderName, text, Number(chatId), isPrivate, photoInfo);
            if (response) {
                // Calculate realistic delay (5-30 seconds base + typing time)
                const hour = new Date().getHours();
                const isNight = hour >= 23 || hour < 7;
                const baseDelay = 5 + Math.random() * 25; // 5-30 seconds
                const nightMultiplier = isNight ? 2.5 : 1;
                const typingTime = response.length / 5; // ~5 chars per second
                const totalDelaySec = (baseDelay * nightMultiplier) + typingTime;
                const totalDelayMs = Math.round(totalDelaySec * 1000);
                log.info({ characterId: this.characterId, senderId, delaySec: totalDelaySec.toFixed(1) }, 'Delaying response for realism');
                // Wait for the calculated delay, sending typing indicator periodically
                const typingInterval = setInterval(async () => {
                    try {
                        await this.sendTyping(chatId);
                    }
                    catch { /* ignore */ }
                }, 4000);
                await sleep(totalDelayMs);
                clearInterval(typingInterval);
                // Send typing one last time then send message
                await this.sendTyping(chatId);
                await sleep(500 + Math.random() * 1000);
                await this.sendMessage(chatId, response);
                log.info({ characterId: this.characterId, senderId, responseLen: response.length, delaySec: totalDelaySec.toFixed(1) }, 'Response sent after delay');
            }
        }
        catch (err) {
            log.error({ characterId: this.characterId, senderId, error: err.message }, 'Failed to handle message');
        }
    }
}
exports.UserbotClient = UserbotClient;
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
//# sourceMappingURL=client.js.map