const fs = require('fs/promises');
const config = require('./config');
const logger = require('./logger');
const { launchBrowser, createContext, authenticateContext, hasSessionFile } = require('./session');
const { parseItemsFromHtml, countForecastRows } = require('./parser');
const { fetchTargetHtmlWithSession, buildForecastUrl } = require('./fetcher');
const { loadState, saveState, diffItems } = require('./state');
const { sendWebhook } = require('./notifier');
const { syncSignalsSnapshotToDb, logParserError, closeStorage } = require('./storage');
const { nowIso, writeTextFile, sanitizeFilePart } = require('./utils');
const { recognizeForecastText } = require('./forecast-ocr');

let runtimeBrowser = null;

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function isNotFoundTitle(title = '') {
  const normalized = String(title).toLowerCase();
  return normalized.includes('404') || normalized.includes('страница не найдена');
}

function normalizeTabs() {
  const tabs = config.forecastActiveTabs.length > 0 ? config.forecastActiveTabs : [1, 2];
  return [...new Set(tabs)].filter((tab) => tab === 1 || tab === 2);
}

function annotateItemsWithTab(items, activeTab) {
  return items.map((item) => ({
    ...item,
    parserDetectedInactive: item.forecastInactive === true,
    activeTab: Number.isInteger(Number(item.activeTab)) ? Number(item.activeTab) : activeTab,
    forecastInactive: item.forecastInactive === true || activeTab === 2
  }));
}

function dedupeItems(items) {
  const byKey = new Map();
  for (const item of items) {
    if (!byKey.has(item.__key)) {
      byKey.set(item.__key, item);
    }
  }
  return Array.from(byKey.values());
}

function getActiveBots() {
  if (Array.isArray(config.bots) && config.bots.length > 0) {
    return config.bots;
  }

  return [
    {
      key: 'default',
      name: 'Default bot',
      url: config.targetUrl
    }
  ];
}

function annotateItemsWithBot(items, bot) {
  return items.map((item) => ({
    ...item,
    botKey: bot.key,
    botName: bot.name,
    botUrl: bot.url,
    __key: `${bot.key}:${item.__key}`
  }));
}

async function enrichItemsWithForecast(items) {
  const concurrency = 4;
  const enriched = new Array(items.length);

  async function worker(startIndex) {
    for (let index = startIndex; index < items.length; index += concurrency) {
      const item = items[index];
      const forecastResult = await recognizeForecastText(item);
      enriched[index] = {
        ...item,
        forecast: forecastResult.forecast,
        forecastRaw: forecastResult.rawForecast,
        forecastImageUrl: forecastResult.imageUrl,
        forecastSource: forecastResult.source
      };
    }
  }

  await Promise.all(
    Array.from({ length: Math.min(concurrency, items.length) }, (_, index) => worker(index))
  );

  return enriched.filter(Boolean);
}

function getTabStats(snapshot, tab) {
  return snapshot?.stats?.tabs?.[String(tab)] || null;
}

async function fetchHttpPageWithSession({ context, targetUrl }) {
  let result = await fetchTargetHtmlWithSession({ context, config, targetUrl });

  if (result.authRequired && config.authEnabled) {
    logger.warn('HTTP fetch indicates expired session, refreshing authentication in Playwright');
    await authenticateContext({ context, sessionFile: config.sessionFile, config });
    result = await fetchTargetHtmlWithSession({ context, config, targetUrl });
  }

  return result;
}

async function saveDebugHtmlSnapshot({ botKey, activeTab, currentPage, html }) {
  if (!config.debugSaveFilteredHtml) {
    return;
  }

  const timestamp = nowIso().replace(/[:.]/g, '-');
  const baseName = `${sanitizeFilePart(botKey)}_tab-${activeTab}_page-${currentPage}_${timestamp}`;
  const file = await writeTextFile(config.htmlSnapshotDir, baseName, html, 'html');
  logger.info(`Saved debug HTML snapshot: ${file}`);
}

