/**
 * Apple Mail Privacy Protection (MPP) Detection.
 *
 * MPP (seit iOS 15 / macOS Monterey) öffnet ALLE Tracking-Pixel automatisch beim
 * Mail-Eingang über einen Apple-Proxy — ohne dass der User die Mail je gesehen hat.
 * Das pumpt unsere Open-Rate um 30-50 % auf und entwertet sie als KPI.
 *
 * Erkennung über zwei Heuristiken (kombiniert reduziert False-Positives):
 *  1. User-Agent ist exakt `Mozilla/5.0` — Apple sendet keinen weiteren UA-String.
 *     Echte Mail-Clients identifizieren sich (Gmail-Image-Proxy, Outlook, …).
 *  2. IP gehört zur Apple Privacy Relay-Range (`mask-h2.icloud.com`,
 *     `mask.icloud.com` AAAA-Records, regelmäßig veröffentlicht von Apple).
 *
 * Wir loggen den IP-Hash NICHT als Klartext — sondern als SHA-256(ip + secret),
 * sodass Stats-Auswertungen "unique IPs" zählen können, ohne dass Klar-IPs
 * persistiert werden (DSGVO-Compliance: IP gilt als personenbezogenes Datum).
 *
 * Bewusst minimal: keine IP-Range-Lookups in v1 — wir betrachten die /16- bis /24-
 * Prefixes, die Apple in den letzten 2 Jahren genutzt hat (hard-coded in
 * `data/apple-mpp-ranges.json`, manuell pflegbar). Falls leer → nur UA-Check.
 */

import { createHash, createHmac } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

/** Salt-Quelle für den IP-Hash — derselbe Pepper wie für Unsubscribe-Tokens. */
const IP_HASH_SECRET_FALLBACK = 'mailforge-ip-hash-fallback-set-MAILFORGE_UNSUB_SECRET';

function getIpHashSecret(): string {
  return process.env.MAILFORGE_UNSUB_SECRET?.trim() || IP_HASH_SECRET_FALLBACK;
}

/**
 * Extrahiert die Client-IP aus den Headers. `x-forwarded-for` kann eine Komma-
 * separierte Liste sein (`client, proxy1, proxy2`) — wir nehmen den ersten Eintrag.
 * Fallback: `x-real-ip` (nginx) oder `cf-connecting-ip` (Cloudflare).
 */
export function extractClientIp(headers: Headers): string | undefined {
  const xff = headers.get('x-forwarded-for');
  if (xff) {
    const first = xff.split(',')[0]?.trim();
    if (first) return first;
  }
  return headers.get('x-real-ip')?.trim() || headers.get('cf-connecting-ip')?.trim() || undefined;
}

/**
 * Hashed eine IP-Adresse irreversibel via HMAC-SHA256 mit Server-Pepper.
 * Output: 32-Zeichen-Hex (128 bit) — reicht für Unique-Counting ohne PII-Speicherung.
 */
export function hashIp(ip: string | undefined): string | undefined {
  if (!ip) return undefined;
  const h = createHmac('sha256', getIpHashSecret());
  h.update(ip.trim().toLowerCase());
  return h.digest('hex').slice(0, 32);
}

/**
 * Lädt die Apple MPP-IP-Range-Liste (JSON-Datei mit IP-Prefixes).
 * Lazy-loaded und gecached, da hot-path im Open-Pixel.
 */
let cachedMppRanges: string[] | null = null;
function getMppIpPrefixes(): string[] {
  if (cachedMppRanges !== null) return cachedMppRanges;
  try {
    // process.cwd() funktioniert für Next.js (.next-Build vs dev) UND den Worker —
    // beide Prozesse werden aus dem Repo-Root gestartet (siehe package.json scripts).
    const filePath = join(process.cwd(), 'data', 'apple-mpp-ranges.json');
    const raw = readFileSync(filePath, 'utf-8');
    const ranges = JSON.parse(raw) as { prefixes?: string[] };
    cachedMppRanges = ranges.prefixes ?? [];
  } catch {
    cachedMppRanges = [];
  }
  return cachedMppRanges;
}

/**
 * True wenn die IP mit einem der bekannten Apple-MPP-Prefixes startet.
 * Simple String-Prefix-Match — keine CIDR-Mathe nötig, weil die JSON-Datei
 * bereits Prefixes in der Form `17.249.` oder `2620:149:` enthält.
 */
function isAppleMppIp(ip: string | undefined): boolean {
  if (!ip) return false;
  const normalized = ip.trim().toLowerCase();
  const prefixes = getMppIpPrefixes();
  for (const p of prefixes) {
    if (normalized.startsWith(p.toLowerCase())) return true;
  }
  return false;
}

/**
 * Hauptentscheidung: ist dieser Open-Pixel-Call eine Apple-MPP-Vorab-Öffnung?
 *
 * Strategie (logisch OR, weil jedes Signal allein schon stark ist):
 *  - UA exakt 'Mozilla/5.0' → MPP (kein anderer Client sendet diesen UA ohne weitere Tokens)
 *  - IP startet mit bekanntem Apple-Prefix → MPP
 *
 * False-Positive-Risiko: minimal — `Mozilla/5.0` alleine ohne Tokens ist äußerst
 * untypisch für echte Browser. Echte Apple-Mail-Opens (Hand-User) tragen den vollen
 * Safari-UA-String und schlagen hier nicht an.
 */
export function isMpp(userAgent: string | undefined, clientIp: string | undefined): boolean {
  if (userAgent && userAgent.trim() === 'Mozilla/5.0') return true;
  if (isAppleMppIp(clientIp)) return true;
  return false;
}

/**
 * Hash-Helper für `tracking_events.ip_hash`-Column — public weil auch der
 * Click-Endpoint die gleiche Hash-Logik nutzen will.
 */
export function buildIpHash(headers: Headers): string | undefined {
  return hashIp(extractClientIp(headers));
}

/**
 * Aus dem Request gelesener Pixel-Context — bundle für `incrementVariantOpen`.
 */
export interface OpenPixelContext {
  userAgent: string | undefined;
  ipHash: string | undefined;
  isMpp: boolean;
}

export function readOpenPixelContext(headers: Headers): OpenPixelContext {
  const userAgent = headers.get('user-agent') ?? undefined;
  const clientIp = extractClientIp(headers);
  return {
    userAgent: userAgent?.slice(0, 500), // Cap absurd lange UAs
    ipHash: hashIp(clientIp),
    isMpp: isMpp(userAgent, clientIp),
  };
}

// Test-Hook: erlaubt Tests, die Cache zu reset (z.B. nach Datei-Schreiben).
export function _resetMppRangesCacheForTests(): void {
  cachedMppRanges = null;
}

/**
 * Hash-Funktion für andere Pseudonymisierungs-Use-Cases — z.B. recipientId
 * für GA4-Lookup. Deterministisch über App-Lifetime.
 */
export function hashStable(input: string): string {
  return createHash('sha256').update(input).digest('hex').slice(0, 16);
}
