import { request as undiciRequest } from 'undici';
import { FastifyRequest, FastifyReply } from 'fastify';
import { config } from '../config';
import {
  googleMapsApiKey,
  googleMapsProxyAuth,
  rewriteGoogleMapsApiKeys,
  rewriteGoogleMapsKeyInQuery,
} from '../googleMaps/key';

const ALLOWED_HOSTS = new Set([
  'maps.googleapis.com',
  'maps.gstatic.com',
  'khms.googleapis.com',
  'khms0.googleapis.com',
  'khms1.googleapis.com',
  'khms2.googleapis.com',
  'khms3.googleapis.com',
  'khms0.google.com',
  'khms1.google.com',
  'khms2.google.com',
  'khms3.google.com',
  'mts.googleapis.com',
  'mts0.googleapis.com',
  'mts1.googleapis.com',
  'cbks0.googleapis.com',
  'cbks1.googleapis.com',
  'maps.google.com',
  'www.google.com',
  'fonts.googleapis.com',
  'fonts.gstatic.com',
  'streetviewpixels-pa.googleapis.com',
  'geo0.googleapis.com',
  'geo1.googleapis.com',
  'tile.googleapis.com',
  'mapsresources-pa.googleapis.com',
]);

const GOOGLE_MAPS_SCRIPT_RE =
  /(<script)([^>]*\ssrc=["'])\/\/maps\.googleapis\.com(\/maps\/api\/js[^"']*)(["'][^>]*>\s*<\/script>)/i;

