"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createSignalsBot = void 0;
const telegraf_1 = require("telegraf");
const GenerateArbitrageSignal_js_1 = require("../application/use-cases/GenerateArbitrageSignal.js");
const GenerateCategorySignal_js_1 = require("../application/use-cases/GenerateCategorySignal.js");
const GenerateFarmLiquiditySignal_js_1 = require("../application/use-cases/GenerateFarmLiquiditySignal.js");
const GenerateOptionsSignal_js_1 = require("../application/use-cases/GenerateOptionsSignal.js");
const GenerateMarginSignal_js_1 = require("../application/use-cases/GenerateMarginSignal.js");
const GenerateAIStrategySignal_js_1 = require("../application/use-cases/GenerateAIStrategySignal.js");
const GenerateValerisArbitrageSignal_js_1 = require("../application/use-cases/GenerateValerisArbitrageSignal.js");
const GenerateForexSignal_js_1 = require("../application/use-cases/GenerateForexSignal.js");
const GenerateStockSignal_js_1 = require("../application/use-cases/GenerateStockSignal.js");
const ArbitrageCustomPriceCleanupWorker_js_1 = require("../application/services/ArbitrageCustomPriceCleanupWorker.js");
const DailySignalCacheService_js_1 = require("../application/services/DailySignalCacheService.js");
const AuthApiClient_js_1 = require("../infrastructure/api/AuthApiClient.js");
const CoinMarketCapClient_js_1 = require("../infrastructure/api/CoinMarketCapClient.js");
const PlatformApiClient_js_1 = require("../infrastructure/api/PlatformApiClient.js");
const TwelveDataClient_js_1 = require("../infrastructure/api/TwelveDataClient.js");
const SignalFormatter_js_1 = require("../infrastructure/telegram/SignalFormatter.js");
const FileCredentialStore_js_1 = require("../infrastructure/storage/FileCredentialStore.js");
const AppError_js_1 = require("../shared/errors/AppError.js");
const BootstrapAdminAuth_js_1 = require("../application/use-cases/BootstrapAdminAuth.js");
const GetAdminJwt_js_1 = require("../application/use-cases/GetAdminJwt.js");
const subscriptionCommands_js_1 = require("./subscriptionCommands.js");
const menu_js_1 = require("./menu.js");
const types_js_1 = require("../domain/subscriptions/types.js");
const TtlMap_js_1 = require("../shared/utils/TtlMap.js");
const welcomeMessage = [
    "Welcome to Quant Whisper.",
    "",
    "Choose a market to access live trading signals."
].join("\n");
const helpMessage = [
    "Use /start to return to the main market menu.",
    "",
    "Choose Forex, Crypto, or Stocks. Subscribe if needed, then pick a signal. Signals are derived from live market data."
].join("\n");
const VALERIS_CATEGORIES = [
    { key: "farm-liquidity", label: "Farm Liquidity" },
    { key: "trading-options", label: "Trading Options" },
    { key: "margin", label: "Margin" },
    { key: "ai-strategy", label: "AI Strategy" },
    { key: "arbitrage", label: "Arbitrage Opportunities" }
];
const GENERIC_CATEGORIES = [
    { key: "trading-options", label: "Trading Options" },
    { key: "margin", label: "Margin" },
    { key: "ai-strategy", label: "AI Strategy" },
    { key: "arbitrage", label: "Arbitrage Opportunities" }
];
const VALERIS_PLATFORM_CODE = "lychee";
const VALERIS_SIGNAL_CALLBACK_REGEX = /^signal:([^:]+):lychee:([^:]+)$/;
const VALERIS_NETWORK_CALLBACK_REGEX = /^lychee-finance:([^:]+)$/;
const awaitingForexPair = new TtlMap_js_1.TtlMap(10 * 60 * 1000);
// Tracks chats waiting for the user to type a custom stock country.
const awaitingStocksCountry = new TtlMap_js_1.TtlMap(10 * 60 * 1000);
const customStocksCountry = new TtlMap_js_1.TtlMap(10 * 60 * 1000);
const createSignalsBot = (config, logger, subscriptionService, supportModule) => {
    const supportUrl = supportModule?.bot.getDeepLink();
    const platformApi = new PlatformApiClient_js_1.PlatformApiClient(config.restApiBaseUrl);
    const marketData = new CoinMarketCapClient_js_1.CoinMarketCapClient(config.coinMarketCapApiKey, config.trendingCacheTtlSeconds);
    const authApi = new AuthApiClient_js_1.AuthApiClient(config.restApiBaseUrl, config.adminRegisterPath, config.adminLoginPath);
    const credentialStore = new FileCredentialStore_js_1.FileCredentialStore(config.storageDir);
    const bootstrapAdminAuth = new BootstrapAdminAuth_js_1.BootstrapAdminAuth(authApi, credentialStore, logger, {
        adminUsername: config.adminUsername,
        adminEmail: config.adminEmail,
        adminPassword: config.adminPassword,
        adminName: config.adminName
    });
    const getAdminJwt = new GetAdminJwt_js_1.GetAdminJwt(authApi, credentialStore);
    const generateCategorySignal = new GenerateCategorySignal_js_1.GenerateCategorySignal(platformApi);
    const arbitrageCleanupWorker = new ArbitrageCustomPriceCleanupWorker_js_1.ArbitrageCustomPriceCleanupWorker(platformApi, getAdminJwt, logger);
    const generateArbitrageSignal = new GenerateArbitrageSignal_js_1.GenerateArbitrageSignal(marketData, platformApi, getAdminJwt, arbitrageCleanupWorker, logger);
    const generateFarmLiquiditySignal = new GenerateFarmLiquiditySignal_js_1.GenerateFarmLiquiditySignal(platformApi);
    const generateOptionsSignal = new GenerateOptionsSignal_js_1.GenerateOptionsSignal(marketData);
    const generateMarginSignal = new GenerateMarginSignal_js_1.GenerateMarginSignal(marketData);
    const generateAIStrategySignal = new GenerateAIStrategySignal_js_1.GenerateAIStrategySignal(marketData);
    const generateValerisArbitrageSignal = new GenerateValerisArbitrageSignal_js_1.GenerateValerisArbitrageSignal(marketData, platformApi, getAdminJwt, arbitrageCleanupWorker, logger);
    const traditionalMarket = new TwelveDataClient_js_1.TwelveDataClient(config.twelveDataApiKey, config.coinMarketCapApiKey);
    const generateForexSignal = new GenerateForexSignal_js_1.GenerateForexSignal(traditionalMarket);
    const generateStockSignal = new GenerateStockSignal_js_1.GenerateStockSignal(traditionalMarket);
    const valerisCacheService = new DailySignalCacheService_js_1.DailySignalCacheService(VALERIS_PLATFORM_CODE);
    const bot = new telegraf_1.Telegraf(config.telegramBotToken);
    (0, subscriptionCommands_js_1.registerSubscriptionCommands)(bot, subscriptionService, config.adminTelegramIds, logger);
    bot.start(async (ctx) => {
        await subscriptionService.upsertUserFromContext(ctx);
        await ctx.reply(welcomeMessage, (0, menu_js_1.mainMenu)(supportUrl));
    });
    bot.help(async (ctx) => {
        await ctx.reply(helpMessage, (0, menu_js_1.mainMenu)(supportUrl));
    });
    bot.command("support", async (ctx) => {
        if (supportUrl) {
            await ctx.reply("Need help? Click the button below to chat with our support team.\n\nWe're here to assist you with signals, subscriptions, payments, and more.", telegraf_1.Markup.inlineKeyboard([[telegraf_1.Markup.button.url("🆘 Open Support Chat", supportUrl)]]));
        }
        else {
            await ctx.reply("Support is not configured at the moment. Please try again later or contact the team through other channels.", (0, menu_js_1.mainMenu)(supportUrl));
        }
    });
    bot.action("support:contact", async (ctx) => {
        await ctx.answerCbQuery();
        if (supportUrl) {
            await ctx.reply("Need help? Click the button below to chat with our support team.", telegraf_1.Markup.inlineKeyboard([[telegraf_1.Markup.button.url("🆘 Open Support Chat", supportUrl)]]));
        }
        else {
            await ctx.reply("Support is not configured at the moment. Please try again later.", (0, menu_js_1.mainMenu)(supportUrl));
        }
    });
    for (const platform of types_js_1.PLATFORM_OPTIONS.filter((p) => p.code !== "forex" && p.code !== "stocks")) {
        bot.action((0, menu_js_1.platformCallback)(platform.code), async (ctx) => {
            await ctx.answerCbQuery();
            if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, platform.code)))
                return;
            const usage = await getDailyRequestUsage(subscriptionService, ctx, platform.code);
            const usageLine = usage ? `\n\n${formatUsageText(usage)}` : "";
            if (platform.code === VALERIS_PLATFORM_CODE) {
                await ctx.reply(`${platform.name} is unlocked.${usageLine}\n\nChoose a network for your signals, or select "Any Network" to let the system pick randomly.`, (0, menu_js_1.valerisNetworkSelectionMenu)(supportUrl));
                return;
            }
            await ctx.reply(`${platform.name} is unlocked.${usageLine}\n\nChoose a signal category:`, (0, menu_js_1.signalsMenu)(platform.code, undefined, supportUrl));
        });
        // Register generic (non-Lychee) category callbacks.
        if (platform.code !== VALERIS_PLATFORM_CODE) {
            for (const category of GENERIC_CATEGORIES) {
                bot.action((0, menu_js_1.categoryCallback)(category.key, platform.code), async (ctx) => {
                    if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, platform.code)))
                        return;
                    if (!(await checkSignalRequestLimit(ctx, subscriptionService, platform.code)))
                        return;
                    await handleGenericCategory(ctx, category, generateCategorySignal, generateArbitrageSignal, (0, menu_js_1.signalsMenu)(platform.code, undefined, supportUrl), logger, async () => commitSignalRequest(ctx, subscriptionService, platform.code, logger));
                });
            }
        }
    }
    // Main market menu: Forex / Crypto / Stocks.
    bot.action(/^market:(forex|crypto|stocks|back)$/, async (ctx) => {
        const market = (0, menu_js_1.parseMarketCallback)(ctx.match[0]);
        if (!market) {
            await ctx.answerCbQuery("Invalid market selection.");
            return;
        }
        if (market === "back") {
            await ctx.answerCbQuery();
            await ctx.reply("Select a market:", (0, menu_js_1.mainMenu)(supportUrl));
            return;
        }
        if (market === "crypto") {
            await ctx.answerCbQuery();
            await ctx.reply("Select a crypto platform:", (0, menu_js_1.platformsKeyboard)(supportUrl));
            return;
        }
        if (market === "forex") {
            if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, "forex")))
                return;
            const usage = await getDailyRequestUsage(subscriptionService, ctx, "forex");
            const usageLine = usage ? `\n\n${formatUsageText(usage)}` : "";
            await ctx.answerCbQuery();
            await ctx.reply(`Forex is unlocked.${usageLine}\n\nChoose a market:`, (0, menu_js_1.forexTypeMenu)(supportUrl));
            return;
        }
        if (market === "stocks") {
            if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, "stocks")))
                return;
            await ctx.answerCbQuery();
            await ctx.reply("Stocks is unlocked. Select your country:", (0, menu_js_1.stocksCountryMenu)(supportUrl));
        }
    });
    // Forex type selection: Fiat / Crypto (also accepts legacy "forex:fiat" / "forex:crypto" callbacks).
    bot.action(/^forex:(type:(fiat|crypto|back)|fiat$|crypto$)$/, async (ctx) => {
        if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, "forex")))
            return;
        const chatId = ctx.chat?.id;
        if (chatId)
            awaitingForexPair.delete(chatId);
        const type = (0, menu_js_1.parseForexTypeCallback)(ctx.match[0]);
        if (!type) {
            await ctx.answerCbQuery("Invalid Forex selection.");
            return;
        }
        if (type === "back") {
            await ctx.answerCbQuery();
            await ctx.reply("Choose a Forex pair type:", (0, menu_js_1.forexTypeMenu)(supportUrl));
            return;
        }
        await ctx.answerCbQuery(type === "fiat" ? "Fiat pairs" : "Crypto pairs");
        await ctx.reply(`${type === "fiat" ? "💵 Fiat" : "₿ Crypto"} pairs — choose an option:`, (0, menu_js_1.forexActionMenu)(type, supportUrl));
    });
    // Forex random pair selection ("Skip pair selection").
    bot.action(/^forex:random:(fiat|crypto)$/, async (ctx) => {
        if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, "forex")))
            return;
        if (!(await checkSignalRequestLimit(ctx, subscriptionService, "forex")))
            return;
        const chatId = ctx.chat?.id;
        if (chatId)
            awaitingForexPair.delete(chatId);
        const type = (0, menu_js_1.parseForexRandomCallback)(ctx.match[0]);
        if (!type) {
            await ctx.answerCbQuery("Invalid Forex selection.");
            return;
        }
        await ctx.answerCbQuery("Analysing profitable pair...");
        try {
            const signal = await generateForexSignal.execute(type);
            await ctx.reply((0, SignalFormatter_js_1.formatSignal)(signal), { parse_mode: "HTML", ...(0, menu_js_1.forexActionMenu)(type, supportUrl) });
            await commitSignalRequest(ctx, subscriptionService, "forex", logger);
        }
        catch (error) {
            logger.error("Failed to generate Forex signal.", serializeError(error));
            await ctx.reply(friendlyError(error), (0, menu_js_1.forexActionMenu)(type, supportUrl));
        }
    });
    // Forex manual pair selection.
    bot.action(/^forex:select:(fiat|crypto)$/, async (ctx) => {
        if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, "forex")))
            return;
        const type = (0, menu_js_1.parseForexSelectCallback)(ctx.match[0]);
        if (!type) {
            await ctx.answerCbQuery("Invalid Forex selection.");
            return;
        }
        const chatId = ctx.chat?.id;
        if (!chatId) {
            await ctx.answerCbQuery("Could not identify this chat.");
            return;
        }
        awaitingForexPair.set(chatId, { type });
        await ctx.answerCbQuery("Select pair");
        await ctx.reply(`Enter the ${type === "fiat" ? "Fiat" : "Crypto"} pair you want to analyse.\n` +
            `Examples: ${type === "fiat" ? "EUR/USD, GBP/USD, USD/JPY" : "BTC/USD, ETH/USD, SOL/USD"}\n\n` +
            "Type /cancel to go back.");
    });
    // Stocks country selection.
    bot.action(/^stocks:country:([A-Z]+)$/, async (ctx) => {
        if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, "stocks")))
            return;
        const chatId = ctx.chat?.id;
        if (chatId) {
            awaitingStocksCountry.delete(chatId);
            customStocksCountry.delete(chatId);
        }
        const countryCode = (0, menu_js_1.parseStocksCountryCallback)(ctx.match[0]);
        if (!countryCode) {
            await ctx.answerCbQuery("Invalid country selection.");
            return;
        }
        if (countryCode === "OTHER") {
            if (!chatId) {
                await ctx.answerCbQuery("Could not identify this chat.");
                return;
            }
            awaitingStocksCountry.set(chatId, true);
            await ctx.answerCbQuery("Other country");
            await ctx.reply("Please type the country you want stock signals for (e.g. Sweden, South Korea, Norway).\n\nType /cancel to go back.");
            return;
        }
        await ctx.answerCbQuery((0, menu_js_1.countryCodeToName)(countryCode));
        await ctx.reply(`Select stock scope for ${(0, menu_js_1.countryCodeToName)(countryCode)}:`, (0, menu_js_1.stocksScopeMenu)(countryCode, undefined, supportUrl));
    });
    // Stocks scope: local or international.
    bot.action(/^stocks:scope:([A-Z]+):(local|international)$/, async (ctx) => {
        if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, "stocks")))
            return;
        if (!(await checkSignalRequestLimit(ctx, subscriptionService, "stocks")))
            return;
        const parsed = (0, menu_js_1.parseStocksScopeCallback)(ctx.match[0]);
        if (!parsed || (parsed.scope !== "local" && parsed.scope !== "international")) {
            await ctx.answerCbQuery("Invalid stocks scope.");
            return;
        }
        const chatId = ctx.chat?.id;
        const countryName = parsed.countryCode === "CUSTOM" && chatId
            ? customStocksCountry.get(chatId)
            : (0, menu_js_1.countryCodeToName)(parsed.countryCode);
        if (!countryName) {
            await ctx.answerCbQuery("Country not found.");
            await ctx.reply("Please select your country first:", (0, menu_js_1.stocksCountryMenu)(supportUrl));
            return;
        }
        await ctx.answerCbQuery(parsed.scope === "local" ? "Local stocks" : "International stocks");
        try {
            const signal = await generateStockSignal.execute(countryName, parsed.scope === "international");
            await ctx.reply((0, SignalFormatter_js_1.formatSignal)(signal), { parse_mode: "HTML", ...(0, menu_js_1.stocksCountryMenu)(supportUrl) });
            await commitSignalRequest(ctx, subscriptionService, "stocks", logger);
        }
        catch (error) {
            logger.error("Failed to generate stock signal.", serializeError(error));
            await ctx.reply(friendlyError(error), (0, menu_js_1.stocksCountryMenu)(supportUrl));
        }
    });
    bot.action("stocks:back:countries", async (ctx) => {
        const chatId = ctx.chat?.id;
        if (chatId) {
            awaitingStocksCountry.delete(chatId);
            customStocksCountry.delete(chatId);
        }
        await ctx.answerCbQuery();
        await ctx.reply("Select your country:", (0, menu_js_1.stocksCountryMenu)(supportUrl));
    });
    // Lychee network selection.
    bot.action(VALERIS_NETWORK_CALLBACK_REGEX, async (ctx) => {
        if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, VALERIS_PLATFORM_CODE)))
            return;
        const networkCode = (0, menu_js_1.parseValerisNetworkCallback)(ctx.match[0]);
        if (!networkCode) {
            await ctx.answerCbQuery("Invalid network selection.");
            return;
        }
        const networkLabel = networkCode === "ANY" ? "Any Network" : networkCode;
        const usage = await getDailyRequestUsage(subscriptionService, ctx, VALERIS_PLATFORM_CODE);
        const usageLine = usage ? `\n\n${formatUsageText(usage)}` : "";
        await ctx.answerCbQuery(`Network: ${networkLabel}`);
        await ctx.reply(`Lychee Finance— ${networkLabel}.${usageLine}\n\nChoose a signal category:`, (0, menu_js_1.signalsMenu)(VALERIS_PLATFORM_CODE, networkCode, supportUrl));
    });
    // Lychee category callbacks include the selected network.
    bot.action(VALERIS_SIGNAL_CALLBACK_REGEX, async (ctx) => {
        if (!(await (0, subscriptionCommands_js_1.ensurePremiumAccess)(ctx, subscriptionService, VALERIS_PLATFORM_CODE)))
            return;
        if (!(await checkSignalRequestLimit(ctx, subscriptionService, VALERIS_PLATFORM_CODE)))
            return;
        const parsed = (0, menu_js_1.parseCategoryCallback)(ctx.match[0]);
        if (!parsed || parsed.platformCode !== VALERIS_PLATFORM_CODE) {
            await ctx.answerCbQuery("Invalid signal request.");
            return;
        }
        const category = VALERIS_CATEGORIES.find((c) => c.key === parsed.category);
        if (!category) {
            await ctx.answerCbQuery("Unknown signal category.");
            return;
        }
        await handleValerisCategory(ctx, category, parsed.networkCode, {
            generateFarmLiquiditySignal,
            generateOptionsSignal,
            generateMarginSignal,
            generateAIStrategySignal,
            generateValerisArbitrageSignal,
            valerisCacheService
        }, (0, menu_js_1.signalsMenu)(VALERIS_PLATFORM_CODE, parsed.networkCode, supportUrl), logger, async () => commitSignalRequest(ctx, subscriptionService, VALERIS_PLATFORM_CODE, logger));
    });
    bot.action((0, menu_js_1.platformCallback)("back"), async (ctx) => {
        await ctx.answerCbQuery();
        await ctx.reply("Select a trading platform:", (0, menu_js_1.platformsKeyboard)(supportUrl));
    });
    // Handle typed Forex pair and custom stock country input.
    bot.command("cancel", async (ctx) => {
        const chatId = ctx.chat?.id;
        let cleared = false;
        if (chatId) {
            cleared = awaitingForexPair.delete(chatId) || awaitingStocksCountry.delete(chatId);
            customStocksCountry.delete(chatId);
        }
        await ctx.reply(cleared ? "Cancelled. Choose a market:" : "Nothing to cancel. Choose a market:", (0, menu_js_1.mainMenu)(supportUrl));
    });
    bot.on("text", async (ctx, next) => {
        const chatId = ctx.chat?.id;
        if (!chatId)
            return next();
        const text = ctx.message.text.trim();
        if (text.startsWith("/"))
            return next();
        // Custom stock country input.
        if (awaitingStocksCountry.get(chatId)) {
            if (text.length < 2) {
                await ctx.reply("Please enter a valid country name (at least 2 characters).");
                return;
            }
            const normalizedCountry = normalizeCountryName(text);
            awaitingStocksCountry.delete(chatId);
            customStocksCountry.set(chatId, normalizedCountry);
            await ctx.reply(`Select stock scope for ${normalizedCountry}:`, (0, menu_js_1.stocksScopeMenu)("CUSTOM", normalizedCountry, supportUrl));
            return;
        }
        // Forex pair input.
        const state = awaitingForexPair.get(chatId);
        if (!state)
            return next();
        if (!(await checkSignalRequestLimit(ctx, subscriptionService, "forex"))) {
            awaitingForexPair.delete(chatId);
            return;
        }
        const pair = await findForexPair(traditionalMarket, state.type, text);
        if (!pair) {
            await ctx.reply(`I could not find a ${state.type} pair matching "${text}".\n` +
                `Please enter a valid symbol, e.g. ${state.type === "fiat" ? "EUR/USD" : "BTC/USD"}.`, (0, menu_js_1.forexActionMenu)(state.type));
            return;
        }
        awaitingForexPair.delete(chatId);
        await ctx.reply(`Analysing ${pair.symbol}...`);
        try {
            const signal = await generateForexSignal.execute(state.type, pair.symbol);
            await ctx.reply((0, SignalFormatter_js_1.formatSignal)(signal), { parse_mode: "HTML", ...(0, menu_js_1.forexActionMenu)(state.type) });
            await commitSignalRequest(ctx, subscriptionService, "forex", logger);
        }
        catch (error) {
            logger.error("Failed to generate Forex signal.", serializeError(error));
            await ctx.reply(friendlyError(error), (0, menu_js_1.forexActionMenu)(state.type));
        }
    });
    bot.catch((error) => {
        logger.error("Unhandled Telegram bot error.", serializeError(error));
    });
    return {
        bot,
        arbitrageCleanupWorker,
        bootstrap: async () => {
            await subscriptionService.seedPlans();
            try {
                await bootstrapAdminAuth.execute();
            }
            catch (error) {
                logger.warn("Admin bootstrap did not complete. Protected arbitrage price updates may fail.", serializeError(error));
            }
        }
    };
};
exports.createSignalsBot = createSignalsBot;
const handleGenericCategory = async (ctx, category, generateCategorySignal, generateArbitrageSignal, menuMarkup, logger, commitUsage) => {
    if (category.key === "arbitrage") {
        await ctx.answerCbQuery("Scanning market data...");
        try {
            const signal = await generateArbitrageSignal.execute();
            await ctx.reply((0, SignalFormatter_js_1.formatSignal)(signal), { parse_mode: "HTML", ...menuMarkup });
            await commitUsage();
        }
        catch (error) {
            logger.error("Failed to generate arbitrage signal.", serializeError(error));
            await ctx.reply(friendlyError(error), menuMarkup);
        }
        return;
    }
    await ctx.answerCbQuery("Generating signal...");
    try {
        const signal = await generateCategorySignal.execute(category.label);
        await ctx.reply((0, SignalFormatter_js_1.formatSignal)(signal), { parse_mode: "HTML", ...menuMarkup });
        await commitUsage();
    }
    catch (error) {
        logger.error(`Failed to generate ${category.label} signal.`, serializeError(error));
        await ctx.reply(friendlyError(error), menuMarkup);
    }
};
const handleValerisCategory = async (ctx, category, networkCode, generators, menuMarkup, logger, commitUsage) => {
    await ctx.answerCbQuery("Generating Lychee signal...");
    try {
        const signal = await generateValerisSignal(category, networkCode, generators);
        await ctx.reply((0, SignalFormatter_js_1.formatSignal)(signal), { parse_mode: "HTML", ...menuMarkup });
        await commitUsage();
    }
    catch (error) {
        logger.error(`Failed to generate Lychee ${category.label} signal.`, serializeError(error));
        await ctx.reply(friendlyError(error), menuMarkup);
    }
};
const generateValerisSignal = async (category, networkCode, generators) => {
    const { valerisCacheService } = generators;
    switch (category.key) {
        case "farm-liquidity": {
            const records = await valerisCacheService.getOrCreateDailyRecords("Farm Liquidity", networkCode, async (_category, net) => generators.generateFarmLiquiditySignal.execute(net));
            return valerisCacheService.pickRandom(records);
        }
        case "trading-options": {
            const records = await valerisCacheService.getOrCreateDailyRecords("Trading Options", networkCode, async (_category, net) => generators.generateOptionsSignal.execute(net));
            return valerisCacheService.pickRandom(records);
        }
        case "margin": {
            const records = await valerisCacheService.getOrCreateDailyRecords("Margin", networkCode, async (_category, net) => generators.generateMarginSignal.execute(net));
            return valerisCacheService.pickRandom(records);
        }
        case "ai-strategy": {
            const records = await valerisCacheService.getOrCreateDailyRecords("AI Strategy", networkCode, async (_category, net) => generators.generateAIStrategySignal.execute(net));
            return valerisCacheService.pickRandom(records);
        }
        case "arbitrage":
            return generators.generateValerisArbitrageSignal.execute(networkCode);
        default:
            throw new AppError_js_1.AppError(`Unknown Lychee signal category: ${category.label}`, "UNKNOWN_CATEGORY", true);
    }
};
const friendlyError = (error) => {
    if (error instanceof AppError_js_1.AppError && error.isUserSafe) {
        return error.message;
    }
    return "I could not generate that signal right now. Please try again shortly.";
};
const formatUsageText = (usage) => {
    return `Requests today: ${usage.made}/${usage.limit} used (${usage.remaining} left)`;
};
const normalizeForexInput = (input) => input.replace(/[^A-Z0-9]/gi, "").toUpperCase();
const normalizeCountryName = (input) => input
    .trim()
    .toLowerCase()
    .replace(/\s+/g, " ")
    .replace(/\b\w/g, (char) => char.toUpperCase());
