// ═══════════════════════════════════════════════════════════════════════
// CRITERIA-PRIVACY v1.0.0 — Capa de seudonimización previa
// ─────────────────────────────────────────────────────────────────────
// Sustituye identificadores directos por testigos no reversibles ANTES de
// que el texto salga de la infraestructura europea hacia un modelo de
// lenguaje, y los repone en la respuesta.
//
// Marco: medida suplementaria de las Recomendaciones EDPB 01/2020 sobre
// transferencias internacionales (Schrems II, TJUE C-311/18). El mapa de
// reposición vive únicamente en memoria del worker en la UE y muere con
// la petición: nunca se persiste ni se envía a ningún tercero.
//
// Qué detecta (España + genéricos):
//   correo · teléfono · NIF/NIE/CIF · IBAN · tarjeta (Luhn) · nº Seg. Social
//
// Qué NO detecta: nombres propios y direcciones postales. La detección
// fiable de esos elementos exige análisis semántico, que implicaría
// justamente la transferencia que se pretende evitar. Se cubren por
// instrucción de minimización, no por esta capa. Declararlo es parte del
// cumplimiento: una medida técnica solo protege de lo que efectivamente
// cubre.
// ═══════════════════════════════════════════════════════════════════════

export interface ResultadoSeudonimizacion {
  texto: string;                    // texto con testigos, apto para salir del EEE
  mapa: Map<string, string>;        // testigo → valor original (solo en memoria UE)
  detecciones: Record<string, number>;
  hubo: boolean;
}

const LETRAS_NIF = "TRWAGMYFPDXBNJZSQVHLCKE";

function nifValido(numero: string, letra: string): boolean {
  const n = parseInt(numero, 10);
  if (!Number.isFinite(n)) return false;
  return LETRAS_NIF[n % 23] === letra.toUpperCase();
}

function nieValido(inicial: string, resto: string, letra: string): boolean {
  const pos = { X: "0", Y: "1", Z: "2" }[inicial.toUpperCase()];
  if (pos === undefined) return false;
  return nifValido(pos + resto, letra);
}

function luhn(digitos: string): boolean {
  let suma = 0, alterna = false;
  for (let i = digitos.length - 1; i >= 0; i--) {
    let d = digitos.charCodeAt(i) - 48;
    if (d < 0 || d > 9) return false;
    if (alterna) { d *= 2; if (d > 9) d -= 9; }
    suma += d; alterna = !alterna;
  }
  return suma % 10 === 0;
}

// El orden importa: lo más específico primero, para que un IBAN no se
// fragmente en trozos que luego parezcan otra cosa.
const REGLAS: Array<{
  tipo: string;
  re: RegExp;
  valida?: (m: RegExpExecArray) => boolean;
}> = [
  {
    tipo: "IBAN",
    re: /\b[A-Z]{2}\d{2}[ -]?(?:[A-Za-z0-9]{4}[ -]?){3,7}[A-Za-z0-9]{1,4}\b/g,
  },
  {
    tipo: "EMAIL",
    re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
  },
  {
    tipo: "TARJETA",
    re: /\b(?:\d[ -]?){13,19}\b/g,
    valida: (m) => luhn(m[0].replace(/[ -]/g, "")),
  },
  {
    tipo: "NIF",
    re: /\b(\d{8})[ -]?([A-HJ-NP-TV-Za-hj-np-tv-z])\b/g,
    valida: (m) => nifValido(m[1], m[2]),
  },
  {
    tipo: "NIE",
    re: /\b([XYZxyz])[ -]?(\d{7})[ -]?([A-HJ-NP-TV-Za-hj-np-tv-z])\b/g,
    valida: (m) => nieValido(m[1], m[2], m[3]),
  },
  {
    tipo: "CIF",
    re: /\b[ABCDEFGHJNPQRSUVWabcdefghjnpqrsuvw][ -]?\d{7}[ -]?[0-9A-Ja-j]\b/g,
  },
  {
    tipo: "SEGSOCIAL",
    re: /\b\d{2}[ /-]?\d{8,10}[ /-]?\d{2}\b/g,
  },
  {
    // Teléfono español: fijo o móvil, con o sin prefijo +34, admitiendo
    // cualquier agrupación (629107771 · 629 10 77 71 · 881 940 783)
    tipo: "TELEFONO",
    re: /(?:\+34[ -]?)?\b[6-9](?:[ -]?\d){8}\b/g,
  },
];

/**
 * Seudonimiza un texto antes de enviarlo fuera del EEE.
 * Los testigos usan delimitadores poco frecuentes para que el modelo no
 * los confunda con contenido y los devuelva intactos.
 */
export function seudonimizar(entrada: string): ResultadoSeudonimizacion {
  let texto = String(entrada ?? "");
  const mapa = new Map<string, string>();
  const detecciones: Record<string, number> = {};
  const inverso = new Map<string, string>(); // valor original → testigo (mismo valor, mismo testigo)

  for (const regla of REGLAS) {
    const contador = { n: 0 };
    texto = texto.replace(regla.re, (...args) => {
      const m = args as unknown as RegExpExecArray;
      const original = m[0];

      // Nunca tocar un testigo ya insertado
      if (original.includes("⟦") || original.includes("⟧")) return original;
      if (regla.valida && !regla.valida(m)) return original;

      const yaVisto = inverso.get(original);
      if (yaVisto) return yaVisto;

      contador.n += 1;
      const total = (detecciones[regla.tipo] ?? 0) + 1;
      detecciones[regla.tipo] = total;
      const testigo = `⟦${regla.tipo}_${total}⟧`;
      mapa.set(testigo, original);
      inverso.set(original, testigo);
      return testigo;
    });
  }

  return { texto, mapa, detecciones, hubo: mapa.size > 0 };
}

/** Repone los valores originales en la respuesta del modelo, ya en la UE. */
export function reponer(texto: string, mapa: Map<string, string>): string {
  let salida = String(texto ?? "");
  for (const [testigo, original] of mapa) {
    salida = salida.split(testigo).join(original);
  }
  return salida;
}

/** Seudonimiza una conversación completa conservando un único mapa. */
export function seudonimizarConversacion(
  mensajes: Array<{ role: string; content: string }>,
): { mensajes: Array<{ role: string; content: string }>; mapa: Map<string, string>; detecciones: Record<string, number> } {
  const mapa = new Map<string, string>();
  const detecciones: Record<string, number> = {};
  const salida = mensajes.map((m) => {
    const r = seudonimizar(m.content);
    for (const [k, v] of r.mapa) mapa.set(k, v);
    for (const [k, v] of Object.entries(r.detecciones)) detecciones[k] = (detecciones[k] ?? 0) + v;
    return { role: m.role, content: r.texto };
  });
  return { mensajes: salida, mapa, detecciones };
}

/** Resumen sin datos personales, apto para registro de auditoría. */
export function resumenAuditoria(detecciones: Record<string, number>): string {
  const partes = Object.entries(detecciones).map(([t, n]) => `${t}:${n}`);
  return partes.length ? partes.join(" ") : "sin_identificadores";
}
