/**
 * Einmal-Bereinigung für Pentest-Reste (Runde 12 / 2026-05-18):
 *
 * 1. HTML-Tags aus Customer/User-Stringfeldern strippen (M2-Stored-XSS-Reste)
 * 2. Unbekannte App-Settings entfernen, die durch Mass-Assignment in die DB
 *    gerutscht sind, BEVOR die Whitelist eingezogen wurde (M1-Reste).
 *
 * Idempotent: wenn nichts zu tun ist, ändert sich nichts. Bei Bedarf
 * mehrfach aufrufbar.
 */
import prisma from '../src/lib/prisma.js';
import { stripHtml, isValidDocumentPath as isValidDocumentPathShared } from '../src/utils/sanitize.js';
import { ALLOWED_SETTING_KEYS } from '../src/services/appSetting.service.js';

const CUSTOMER_STRING_FIELDS = [
  'salutation', 'firstName', 'lastName', 'companyName',
  'birthPlace', 'email', 'phone', 'mobile',
  'taxNumber', 'commercialRegisterNumber', 'notes',
];

const USER_STRING_FIELDS = [
  'firstName', 'lastName', 'email',
  'whatsappNumber', 'telegramUsername', 'signalNumber',
];

async function cleanupXss() {
  const customers = await prisma.customer.findMany();
  let touched = 0;
  for (const c of customers) {
    const updates: Record<string, string> = {};
    for (const field of CUSTOMER_STRING_FIELDS) {
      const v = (c as any)[field];
      if (typeof v === 'string') {
        const cleaned = stripHtml(v) as string;
        if (cleaned !== v) updates[field] = cleaned;
      }
    }
    if (Object.keys(updates).length > 0) {
      console.log(`  Customer #${c.id}: bereinigt:`, Object.keys(updates).join(', '));
      await prisma.customer.update({ where: { id: c.id }, data: updates });
      touched++;
    }
  }
  console.log(`  → Customer bereinigt: ${touched}`);

  const users = await prisma.user.findMany();
  let userTouched = 0;
  for (const u of users) {
    const updates: Record<string, string> = {};
    for (const field of USER_STRING_FIELDS) {
      const v = (u as any)[field];
      if (typeof v === 'string') {
        const cleaned = stripHtml(v) as string;
        if (cleaned !== v) updates[field] = cleaned;
      }
    }
    if (Object.keys(updates).length > 0) {
      console.log(`  User #${u.id}: bereinigt:`, Object.keys(updates).join(', '));
      await prisma.user.update({ where: { id: u.id }, data: updates });
      userTouched++;
    }
  }
  console.log(`  → User bereinigt: ${userTouched}`);
}

// HTML in Plain-Text-Settings strippen: WYSIWYG-Editoren liefern
// absichtlich HTML, alles andere (companyName, defaultEmailDomain, ...)
// muss reiner Text bleiben. Pentest 2026-05-19, MEDIUM.
const HTML_ALLOWED_SETTING_KEYS = new Set([
  'authorizationTemplateHtml',
  'imprintHtml',
  'privacyPolicyHtml',
  'websitePrivacyPolicyHtml',
]);

function stripHtmlString(s: string): string {
  return s
    .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
    .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
    .replace(/<\/?[a-z][^>]*>/gi, '')
    .replace(/(?:javascript|data|vbscript)\s*:/gi, 'blocked:');
}

// Legitime CustomerConsent.source-Werte. Alles andere wird beim Cleanup
// auf 'unknown' normalisiert. Pentest 2026-05-20.
const ALLOWED_CONSENT_SOURCES = new Set([
  'portal',
  'public-link',
  'telefon',
  'papier',
  'email',
  'crm-backend',
]);

// Legitimer documentPath: relativer Pfad unter uploads/, keine ".."-Segmente.
// Pentest 27.1 (2026-05-20). Helper jetzt zentral in utils/sanitize – wir
// re-exportieren hier nur, damit der bestehende Code keinen Schaden nimmt
// und wir nur EINE Quelle der Wahrheit pflegen müssen.
const isValidDocumentPath = isValidDocumentPathShared;