async function loadFromHttp({ context, targetUrl }) {
  const tabs = normalizeTabs();
  const allItems = [];
  let lastUrl = targetUrl;
  let lastTitle = '';
  const byTab = {};

  logger.info(
    `Fetching target pages via HTTP with tabs=${tabs.join(',')}, perPage=${config.forecastPerPage}, maxPages=${config.forecastMaxPages}`
  );

  for (const activeTab of tabs) {
    byTab[String(activeTab)] = {
      pagesFetched: 0,
      itemsParsed: 0,
      rowsDetected: 0
    };

    for (let offset = 0; offset < config.forecastMaxPages; offset += 1) {
      const currentPage = config.forecastStartPage + offset;
      const forecastUrl = buildForecastUrl(targetUrl, {
        activeTab,
        perPage: config.forecastPerPage,
        page: currentPage
      });

      const result = await fetchHttpPageWithSession({ context, targetUrl: forecastUrl });
      lastUrl = result.finalUrl;
      lastTitle = result.title || lastTitle;

      if (result.authRequired) {
        throw new Error(`Authentication required for target page: ${result.finalUrl}`);
      }
      if (result.status >= 400) {
        throw new Error(`Target page returned status ${result.status}: ${result.finalUrl}`);
      }
      if (isNotFoundTitle(result.title)) {
        throw new Error(`Target page returned not found: ${result.finalUrl}`);
      }

      const parsedItems = parseItemsFromHtml(result.html, config.selectors, activeTab);
      const detectedRows = countForecastRows(result.html, config.selectors, activeTab);
      byTab[String(activeTab)].rowsDetected += detectedRows;
      logger.info(`Parsed items for tab=${activeTab} page=${currentPage}: ${parsedItems.length}`);

      if (parsedItems.length === 0) {
        if (detectedRows > 0) {
          logger.info(
            `Detected ${detectedRows} forecast rows for tab=${activeTab} page=${currentPage}, but all were filtered out as hidden or incomplete`
          );
          await saveDebugHtmlSnapshot({
            botKey: new URL(targetUrl).pathname.split('/').filter(Boolean).pop() || 'unknown-bot',
            activeTab,
            currentPage,
            html: result.html
          });
          byTab[String(activeTab)].pagesFetched += 1;
        }
        break;
      }

      byTab[String(activeTab)].pagesFetched += 1;
      byTab[String(activeTab)].itemsParsed += parsedItems.length;
      allItems.push(...annotateItemsWithTab(parsedItems, activeTab));

      if (parsedItems.length < config.forecastPerPage) {
        break;
      }
    }
  }

  const dedupedItems = dedupeItems(allItems);
  const stats = {
    rawItems: allItems.length,
    dedupedItems: dedupedItems.length,
    detectedRows: Object.values(byTab).reduce((total, tabStats) => total + (tabStats.rowsDetected || 0), 0),
    tabs: byTab
  };

  return {
    items: dedupedItems,
    currentUrl: lastUrl,
    title: lastTitle,
    source: 'http',
    stats
  };
}

async function writeHeartbeat(status, extra = {}) {
  await fs.writeFile(
    config.heartbeatFile,
    JSON.stringify(
      {
        status,
        timestamp: nowIso(),
        ...extra
      },
      null,
      2
    ),
    'utf8'
  );
}

