// KI-Shield — Offscreen-Extraktor.
// Extrahiert Text aus PDF/DOCX/Bild rein im Browser (portiert aus der
// KIShieldGuard-Webapp: pdf.js getTextContent, mammoth.extractRawText,
// Tesseract v5 'deu+eng'/OEM 1). Der extrahierte Text geht zurück an den
// Service Worker → content.js fügt ihn ins Eingabefeld ein → normale
// Server-Maskierung greift. NICHTS verlässt hier den Rechner.
//
// Zwei Ladewege, gleiche Logik:
//   Chrome  — als Offscreen-Dokument (chrome.offscreen), Aufruf per Message.
//   Safari  — als Skript der Background-PAGE (kein chrome.offscreen vorhanden),
//             Aufruf direkt über self.KIShieldExtractor (siehe Ende der Datei).
// Deshalb heißt die Log-Konstante hier EXTRACTOR_NAME und nicht EXTRACTOR_NAME:
// in der Safari-Background-Page liegt background.js im SELBEN globalen Scope,
// und ein zweites `const EXTRACTOR_NAME` wäre dort ein sofortiger SyntaxError.

const EXTRACTOR_NAME = 'KI-Shield';
const RESULT_BATCH_CHARS = 100000;
const JOB_IDLE_TTL_MS = 6 * 60 * 60 * 1000;
const PDF_TEXT_LAYER_MIN_CHARS = 24;
const OCR_WORKER_INIT_TIMEOUT_MS = 25000;
const extractionJobs = new Map();

// pdf.js braucht seinen Worker als eigene Extension-URL (nicht Blob → CSP-safe).
try {
  if (self.pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
    pdfjsLib.GlobalWorkerOptions.workerSrc = chrome.runtime.getURL('vendor/js/pdf.worker.min.js');
  }
} catch (_) {}

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
  if (!msg || typeof msg.target !== 'string') return false;
  let operation = null;
  if (msg.target === 'offscreen-extract') operation = () => extract(msg);
  if (msg.target === 'offscreen-job-start') operation = () => startExtractionJob(msg);
  if (msg.target === 'offscreen-job-chunk') operation = () => appendExtractionChunk(msg);
  if (msg.target === 'offscreen-job-finish') operation = () => finishExtractionJob(msg);
  if (msg.target === 'offscreen-job-result') operation = () => readExtractionBatch(msg);
  if (msg.target === 'offscreen-job-release') operation = () => releaseExtractionJob(msg);
  if (!operation) return false;
  Promise.resolve()
    .then(operation)
    .then((result) => sendResponse(result))
    .catch((error) => sendResponse({
      ok: false,
      error: String((error && error.message) || error),
    }));
  return true;
});

function dataUrlToArrayBuffer(dataUrl) {
  const comma = dataUrl.indexOf(',');
  const b64 = dataUrl.slice(comma + 1);
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes.buffer;
}

function base64ToBytes(value) {
  const bin = atob(String(value || ''));
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes;
}

function validJobId(value) {
  const jobId = String(value || '');
  if (!/^[a-zA-Z0-9_.-]{8,160}$/.test(jobId)) {
    throw new Error('Ungueltige Dokument-Job-ID.');
  }
  return jobId;
}

function pruneExtractionJobs() {
  const threshold = Date.now() - JOB_IDLE_TTL_MS;
  for (const [jobId, job] of extractionJobs.entries()) {
    if ((job.updatedAt || job.createdAt || 0) < threshold) extractionJobs.delete(jobId);
  }
}

function startExtractionJob(payload) {
  pruneExtractionJobs();
  const jobId = validJobId(payload.jobId);
  extractionJobs.set(jobId, {
    jobId,
    name: String(payload.name || 'Dokument'),
    mime: String(payload.mime || ''),
    declaredSize: Math.max(0, Number(payload.size) || 0),
    receivedSize: 0,
    chunks: [],
    resultBatches: null,
    createdAt: Date.now(),
    updatedAt: Date.now(),
  });
  return { ok: true, jobId };
}