async function cleanupConsents() {
  // version + documentPath: HTML strippen (waren ohne Validierung).
  // source: Whitelist erzwingen.
  // documentPath zusätzlich gegen Pfad-Traversal absichern (27.1).
  let versionStripped = 0;
  let pathNulled = 0;
  let sourceFixed = 0;
  const consents = await prisma.customerConsent.findMany({
    select: { id: true, source: true, documentPath: true, version: true },
  });
  for (const c of consents) {
    const data: Record<string, string | null> = {};
    if (c.version && c.version !== stripHtmlString(c.version)) {
      data.version = stripHtmlString(c.version);
      versionStripped++;
    }
    if (c.documentPath && !isValidDocumentPath(c.documentPath)) {
      // ".../etc/passwd", "<script>", "javascript:..." etc. → NULL.
      // Legitime Uploads bleiben unberührt (siehe isValidDocumentPath).
      data.documentPath = null;
      pathNulled++;
    }
    if (c.source && !ALLOWED_CONSENT_SOURCES.has(c.source)) {
      data.source = 'unknown';
      sourceFixed++;
    }
    if (Object.keys(data).length > 0) {
      await prisma.customerConsent.update({ where: { id: c.id }, data });
    }
  }
  console.log(
    `  → Consent bereinigt: version-stripped=${versionStripped}, ` +
    `documentPath-genullt=${pathNulled}, source-whitelist=${sourceFixed}`,
  );
}

// documentPath in den weiteren Tabellen prüfen. Schreibend wird er
// server-seitig vom multer-Upload erzeugt – falls dort doch mal ein
// dreckiger Wert reingerutscht ist (z.B. aus einem importierten Backup
// vor unseren Sanitization-Fixes), nullen wir ihn hier raus.
// ContractDocument hat documentPath NOT NULL → wir berichten dort nur,
// löschen aber nicht (Records müssten manuell angeschaut werden).
async function cleanupDocumentPaths() {
  const findings: { table: string; id: number; value: string }[] = [];

  const optional: Array<{
    label: string;
    fetch: () => Promise<{ id: number; documentPath: string | null }[]>;
    update: (id: number) => Promise<unknown>;
  }> = [
    {
      label: 'BankCard',
      fetch: () => prisma.bankCard.findMany({ select: { id: true, documentPath: true } }),
      update: (id) => prisma.bankCard.update({ where: { id }, data: { documentPath: null } }),
    },
    {
      label: 'IdentityDocument',
      fetch: () => prisma.identityDocument.findMany({ select: { id: true, documentPath: true } }),
      update: (id) => prisma.identityDocument.update({ where: { id }, data: { documentPath: null } }),
    },
    {
      label: 'Invoice',
      fetch: () => prisma.invoice.findMany({ select: { id: true, documentPath: true } }),
      update: (id) => prisma.invoice.update({ where: { id }, data: { documentPath: null } }),
    },
    {
      label: 'RepresentativeAuthorization',
      fetch: () => prisma.representativeAuthorization.findMany({
        select: { id: true, documentPath: true },
      }),
      update: (id) => prisma.representativeAuthorization.update({
        where: { id }, data: { documentPath: null },
      }),
    },
  ];

  let nulled = 0;
  for (const t of optional) {
    const rows = await t.fetch();
    for (const r of rows) {
      if (r.documentPath && !isValidDocumentPath(r.documentPath)) {
        findings.push({ table: t.label, id: r.id, value: r.documentPath.slice(0, 80) });
        await t.update(r.id);
        nulled++;
      }
    }
  }

  // ContractDocument: documentPath ist NOT NULL → wir berichten nur.
  const contractDocs = await prisma.contractDocument.findMany({
    select: { id: true, documentPath: true },
  });
  let contractDocsDirty = 0;
  for (const d of contractDocs) {
    if (!isValidDocumentPath(d.documentPath)) {
      findings.push({ table: 'ContractDocument', id: d.id, value: d.documentPath.slice(0, 80) });
      contractDocsDirty++;
    }
  }

  console.log(`  → documentPath bereinigt: ${nulled} genullt, ${contractDocsDirty} ContractDocument-Records auffällig (NOT NULL, manuell prüfen)`);
  for (const f of findings.slice(0, 10)) {
    console.log(`     [${f.table}#${f.id}] "${f.value}"`);
  }
}

async function reportOrphanedUsers() {
  // User ohne jegliche Rollenzuordnung können sich zwar einloggen, sind aber
  // im Permission-System unsichtbar. Meist Überrest von gescheiterten Seeds
  // oder manuellen DB-Edits. Wir löschen NICHT (könnte legitime
  // Spezial-User treffen) – nur warnen.
  const orphans = await prisma.user.findMany({
    where: { roles: { none: {} } },
    select: { id: true, email: true, createdAt: true },
  });
  if (orphans.length === 0) {
    console.log('  → Keine User ohne Rollenzuordnung.');
    return;
  }
  console.log(`  ⚠️  ${orphans.length} User ohne Rollenzuordnung:`);
  for (const u of orphans.slice(0, 10)) {
    console.log(`     [User#${u.id}] ${u.email} (created ${u.createdAt.toISOString()})`);
  }
  console.log('     → Rolle zuweisen oder User löschen.');
}

