/**
 * KI-Shield Proxy — gemeinsamer, seiteneffektfreier Kern.
 *
 * Diese Datei läuft als klassisches Script in Popup, ISOLATED- und MAIN-World
 * und lässt sich zugleich direkt mit Node testen. Keine Chrome-APIs, kein DOM.
 */
(function exposeCore(root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  if (root) root.KIShieldCore = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function createCore() {
  'use strict';

  const STORAGE = Object.freeze({
    ENABLED: 'kishield_enabled',
    LIVE_DETECTION: 'kishield_live_detection',
    DISABLED_TYPES: 'kishield_disabled_types',
    MANUAL_TERMS: 'kishield_manual_terms',
    NEVER_TERMS: 'kishield_never_terms',
    MANUAL_COUNTER: 'kishield_manual_counter',
  });

  const PII_NAMES = Object.freeze({
    PERSON: 'Person',
    EMAIL: 'E-Mail',
    PHONE_NUMBER: 'Telefonnummer',
    IBAN: 'IBAN',
    TAX_ID: 'Steuer-ID',
    DATE_OF_BIRTH: 'Geburtsdatum',
    DATE_TIME: 'Datum/Uhrzeit',
    LICENSE_PLATE: 'Kennzeichen',
    SOCIAL_SECURITY: 'Sozialversicherungsnummer',
    ADDRESS: 'Adresse',
    HEALTH_DATA: 'Gesundheitsdaten',
    LOCATION: 'Standort',
    NRP: 'Nationale Kennung',
    ORGANIZATION: 'Organisation',
    CREDIT_CARD: 'Kreditkarte',
    ID_DOCUMENT: 'Ausweisdokument',
    IP_ADDRESS: 'IP-Adresse',
    BIC_SWIFT: 'BIC/SWIFT',
    VIN_NUMBER: 'Fahrzeug-Identnummer',
    GPS_COORDINATES: 'GPS-Koordinaten',
    DRIVERS_LICENSE: 'Führerschein',
    HEALTH_INSURANCE_NR: 'Krankenversicherungsnummer',
    HANDELSREGISTER: 'Handelsregister',
    AKTENZEICHEN: 'Aktenzeichen',
    MAC_ADDRESS: 'MAC-Adresse',
    IMEI_NUMBER: 'IMEI-Nummer',
    SECRET_CREDENTIAL: 'Zugangsdaten',
    REFERENCE_NUMBER: 'Referenznummer',
    GENETIC_DATA: 'Genetische Daten',
    BIOMETRIC_DATA: 'Biometrische Daten',
    ETHNIC_ORIGIN: 'Ethnische Herkunft',
    POLITICAL_OPINION: 'Politische Meinung',
    RELIGIOUS_BELIEF: 'Religionszugehörigkeit',
    UNION_MEMBERSHIP: 'Gewerkschaftszugehörigkeit',
    SEXUAL_ORIENTATION: 'Sexuelle Orientierung',
    CRIMINAL_DATA: 'Strafrechtliche Daten',
    CHILD_DATA: 'Kinderdaten',
    FINANCIAL_DATA: 'Finanzdaten',
    EMPLOYMENT_DATA: 'Beschäftigungsdaten',
    EDUCATION_DATA: 'Bildungsdaten',
    SOCIAL_BENEFITS: 'Sozialleistungen',
    INSURANCE_DATA: 'Versicherungsdaten',
  });

  const PII_GROUPS = Object.freeze([
    Object.freeze({
      label: 'Identität & Kontakt',
      types: Object.freeze([
        'PERSON', 'EMAIL', 'PHONE_NUMBER', 'ADDRESS', 'LOCATION',
        'DATE_OF_BIRTH', 'DATE_TIME', 'ID_DOCUMENT', 'DRIVERS_LICENSE',
        'NRP', 'ORGANIZATION',
      ]),
    }),
    Object.freeze({
      label: 'Finanzen & Verträge',
      types: Object.freeze([
        'IBAN', 'BIC_SWIFT', 'CREDIT_CARD', 'TAX_ID', 'FINANCIAL_DATA',
        'INSURANCE_DATA', 'SOCIAL_BENEFITS', 'HANDELSREGISTER',
        'AKTENZEICHEN', 'REFERENCE_NUMBER',
      ]),
    }),
    Object.freeze({
      label: 'Technik & Kennungen',
      types: Object.freeze([
        'IP_ADDRESS', 'MAC_ADDRESS', 'IMEI_NUMBER', 'VIN_NUMBER',
        'LICENSE_PLATE', 'GPS_COORDINATES', 'SECRET_CREDENTIAL',
        'SOCIAL_SECURITY', 'HEALTH_INSURANCE_NR',
      ]),
    }),
    Object.freeze({
      label: 'Arbeit & Bildung',
      types: Object.freeze(['EMPLOYMENT_DATA', 'EDUCATION_DATA']),
    }),
    Object.freeze({
      label: 'Besonders sensible Daten',
      types: Object.freeze([
        'HEALTH_DATA', 'GENETIC_DATA', 'BIOMETRIC_DATA', 'ETHNIC_ORIGIN',
        'POLITICAL_OPINION', 'RELIGIOUS_BELIEF', 'UNION_MEMBERSHIP',
        'SEXUAL_ORIENTATION', 'CRIMINAL_DATA', 'CHILD_DATA',
      ]),
    }),
  ]);

  const ALL_PII_TYPES = Object.freeze(PII_GROUPS.flatMap((group) => group.types));
  // Der Server erlaubt seit 2026-07-17 alle Kategorien als per-Request-
  // Ausschluss. Die Exporte bleiben leer für Abwärtskompatibilität.
  const SERVER_LOCKED_PII_TYPES = Object.freeze([]);
  const LOCKED_PII_TYPES = Object.freeze([]);
  const ALL_SET = new Set(ALL_PII_TYPES);

  const MAPPING_TTL_MS = 24 * 60 * 60 * 1000;
  // Die Freigabe gilt weiterhin nur für den exakt geprüften Text. Gemini kann
  // den prompt-tragenden StreamGenerate-Request deutlich verzögert auslösen.
  const SEND_APPROVAL_TTL_MS = 120 * 1000;

  function sanitizeDisabledTypes(input) {
    if (!Array.isArray(input)) return [];
    return [...new Set(input)]
      .filter((type) => typeof type === 'string' && ALL_SET.has(type))
      .sort();
  }

  function enableAllPii() {
    return [];
  }

  function enabledPiiCount(disabled) {
    return ALL_PII_TYPES.length - sanitizeDisabledTypes(disabled).length;
  }

  function allPiiEnabled(disabled) {
    return sanitizeDisabledTypes(disabled).length === 0;
  }

  function protectionEnabled(storedValue) {
    return storedValue !== false;
  }

  function liveDetectionEnabled(storedValue) {
    return storedValue === true;
  }

  function manualPseudo(type, counter) {
    const raw = String(type || 'PERSON').toUpperCase().replace(/[^A-ZÄÖÜ_]/g, '_');
    const normalizedType = raw.replace(/^_+|_+$/g, '') || 'PERSON';
    const safeCounter = Math.max(1, Math.floor(Number(counter) || 1));
    return `${normalizedType}_MANUELL_${String(safeCounter).padStart(6, '0')}`;
  }

  function platformHost(hostname) {
    const host = String(hostname || '').toLowerCase().replace(/\.$/, '');
    if (host === 'claude.ai' || host.endsWith('.claude.ai')) return 'claude';
    if (host === 'chatgpt.com' || host.endsWith('.chatgpt.com') || host === 'chat.openai.com') return 'chatgpt';
    if (host === 'gemini.google.com' || host.endsWith('.gemini.google.com') || host === 'bard.google.com') return 'gemini';
    return null;
  }

  function safeUrl(rawUrl, currentHostname) {
    try {
      return new URL(String(rawUrl), `https://${String(currentHostname || '')}/`);
    } catch (_) {
      return null;
    }
  }

  function isStaticAsset(pathname) {
    return /\.(?:js|css|png|jpe?g|gif|svg|woff2?|ico|map)(?:$|\?)/i.test(pathname);
  }

  function isAiApiUrl(rawUrl, currentHostname) {
    const url = safeUrl(rawUrl, currentHostname);
    if (!url) return false;
    const currentPlatform = platformHost(currentHostname);
    const targetPlatform = platformHost(url.hostname);
    if (!currentPlatform || targetPlatform !== currentPlatform || isStaticAsset(url.pathname)) return false;

    const pathAndQuery = url.pathname + url.search;
    if (targetPlatform === 'claude') {
      return /\/(?:completion|chat_conversations|append_message|messages)(?:\/|$|\?)/i.test(pathAndQuery);
    }
    if (targetPlatform === 'chatgpt') {
      return /\/conversation(?:\/|$|\?)/i.test(pathAndQuery);
    }
    if (
      pathAndQuery.includes('/_/scs/') ||
      pathAndQuery.includes('/log?') ||
      pathAndQuery.includes('/gen_204') ||
      pathAndQuery.includes('/jserror')
    ) {
      return false;
    }
    return /batchexecute|StreamGenerate|BardFrontendService/i.test(pathAndQuery);
  }

  function requiresSendApproval(rawUrl, currentHostname, method) {
    if (String(method || 'GET').toUpperCase() !== 'POST') return false;
    const url = safeUrl(rawUrl, currentHostname);
    if (!url) return false;
    const currentPlatform = platformHost(currentHostname);
    if (!currentPlatform || platformHost(url.hostname) !== currentPlatform) return false;
    if (currentPlatform === 'chatgpt') {
      // Echte Prompt-Sends enden exakt auf /conversation. Steuerpfade wie
      // /conversation/init und /f/conversation/prepare transportieren keinen
      // vom Nutzer freigegebenen Prompt und dürfen keinen Fehlalarm auslösen.
      return /\/(?:f\/)?conversation\/?$/i.test(url.pathname);
    }
    if (currentPlatform === 'claude') {
      // Nur Endpunkte, die tatsächlich einen Nutzer-Prompt transportieren.
      // Conversation-Titel, GET-Replays und Metadatenpflege laufen ebenfalls
      // unter /chat_conversations, sind aber keine Sends.
      return /\/(?:completion|append_message|messages)\/?$/i.test(url.pathname) ||
        /\/chat_conversations\/?$/i.test(url.pathname);
    }
    if (currentPlatform === 'gemini') {
      // Gemini bündelt zahlreiche voneinander unabhängige Hintergrund-RPCs unter
      // /batchexecute. Sie pauschal zu sperren zerstört den Seiten-Bootstrap.
      // Der dedizierte Prompt-Kanal bleibt hart freigabepflichtig; batchexecute-
      // Promptvarianten werden zusätzlich im Network-Layer inhaltlich erkannt.
      return /StreamGenerate\/?$/i.test(url.pathname);
    }
    return false;
  }

  function sendRetriggerStrategy(platform, triggerType, hasSendButton) {
    if (String(triggerType || '').toLowerCase() !== 'enter') return 'original';
    if (String(platform || '').toLowerCase() === 'gemini' && hasSendButton) {
      return 'button-click';
    }
    return 'keyboard';
  }

  function positionFloatingPanel(anchorRect, panelSize, viewportSize) {
    const margin = 8;
    const gap = 8;
    const width = Math.max(0, Number(panelSize && panelSize.width) || 0);
    const height = Math.max(0, Number(panelSize && panelSize.height) || 0);
    const viewportWidth = Math.max(width + margin * 2, Number(viewportSize && viewportSize.width) || 0);
    const viewportHeight = Math.max(height + margin * 2, Number(viewportSize && viewportSize.height) || 0);
    const anchorTop = Number(anchorRect && anchorRect.top) || 0;
    const anchorBottom = Number(anchorRect && anchorRect.bottom) || anchorTop;
    const anchorLeft = Number(anchorRect && anchorRect.left) || 0;
    const fitsBelow = anchorBottom + gap + height <= viewportHeight - margin;
    const placement = fitsBelow ? 'below' : 'above';
    const desiredTop = fitsBelow ? anchorBottom + gap : anchorTop - height - gap;
    return {
      top: Math.min(Math.max(margin, desiredTop), viewportHeight - height - margin),
      left: Math.min(Math.max(margin, anchorLeft), viewportWidth - width - margin),
      placement,
    };
  }

  function isTagPopupInteraction(target) {
    try {
      return Boolean(
        target &&
        typeof target.closest === 'function' &&
        target.closest('#kishield-tag-popup')
      );
    } catch (_) {
      return false;
    }
  }

  function normalizeNeverTerm(rawTerm) {
    if (typeof rawTerm !== 'string') return '';
    const normalized = rawTerm.trim().replace(/\s+/g, ' ');
    return normalized.length > 0 && normalized.length <= 200 ? normalized : '';
  }

  function sanitizeNeverTerms(input) {
    const result = [];
    const seen = new Set();
    for (const rawTerm of Array.isArray(input) ? input : []) {
      const term = normalizeNeverTerm(rawTerm);
      const key = term.toLocaleLowerCase('de-DE');
      if (!term || seen.has(key)) continue;
      seen.add(key);
      result.push(term);
    }
    return result;
  }

  function addNeverTerm(existingTerms, rawTerm) {
    return sanitizeNeverTerms([...(Array.isArray(existingTerms) ? existingTerms : []), rawTerm]);
  }

  function falsePositiveFeedbackPayload(entityText, currentType) {
    const text = typeof entityText === 'string' ? entityText.trim().replace(/\s+/g, ' ') : '';
    if (!text || text.length > 1000 || !ALL_SET.has(currentType)) return null;
    return {
      message_id: null,
      entity_text: text,
      current_type: currentType,
      new_type: currentType,
      feedback_type: 'false_positive',
    };
  }

  function normalizeApprovalText(value) {
    let text = String(value == null ? '' : value);
    try { text = text.normalize('NFKC'); } catch (_) {}
    return text.replace(/\s+/gu, ' ').trim();
  }

  function stringContainsCandidate(value, candidates) {
    if (typeof value !== 'string') return false;
    if (candidates.some((candidate) => candidate && value.includes(candidate))) return true;
    const normalizedValue = normalizeApprovalText(value);
    return candidates.some((candidate) => {
      const normalizedCandidate = normalizeApprovalText(candidate);
      return normalizedCandidate && normalizedValue.includes(normalizedCandidate);
    });
  }

  function valueContainsCandidate(value, candidates, depth) {
    if (depth > 40) return false;
    if (typeof value === 'string') {
      if (stringContainsCandidate(value, candidates)) return true;
      try {
        const nested = JSON.parse(value);
        if (nested !== value) return valueContainsCandidate(nested, candidates, depth + 1);
      } catch (_) {}
      return false;
    }
    if (Array.isArray(value)) {
      return value.some((item) => valueContainsCandidate(item, candidates, depth + 1));
    }
    if (value && typeof value === 'object') {
      return Object.values(value).some((item) => valueContainsCandidate(item, candidates, depth + 1));
    }
    return false;
  }

  function collectStringValues(value, output, depth, acceptString) {
    if (depth > 40 || output.length >= 2000) return;
    if (typeof value === 'string') {
      if (acceptString !== false) output.push(value);
      return;
    }
    if (Array.isArray(value)) {
      for (const item of value) collectStringValues(item, output, depth + 1, true);
      return;
    }
    if (value && typeof value === 'object') {
      for (const [key, item] of Object.entries(value)) {
        const contentKey = /(?:text|content|prompt|message|query|input)/i.test(key);
        collectStringValues(item, output, depth + 1, typeof item !== 'string' || contentKey);
      }
    }
  }

  function structuredValueContainsCandidate(value, candidates) {
    if (valueContainsCandidate(value, candidates, 0)) return true;
    const strings = [];
    collectStringValues(value, strings, 0, true);
    if (strings.length < 2) return false;
    return stringContainsCandidate(strings.join(' '), candidates);
  }

  function bodyContainsApprovedText(bodyText, candidateTexts) {
    const text = String(bodyText == null ? '' : bodyText);
    const candidates = [...new Set((candidateTexts || []).filter((value) => typeof value === 'string' && value))];
    if (!text || candidates.length === 0) return false;
    if (stringContainsCandidate(text, candidates)) return true;

    try {
      if (structuredValueContainsCandidate(JSON.parse(text), candidates)) return true;
    } catch (_) {}

    try {
      const params = new URLSearchParams(text);
      for (const value of params.values()) {
        if (stringContainsCandidate(value, candidates)) return true;
        try {
          if (structuredValueContainsCandidate(JSON.parse(value), candidates)) return true;
        } catch (_) {}
      }
    } catch (_) {}
    return false;
  }

  function geminiStreamRequestCarriesPrompt(bodyText) {
    const text = String(bodyText == null ? '' : bodyText);
    if (!text) return true;

    try {
      const encodedRequest = new URLSearchParams(text).get('f.req');
      if (!encodedRequest) return true;
      const outer = JSON.parse(encodedRequest);
      if (!Array.isArray(outer) || outer.length < 2 || typeof outer[1] !== 'string') return true;

      const envelope = outer[1].trim();
      if (!envelope) return false;
      try {
        const nested = JSON.parse(envelope);
        if (!Array.isArray(nested) || !Array.isArray(nested[0])) return true;
        const promptSlot = nested[0][0];
        if (promptSlot == null) return false;
        if (typeof promptSlot !== 'string') return true;
        return normalizeApprovalText(promptSlot).length > 0;
      } catch (_) {
        // Ältere Gemini-Varianten legen den Prompt direkt in outer[1] ab.
        return normalizeApprovalText(envelope).length > 0;
      }
    } catch (_) {
      // Unbekannte Formate bleiben fail-closed freigabepflichtig.
      return true;
    }
  }

  function formatDocumentPages(pages) {
    const normalized = (Array.isArray(pages) ? pages : [])
      .map((page) => {
        if (page && typeof page === 'object' && 'text' in page) return String(page.text || '');
        return String(page == null ? '' : page);
      });
    if (normalized.length === 0) return '';
    if (normalized.length === 1) return normalized[0];
    return normalized.map((pageText, index) => (
      `===== Seite ${index + 1} von ${normalized.length} =====\n${pageText}\n`
    )).join('\n');
  }

  function splitDocumentText(text, maxChars) {
    const source = String(text == null ? '' : text);
    if (!source) return [];
    const limit = Math.max(1000, Math.floor(Number(maxChars) || 40000));
    const chunks = [];
    let start = 0;
    while (start < source.length) {
      const hardEnd = Math.min(source.length, start + limit);
      if (hardEnd === source.length) {
        chunks.push(source.slice(start));
        break;
      }
      const softStart = start + Math.floor(limit * 0.6);
      const windowText = source.slice(start, hardEnd);
      const boundaries = ['\n\n', '\n', '. ', '; ', ', ', ' '];
      let cut = hardEnd;
      for (const boundary of boundaries) {
        const relative = windowText.lastIndexOf(boundary);
        const candidate = relative < 0 ? -1 : start + relative + boundary.length;
        if (candidate >= softStart) {
          cut = candidate;
          break;
        }
      }
      if (cut <= start) cut = hardEnd;
      chunks.push(source.slice(start, cut));
      start = cut;
    }
    return chunks;
  }

  function splitDocumentPages(pages, maxChars) {
    return splitDocumentText(formatDocumentPages(pages), maxChars);
  }

  return Object.freeze({
    STORAGE,
    PII_NAMES,
    PII_GROUPS,
    ALL_PII_TYPES,
    SERVER_LOCKED_PII_TYPES,
    LOCKED_PII_TYPES,
    MAPPING_TTL_MS,
    SEND_APPROVAL_TTL_MS,
    sanitizeDisabledTypes,
    enableAllPii,
    enabledPiiCount,
    allPiiEnabled,
    protectionEnabled,
    liveDetectionEnabled,
    manualPseudo,
    platformHost,
    isAiApiUrl,
    requiresSendApproval,
    sendRetriggerStrategy,
    positionFloatingPanel,
    isTagPopupInteraction,
    sanitizeNeverTerms,
    addNeverTerm,
    falsePositiveFeedbackPayload,
    bodyContainsApprovedText,
    geminiStreamRequestCarriesPrompt,
    formatDocumentPages,
    splitDocumentText,
    splitDocumentPages,
  });
});