function appendExtractionChunk(payload) {
  const jobId = validJobId(payload.jobId);
  const job = extractionJobs.get(jobId);
  if (!job) throw new Error('Dokument-Job nicht gefunden.');
  if (job.resultBatches) throw new Error('Dokument-Job ist bereits abgeschlossen.');
  const bytes = base64ToBytes(payload.chunk);
  job.chunks.push(bytes);
  job.receivedSize += bytes.byteLength;
  job.updatedAt = Date.now();
  return { ok: true, receivedSize: job.receivedSize };
}

function combineUploadChunks(job) {
  const bytes = new Uint8Array(job.receivedSize);
  let offset = 0;
  for (const chunk of job.chunks) {
    bytes.set(chunk, offset);
    offset += chunk.byteLength;
  }
  job.chunks = [];
  return bytes;
}

function splitResultPage(page) {
  const text = String((page && page.text) || '');
  if (text.length <= RESULT_BATCH_CHARS) return [{
    number: page.number,
    text,
    source: page.source,
  }];
  const result = [];
  for (let start = 0; start < text.length; start += RESULT_BATCH_CHARS) {
    result.push({
      number: page.number,
      text: text.slice(start, start + RESULT_BATCH_CHARS),
      source: page.source,
      continuation: start > 0,
    });
  }
  return result;
}

function batchResultPages(pages) {
  const batches = [];
  let current = [];
  let currentChars = 0;
  for (const page of pages || []) {
    for (const fragment of splitResultPage(page)) {
      if (current.length && currentChars + fragment.text.length > RESULT_BATCH_CHARS) {
        batches.push(current);
        current = [];
        currentChars = 0;
      }
      current.push(fragment);
      currentChars += fragment.text.length;
    }
  }
  if (current.length || batches.length === 0) batches.push(current);
  return batches;
}

async function finishExtractionJob(payload) {
  const jobId = validJobId(payload.jobId);
  const job = extractionJobs.get(jobId);
  if (!job) throw new Error('Dokument-Job nicht gefunden.');
  if (job.resultBatches) throw new Error('Dokument-Job ist bereits abgeschlossen.');
  if (job.declaredSize && job.receivedSize !== job.declaredSize) {
    throw new Error(
      `Dokument unvollstaendig empfangen (${job.receivedSize}/${job.declaredSize} Bytes).`
    );
  }
  const bytes = combineUploadChunks(job);
  const result = await extractBuffer(bytes.buffer, job.mime, job.name, { jobId });
  job.resultBatches = batchResultPages(result.pages);
  job.pageCount = result.pages.length;
  job.ocrPageCount = result.ocrPageCount || 0;
  job.updatedAt = Date.now();
  return {
    ok: true,
    jobId,
    pageCount: job.pageCount,
    ocrPageCount: job.ocrPageCount,
    batchCount: job.resultBatches.length,
  };
}

function readExtractionBatch(payload) {
  const jobId = validJobId(payload.jobId);
  const job = extractionJobs.get(jobId);
  if (!job || !job.resultBatches) throw new Error('Dokument-Ergebnis nicht gefunden.');
  const index = Math.floor(Number(payload.index));
  if (!Number.isInteger(index) || index < 0 || index >= job.resultBatches.length) {
    throw new Error('Ungueltiger Ergebnisabschnitt.');
  }
  job.updatedAt = Date.now();
  return { ok: true, pages: job.resultBatches[index] };
}

function releaseExtractionJob(payload) {
  const jobId = validJobId(payload.jobId);
  extractionJobs.delete(jobId);
  return { ok: true };
}

async function extract(payload) {
  const result = await extractBuffer(
    dataUrlToArrayBuffer(payload.dataUrl),
    payload.mime,
    payload.name,
    { jobId: payload.jobId }
  );
  return {
    ok: true,
    text: result.pages.map((page) => page.text).join('\n\n').trim(),
    pages: result.pages,
    pageCount: result.pages.length,
    ocrPageCount: result.ocrPageCount || 0,
  };
}