const BROKEN_SHOPS_SCRIPT_RE =
  /<script[^>]*type=["']text\/javascript["']src=["'][^"']*\/sinsay\/shops\.js["'][^>]*>\s*<\/script>\s*/gi;

const LEAFLET_RE =
  /<link[^>]*leaflet[^>]*>\s*|<script[^>]*leaflet[^>]*>\s*<\/script>\s*|<script[^>]*storelocator-map\.js[^>]*>\s*<\/script>\s*|<script>\(function\(\)\{if\(window\.google&&window\.google\.maps\)return[\s\S]*?<\/script>/gi;

const INLINE_MAPS_URL_RE =
  /url:\s*['"]\/\/maps\.googleapis\.com\/maps\/api\/js['"]/g;

const PROXIED_MAPS_SCRIPT_RE =
  /<script[^>]*src=["'][^"']*\/__proxy\/google\/maps\.googleapis\.com\/maps\/api\/js[^"']*["'][^>]*>\s*<\/script>/i;

function requestBodyBuffer(body: unknown): Buffer | undefined {
  if (body == null) return undefined;
  if (Buffer.isBuffer(body)) return body.length ? body : undefined;
  if (typeof body === 'string') return Buffer.from(body);
  if (body instanceof Uint8Array) return Buffer.from(body);
  if (Array.isArray(body)) {
    const parts = body.filter((p): p is Buffer => Buffer.isBuffer(p));
    if (parts.length) return Buffer.concat(parts);
  }
  return undefined;
}

function isAllowedHost(host: string): boolean {
  return ALLOWED_HOSTS.has(host.toLowerCase());
}

function isAllowedPath(host: string, path: string): boolean {
  if (host === 'www.google.com') {
    return path.startsWith('/maps/') || path.startsWith('/mapfiles/');
  }
  if (host === 'maps.google.com') {
    return path.startsWith('/maps/') || path.startsWith('/mapfiles/');
  }
  return true;
}

/** Rewrite client origin/referer in query strings to values Google accepts. */
function sanitizeUpstreamQuery(query: string): string {
  if (!query) return query;
  const auth = googleMapsProxyAuth();
  let q = rewriteGoogleMapsKeyInQuery(query)
    .replace(
      /([?&])origin=[^&]*/gi,
      `$1origin=${encodeURIComponent(auth.origin)}`,
    )
    .replace(
      /([?&])referer=[^&]*/gi,
      `$1referer=${encodeURIComponent(auth.referer)}`,
    );
  if (!config.googleMaps.useMirrorKey) {
    const proxyOrigin = config.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const domain = config.domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    q = q
      .replace(new RegExp(proxyOrigin, 'gi'), auth.origin)
      .replace(new RegExp(domain, 'gi'), 'www.sinsay.com');
  }
  return q;
}

/** Replace our domain with sinsay.com inside request bodies (protobuf tile/RPC payloads). */
function sanitizeRequestBody(body: Buffer | undefined): Buffer | undefined {
  if (!body?.length || config.googleMaps.useMirrorKey) return body;
  const auth = googleMapsProxyAuth();
  const domain = config.domain;
  const origin = config.origin;
  const hay = body.toString('latin1');
  if (!hay.includes(domain) && !hay.includes('sinsaybuy')) return body;
  return Buffer.from(
    hay
      .split(origin)
      .join(auth.origin)
      .split(`https://${domain}`)
      .join(auth.origin)
      .split(domain)
      .join('www.sinsay.com'),
    'latin1',
  );
}

function rewriteGoogleUrls(body: string): string {
  const origin = config.origin;
  const prefix = `${origin}${config.intercept.googleMapsProxyPrefix}/`;
  for (const host of ALLOWED_HOSTS) {
    body = body.replaceAll(`https://${host}`, `${prefix}${host}`);
    body = body.replaceAll(`http://${host}`, `${prefix}${host}`);
    body = body.replaceAll(`//${host}`, `${prefix}${host}`);
  }
  return body;
}

function collectGoogleClientHeaders(
  headers: Record<string, string | string[] | undefined>,
): Record<string, string> {
  const out: Record<string, string> = {};
  for (const [key, value] of Object.entries(headers)) {
    const lk = key.toLowerCase();
    if (
      lk.startsWith('x-goog') ||
      lk === 'x-client-data' ||
      lk === 'x-user-agent'
    ) {
      out[key] = Array.isArray(value) ? value.join(', ') : String(value);
    }
  }
  return out;
}

/** Client bridge: route Google Maps network calls through our referer proxy. */
export function buildGoogleMapsBridgeScript(): string {
  const hosts = [...ALLOWED_HOSTS].map((h) => `'${h}':1`).join(',');
  const prefix = config.intercept.googleMapsProxyPrefix;
  const auth = googleMapsProxyAuth();
  return `(function(){
var HOSTS={${hosts}};
var PREFIX='${prefix}';
function proxyUrl(url){
  try{
    if(typeof url!=='string')return url;
    if(url.indexOf(PREFIX+'/')===0)return url;
    var u=new URL(url,location.href);
    if(!HOSTS[u.hostname])return url;
    return location.origin+PREFIX+'/'+u.hostname+u.pathname+sanitizeQs(u.search);
  }catch(e){return url;}
}
function sanitizeQs(q){
  if(!q)return q;
  q=q.replace(/([?&])origin=[^&]*/gi,'$1origin='+encodeURIComponent('${auth.origin}'))
    .replace(/([?&])referer=[^&]*/gi,'$1referer='+encodeURIComponent('${auth.referer}'));
  return q.replace(/([?&]key=)(AIzaSy[A-Za-z0-9_-]+)/gi,'$1'+encodeURIComponent('${googleMapsApiKey()}'));
}
var of=window.fetch;
function replayBody(req){
  if(req.body==null)return Promise.resolve(undefined);
  var ct=(req.headers.get('content-type')||'').toLowerCase();
  if(ct.indexOf('multipart/form-data')>=0&&typeof req.formData==='function'){
    return req.formData();
  }
  if(ct.indexOf('application/x-www-form-urlencoded')>=0){
    return req.text().then(function(t){return t||undefined;});
  }
  return req.clone().arrayBuffer().then(function(b){
    return b&&b.byteLength?b:undefined;
  });
}
function forwardFetch(input,init){
  if(typeof input==='string')return of(proxyUrl(input),init);
  if(!input||!input.url)return of(input,init);
  var proxied=proxyUrl(input.url);
  if(proxied===input.url)return of(input,init);
  var m=input.method;
  var base={
    method:m,
    headers:input.headers,
    mode:input.mode,
    credentials:input.credentials,
    cache:input.cache,
    redirect:input.redirect,
    referrer:input.referrer,
    integrity:input.integrity,
    keepalive:input.keepalive,
    signal:(init&&init.signal)||input.signal
  };
  if(m==='GET'||m==='HEAD'||input.body==null)return of(proxied,base);
  return replayBody(input).then(function(body){
    var opts=base;
    if(body!=null)opts.body=body;
    return of(proxied,opts);
  });
}
if(of)window.fetch=function(input,init){return forwardFetch(input,init);};
var oo=XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open=function(m,url){
  arguments[1]=proxyUrl(url);
  return oo.apply(this,arguments);
};
var os=Element.prototype.setAttribute;
Element.prototype.setAttribute=function(n,v){
  if((n==='src'||n==='href')&&typeof v==='string')v=proxyUrl(v);
  return os.call(this,n,v);
};
var oce=document.createElement;
document.createElement=function(tag){
  var el=oce.call(document,tag);
  if(tag&&/^(script|img|link|iframe)$/i.test(tag)){
    var _set=el.setAttribute.bind(el);
    el.setAttribute=function(n,v){
      if((n==='src'||n==='href')&&typeof v==='string')v=proxyUrl(v);
      return _set(n,v);
    };
  }
  return el;
};
function patchProtoSrc(Proto){
  try{
    var d=Object.getOwnPropertyDescriptor(Proto.prototype,'src');
    if(!d||!d.set)return;
    Object.defineProperty(Proto.prototype,'src',{
      set:function(v){d.set.call(this,proxyUrl(String(v)));},
      get:d.get,
      configurable:true
    });
  }catch(e){}
}
patchProtoSrc(HTMLScriptElement);
patchProtoSrc(HTMLImageElement);
patchProtoSrc(HTMLIFrameElement);
})();`;
}

/** Inline bridge must run synchronously in <head> before checkout loads Maps via script.src. */
export function buildGoogleMapsBridgeInlineTag(): string {
  return `<script data-proxy="google-bridge">${buildGoogleMapsBridgeScript()}</script>`;
}

/** Legacy no-op service worker body; old clients may still request this URL. */
export function buildGoogleMapsServiceWorker(): string {
  return `self.addEventListener('install',function(ev){self.skipWaiting();});
self.addEventListener('activate',function(ev){
  ev.waitUntil(self.registration.unregister().then(function(){return self.clients.claim();}));
});`;
}

export async function handleGoogleMapsBridge(
  _request: FastifyRequest,
  reply: FastifyReply,
): Promise<void> {
  reply
    .header('content-type', 'application/javascript; charset=utf-8')
    .header('cache-control', 'public, max-age=3600, stale-while-revalidate=600')
    .send(buildGoogleMapsBridgeScript());
}

export async function handleGoogleMapsServiceWorker(
  _request: FastifyRequest,
  reply: FastifyReply,
): Promise<void> {
  reply
    .header('content-type', 'application/javascript; charset=utf-8')
    .header('service-worker-allowed', '/')
    .header('cache-control', 'public, max-age=3600, stale-while-revalidate=600')
    .send(buildGoogleMapsServiceWorker());
}

export async function handleGoogleMapsProxy(
  request: FastifyRequest<{ Params: { host: string; '*': string } }>,
  reply: FastifyReply,
): Promise<void> {
  if (request.method === 'OPTIONS') {
    reply
      .header('access-control-allow-origin', '*')
      .header('access-control-allow-methods', 'GET, POST, OPTIONS')
      .header('access-control-allow-headers', '*')
      .status(204)
      .send();
    return;
  }

  const host = (request.params.host ?? '').toLowerCase();
  if (!isAllowedHost(host)) {
    reply.status(403).send('Forbidden host');
    return;
  }

  const star = request.params['*'] ?? '';
  const qIndex = request.url.indexOf('?');
  const rawQuery = qIndex >= 0 ? request.url.slice(qIndex) : '';
  const query = sanitizeUpstreamQuery(rawQuery);
  const upstreamPath = `/${star}${query}`;

  if (!isAllowedPath(host, `/${star.split('?')[0]}`)) {
    reply.status(403).send('Forbidden path');
    return;
  }

  const mapsAuth = googleMapsProxyAuth();
  const upstreamHeaders: Record<string, string> = {
    referer: mapsAuth.referer,
    origin: mapsAuth.origin,
    'user-agent':
      (request.headers['user-agent'] as string) ??
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    accept: (request.headers.accept as string) ?? '*/*',
    'accept-language': (request.headers['accept-language'] as string) ?? 'pl-PL,pl;q=0.9',
    ...collectGoogleClientHeaders(request.headers),
  };

  const reqContentType = request.headers['content-type'];
  if (reqContentType) upstreamHeaders['content-type'] = reqContentType as string;

  let body =
    request.method !== 'GET' && request.method !== 'HEAD'
      ? requestBodyBuffer(request.body)
      : undefined;
  body = sanitizeRequestBody(body);

  const { statusCode, headers, body: resBody } = await undiciRequest(
    `https://${host}${upstreamPath}`,
    {
      method: request.method as 'GET' | 'POST' | 'HEAD' | 'OPTIONS',
      headers: upstreamHeaders,
      body,
      throwOnError: false,
      maxRedirections: 5,
    },
  );

  const chunks: Buffer[] = [];
  for await (const chunk of resBody) {
    chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
  }
  let responseBody = Buffer.concat(chunks);

  const contentType = String(headers['content-type'] ?? '');
  const isJavaScript =
    contentType.includes('javascript') || contentType.includes('ecmascript');

  if (isJavaScript) {
    let text = responseBody.toString('utf8');
    text = rewriteGoogleUrls(text);
    text = rewriteGoogleMapsApiKeys(text);
    responseBody = Buffer.from(text, 'utf8');
  }

  reply.header('access-control-allow-origin', '*');
  reply.header('access-control-allow-methods', 'GET, POST, OPTIONS');
  reply.header('cache-control', 'public, max-age=3600, stale-while-revalidate=300');

  if (contentType) reply.type(contentType);

  const etag = headers.etag;
  if (etag) reply.header('etag', etag as string);

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

/** Inject referer-proxy bridge at the start of <head> (checkout order, etc.). */
export function injectGoogleMapsBridge(html: string): string {
  if (html.includes('data-proxy="google-bridge"')) return html;
  const tag = buildGoogleMapsBridgeInlineTag();
  const headMatch = html.match(/<head\b[^>]*>/i);
  if (headMatch && headMatch.index !== undefined) {
    const insertAt = headMatch.index + headMatch[0].length;
    return html.slice(0, insertAt) + tag + html.slice(insertAt);
  }
  return tag + html;
}

export function patchStoreLocatorPage(html: string): string {
  let patched = html.replace(LEAFLET_RE, '');
  patched = patched.replace(BROKEN_SHOPS_SCRIPT_RE, '');

  const proxyPrefix = `${config.origin}${config.intercept.googleMapsProxyPrefix}`;
  patched = patched.replace(
    INLINE_MAPS_URL_RE,
    `url: '${proxyPrefix}/maps.googleapis.com/maps/api/js'`,
  );
  // Remove direct Google Maps script tags (only proxied script should load).
  patched = patched.replace(
    /<script[^>]*src=["']https?:\/\/maps\.googleapis\.com\/maps\/api\/js[^"']*["'][^>]*>\s*<\/script>\s*/gi,
    '',
  );
  patched = patched.replace(
    /<script[^>]*src=["']\/\/maps\.googleapis\.com\/maps\/api\/js[^"']*["'][^>]*>\s*<\/script>\s*/gi,
    '',
  );

  const bridge = buildGoogleMapsBridgeInlineTag();

  if (GOOGLE_MAPS_SCRIPT_RE.test(patched)) {
    patched = patched.replace(
      GOOGLE_MAPS_SCRIPT_RE,
      `${bridge}$1$2${proxyPrefix}/maps.googleapis.com$3$4`,
    );
  } else if (!PROXIED_MAPS_SCRIPT_RE.test(patched)) {
    const mapsTag = `<script src="${proxyPrefix}/maps.googleapis.com/maps/api/js?v=3&amp;key=${googleMapsApiKey()}" type="text/javascript"></script>`;
    patched = patched.replace(
      /(<script[^>]*markerclusterer\.js[^>]*>\s*<\/script>)/i,
      `${bridge}${mapsTag}$1`,
    );
  }

  return patched;
}