const findForexPair = async (marketData, type, input) => {
    const pairs = await marketData.getMarketPairs(type);
    const normalized = normalizeForexInput(input);
    return pairs.find((pair) => {
        const normalizedSymbol = normalizeForexInput(pair.symbol);
        const normalizedBaseQuote = normalizeForexInput(pair.base) + normalizeForexInput(pair.quote);
        return normalizedSymbol === normalized || normalizedBaseQuote === normalized;
    });
};
const getDailyRequestUsage = async (subscriptionService, ctx, platformCode) => {
    const telegramId = ctx.from?.id ? String(ctx.from.id) : undefined;
    if (!telegramId)
        return undefined;
    return subscriptionService.getDailyRequestUsage(telegramId, platformCode);
};
const checkSignalRequestLimit = async (ctx, subscriptionService, platformCode) => {
    const telegramId = ctx.from?.id ? String(ctx.from.id) : undefined;
    if (!telegramId)
        return false;
    const result = await subscriptionService.checkSignalRequestLimit(telegramId, platformCode);
    if (result.limit <= 0 || result.allowed)
        return true;
    await ctx.answerCbQuery("Daily request limit reached.");
    await ctx.reply(`You've reached your daily request limit for this platform (${result.made}/${result.limit}).\n\nYour limit resets at: ${result.resetAt.toISOString()}`);
    return false;
};
const commitSignalRequest = async (ctx, subscriptionService, platformCode, logger) => {
    const telegramId = ctx.from?.id ? String(ctx.from.id) : undefined;
    if (!telegramId)
        return;
    try {
        await subscriptionService.recordSignalRequest(telegramId, platformCode);
    }
    catch (error) {
        logger.error("Failed to record signal request usage.", serializeError(error));
    }
};
const serializeError = (error) => {
    if (error instanceof Error) {
        return {
            name: error.name,
            message: error.message,
            stack: error.stack
        };
    }
    return { error };
};