async function extractBuffer(arrayBuffer, mime, name, options) {
  const m = String(mime || '').toLowerCase();
  const n = String(name || '').toLowerCase();
  if (m === 'application/pdf' || n.endsWith('.pdf')) {
    return extractPDF(arrayBuffer, options);
  }
  if (m.includes('wordprocessingml') || m.includes('msword') || n.endsWith('.docx')) {
    const text = await extractDOCX(arrayBuffer);
    return { pages: [{ number: 1, text: (text || '').trim(), source: 'text' }], ocrPageCount: 0 };
  }
  if (m.startsWith('image/') || /\.(png|jpe?g|webp|bmp|gif|tiff?)$/.test(n)) {
    const text = await extractImage(arrayBuffer, m, options);
    return { pages: [{ number: 1, text: (text || '').trim(), source: 'ocr' }], ocrPageCount: 1 };
  }
  if (m.startsWith('text/') || /\.(txt|csv|md|json|log)$/.test(n)) {
    const text = new TextDecoder('utf-8').decode(arrayBuffer);
    return { pages: [{ number: 1, text: (text || '').trim(), source: 'text' }], ocrPageCount: 0 };
  }
  throw new Error('Nicht unterstuetzter Dateityp: ' + (mime || name || 'unbekannt'));
}

function shouldOcrPdfPage(text) {
  return String(text || '').replace(/\s+/g, '').length < PDF_TEXT_LAYER_MIN_CHARS;
}

async function reportProgress(jobId, details) {
  if (!jobId) return;
  const message = Object.assign({
    type: 'kishield-extract-progress',
    jobId,
  }, details || {});
  try {
    // Safari setzt hier eine eigene Senke, die den Fortschritt per
    // tabs.sendMessage an das Content-Script des anfragenden Tabs zustellt.
    // runtime.sendMessage erreicht Content-Scripts NICHT — es adressiert nur
    // Extension-Kontexte (Background/Popup/Offscreen).
    if (typeof self.KIShieldProgressSink === 'function') {
      await self.KIShieldProgressSink(message);
      return;
    }
    await chrome.runtime.sendMessage(message);
  } catch (_) {}
}

async function createOcrWorker(jobId, timeoutMs = OCR_WORKER_INIT_TIMEOUT_MS) {
  if (!self.Tesseract) throw new Error('OCR-Bibliothek nicht geladen.');
  const safeTimeoutMs = Math.max(1, Number(timeoutMs) || OCR_WORKER_INIT_TIMEOUT_MS);
  let timeoutHandle = null;
  let timedOut = false;

  await reportProgress(jobId, {
    phase: 'ocr-init',
    stage: 'starting',
    ocr: true,
  });

  const workerPromise = Promise.resolve().then(() => Tesseract.createWorker('deu+eng', 1, {
    cacheMethod: 'none',
    corePath: chrome.runtime.getURL('vendor/tesseract/'),
    workerPath: chrome.runtime.getURL('vendor/tesseract/worker.min.js'),
    langPath: chrome.runtime.getURL('vendor/tesseract/lang/'),
    workerBlobURL: false,
    logger: (message) => {
      const status = String((message && message.status) || '');
      const progress = Math.max(0, Math.min(1, Number(message && message.progress) || 0));
      if (status && status !== 'recognizing text') {
        void reportProgress(jobId, {
          phase: 'ocr-init',
          stage: 'progress',
          status,
          progress,
          ocr: true,
        });
      }
      if (message && message.status === 'recognizing text') {
        console.log(
          '[' + EXTRACTOR_NAME + '][OCR] ' +
          Math.round(progress * 100) + '%'
        );
      }
    },
    errorHandler: (error) => console.error('[' + EXTRACTOR_NAME + '][OCR worker]', error),
  })).then((worker) => {
    if (timedOut && worker && typeof worker.terminate === 'function') {
      Promise.resolve(worker.terminate()).catch(() => {});
    }
    return worker;
  });

  try {
    const worker = await Promise.race([
      workerPromise,
      new Promise((_resolve, reject) => {
        timeoutHandle = setTimeout(() => {
          timedOut = true;
          reject(new Error(
            'OCR-Initialisierung hat das Zeitlimit von ' +
            Math.ceil(safeTimeoutMs / 1000) +
            ' Sekunden überschritten.'
          ));
        }, safeTimeoutMs);
      }),
    ]);
    if (timeoutHandle) clearTimeout(timeoutHandle);
    await reportProgress(jobId, {
      phase: 'ocr-init',
      stage: 'ready',
      ocr: true,
    });
    return worker;
  } catch (error) {
    if (timeoutHandle) clearTimeout(timeoutHandle);
    await reportProgress(jobId, {
      phase: 'ocr-init',
      stage: timedOut ? 'timeout' : 'error',
      error: String((error && error.message) || error),
      ocr: true,
    });
    if (timedOut) throw error;
    throw new Error(
      'OCR-Initialisierung fehlgeschlagen: ' +
      String((error && error.message) || error)
    );
  }
}