async function run() {
  const browser = await launchBrowser({ headless: config.headless });
  runtimeBrowser = browser;
  const context = await createContext({ browser, sessionFile: config.sessionFile });

  logger.info('Parser fetch mode: http');
  logger.info(
    `Configured bots: ${getActiveBots()
      .map((bot) => `${bot.key} -> ${bot.url}`)
      .join(', ')}`
  );
  let firstRun = true;

  while (true) {
    try {
      await writeHeartbeat('running');

      if (!hasSessionFile(config.sessionFile) && config.authEnabled) {
        logger.warn('Session file is missing, performing background authentication before HTTP fetch');
        await authenticateContext({ context, sessionFile: config.sessionFile, config });
      }

      const botSnapshots = [];
      const aggregatedItems = [];

      for (const bot of getActiveBots()) {
        logger.info(`Loading bot "${bot.name}" (${bot.url})`);

        const botSnapshot = await loadFromHttp({ context, targetUrl: bot.url });
        let syncEligible = true;
        let syncWarning = null;
        const activeTabStats = getTabStats(botSnapshot, 1);
        const activeListIsEmpty = Boolean(activeTabStats)
          && (activeTabStats.itemsParsed || 0) === 0
          && (activeTabStats.rowsDetected || 0) === 0;

        if (botSnapshot.items.length === 0) {
          if (activeListIsEmpty) {
            syncEligible = true;
            syncWarning = 'empty_active_list';
            logger.warn(
              `Bot "${bot.name}" returned an empty active list. All current matches for this bot will be marked inactive`
            );
          } else if ((botSnapshot.stats?.detectedRows || 0) > 0) {
            syncEligible = true;
            syncWarning = 'filtered_hidden_or_incomplete_rows';
            logger.info(
              `Bot "${bot.name}" returned only hidden or incomplete rows. Sync will continue with an empty valid snapshot`
            );
          } else {
            syncEligible = false;
            syncWarning = 'empty_snapshot';
            logger.warn(
              `Bot "${bot.name}" returned 0 items. Skipping automatic removals for this bot in the current sync cycle`
            );
          }
        }

        const botItems = await enrichItemsWithForecast(annotateItemsWithBot(botSnapshot.items, bot));
        aggregatedItems.push(...botItems);
        botSnapshots.push({
          key: bot.key,
          name: bot.name,
          url: bot.url,
          currentUrl: botSnapshot.currentUrl,
          title: botSnapshot.title,
          source: botSnapshot.source,
          stats: botSnapshot.stats,
          items: botItems.length,
          syncEligible,
          syncWarning
        });
      }

      const snapshot = {
        items: dedupeItems(aggregatedItems),
        currentUrl: botSnapshots[0]?.currentUrl || config.targetUrl,
        title: botSnapshots.map((bot) => bot.title).filter(Boolean).join(' | '),
        source: botSnapshots.map((bot) => `${bot.key}:${bot.source}`).join(','),
        stats: {
          bots: botSnapshots
        }
      };

      logger.info('Loaded snapshot', {
        source: snapshot.source,
        currentUrl: snapshot.currentUrl,
        title: snapshot.title,
        items: snapshot.items.length
      });
      if (snapshot.stats) {
        logger.info('Snapshot stats', snapshot.stats);
      }

      const hasSyncEligibleBots = botSnapshots.some((bot) => bot.syncEligible);

      if (snapshot.items.length === 0 && !hasSyncEligibleBots) {
        throw new Error(`No parsed items found on page: ${snapshot.currentUrl}`);
      }

      const previousState = await loadState(config.stateFile);
      const previousItems = previousState?.items || [];
      const changes = diffItems(previousItems, snapshot.items);

      const shouldNotify = firstRun ? config.notifyOnStartup && changes.length > 0 : changes.length > 0;

      if (!previousState) {
        logger.info('No previous state found, saving initial state');
        await saveState(config.stateFile, snapshot.items);
      } else if (changes.length > 0) {
        logger.info(`Detected changes: ${changes.length}`);
        await saveState(config.stateFile, snapshot.items);
      } else {
        logger.info('No changes detected');
      }

      const dbResult = await syncSignalsSnapshotToDb(snapshot.items, {
        currentUrl: snapshot.currentUrl,
        title: snapshot.title,
        source: snapshot.source,
        stats: snapshot.stats,
        changesCount: changes.length
      });
      logger.info('Signals snapshot queued', dbResult);

      if (shouldNotify) {
        await sendWebhook(config.webhookUrl, config.webhookSecret, {
          event: 'alpinbet_changes_detected',
          timestamp: nowIso(),
          currentUrl: snapshot.currentUrl,
          title: snapshot.title,
          source: snapshot.source,
          bots: snapshot.stats?.bots,
          count: changes.length,
          changes
        });
      }

      firstRun = false;
      await writeHeartbeat('ok', { changesCount: changes.length });
    } catch (error) {
      logger.error(`Parser loop failed: ${error.message}`);
      await logParserError(error, { phase: 'loop' }).catch((logError) => {
        logger.warn(`Failed to persist parser error: ${logError.message}`);
      });
      await writeHeartbeat('error', { message: error.message });

      await sendWebhook(config.webhookUrl, config.webhookSecret, {
        event: 'alpinbet_parser_error',
        timestamp: nowIso(),
        message: error.message,
        stack: error.stack
      }).catch((notifyError) => {
        logger.warn(`Failed to send error webhook: ${notifyError.message}`);
      });
    }

    logger.info(`Sleeping for ${config.pollIntervalMs} ms`);
    await sleep(config.pollIntervalMs);
  }
}

run().catch((error) => {
  logger.error(`Fatal startup error: ${error.message}`);
  logParserError(error, { phase: 'startup' }).catch(() => undefined);
  fs
    .writeFile(
      config.heartbeatFile,
      JSON.stringify({ status: 'fatal', timestamp: nowIso(), message: error.message }, null, 2),
      'utf8'
    )
    .catch(() => undefined);
  runtimeBrowser?.close().catch(() => undefined);
  closeStorage().catch(() => undefined);
  process.exit(1);
});

process.on('SIGINT', async () => {
  await runtimeBrowser?.close().catch(() => undefined);
  await closeStorage().catch(() => undefined);
  process.exit(0);
});

process.on('SIGTERM', async () => {
  await runtimeBrowser?.close().catch(() => undefined);
  await closeStorage().catch(() => undefined);
  process.exit(0);
});