async function cleanupAppSettings() {
  const settings = await prisma.appSetting.findMany();
  const removed: string[] = [];
  let stripped = 0;
  for (const s of settings) {
    if (!ALLOWED_SETTING_KEYS.has(s.key)) {
      removed.push(s.key);
      await prisma.appSetting.delete({ where: { key: s.key } });
      continue;
    }
    if (!HTML_ALLOWED_SETTING_KEYS.has(s.key)) {
      const cleaned = stripHtmlString(s.value);
      if (cleaned !== s.value) {
        await prisma.appSetting.update({ where: { key: s.key }, data: { value: cleaned } });
        stripped++;
      }
    }
  }
  console.log(`  → AppSettings entfernt: ${removed.length}${removed.length ? ' (' + removed.join(', ') + ')' : ''}`);
  if (stripped > 0) {
    console.log(`  → AppSettings HTML-gestrippt: ${stripped}`);
  }
}

// Pattern, die auf typische Pentest-/Test-Daten hindeuten. Bewusst eng
// gefasst – legitime Kunden mit "Hacker" als Nachnamen sollen nicht
// fälschlich getroffen werden (gibt's reichlich, gerade hier).
// Konkret weggelassen: `^hacker@` würde Verwandte/Kunden mit
// `hacker@familie-hacker.de` o.ä. fängen.
const PENTEST_MARKERS = [
  /@evil\./i,
  /^attacker@/i,
  /^pentest@/i,
  /<script\b/i,        // unverwechselbarer XSS-Marker
  /\bonerror\s*=/i,    // <img onerror=…>
  /javascript:/i,      // javascript:-URL
  /'\s*OR\s*'1'\s*=\s*'1/i, // SQL-Injection
  /\.\.\/.*etc\/passwd/i,   // Path-Traversal
];

function looksLikePentestData(value: unknown): boolean {
  if (typeof value !== 'string') return false;
  return PENTEST_MARKERS.some((re) => re.test(value));
}

async function findOrPurgePentestRecords() {
  const purge = process.env.CLEANUP_PURGE_PENTEST === 'true';
  const suspect: Array<{ kind: string; id: number; reason: string }> = [];

  const customers = await prisma.customer.findMany();
  for (const c of customers) {
    for (const f of ['email', 'phone', 'mobile', 'firstName', 'lastName', 'companyName', 'notes']) {
      if (looksLikePentestData((c as any)[f])) {
        suspect.push({ kind: 'Customer', id: c.id, reason: `${f}=${JSON.stringify((c as any)[f]).slice(0, 60)}` });
        break;
      }
    }
  }
  const users = await prisma.user.findMany();
  for (const u of users) {
    for (const f of ['email', 'firstName', 'lastName']) {
      if (looksLikePentestData((u as any)[f])) {
        suspect.push({ kind: 'User', id: u.id, reason: `${f}=${JSON.stringify((u as any)[f]).slice(0, 60)}` });
        break;
      }
    }
  }

  if (suspect.length === 0) {
    console.log('  → Keine Pentest-Marker in Customer/User-Records gefunden.');
    return;
  }

  console.log(`  → ${suspect.length} verdächtige Records (Pentest-Marker):`);
  for (const s of suspect) {
    console.log(`     [${s.kind}#${s.id}] ${s.reason}`);
  }

  if (!purge) {
    console.log('  ℹ️  Zum Löschen Container mit CLEANUP_PURGE_PENTEST=true neu starten,');
    console.log('     oder Records manuell über adminer entfernen.');
    return;
  }

  for (const s of suspect) {
    if (s.kind === 'Customer') {
      await prisma.customer.delete({ where: { id: s.id } }).catch((e: any) => {
        console.log(`     [Customer#${s.id}] Löschen fehlgeschlagen: ${e.message?.slice(0, 80)}`);
      });
    } else if (s.kind === 'User') {
      await prisma.user.delete({ where: { id: s.id } }).catch((e: any) => {
        console.log(`     [User#${s.id}] Löschen fehlgeschlagen: ${e.message?.slice(0, 80)}`);
      });
    }
  }
  console.log(`  → ${suspect.length} verdächtige Records gelöscht.`);
}

async function main() {
  console.log('=== Cleanup: XSS-Reste + Mass-Assignment-AppSettings ===');
  await cleanupXss();
  await cleanupAppSettings();
  await cleanupConsents();
  await cleanupDocumentPaths();
  await reportOrphanedUsers();
  await findOrPurgePentestRecords();
  console.log('=== Fertig. ===');
}

main()
  .catch((e) => {
    console.error('Cleanup fehlgeschlagen:', e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