async function extractPDF(arrayBuffer, options) {
  if (!self.pdfjsLib) throw new Error('PDF-Bibliothek nicht verfuegbar.');
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
  const pages = [];
  let ocrWorker = null;
  let ocrPageCount = 0;
  try {
    for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
      await reportProgress(options && options.jobId, {
        phase: 'page',
        page: pageNumber,
        totalPages: pdf.numPages,
        ocr: false,
      });
      const page = await pdf.getPage(pageNumber);
      const content = await page.getTextContent();
      let text = content.items.map((item) => item.str).join(' ').trim();
      let source = 'text';
      if (shouldOcrPdfPage(text)) {
        if (!ocrWorker) ocrWorker = await createOcrWorker(options && options.jobId);
        const viewport = page.getViewport({ scale: 1.75 });
        const canvas = document.createElement('canvas');
        canvas.width = Math.max(1, Math.ceil(viewport.width));
        canvas.height = Math.max(1, Math.ceil(viewport.height));
        const canvasContext = canvas.getContext('2d', { alpha: false });
        await reportProgress(options && options.jobId, {
          phase: 'ocr',
          page: pageNumber,
          totalPages: pdf.numPages,
          ocr: true,
        });
        try {
          await page.render({ canvasContext, viewport }).promise;
          const ocrResult = await ocrWorker.recognize(canvas);
          text = String((ocrResult && ocrResult.data && ocrResult.data.text) || '').trim();
          source = 'ocr';
          ocrPageCount++;
        } finally {
          canvas.width = 0;
          canvas.height = 0;
        }
      }
      pages.push({ number: pageNumber, text, source });
      try { if (page.cleanup) page.cleanup(); } catch (_) {}
    }
  } finally {
    if (ocrWorker) {
      try { await ocrWorker.terminate(); } catch (_) {}
    }
  }
  return { pages, ocrPageCount };
}

async function extractDOCX(arrayBuffer) {
  if (!self.mammoth) throw new Error('DOCX-Bibliothek nicht verfuegbar.');
  const result = await mammoth.extractRawText({ arrayBuffer: arrayBuffer });
  return result.value;
}

async function extractImage(arrayBuffer, mime, options) {
  const worker = await createOcrWorker(options && options.jobId);
  try {
    await reportProgress(options && options.jobId, {
      phase: 'ocr',
      page: 1,
      totalPages: 1,
      ocr: true,
    });
    const result = await worker.recognize(new Blob([arrayBuffer], { type: mime || 'image/png' }));
    return result.data.text;
  } finally {
    try { await worker.terminate(); } catch (_) {}
  }
}

// Direkter Einstieg für Builds ohne chrome.offscreen (Safari). Dort lädt die
// Background-PAGE diese Datei im selben Kontext und ruft die Extraktion ohne
// Message-Umweg auf. In Chrome bleibt der Export ungenutzt — der Aufruf läuft
// dort weiter über den onMessage-Listener oben.
self.KIShieldExtractor = {
  startExtractionJob,
  appendExtractionChunk,
  finishExtractionJob,
  readExtractionBatch,
  releaseExtractionJob,
  extract,
};

console.log('[' + EXTRACTOR_NAME + '] Extraktor bereit.');
