'use strict';

const PROFILE_REMOVE = 'remove Image';
const PROFILE_NOTHING = 'do nothing';

const RESIZE_ALGORITHMS = [
  { value: 'auto',     label: browser.i18n.getMessage('resize_auto') },
  { value: 'lanczos',  label: browser.i18n.getMessage('resize_lanczos') },
  { value: 'mitchell', label: browser.i18n.getMessage('resize_mitchell') },
  { value: 'bicubic',  label: browser.i18n.getMessage('resize_bicubic') },
  { value: 'bilinear', label: browser.i18n.getMessage('resize_bilinear') },
  { value: 'nearest',  label: browser.i18n.getMessage('resize_nearest') },
];

const PREDEFINED_TEMPLATES = {
  '1. visually lossless': {
    preserveMeta: true, crossConvert: true, scale: false, antialias: 2.0, resizeAlgorithm: 'auto',
    jpegQuality: 90, chromaSubsampling: '444', maxColors: 256, minQuality: 75, maxQuality: 100,
    dithering: 0.5, speed: 1, oxipngAlpha: true, oxipngLevel: 4,
  },
  '2. quality optimized': {
    preserveMeta: true, crossConvert: true, scale: false, antialias: 2.0, resizeAlgorithm: 'auto',
    jpegQuality: 83, chromaSubsampling: '444', maxColors: 256, minQuality: 60, maxQuality: 85,
    dithering: 0.5, speed: 1, oxipngAlpha: true, oxipngLevel: 4,
  },
  '3. size optimized': {
    preserveMeta: true, crossConvert: true, scale: true, antialias: 1.5, resizeAlgorithm: 'auto',
    jpegQuality: 76, chromaSubsampling: '420', maxColors: 256, minQuality: 40, maxQuality: 65,
    dithering: 0.5, speed: 1, oxipngAlpha: true, oxipngLevel: 4,
  },
  '4. pedantic': {
    preserveMeta: false, crossConvert: true, scale: true, antialias: 1.3, resizeAlgorithm: 'auto',
    jpegQuality: 70, chromaSubsampling: '420', maxColors: 128, minQuality: 20, maxQuality: 45,
    dithering: 0.5, speed: 1, oxipngAlpha: true, oxipngLevel: 5,
  },
};

const DEFAULT_TEMPLATE = {
 preserveMeta: true, crossConvert: true, scale: false, antialias: 2.0, resizeAlgorithm: 'auto',
 jpegQuality: 83, chromaSubsampling: '444', maxColors: 256, minQuality: 60, maxQuality: 85,
 dithering: 0.5, speed: 1, oxipngAlpha: true, oxipngLevel: 4,
};

const DEFAULT_SETTINGS = {
  sendMode: 'ask', minFileSize: 1, minSavingsPercent: 10, replaceText: browser.i18n.getMessage('default_replaceText'),
  attachProfile: '2. quality optimized', inlineProfile: '2. quality optimized', quoteProfile: '3. size optimized',
  precalc: true, workerPoolSize: 1, convertWebp: false, convertGif: true, convertBmp: true,
  templates: {}, excludedHashes: [],
};

let settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
// ── Worker-Pool: parallele Worker (Anzahl in den Einstellungen, Default 1), ──
// ── jeder mit eigenen WASM-Instanzen ──
const WORKER_POOL_SIZE_MAX = 8;
const workers = [];
let workerRoundRobin = 0;

function getWorkerPoolSize() {
  return Math.min(WORKER_POOL_SIZE_MAX, Math.max(1, settings.workerPoolSize ?? 1));
}
let workerIdleTimer = null;
const WORKER_IDLE_TIMEOUT = 3 * 60 * 1000; // 3 Minuten
const pending = new Map();
let nextRequestId = 0;
const processing = new Set();
const pendingSendConfirmations = new Map();
const tabSignatures = new Map();
const tabStates = new Map();
const attachmentBackups = new Map();
const dialogImageState = new Map();
let dialogImageCounter = 0;

const dialogExcludedImages = new Map();

function getDialogState(tabId) {
  if (!dialogImageState.has(tabId)) dialogImageState.set(tabId, new Map());
  return dialogImageState.get(tabId);
}

function getExcludedImages(tabId) {
  if (!dialogExcludedImages.has(tabId)) dialogExcludedImages.set(tabId, []);
  return dialogExcludedImages.get(tabId);
}

function getMinFileSizeBytes() { return (settings.minFileSize || 0) * 1024; }
function getMinSavingsRatio() { return (settings.minSavingsPercent ?? 10) / 100; }

function isExcluded(hash) {
  return hash && settings.excludedHashes && settings.excludedHashes.includes(hash);
}

async function computeHash(bytes) {
  const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
  return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
}

function base64ToBytes(base64) {
  const binaryString = atob(base64);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
  return bytes;
}

function getTemplate(name) {
  if (PREDEFINED_TEMPLATES[name]) return PREDEFINED_TEMPLATES[name];
  return settings.templates?.[name] || DEFAULT_TEMPLATE;
}

function getCategoryState(state, category) {
  const profileName = state[category + 'Profile'] || settings[category + 'Profile'] || '2. quality optimized';
  if (profileName === PROFILE_REMOVE || profileName === PROFILE_NOTHING) {
    return { cross: false, scale: false, antialias: 1.0, preserveMeta: true };
  }
  const t = getTemplate(profileName);
  return { cross: t.crossConvert ?? true, scale: category === 'attach' ? false : (t.scale ?? false),
        antialias: category === 'attach' ? 1.0 : (t.antialias ?? 1.0), preserveMeta: t.preserveMeta ?? true };
}

function getCategoryOptions(category) {
  const t = getTemplate(settings[category + 'Profile'] || '2. quality optimized');
  return { preserveMeta: t.preserveMeta ?? true, antialias: t.antialias ?? 1.0, resizeAlgorithm: t.resizeAlgorithm ?? 'auto', jpegQuality: t.jpegQuality ?? 83, chromaSubsampling: t.chromaSubsampling ?? '444', maxColors: t.maxColors ?? 256, minQuality: t.minQuality ?? 60, maxQuality: t.maxQuality ?? 85, dithering: t.dithering ?? 0.5, speed: t.speed ?? 1, oxipngLevel: t.oxipngLevel ?? 4, oxipngAlpha: t.oxipngAlpha ?? true };
}

function getProfileOptions(profileName) {
  const t = getTemplate(profileName);
  return { preserveMeta: t.preserveMeta ?? true, antialias: t.antialias ?? 1.0, resizeAlgorithm: t.resizeAlgorithm ?? 'auto', jpegQuality: t.jpegQuality ?? 83, chromaSubsampling: t.chromaSubsampling ?? '444', maxColors: t.maxColors ?? 256, minQuality: t.minQuality ?? 60, maxQuality: t.maxQuality ?? 85, dithering: t.dithering ?? 0.5, speed: t.speed ?? 1, oxipngLevel: t.oxipngLevel ?? 4, oxipngAlpha: t.oxipngAlpha ?? true };
}

function getProfileState(profileName) {
  if (profileName === PROFILE_REMOVE || profileName === PROFILE_NOTHING) return { cross: false, scale: false };
  const t = getTemplate(profileName);
  return { cross: t.crossConvert ?? true, scale: t.scale ?? false };
}

function getTabState(tabId) {
  if (!tabStates.has(tabId)) {
    tabStates.set(tabId, { sendMode: settings.sendMode, attachProfile: settings.attachProfile, inlineProfile: settings.inlineProfile, quoteProfile: settings.quoteProfile, resizeAlgorithm: 'auto' });
  }
  return tabStates.get(tabId);
}

function getTabSignatures(tabId) { if (!tabSignatures.has(tabId)) tabSignatures.set(tabId, new Set()); return tabSignatures.get(tabId); }
function getAttachmentBackups(tabId) { if (!attachmentBackups.has(tabId)) attachmentBackups.set(tabId, []); return attachmentBackups.get(tabId); }

browser.tabs.onRemoved.addListener((tabId) => { tabStates.delete(tabId); tabSignatures.delete(tabId); attachmentBackups.delete(tabId); dialogImageState.delete(tabId); dialogExcludedImages.delete(tabId); cacheClearTab(tabId); });

async function loadSettings() {
  const result = await browser.storage.local.get('settings');
  settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
  if (result.settings) {
    Object.assign(settings, result.settings);
    if (!settings.templates) settings.templates = {};
    if (!settings.excludedHashes) settings.excludedHashes = [];
    if (settings.quoteMode === 'replace-boxes') { settings.quoteProfile = PROFILE_REMOVE; delete settings.quoteMode; }
    delete settings.quoteMode;
  }
}

// Beendet den Worker nach Leerlauf, damit die WASM-Heaps (libimagequant,
// cjpegli, oxipng — nach großen Bildern u. U. mehrere hundert MB) freigegeben
// werden. acquireWorker() baut den Pool bei Bedarf transparent neu auf.
function scheduleWorkerTeardown() {
  if (workerIdleTimer) clearTimeout(workerIdleTimer);
  workerIdleTimer = setTimeout(() => {
    if (workers.length > 0 && pending.size === 0) {
      for (const w of workers) w.terminate();
      workers.length = 0;
      console.log('[PPP] Worker-Pool nach Inaktivität beendet (WASM-Speicher freigegeben)');
    }
  }, WORKER_IDLE_TIMEOUT);
}

function spawnWorker() {
  const w = new Worker(browser.runtime.getURL('worker.js'), { type: 'module' });
  w.pppIds = new Set(); // offene Job-IDs dieses Workers (für gezieltes onerror-Reject)
  w.onmessage = (e) => {
    const { id, bytes, format, error, stages } = e.data;
    const cb = pending.get(id);
    if (!cb) return;
    pending.delete(id);
    w.pppIds.delete(id);
    if (error) { cb.reject(new Error(error)); }
    else { cb.resolve({ bytes: new Uint8Array(bytes), format, stages: stages || null }); }
    if (pending.size === 0) scheduleWorkerTeardown();
  };
  w.onerror = (e) => {
    console.error('[PPP] Worker-Fehler:', e.message || e);
    for (const id of w.pppIds) {
      const cb = pending.get(id);
      if (cb) { pending.delete(id); cb.reject(new Error(browser.i18n.getMessage('error_workerError', e.message || browser.i18n.getMessage('text_unknown')))); }
    }
    const idx = workers.indexOf(w);
    if (idx >= 0) workers.splice(idx, 1);
    if (workers.length === 0 && workerIdleTimer) { clearTimeout(workerIdleTimer); workerIdleTimer = null; }
  };
  return w;
}

function acquireWorker() {
  const target = getWorkerPoolSize();
  // Überschüssige Idle-Worker abbauen (z. B. nach Verringerung in den Einstellungen).
  // Beschäftigte Worker bleiben bis zum Leerlauf-Teardown bestehen.
  for (let i = workers.length - 1; i >= 0 && workers.length > target; i--) {
    if (workers[i].pppIds.size === 0) {
      workers[i].terminate();
      workers.splice(i, 1);
    }
  }
  while (workers.length < target) workers.push(spawnWorker());
  const w = workers[workerRoundRobin % workers.length];
  workerRoundRobin = (workerRoundRobin + 1) % workers.length;
  return w;
}

function compressImage(bytes, options, format, tryPng, tryJpeg, targetWidth, targetHeight, resizePercent, forcePng) {
  return postWorkerJob({ bytes, options, format, tryPng: tryPng || false, tryJpeg: tryJpeg || false, targetWidth: targetWidth || null, targetHeight: targetHeight || null, resizePercent: resizePercent || 100, forcePng: forcePng || false });
}

// Generischer Job-Dispatcher auf den Worker-Pool. Uint8Array-Felder werden
// als Transferables übergeben (Zero-Copy).
function postWorkerJob(payload) {
  return new Promise((resolve, reject) => {
    const w = acquireWorker();
    if (workerIdleTimer) { clearTimeout(workerIdleTimer); workerIdleTimer = null; }
    const id = nextRequestId++;
    pending.set(id, { resolve, reject });
    w.pppIds.add(id);
    const transfers = [];
    for (const key of ['bytes', 'originalBytes', 'pngBytes']) {
      if (payload[key] instanceof Uint8Array) {
        const buf = payload[key].buffer.slice(payload[key].byteOffset, payload[key].byteOffset + payload[key].byteLength);
        payload[key] = buf;
        transfers.push(buf);
      }
    }
    w.postMessage({ id, ...payload }, transfers);
  });
}

// ════════════════════════════════════════════════════════════════════════════
// Time-Memory-Tradeoff: Ergebnis-Cache + Stufen-Cache + Pre-Kalkulation
// ════════════════════════════════════════════════════════════════════════════
// Der Cache lebt im Background-Skript und überlebt damit den Worker-Idle-
// Teardown. Er ist pro Tab namenspaciert und wird beim Schließen des Tabs
// bzw. beim Senden der E-Mail geleert (cacheClearTab).

const CACHE_MAX_BYTES = 512 * 1024 * 1024; // LRU-Obergrenze
const resultCache = new Map(); // key → { bytes: Uint8Array, format, size }
let resultCacheBytes = 0;
let cacheHits = 0;
let cacheMisses = 0;

function cacheSetRaw(key, bytes, format) {
  const size = bytes.byteLength;
  const old = resultCache.get(key);
  if (old) resultCacheBytes -= old.size;
  resultCache.delete(key); // Reinsert ans Ende (LRU-Reihenfolge)
  resultCache.set(key, { bytes, format, size });
  resultCacheBytes += size;
  while (resultCacheBytes > CACHE_MAX_BYTES && resultCache.size > 0) {
    const oldestKey = resultCache.keys().next().value;
    const oldest = resultCache.get(oldestKey);
    resultCacheBytes -= oldest.size;
    resultCache.delete(oldestKey);
  }
}

function cacheGetRaw(key) {
  const e = resultCache.get(key);
  if (!e) return null;
  resultCache.delete(key); // Touch: ans Ende verschieben
  resultCache.set(key, e);
  return e;
}

function cacheClearTab(tabId) {
  const prefix = tabId + '|';
  let freed = 0;
  let removed = 0;
  for (const [k, v] of resultCache) {
    if (k.startsWith(prefix)) { freed += v.size; resultCache.delete(k); removed++; }
  }
  resultCacheBytes -= freed;
  if (removed > 0) console.log(`[PPP] Cache Tab ${tabId} geleert: ${removed} Einträge, ${formatBytes(freed)}`);
}

function stableStringify(obj) {
  if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
  if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
  return '{' + Object.keys(obj).sort().map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}';
}

// Finaler Schlüssel: Eingangs-Hash + alle Parameter, die das Ergebnis beeinflussen.
function finalCacheKey(tabId, inputHash, format, options, tryPng, tryJpeg, targetWidth, targetHeight, resizePercent, forcePng) {
  return `${tabId}|F|${inputHash}|${format}|${stableStringify(options)}|${tryPng ? 1 : 0}${tryJpeg ? 1 : 0}|${targetWidth || 0}x${targetHeight || 0}|${resizePercent || 100}|${forcePng ? 1 : 0}`;
}

// pngquant-Stufe: nur Parameter, die die pngquant-Ausgabe beeinflussen.
function pqStageKey(tabId, inputHash, format, options, targetWidth, targetHeight, resizePercent) {
  const p = {
    maxColors: options?.maxColors ?? 256,
    minQuality: options?.minQuality ?? 0,
    maxQuality: options?.maxQuality ?? 100,
    dithering: options?.dithering ?? 0.5,
    speed: options?.speed ?? 1,
    resizeAlgorithm: options?.resizeAlgorithm ?? 'auto',
  };
  return `${tabId}|S|${inputHash}|pq|${format}|${stableStringify(p)}|${targetWidth || 0}x${targetHeight || 0}|${resizePercent || 100}`;
}

// oxipng-Stufe: keyed über den Hash des oxipng-EINGANGS (pngquant-Ausgabe) —
// Wiederverwendung nur bei identischem Eingangsbild. Wert ist das PNG
// VOR der Metadaten-Injektion (Metadaten werden bei Bedarf separat injiziert).
function oxStageKey(tabId, pqHash, options) {
  const p = { level: options?.oxipngLevel ?? 2, alpha: options?.oxipngAlpha !== false };
  return `${tabId}|S|${pqHash}|ox|${stableStringify(p)}`;
}

// Gecachte Variante von compressImage. Gleiche Signatur plus tabId vorne.
async function compressImageCached(tabId, bytes, options, format, tryPng, tryJpeg, targetWidth, targetHeight, resizePercent, forcePng) {
  const inputHash = await computeHash(bytes);
  const finalKey = finalCacheKey(tabId, inputHash, format, options, tryPng, tryJpeg, targetWidth, targetHeight, resizePercent, forcePng);

  const hit = cacheGetRaw(finalKey);
  if (hit) {
    cacheHits++;
    console.log(`[PPP] Cache-Treffer (final): ${formatBytes(bytes.byteLength)} → ${formatBytes(hit.bytes.byteLength)} [${cacheHits}/${cacheHits + cacheMisses}]`);
    return { bytes: hit.bytes, format: hit.format, stages: null };
  }

  // Stufen-Fast-Path nur bei Vollbild-Läufen ohne Cross-Vergleich:
  // Der No-Resize-Check und der PNG/JPEG-Formatvergleich wären sonst nicht
  // reproduzierbar — dann wird bewusst voll neu gerechnet.
  const fullRes = (resizePercent || 100) === 100 && !targetWidth && !targetHeight;
  if (fullRes && format === 'png' && tryPng && !tryJpeg) {
    const pqHit = cacheGetRaw(pqStageKey(tabId, inputHash, format, options, targetWidth, targetHeight, resizePercent));
    if (pqHit) {
      const pqHash = await computeHash(pqHit.bytes);
      const oxHit = cacheGetRaw(oxStageKey(tabId, pqHash, options));
      const preserveMeta = options?.preserveMeta !== false;
      if (oxHit && !preserveMeta) {
        // oxipng-Ergebnis direkt verwendbar (keine Metadaten zu injizieren)
        cacheHits++;
        cacheSetRaw(finalKey, oxHit.bytes, 'png');
        console.log(`[PPP] Cache-Treffer (oxipng-Stufe): ${formatBytes(oxHit.bytes.byteLength)}`);
        return { bytes: oxHit.bytes, format: 'png', stages: null };
      }
      // pngquant-Stufe wiederverwenden: nur oxipng bzw. nur Metadaten im Worker
      const out = await postWorkerJob({
        oxipngOnly: true,
        metaOnly: !!oxHit, // oxipng schon gecacht → nur Metadaten injizieren
        originalBytes: bytes,
        pngBytes: oxHit ? oxHit.bytes : pqHit.bytes,
        options,
      });
      if (out.stages?.pngFinal && !oxHit) {
        cacheSetRaw(oxStageKey(tabId, pqHash, options), new Uint8Array(out.stages.pngFinal), 'png');
      }
      cacheSetRaw(finalKey, out.bytes, 'png');
      console.log(`[PPP] oxipng-Fast-Path (pngquant ${oxHit ? '+ oxipng ' : ''}wiederverwendet): ${formatBytes(bytes.byteLength)} → ${formatBytes(out.bytes.byteLength)}`);
      return { bytes: out.bytes, format: 'png', stages: null };
    }
  }

  cacheMisses++;
  const result = await compressImage(bytes, options, format, tryPng, tryJpeg, targetWidth, targetHeight, resizePercent, forcePng);
  cacheSetRaw(finalKey, result.bytes, result.format);

  // Vom Worker mitgelieferte Zwischenstufen in den Cache legen
  if (result.stages) {
    try {
      if (result.stages.pqBytes) {
        const pqBytes = new Uint8Array(result.stages.pqBytes);
        cacheSetRaw(pqStageKey(tabId, inputHash, format, options, targetWidth, targetHeight, resizePercent), pqBytes, 'png');
        if (result.stages.pngFinal) {
          const pqHash = await computeHash(pqBytes);
          cacheSetRaw(oxStageKey(tabId, pqHash, options), new Uint8Array(result.stages.pngFinal), 'png');
        }
      }
    } catch (e) { console.warn('[PPP] Stufen-Caching fehlgeschlagen:', e.message); }
  }
  return result;
}

// ── Pre-Kalkulation ─────────────────────────────────────────────────────────
// Berechnet ein Bild im hinterlegten Profil und legt es NUR in den Cache.
// Kein DOM-Eingriff, kein Anhangs-Tausch.
const selfAddedAttachments = new Set(); // "name:size" — von der Extension selbst hinzugefügt

async function precalcImageBytes(tabId, original, format, category, displayW, displayH, naturalW, naturalH) {
  try {
    if (settings.precalc === false) return; // Pre-Compute abgeschaltet
    if (!original || original.byteLength < getMinFileSizeBytes()) return;
    if (getIgnoredInputReason(original)) return;
    const hash = await computeHash(original);
    if (isExcluded(hash)) return;
    const tabState = getTabState(tabId);
    const cat = category === 'attach' ? 'attach' : (category === 'quote' ? 'quote' : 'inline');
    const profileName = tabState[cat + 'Profile'] || settings[cat + 'Profile'] || '2. quality optimized';
    if (profileName === PROFILE_REMOVE || profileName === PROFILE_NOTHING) return;
    const options = getProfileOptions(profileName);
    const cs = getCategoryState(tabState, cat);
    const pipeFormat = getPipelineFormat(format);
    const tryPng = pipeFormat === 'png' ? true : cs.cross;
    const tryJpeg = pipeFormat === 'jpeg' ? true : cs.cross;
    if (!tryPng && !tryJpeg) return;
    // Zielauflösung exakt wie in composeScript.js (processInlineImages) ableiten,
    // damit die Cache-Schlüssel beim späteren Anwenden übereinstimmen.
    let targetWidth = null;
    let targetHeight = null;
    if (cat !== 'attach' && cs.scale && displayW > 0 && displayH > 0) {
      const f = (typeof cs.antialias === 'number' && cs.antialias > 0) ? cs.antialias : 1.0;
      targetWidth = Math.max(1, Math.round(displayW * f));
      targetHeight = Math.max(1, Math.round(displayH * f));
      if (naturalW > 0 && targetWidth > naturalW) targetWidth = naturalW;
      if (naturalH > 0 && targetHeight > naturalH) targetHeight = naturalH;
    }
    await compressImageCached(tabId, original, options, pipeFormat, tryPng, tryJpeg, targetWidth, targetHeight, 100, false);
  } catch (e) {
    console.warn('[PPP] Pre-Kalkulation fehlgeschlagen:', e.message);
  }
}

// ── Eingabeformate ─────────────────────────────────────────────────────────
// Zusätzlich zu PNG/JPEG (immer aktiv): webp, bmp, gif — schaltbar über die
// Einstellungen (convertWebp/convertGif/convertBmp). Reine EINGABEformate:
// die Ausgabe bleibt PNG oder JPEG (E-Mail-kompatibel).
// Ignoriert werden außerdem: animierte GIFs und animierte WebPs.
function isCompressible(name) {
  const n = (name || '').toLowerCase();
  if (n.endsWith('.png') || n.endsWith('.jpg') || n.endsWith('.jpeg')) return true;
  if (n.endsWith('.webp')) return isInputFormatEnabled('webp');
  if (n.endsWith('.bmp')) return isInputFormatEnabled('bmp');
  if (n.endsWith('.gif')) return isInputFormatEnabled('gif');
  return false;
}
function getFormat(name) {
  const n = (name || '').toLowerCase();
  if (n.endsWith('.jpg') || n.endsWith('.jpeg')) return 'jpeg';
  if (n.endsWith('.webp')) return 'webp';
  if (n.endsWith('.bmp')) return 'bmp';
  if (n.endsWith('.gif')) return 'gif';
  return 'png';
}
function swapExtension(name, newFormat) { const newExt = newFormat === 'jpeg' ? '.jpg' : '.png'; return name.replace(/\.(png|jpe?g|webp|bmp|gif)$/i, '') + newExt; }

// Ob ein Eingabeformat in den Einstellungen aktiviert ist (png/jpeg: immer).
function isInputFormatEnabled(format) {
  if (format === 'webp') return settings.convertWebp ?? false;
  if (format === 'bmp') return settings.convertBmp ?? true;
  if (format === 'gif') return settings.convertGif ?? true;
  return true;
}

// Pipeline-Format für den Worker: alles außer JPEG läuft über den PNG-Pfad
// (Quantisierung). webp/bmp/gif werden also zu 'png' gemappt.
function getPipelineFormat(format) { return format === 'jpeg' ? 'jpeg' : 'png'; }

const FORMAT_MIME = { jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', bmp: 'image/bmp', gif: 'image/gif' };
function formatMime(format) { return FORMAT_MIME[format] || 'image/png'; }

// ── Eingabe-Erkennung per Magic Bytes ──────────────────────────────────────
function sniffImageFormat(bytes) {
  if (!bytes || bytes.length < 12) return null;
  if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x47 && bytes[3] === 0x4E) return 'png';
  if (bytes[0] === 0xFF && bytes[1] === 0xD8) return 'jpeg';
  if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
      bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) return 'webp';
  if (bytes[0] === 0x42 && bytes[1] === 0x4D) return 'bmp';
  if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38 &&
      (bytes[4] === 0x37 || bytes[4] === 0x39) && bytes[5] === 0x61) return 'gif';
  return null;
}

// Zählt GIF-Frames über die Block-Struktur (Image Descriptor 0x2C).
// Bricht bei 2 Frames ab — mehr interessiert nicht. 0 = ungültig.
function countGifFrames(bytes) {
  if (bytes.length < 13) return 0;
  let pos = 13; // Header (6 B) + Logical Screen Descriptor (7 B)
  if (bytes[10] & 0x80) pos += 3 * (2 ** ((bytes[10] & 0x07) + 1)); // Global Color Table
  let frames = 0;
  while (pos < bytes.length) {
    const introducer = bytes[pos];
    if (introducer === 0x3B) break; // Trailer
    if (introducer === 0x21) { // Extension
      pos += 2; // Introducer + Label, danach Daten-Sub-Blöcke
    } else if (introducer === 0x2C) { // Image Descriptor
      frames++;
      if (frames > 1) return frames;
      if (pos + 10 > bytes.length) return frames;
      const packed = bytes[pos + 9];
      pos += 10;
      if (packed & 0x80) pos += 3 * (2 ** ((packed & 0x07) + 1)); // Local Color Table
      pos += 1; // LZW Minimum Code Size
    } else {
      return frames; // ungültige Struktur
    }
    while (pos < bytes.length) { // Daten-Sub-Blöcke (size-prefixed) überspringen
      const size = bytes[pos];
      pos += 1 + size;
      if (size === 0) break;
    }
  }
  return frames;
}

// Erkennt animierte WebPs anhand des ANIM-Chunks im RIFF-Container.
function hasAnimatedWebp(bytes) {
  for (let pos = 12; pos + 8 <= bytes.length;) {
    const size = bytes[pos + 4] | (bytes[pos + 5] << 8) | (bytes[pos + 6] << 16) | (bytes[pos + 7] * 0x1000000);
    if (bytes[pos] === 0x41 && bytes[pos + 1] === 0x4E && bytes[pos + 2] === 0x49 && bytes[pos + 3] === 0x4D) return true; // 'ANIM'
    pos += 8 + size + (size & 1);
  }
  return false;
}

// Grund, warum eine Eingabedatei ignoriert wird — oder null.
// - webp/bmp/gif, wenn in den Einstellungen deaktiviert
// - animierte GIFs (≠ 1 Frame) und animierte WebPs (ANIM-Chunk)
function getIgnoredInputReason(bytes) {
  const sniffed = sniffImageFormat(bytes);
  if (!sniffed) return null;
  if (!isInputFormatEnabled(sniffed)) return `${sniffed.toUpperCase()} in den Einstellungen deaktiviert — ignoriert`;
  if (sniffed === 'gif' && countGifFrames(bytes) !== 1) return 'animiertes GIF — ignoriert';
  if (sniffed === 'webp' && hasAnimatedWebp(bytes)) return 'animiertes WebP — ignoriert';
  return null;
}
function formatBytes(n) { if (n < 1024) return n + ' B'; if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'; return (n / 1048576).toFixed(1) + ' MB'; }

async function sendToTab(tabId, message) {
  try {
    return await browser.tabs.sendMessage(tabId, message);
  } catch (err) {
    // FIX: composeScripts.register() wirkt nur auf NEU geöffnete Compose-
    // Fenster. Für bereits offene Fenster (z. B. Extension neu geladen bei
    // offenem Entwurf) muss direkt in den Tab injiziert werden.
    console.warn('[PPP] Compose-Skript nicht erreichbar, injiziere direkt in Tab', tabId);
    try {
      await browser.tabs.executeScript(tabId, { file: 'composeScript.js' });
      return await browser.tabs.sendMessage(tabId, message);
    } catch (retryErr) {
      console.error('[PPP] Compose-Skript nicht injizierbar:', retryErr.message || retryErr);
      return null;
    }
  }
}

async function createThumbnail(file, maxDim = 240) {
  try {
    const bitmap = await createImageBitmap(file);
    const origW = bitmap.width; const origH = bitmap.height;
    const scale = Math.min(1, maxDim / Math.max(origW, origH));
    const w = Math.max(1, Math.round(origW * scale)); const h = Math.max(1, Math.round(origH * scale));
    const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h;
    canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h); bitmap.close();
    return { preview: canvas.toDataURL('image/jpeg', 0.85), width: origW, height: origH };
  } catch (e) { return { preview: null, width: 0, height: 0 }; }
}


// ── Metadaten-Erkennung: true, wenn das Bild preserve-Metadaten enthält ─────
// PNG: Chunks aus dem Preserve-Set (tEXt, iTXt, zTXt, eXIf, iCCP, sRGB, gAMA,
// cHRM, pHYs, tIME, sTER). JPEG: APP1–APP15 oder COM (APP0/JFIF wird ignoriert,
// da praktisch in jedem JPEG vorhanden).
const META_PNG_CHUNKS = new Set(['tEXt', 'iTXt', 'zTXt', 'eXIf', 'iCCP', 'sRGB', 'gAMA', 'cHRM', 'pHYs', 'tIME', 'sTER']);

function hasImageMetadata(bytes, format) {
  if (!bytes || bytes.length < 8) return false;
  if (format === 'png') {
    if (bytes[0] !== 0x89 || bytes[1] !== 0x50 || bytes[2] !== 0x47) return false;
    let offset = 8;
    while (offset + 8 <= bytes.length) {
      const length = (bytes[offset] * 0x1000000) + ((bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]);
      const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
      if (type === 'IDAT') break;
      if (META_PNG_CHUNKS.has(type)) return true;
      offset += 12 + length;
    }
    return false;
  }
  if (bytes[0] !== 0xFF || bytes[1] !== 0xD8) return false;
  let offset = 2;
  while (offset + 3 < bytes.length) {
    if (bytes[offset] !== 0xFF) { offset++; continue; }
    let marker = bytes[offset + 1];
    while (marker === 0xFF && offset + 2 < bytes.length) { offset++; marker = bytes[offset + 1]; }
    if (marker === 0xDA) break;
    if (marker === 0xD8 || marker === 0xD9 || (marker >= 0xD0 && marker <= 0xD7)) { offset += 2; continue; }
    const length = (bytes[offset + 2] << 8) | bytes[offset + 3];
    if (offset + 2 + length > bytes.length) break;
    if ((marker >= 0xE1 && marker <= 0xEF) || marker === 0xFE) return true;
    offset += 2 + length;
  }
  return false;
}

async function gatherImages(tabId) {
  const state = getDialogState(tabId);
  const hadExistingState = state.size > 0;
  const attachOrder = new Map();
  if (hadExistingState) { for (const [id, img] of state) { if (img.category === 'attach') { const origName = img.originalFile?.name || img.name; attachOrder.set(origName, parseInt(id.replace('img-', ''), 10)); } } }
  state.clear();
  // FIX (Multi-Fenster): dialogImageCounter wird NICHT zurückgesetzt. Der
  // Zähler ist global; ein Reset könnte bei zwei gleichzeitig geöffneten
  // Dialogen IDs erzeugen, die in der State-Map des anderen Fensters bereits
  // existieren — state.set() würde dort Einträge überschreiben. Die IDs
  // müssen nur pro Tab eindeutig sein, ein monotoner Zähler reicht.
  const excludedImages = getExcludedImages(tabId); excludedImages.length = 0;
  const images = [];
  const profileNames = [PROFILE_REMOVE, PROFILE_NOTHING, ...Object.keys(PREDEFINED_TEMPLATES), ...Object.keys(settings.templates || {})];
  const profileSettings = {};
  for (const name of profileNames) { if (name === PROFILE_REMOVE || name === PROFILE_NOTHING) continue; const t = getTemplate(name); profileSettings[name] = { preserveMeta: t.preserveMeta ?? true, scale: t.scale ?? false, antialias: t.antialias ?? 1.0 }; }
  const tabState = getTabState(tabId);
  const defaults = { attach: tabState.attachProfile || settings.attachProfile || '2. quality optimized', inline: tabState.inlineProfile || settings.inlineProfile || '2. quality optimized', quote: tabState.quoteProfile || settings.quoteProfile || '3. size optimized' };
  const minBytes = getMinFileSizeBytes(); const seenPayloads = new Map(); const backups = getAttachmentBackups(tabId);

  try {
    const rawAttachments = await browser.compose.listAttachments(tabId);
    const attachments = rawAttachments.map(att => { const backup = backups.find(b => b.name === att.name); const origName = backup ? backup.originalFile.name : att.name; return { att, order: attachOrder.get(origName) ?? 9999 }; }).sort((a, b) => a.order - b.order);
    for (const { att } of attachments) {
      if (!isCompressible(att.name)) continue;
      if ((att.size || 0) > 0 && (att.size || 0) < minBytes) continue;
      const format = getFormat(att.name); const imageId = `img-${dialogImageCounter++}`;
      let preview = null, width = 0, height = 0; let payload = null; let hash = null; let hasMeta = false;
      const backupEntry = backups.find((b) => b.name === att.name);
      try {
        const file = await browser.compose.getAttachmentFile(att.id);
        const thumb = await createThumbnail(file); preview = thumb.preview; width = thumb.width; height = thumb.height;
        const fileBytes = new Uint8Array(await file.arrayBuffer());
        if (fileBytes.byteLength < minBytes) continue;
        hash = await computeHash(fileBytes); hasMeta = hasImageMetadata(fileBytes, format);
        if (isExcluded(hash)) { excludedImages.push({ category: 'attach', name: att.name, format, size: att.size || 0, width, height, preview, attachmentId: att.id, originalFile: file, hash }); continue; }
        payload = `attach:${fileBytes.length}:${Array.from(fileBytes.slice(0, 64)).map(b => b.toString(16).padStart(2, '0')).join('')}`;
        if (backupEntry) { state.set(imageId, { id: imageId, category: 'attach', name: backupEntry.originalFile.name, format: getFormat(backupEntry.originalFile.name), size: backupEntry.originalSize || att.size || 0, width, height, preview, attachmentId: att.id, originalFile: backupEntry.originalFile, state: 'compressed', compressedAttachmentId: att.id, compressedName: att.name, compressedFormat: format, compressedSize: att.size || fileBytes.byteLength, hash }); }
        else { state.set(imageId, { id: imageId, category: 'attach', name: att.name, format, size: att.size || 0, width, height, preview, attachmentId: att.id, originalFile: file, state: 'original', compressedAttachmentId: null, hash }); }
      } catch (e) { console.warn(`[PPP] Thumbnail für ${att.name} fehlgeschlagen:`, e.message); if (backupEntry) { state.set(imageId, { id: imageId, category: 'attach', name: backupEntry.originalFile.name, format: getFormat(backupEntry.originalFile.name), size: backupEntry.originalSize || att.size || 0, width: 0, height: 0, preview: null, attachmentId: att.id, originalFile: backupEntry.originalFile, state: 'compressed', compressedAttachmentId: att.id, compressedSize: att.size || 0, hash: null }); } else { state.set(imageId, { id: imageId, category: 'attach', name: att.name, format, size: att.size || 0, width: 0, height: 0, preview: null, attachmentId: att.id, originalFile: null, state: 'original', compressedAttachmentId: null, hash: null }); } }
      if (payload && seenPayloads.has(payload)) {
        // Duplikat: am Primär-Bild merken, damit Aktionen auf alle Kopien wirken
        const primary = state.get(seenPayloads.get(payload));
        if (primary) { if (!primary.duplicates) primary.duplicates = []; primary.duplicates.push({ attachmentId: att.id, name: att.name }); }
        state.delete(imageId); dialogImageCounter--; continue;
      }
      if (payload) seenPayloads.set(payload, imageId);
      const imgState = state.get(imageId);
      images.push({ id: imageId, category: 'attach', name: imgState.name, format: imgState.format, size: imgState.size, width, height, naturalWidth: width, naturalHeight: height, preview, state: imgState.state, compressedSize: imgState.compressedSize || null, compressedFormat: imgState.compressedFormat || null, hash: imgState.hash, hasMeta });
    }
  } catch (e) { console.warn('[PPP] Anhänge konnten nicht geladen werden:', e.message); }

  try {
    const r = await sendToTab(tabId, { type: 'get-image-list' });
    if (r && r.images) {
      for (const img of r.images) {
        if ((img.originalSize || 0) <= 0 || (img.originalSize || 0) < minBytes) continue;
        const match = img.id ? img.id.match(/^(inline|quote)-(\d+)$/) : null;
        const csCategory = match ? match[1] : img.category; const csIndex = match ? parseInt(match[2], 10) : 0;
        let hash = null; let hasMeta = false;
        if (img.payload) { try { const bytes = base64ToBytes(img.payload); hash = await computeHash(bytes); hasMeta = hasImageMetadata(bytes, img.format || 'png'); if (isExcluded(hash)) { excludedImages.push({ category: csCategory, name: csCategory === 'quote' ? 'zitat-bild' : 'inline-bild', format: img.format || 'png', size: img.originalSize || 0, width: img.width || 0, height: img.height || 0, naturalWidth: img.naturalWidth || img.width || 0, naturalHeight: img.naturalHeight || img.height || 0, preview: img.thumbnail || null, csCategory, csIndex, hash }); continue; } } catch (e) { console.warn('[PPP] Hash-Berechnung fehlgeschlagen:', e.message); } }
        const imageId = `img-${dialogImageCounter++}`; const payload = `inline:${img.payload || ''}`;
        if (img.payload && seenPayloads.has(payload)) {
          // Duplikat: am Primär-Bild merken (Kategorie + Index im Compose-Fenster)
          const primary = state.get(seenPayloads.get(payload));
          if (primary) { if (!primary.duplicates) primary.duplicates = []; primary.duplicates.push({ csCategory, csIndex }); }
          dialogImageCounter--; continue;
        }
        if (img.payload) seenPayloads.set(payload, imageId);
        const persistedState = img.removed ? 'removed' : (img.done ? (img.compressedSize != null ? 'compressed' : 'incompressible') : 'original');
        state.set(imageId, { id: imageId, category: csCategory, name: csCategory === 'quote' ? browser.i18n.getMessage('imgname_quote') : browser.i18n.getMessage('imgname_inline'), format: img.format || 'png', size: img.originalSize || 0, width: img.width || 0, height: img.height || 0, naturalWidth: img.naturalWidth || img.width || 0, naturalHeight: img.naturalHeight || img.height || 0, preview: img.thumbnail || null, csCategory, csIndex, state: persistedState, compressedSize: img.compressedSize || null, compressedFormat: img.compressedFormat || null, compressedWidth: img.compressedWidth || null, compressedHeight: img.compressedHeight || null, compressedInlineWidth: img.compressedInlineWidth || null, compressedInlineHeight: img.compressedInlineHeight || null, compressedAttachmentId: null, hash });
        images.push({ id: imageId, category: csCategory, name: csCategory === 'quote' ? browser.i18n.getMessage('imgname_quote') : browser.i18n.getMessage('imgname_inline'), format: img.format || 'png', size: img.originalSize || 0, width: img.width || 0, height: img.height || 0, naturalWidth: img.naturalWidth || img.width || 0, naturalHeight: img.naturalHeight || img.height || 0, preview: img.thumbnail || null, state: persistedState, compressedSize: img.compressedSize || null, compressedFormat: img.compressedFormat || null, compressedWidth: img.compressedWidth || null, compressedHeight: img.compressedHeight || null, compressedInlineWidth: img.compressedInlineWidth || null, compressedInlineHeight: img.compressedInlineHeight || null, hash, hasMeta });
      }
    }
  } catch (e) { console.warn('[PPP] Inline/Zitat-Bilder konnten nicht geladen werden:', e.message); }

  return { images, profiles: profileNames, defaults, resizeAlgorithms: RESIZE_ALGORITHMS, resizeAlgorithm: tabState.resizeAlgorithm, profileSettings, excludedCount: excludedImages.length };
}

async function dialogCompressImage(tabId, imageId, profileName, resizePercent, preserveMetaOverride, scaleOverride, keepInlineSize, forcePng) {
  const state = getDialogState(tabId);
  const img = state.get(imageId);
  if (!img) return { ok: false, error: browser.i18n.getMessage('error_imageNotFound') };

  if (profileName === PROFILE_REMOVE) {
    if (img.state === 'compressed' && (img.compressedAttachmentId != null || img.csIndex != null)) { await dialogUndoImage(tabId, imageId); }
    if (img.state === 'removed') return { ok: true, alreadyRemoved: true };
    if (img.category === 'attach') {
      if (!img.originalFile) { img.originalFile = await browser.compose.getAttachmentFile(img.attachmentId); }
      await browser.compose.removeAttachment(tabId, img.attachmentId);
      // Duplikat-Anhänge ebenfalls entfernen (Originale für Undo sichern)
      if (Array.isArray(img.duplicates)) {
        for (const dup of img.duplicates) {
          try {
            if (!dup.originalFile) dup.originalFile = await browser.compose.getAttachmentFile(dup.attachmentId);
            await browser.compose.removeAttachment(tabId, dup.attachmentId);
            dup.removed = true;
          } catch (e) { console.warn(`[PPP] Duplikat ${dup.name} konnte nicht entfernt werden:`, e.message); }
        }
      }
      img.state = 'removed'; return { ok: true, removed: true };
    }
    else {
      await sendToTab(tabId, { type: 'remove-single', index: img.csIndex, replaceText: settings.replaceText || 'Image removed' });
      // Zitat-Duplikate ebenfalls entfernen
      if (Array.isArray(img.duplicates)) {
        for (const dup of img.duplicates) {
          if (dup.csCategory !== 'quote') continue;
          await sendToTab(tabId, { type: 'remove-single', index: dup.csIndex, replaceText: settings.replaceText || 'Image removed' });
          dup.removed = true;
        }
      }
      img.state = 'removed'; return { ok: true, removed: true };
    }
  }

  if (profileName === PROFILE_NOTHING) {
    if (img.state === 'compressed' || img.state === 'removed') { await dialogUndoImage(tabId, imageId); }
    return { ok: true };
  }

  if (img.state === 'compressed' && (img.compressedAttachmentId != null || img.csIndex != null)) { await dialogUndoImage(tabId, imageId); }

  const ps = getProfileState(profileName);
  const options = getProfileOptions(profileName);
  const tabState = getTabState(tabId);
  options.resizeAlgorithm = tabState.resizeAlgorithm || options.resizeAlgorithm || 'auto';
  if (preserveMetaOverride !== undefined && preserveMetaOverride !== null) options.preserveMeta = preserveMetaOverride;
  if (scaleOverride !== undefined && scaleOverride !== null) ps.scale = scaleOverride;
  const format = img.format;
  const pipeFormat = getPipelineFormat(format);
  let tryPng, tryJpeg;
  if (forcePng) { tryPng = true; tryJpeg = false; console.log(`[PPP] Force PNG: ${format.toUpperCase()} → PNG`); }
  else { tryPng = pipeFormat === 'png' ? true : ps.cross; tryJpeg = pipeFormat === 'jpeg' ? true : ps.cross; }

  if (img.category === 'attach') {
    if (!img.originalFile) { img.originalFile = await browser.compose.getAttachmentFile(img.attachmentId); }
    const originalBytes = new Uint8Array(await img.originalFile.arrayBuffer());
    const originalSize = originalBytes.byteLength;
    const ignoreReason = getIgnoredInputReason(originalBytes);
    if (ignoreReason) { console.log(`[PPP] ${img.name}: ${ignoreReason}`); img.state = 'incompressible'; return { ok: true, incompressible: true }; }
    const result = await compressImageCached(tabId, originalBytes, options, pipeFormat, tryPng, tryJpeg, null, null, resizePercent || 100, forcePng);
    if (!forcePng) { const savingsRatio = 1 - (result.bytes.byteLength / originalBytes.byteLength); if (savingsRatio < getMinSavingsRatio()) { console.log(`[PPP] ${img.name}: nur ${(savingsRatio * 100).toFixed(1)}% gespart — inkompressibel`); img.state = 'incompressible'; return { ok: true, incompressible: true }; } }
    const newFormat = result.format; const newSize = result.bytes.byteLength; const formatChanged = newFormat !== format;
    const outName = formatChanged ? swapExtension(img.name, newFormat) : img.name; const outMime = formatMime(newFormat);
    const compressedFile = new File([result.bytes], outName, { type: outMime });
    selfAddedAttachments.add(`${outName}:${result.bytes.byteLength}`); const newAtt = await browser.compose.addAttachment(tabId, { file: compressedFile });
    await browser.compose.removeAttachment(tabId, img.attachmentId);
    img.compressedAttachmentId = newAtt.id; img.compressedName = outName; img.compressedFormat = newFormat; img.compressedSize = newSize; img.state = 'compressed';
    const backups = getAttachmentBackups(tabId); const existingIdx = backups.findIndex((b) => b.name === outName);
    const backupEntry = { name: outName, originalFile: new File([originalBytes], img.name, { type: formatMime(format) }), compressedId: newAtt.id, originalSize: originalSize };
    if (existingIdx >= 0) backups[existingIdx] = backupEntry; else backups.push(backupEntry);
    // Duplikat-Anhänge mit demselben Ergebnis ersetzen (voller Byte-Vergleich zur Sicherheit)
    if (Array.isArray(img.duplicates)) {
      for (const dup of img.duplicates) {
        try {
          const dupFile = await browser.compose.getAttachmentFile(dup.attachmentId);
          const dupBytes = new Uint8Array(await dupFile.arrayBuffer());
          const identical = dupBytes.byteLength === originalBytes.byteLength && dupBytes.every((b, i) => b === originalBytes[i]);
          if (!identical) { console.warn(`[PPP] Duplikat ${dup.name}: Inhalt weicht ab — übersprungen`); continue; }
          const dupOutName = formatChanged ? swapExtension(dup.name, newFormat) : dup.name;
          selfAddedAttachments.add(`${dupOutName}:${result.bytes.byteLength}`); const dupNewAtt = await browser.compose.addAttachment(tabId, { file: new File([result.bytes], dupOutName, { type: outMime }) });
          await browser.compose.removeAttachment(tabId, dup.attachmentId);
          backups.push({ name: dupOutName, originalFile: new File([dupBytes], dup.name, { type: formatMime(format) }), compressedId: dupNewAtt.id, originalSize: dupBytes.byteLength });
          dup.compressedAttachmentId = dupNewAtt.id; dup.compressedName = dupOutName;
          console.log(`[PPP] Duplikat ${dup.name} → ${dupOutName} mitkomprimiert`);
        } catch (e) { console.warn(`[PPP] Duplikat ${dup.name} fehlgeschlagen:`, e.message); }
      }
    }
    let newWidth = img.width, newHeight = img.height;
    if (resizePercent && resizePercent < 100) { newWidth = Math.round(img.width * resizePercent / 100); newHeight = Math.round(img.height * resizePercent / 100); }
    console.log(`[PPP] Dialog: ${img.name} → ${outName}: ${formatBytes(originalBytes.byteLength)} -> ${formatBytes(newSize)}${forcePng ? ' (force png)' : ''}`);
    return { ok: true, newSize, newFormat, newWidth, newHeight };
  } else {
    const r = await sendToTab(tabId, { type: 'compress-single', category: img.csCategory, index: img.csIndex, resizePercent: resizePercent || 100, profileName, scale: ps.scale, keepInlineSize: !!keepInlineSize, forcePng: !!forcePng, resizeAlgorithm: options.resizeAlgorithm });
    if (!r) return { ok: false, error: browser.i18n.getMessage('error_composeScriptUnreachable') };
    if (!r.success) {
      if (r.reason === 'no_improvement' && !forcePng) { img.state = 'incompressible'; return { ok: true, incompressible: true }; }
      if (r.reason === 'no_improvement' && forcePng) { return { ok: false, error: browser.i18n.getMessage('error_compressionFailedForcePng') }; }
      return { ok: false, error: r.error || r.reason || browser.i18n.getMessage('error_noReduction') };
    }
    img.compressedSize = r.compressedSize; img.compressedFormat = r.format; img.state = 'compressed';
    // Duplikate im Compose-Fenster mit demselben Ergebnis aktualisieren
    if (Array.isArray(img.duplicates) && img.duplicates.length > 0) {
      await sendToTab(tabId, { type: 'apply-to-duplicates', category: img.csCategory, index: img.csIndex, duplicates: img.duplicates.map((d) => ({ category: d.csCategory, index: d.csIndex })), keepInlineSize: !!keepInlineSize, resizePercent: resizePercent || 100 });
    }
    return { ok: true, newSize: r.compressedSize, newFormat: r.format, newWidth: r.newWidth, newHeight: r.newHeight };
  }
}

async function dialogUndoImage(tabId, imageId) {
  const state = getDialogState(tabId); const img = state.get(imageId);
  if (!img) return { ok: false, error: 'Bild nicht gefunden' };
  if (img.state === 'original' || img.state === 'incompressible') return { ok: true };
  if (img.category === 'attach') {
    if (img.compressedAttachmentId != null) { await browser.compose.removeAttachment(tabId, img.compressedAttachmentId); if (img.originalFile) { const newAtt = await browser.compose.addAttachment(tabId, { file: img.originalFile }); img.attachmentId = newAtt.id; } const backups = getAttachmentBackups(tabId); const backupIdx = backups.findIndex((b) => b.compressedId === img.compressedAttachmentId); if (backupIdx >= 0) backups.splice(backupIdx, 1); }
    else if (img.state === 'removed' && img.originalFile) { const newAtt = await browser.compose.addAttachment(tabId, { file: img.originalFile }); img.attachmentId = newAtt.id; }
    // Duplikat-Anhänge wiederherstellen (komprimiert oder entfernt)
    if (Array.isArray(img.duplicates)) {
      for (const dup of img.duplicates) {
        try {
          if (dup.compressedAttachmentId != null) {
            await browser.compose.removeAttachment(tabId, dup.compressedAttachmentId);
            const dupBackups = getAttachmentBackups(tabId);
            const dupBackupIdx = dupBackups.findIndex((b) => b.compressedId === dup.compressedAttachmentId);
            if (dupBackupIdx >= 0) {
              const dupNewAtt = await browser.compose.addAttachment(tabId, { file: dupBackups[dupBackupIdx].originalFile });
              dup.attachmentId = dupNewAtt.id;
              dupBackups.splice(dupBackupIdx, 1);
            }
            dup.compressedAttachmentId = null;
          } else if (dup.removed && dup.originalFile) {
            const dupNewAtt = await browser.compose.addAttachment(tabId, { file: dup.originalFile });
            dup.attachmentId = dupNewAtt.id;
            dup.removed = false;
          }
        } catch (e) { console.warn(`[PPP] Duplikat-Undo ${dup.name || ''} fehlgeschlagen:`, e.message); }
      }
    }
  } else if (img.state === 'removed' && img.csIndex != null) {
    await sendToTab(tabId, { type: 'undo-removed', index: img.csIndex });
    // Entfernte Zitat-Duplikate wiederherstellen
    if (Array.isArray(img.duplicates)) { for (const dup of img.duplicates) { if (dup.removed && dup.csCategory === 'quote') { await sendToTab(tabId, { type: 'undo-removed', index: dup.csIndex }); dup.removed = false; } } }
  }
  else if (img.csIndex != null) {
    await sendToTab(tabId, { type: 'undo-single', category: img.csCategory, index: img.csIndex });
    // Komprimierte Duplikate wiederherstellen
    if (Array.isArray(img.duplicates)) { for (const dup of img.duplicates) { await sendToTab(tabId, { type: 'undo-single', category: dup.csCategory, index: dup.csIndex }); } }
  }
  img.state = 'original'; img.compressedAttachmentId = null; img.compressedFormat = null; img.compressedSize = null;
  return { ok: true };
}

async function dialogDownloadImage(tabId, imageId) {
  const state = getDialogState(tabId);
  const img = state.get(imageId);
  if (!img) return { ok: false, error: 'Bild nicht gefunden' };

  if (img.category === 'attach') {
    let file;
    if (img.state === 'compressed' && img.compressedAttachmentId != null) {
      file = await browser.compose.getAttachmentFile(img.compressedAttachmentId);
    } else if (img.originalFile) {
      file = img.originalFile;
    } else if (img.attachmentId) {
      file = await browser.compose.getAttachmentFile(img.attachmentId);
    } else {
      return { ok: false, error: browser.i18n.getMessage('error_fileNotAvailable') };
    }
    const bytes = new Uint8Array(await file.arrayBuffer());
    const format = img.state === 'compressed' ? (img.compressedFormat || img.format) : img.format;
    const name = img.state === 'compressed' ? (img.compressedName || img.name) : img.name;
    return { ok: true, bytes, format, name };
  } else {
    let r = await sendToTab(tabId, { type: 'get-image-data', index: img.csIndex, category: img.csCategory });
    if (r && r.success) {
      return { ok: true, dataUrl: r.dataUrl, format: img.format, name: img.name };
    }
    r = await sendToTab(tabId, { type: 'get-image-list' });
    if (r && r.images) {
      const targetId = `${img.csCategory}-${img.csIndex}`;
      const imgData = r.images.find(i => i.id === targetId);
      if (imgData) {
        const payload = imgData.currentPayload || imgData.payload;
        if (payload) {
          const fmt = imgData.currentPayload
            ? (imgData.compressedFormat || imgData.format || img.format || 'png')
            : (imgData.format || img.format || 'png');
          const mime = formatMime(fmt);
          return { ok: true, dataUrl: `data:${mime};base64,${payload}`, format: fmt, name: img.name };
        }
      }
    }
    return { ok: false, error: browser.i18n.getMessage('error_imageNotAccessible') };
  }
}

async function processAttachment(tabId, attachment) {
  const name = attachment.name || ''; if (!isCompressible(name)) return false; if (processing.has(attachment.id)) return false;
  const state = getTabState(tabId); const as = getCategoryState(state, 'attach'); const format = getFormat(name);
  const pipeFormat = getPipelineFormat(format);
  const tryPng = pipeFormat === 'png' ? true : as.cross; const tryJpeg = pipeFormat === 'jpeg' ? true : as.cross;
  const declaredSize = attachment.size || 0; const signature = `${name}:${declaredSize}`; const sigs = getTabSignatures(tabId);
  if (sigs.has(signature)) return false; const minBytes = getMinFileSizeBytes();
  if (declaredSize > 0 && declaredSize < minBytes) { sigs.add(signature); return false; }
  processing.add(attachment.id);
  try {
    const file = await browser.compose.getAttachmentFile(attachment.id); const original = new Uint8Array(await file.arrayBuffer());
    if (original.byteLength < minBytes) { sigs.add(signature); processing.delete(attachment.id); return false; }
    const ignoreReason = getIgnoredInputReason(original);
    if (ignoreReason) { console.log(`[PPP] ${name}: ${ignoreReason}`); sigs.add(signature); processing.delete(attachment.id); return false; }
    const hash = await computeHash(original); if (isExcluded(hash)) { sigs.add(signature); processing.delete(attachment.id); return false; }
    const result = await compressImageCached(tabId, original, getCategoryOptions('attach'), pipeFormat, tryPng, tryJpeg, null, null, 100, false);
    sigs.add(signature);
    const savingsRatio = 1 - (result.bytes.byteLength / original.byteLength);
    if (savingsRatio < getMinSavingsRatio()) { console.log(`[PPP] ${name}: nur ${(savingsRatio * 100).toFixed(1)}% gespart — übersprungen`); processing.delete(attachment.id); return false; }
    const outMime = formatMime(result.format); const formatChanged = result.format !== format;
    const outName = formatChanged ? swapExtension(name, result.format) : name;
    if (formatChanged) console.log(`[PPP] ${name} → ${outName} (${format} → ${result.format})`);
    const compressedFile = new File([result.bytes], outName, { type: outMime });
    selfAddedAttachments.add(`${outName}:${result.bytes.byteLength}`); const newAtt = await browser.compose.addAttachment(tabId, { file: compressedFile }); await browser.compose.removeAttachment(tabId, attachment.id);
    const origMime = formatMime(format); const originalFileForUndo = new File([original], name, { type: origMime });
    getAttachmentBackups(tabId).push({ name: outName, originalFile: originalFileForUndo, compressedId: newAtt.id });
    const saved = original.byteLength - result.bytes.byteLength; const pct = ((saved / original.byteLength) * 100).toFixed(1);
    console.log(`[PPP] ${name}${formatChanged ? ' → ' + outName : ''}: ${formatBytes(original.byteLength)} -> ${formatBytes(result.bytes.byteLength)} (-${pct}%)`);
    sigs.add(`${outName}:${result.bytes.byteLength}`); processing.delete(attachment.id); return true;
  } catch (err) { console.error(`[PPP] Fehler bei ${name}:`, err.message || err); processing.delete(attachment.id); return false; }
}

async function processAllImageAttachments(tabId, force) {
  if (force) { getAttachmentBackups(tabId).length = 0; }
  const attachments = await browser.compose.listAttachments(tabId);
  let total = 0, compressed = 0;
  for (const att of attachments) { if (isCompressible(att.name)) { total++; if (await processAttachment(tabId, att)) compressed++; } }
  console.log(`[PPP] ${compressed}/${total} Anhänge komprimiert`); return { total, compressed };
}

async function undoAttachments(tabId) {
  const backups = getAttachmentBackups(tabId); if (backups.length === 0) return { restored: 0 }; let restored = 0;
  for (const backup of backups) { try { await browser.compose.removeAttachment(tabId, backup.compressedId); await browser.compose.addAttachment(tabId, { file: backup.originalFile }); restored++; } catch (err) { console.error(`[PPP] Undo Anhang ${backup.name}:`, err.message); } }
  backups.length = 0; getTabSignatures(tabId).clear(); return { restored };
}

async function handleImageCompress(tabId, byteArray, format, tryPng, tryJpeg, targetWidth, targetHeight, category, profileName, resizePercent, forcePng, resizeAlgorithm) {
  try {
    const original = new Uint8Array(byteArray); const minBytes = getMinFileSizeBytes();
    if (original.byteLength < minBytes) return { bytes: null, format: null };
    const ignoreReason = getIgnoredInputReason(original);
    if (ignoreReason) { console.log(`[PPP] ${ignoreReason}`); return { bytes: null, format: null }; }
    const pipeFormat = getPipelineFormat(format);
    const hash = await computeHash(original); if (isExcluded(hash)) { console.log('[PPP] Bild ausgeschlossen (Ausnahmeliste)'); return { bytes: null, format: null }; }
    let options, effectiveTryPng, effectiveTryJpeg;
    if (profileName) { if (profileName === PROFILE_REMOVE || profileName === PROFILE_NOTHING) return { bytes: null, format: null }; options = getProfileOptions(profileName); const ps = getProfileState(profileName); effectiveTryPng = pipeFormat === 'png' ? true : ps.cross; effectiveTryJpeg = pipeFormat === 'jpeg' ? true : ps.cross; }
    else { options = getCategoryOptions(category || 'inline'); effectiveTryPng = tryPng || false; effectiveTryJpeg = tryJpeg || false; }
    if (resizeAlgorithm) { options.resizeAlgorithm = resizeAlgorithm; }
    if (forcePng) { effectiveTryPng = true; effectiveTryJpeg = false; }
    if (!effectiveTryPng && !effectiveTryJpeg) { effectiveTryPng = true; effectiveTryJpeg = true; }
    const result = await compressImageCached(tabId, original, options, pipeFormat, effectiveTryPng, effectiveTryJpeg, targetWidth || null, targetHeight || null, resizePercent || 100, forcePng);
    if (!forcePng) { const savingsRatio = 1 - (result.bytes.byteLength / original.byteLength); if (savingsRatio < getMinSavingsRatio()) { console.log(`[PPP] ${format.toUpperCase()}: nur ${(savingsRatio * 100).toFixed(1)}% gespart — übersprungen`); return { bytes: null, format: null }; } }
    const saved = original.byteLength - result.bytes.byteLength; const pct = ((saved / original.byteLength) * 100).toFixed(1);
    console.log(`[PPP] ${format.toUpperCase()}->${result.format.toUpperCase()}: ${formatBytes(original.byteLength)} -> ${formatBytes(result.bytes.byteLength)} (-${pct}%)${forcePng ? ' (force png)' : ''}`);
    return { bytes: result.bytes, format: result.format };
  } catch (err) { console.error('[PPP] Fehler bei Bild-Kompression:', err.message || err); return { bytes: null, format: null }; }
}

async function executeActions(tabId, force) {
  const state = getTabState(tabId);
  const qp = state.quoteProfile || settings.quoteProfile || '3. size optimized';
  if (qp === PROFILE_REMOVE) { try { const r = await sendToTab(tabId, { type: 'process-quotes', mode: 'replace-boxes', force: !!force, replaceText: settings.replaceText || 'Image removed' }); if (r) console.log(`[PPP] Zitat-Bilder: ${r.processed}/${r.total} ersetzt`); } catch (err) { console.error('[PPP] Fehler bei Zitat-Bildern:', err); } }
  else if (qp !== PROFILE_NOTHING) { const qs = getCategoryState(state, 'quote'); try { const r = await sendToTab(tabId, { type: 'process-quotes', mode: 'compress', png: true, jpeg: true, cross: qs.cross, scale: qs.scale, antialias: qs.antialias, force: !!force, replaceText: settings.replaceText || 'Image removed' }); if (r) console.log(`[PPP] Zitat-Bilder: ${r.processed}/${r.total} verarbeitet`); } catch (err) { console.error('[PPP] Fehler bei Zitat-Bildern:', err); } }
  const ip = state.inlineProfile || settings.inlineProfile || '2. quality optimized';
  if (ip === PROFILE_REMOVE) { try { const r = await sendToTab(tabId, { type: 'process-inline', mode: 'replace-boxes', force: !!force, replaceText: settings.replaceText || 'Image removed' }); if (r) console.log(`[PPP] Inline-Bilder: ${r.processed}/${r.total} ersetzt`); } catch (err) { console.error('[PPP] Fehler bei Inline-Bildern:', err); } }
  else if (ip !== PROFILE_NOTHING) { const is = getCategoryState(state, 'inline'); try { const r = await sendToTab(tabId, { type: 'process-inline', png: true, jpeg: true, cross: is.cross, scale: is.scale, antialias: is.antialias, force: !!force }); if (r) console.log(`[PPP] ${r.compressed}/${r.total} Inline-Bilder komprimiert`); } catch (err) { console.error('[PPP] Fehler bei Inline-Bildern:', err); } }
  const ap = state.attachProfile || settings.attachProfile || '2. quality optimized';
  if (ap === PROFILE_REMOVE) { try { const attachments = await browser.compose.listAttachments(tabId); let removed = 0; for (const att of attachments) { if (isCompressible(att.name)) { await browser.compose.removeAttachment(tabId, att.id); removed++; } } console.log(`[PPP] ${removed} Anhänge entfernt`); } catch (err) { console.error('[PPP] Fehler bei Anhang-Entfernung:', err); } }
  else if (ap !== PROFILE_NOTHING) { try { await processAllImageAttachments(tabId, force); } catch (err) { console.error('[PPP] Fehler bei Anhängen:', err); } }
}

// ── findUnprocessedItems — geändert: Mindestgröße, Profil-Check, Popup-Kompression, Ausnahmen ──
async function findUnprocessedItems(tabId) {
  const state = getTabState(tabId);
  const items = [];
  const minBytes = getMinFileSizeBytes();
  const backups = getAttachmentBackups(tabId);
  const dialogState = getDialogState(tabId);
  const excludedHashes = settings.excludedHashes || [];

  // IDs von Anhängen, die im Dialog verarbeitet wurden (komprimiert/inkompressibel/ausgeschlossen)
  const dialogHandledAttachIds = new Set();
  for (const [, img] of dialogState) {
    if (img.category !== 'attach' || img.state === 'original') continue;
    if (img.state === 'compressed' && img.compressedAttachmentId != null) {
      dialogHandledAttachIds.add(img.compressedAttachmentId);
    } else if (img.attachmentId != null) {
      dialogHandledAttachIds.add(img.attachmentId);
    }
  }

  // IDs von Anhängen, die im Dialog als Ausnahme erkannt wurden (gatherImages → dialogExcludedImages)
  const dialogExcludedAttachIds = new Set();
  for (const ex of getExcludedImages(tabId)) {
    if (ex.category === 'attach' && ex.attachmentId != null) {
      dialogExcludedAttachIds.add(ex.attachmentId);
    }
  }

  // IDs von Anhängen, die komprimiert wurden (Backup-Einträge aus Dialog ODER Auto-Process)
  const backupCompressedIds = new Set(backups.map(b => b.compressedId));

  // Zitat-Bilder
  const qp = state.quoteProfile || settings.quoteProfile || '3. size optimized';
  if (qp !== PROFILE_NOTHING) {
    try {
      const r = await sendToTab(tabId, {
        type: 'count-quote-images',
        minBytes,
        isRemove: qp === PROFILE_REMOVE,
        excludedHashes,
      });
      if (r && r.count > 0) items.push(`${r.count} Zitat-Bild(er)`);
    } catch {}
  }

  // Inline-Bilder
  const ip = state.inlineProfile || settings.inlineProfile || '2. quality optimized';
  if (ip !== PROFILE_NOTHING) {
    try {
      const r = await sendToTab(tabId, {
        type: 'count-unprocessed-inline',
        minBytes,
        excludedHashes,
      });
      if (r && r.count > 0) items.push(`${r.count} Inline-Bild(er)`);
    } catch {}
  }

  // Anhänge
  const ap = state.attachProfile || settings.attachProfile || '2. quality optimized';
  if (ap !== PROFILE_NOTHING) {
    const attachments = await browser.compose.listAttachments(tabId);
    const sigs = getTabSignatures(tabId);
    for (const att of attachments) {
      if (!isCompressible(att.name)) continue;
      // Bedingung (a): Mindestgröße erfüllt
      if ((att.size || 0) > 0 && (att.size || 0) < minBytes) continue;
      // Bedingung (c): nicht schon im Popup komprimiert (Backup-Eintrag)
      if (backupCompressedIds.has(att.id)) continue;
      // Bedingung (c): nicht im Dialog als inkompressibel/ausgeschlossen markiert
      if (dialogHandledAttachIds.has(att.id)) continue;
      // Ausnahmen: im Dialog als Ausnahme erkannt (gatherImages)
      if (dialogExcludedAttachIds.has(att.id)) continue;
      // Bedingung (c): nicht schon per Auto-Process verarbeitet (Signatur)
      if (sigs.has(`${att.name}:${att.size || 0}`)) continue;
      // Ignorierte Eingabeformate (animierte GIFs/WebPs) nicht mitzählen
      if (/\.(gif|webp)$/i.test(att.name || '')) {
        try {
          const file = await browser.compose.getAttachmentFile(att.id);
          const fileBytes = new Uint8Array(await file.arrayBuffer());
          if (getIgnoredInputReason(fileBytes)) continue;
        } catch (e) { /* bei Fehler: konservativ zählen */ }
      }
      // Letzter Check: Hash berechnen und Ausnahmeliste prüfen
      if (excludedHashes.length > 0) {
        try {
          const file = await browser.compose.getAttachmentFile(att.id);
          const fileBytes = new Uint8Array(await file.arrayBuffer());
          const hash = await computeHash(fileBytes);
          if (isExcluded(hash)) continue;
        } catch (e) { /* bei Fehler: konservativ zählen */ }
      }
      items.push(att.name);
    }
  }

  return items;
}

async function askUserToConfirmSend(tabId) {
  const url = browser.runtime.getURL('confirmSend.html') + '?tabId=' + tabId;
  const win = await browser.windows.create({ type: 'popup', url, width: 600, height: 780 });
  return new Promise((resolve) => {
    const cleanup = () => { pendingSendConfirmations.delete(win.id); browser.windows.onRemoved.removeListener(onRemovedListener); };
    const onRemovedListener = (closedWindowId) => { if (closedWindowId === win.id) { cleanup(); resolve('cancel'); } };
    browser.windows.onRemoved.addListener(onRemovedListener);
    pendingSendConfirmations.set(win.id, (result) => { cleanup(); resolve(result); });
  });
}

browser.compose.onBeforeSend.addListener(async (tab, details) => {
  const state = getTabState(tab.id);
  if (state.sendMode === 'manual') { cacheClearTab(tab.id); return {}; }
  try {
    if (state.sendMode === 'auto') { await executeActions(tab.id, false); cacheClearTab(tab.id); return {}; }
    const unprocessedItems = await findUnprocessedItems(tab.id);
    if (unprocessedItems.length === 0) return {};
    const result = await askUserToConfirmSend(tab.id);
    if (result === 'send') { cacheClearTab(tab.id); return {}; }
    return { cancel: true };
  } catch (err) { console.error('[PPP] Fehler bei onBeforeSend:', err); return {}; }
});

browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.type === 'messageCompose') {
    const state = getTabState(tabId);
    if (state.quoteProfile === PROFILE_REMOVE) { try { await sendToTab(tabId, { type: 'process-quotes', mode: 'replace-boxes', force: false, replaceText: settings.replaceText || 'Image removed' }); } catch {} }
  }
});

browser.runtime.onMessage.addListener((message, sender) => {
  if (message?.type === 'precalc-image') {
    const tabId = sender.tab?.id;
    if (tabId != null && message.bytes) {
      const bytes = message.bytes instanceof Uint8Array ? message.bytes : new Uint8Array(message.bytes);
      precalcImageBytes(tabId, bytes, message.format, message.category,
        message.displayW || 0, message.displayH || 0, message.naturalW || 0, message.naturalH || 0)
        .catch((e) => console.warn('[PPP] Pre-Kalkulation:', e.message));
    }
    return Promise.resolve({ ok: true });
  }
  if (message?.type === 'dialog-init') return gatherImages(message.tabId);
  if (message?.type === 'dialog-compress') return dialogCompressImage(message.tabId, message.imageId, message.profileName, message.resizePercent, message.preserveMeta, message.scale, message.keepInlineSize, message.forcePng);
  if (message?.type === 'dialog-undo') return dialogUndoImage(message.tabId, message.imageId);
  if (message?.type === 'dialog-download') return dialogDownloadImage(message.tabId, message.imageId);
  if (message?.type === 'dialog-done') {
    const windowId = sender.tab?.windowId;
    if (windowId != null && pendingSendConfirmations.has(windowId)) pendingSendConfirmations.get(windowId)(message.action);
    if (windowId != null) browser.windows.remove(windowId).catch(() => {});
    return Promise.resolve({ ok: true });
  }
  if (message?.type === 'compress-image') return handleImageCompress(sender.tab?.id ?? 'global', message.bytes, message.format, message.tryPng, message.tryJpeg, message.targetWidth, message.targetHeight, message.category, message.profileName, message.resizePercent, message.forcePng, message.resizeAlgorithm);
  if (message?.type === 'get-template-names') return Promise.resolve([PROFILE_REMOVE, PROFILE_NOTHING, ...Object.keys(PREDEFINED_TEMPLATES), ...Object.keys(settings.templates || {})]);
  if (message?.type === 'get-resize-algorithms') return Promise.resolve(RESIZE_ALGORITHMS);
  if (message?.type === 'undo-all') { sendToTab(message.tabId, { type: 'undo-all' }); undoAttachments(message.tabId).catch((err) => console.error('[PPP] Undo Anhänge:', err)); return Promise.resolve({ ok: true }); }
  if (message?.type === 'get-tab-state') return Promise.resolve({ state: getTabState(message.tabId) });
  if (message?.type === 'set-tab-state') { Object.assign(getTabState(message.tabId), message.state); return Promise.resolve({ ok: true }); }

  if (message?.type === 'dialog-show-excluded') {
    const tabId = message.tabId; const excludedImages = getExcludedImages(tabId); const state = getDialogState(tabId); const result = [];
    for (const ex of excludedImages) {
      const imageId = `img-${dialogImageCounter++}`;
      const imgData = { id: imageId, category: ex.category, name: ex.name, format: ex.format, size: ex.size, width: ex.width, height: ex.height, preview: ex.preview, state: 'excluded', compressedAttachmentId: null, compressedSize: null, compressedFormat: null, hash: ex.hash };
      if (ex.category === 'attach') { imgData.attachmentId = ex.attachmentId; imgData.originalFile = ex.originalFile; imgData.naturalWidth = ex.width; imgData.naturalHeight = ex.height; }
      else { imgData.csCategory = ex.csCategory; imgData.csIndex = ex.csIndex; imgData.naturalWidth = ex.naturalWidth || ex.width; imgData.naturalHeight = ex.naturalHeight || ex.height; imgData.compressedWidth = null; imgData.compressedHeight = null; }
      state.set(imageId, imgData);
      result.push({ id: imageId, category: ex.category, name: ex.name, format: ex.format, size: ex.size, width: ex.width, height: ex.height, naturalWidth: imgData.naturalWidth, naturalHeight: imgData.naturalHeight, preview: ex.preview, state: 'excluded', compressedSize: null, compressedFormat: null, hash: ex.hash });
    }
    excludedImages.length = 0; return Promise.resolve(result);
  }

  if (message?.type === 'exclude-image') {
    if (!settings.excludedHashes) settings.excludedHashes = [];
    if (message.hash && !settings.excludedHashes.includes(message.hash)) { settings.excludedHashes.push(message.hash); browser.storage.local.set({ settings }); console.log(`[PPP] Bild als Ausnahme gespeichert: ${message.hash.substring(0, 12)}... (${settings.excludedHashes.length} insgesamt)`); }
    if (message.tabId != null) { const state = getDialogState(message.tabId); for (const [, img] of state) { if (img.hash === message.hash) img.state = 'excluded'; } }
    return Promise.resolve({ ok: true });
  }
  if (message?.type === 'unexclude-image') {
    if (settings.excludedHashes && message.hash) { settings.excludedHashes = settings.excludedHashes.filter(h => h !== message.hash); browser.storage.local.set({ settings }); console.log(`[PPP] Ausnahme entfernt: ${message.hash.substring(0, 12)}... (${settings.excludedHashes.length} verbleibend)`); }
    if (message.tabId != null) { const state = getDialogState(message.tabId); for (const [, img] of state) { if (img.hash === message.hash) img.state = 'original'; } }
    return Promise.resolve({ ok: true });
  }
  return false;
});

browser.storage.onChanged.addListener((changes, area) => {
  if (area === 'local' && changes.settings) {
    settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
    Object.assign(settings, changes.settings.newValue);
    if (!settings.templates) settings.templates = {};
    if (!settings.excludedHashes) settings.excludedHashes = [];
    if (settings.quoteMode === 'replace-boxes') { settings.quoteProfile = PROFILE_REMOVE; delete settings.quoteMode; }
    delete settings.quoteMode;
  }
});

// Registrierung merken: Jedes composeScripts.register() legt eine zusätzliche
// Registrierung an (Mehrfach-Injektion in künftigen Fenstern).
let composeScriptRegistration = null;

async function registerComposeScript() {
  if (composeScriptRegistration) return;
  try {
    composeScriptRegistration = await browser.composeScripts.register({ js: [{ file: 'composeScript.js' }] });
    console.log('[PPP] Compose-Skript registriert');
  } catch (err) {
    console.error('[PPP] Fehler beim Registrieren:', err);
  }
}

loadSettings();
registerComposeScript();

// ── Pre-Kalkulation: Anhänge sofort nach dem Einfügen vorberechnen (nur Cache) ──
browser.compose.onAttachmentAdded.addListener(async (tab, attachment) => {
  try {
    if (!isCompressible(attachment.name)) return;
    const marker = `${attachment.name}:${attachment.size || 0}`;
    if (selfAddedAttachments.has(marker)) { selfAddedAttachments.delete(marker); return; }
    const tabState = getTabState(tab.id);
    const ap = tabState.attachProfile || settings.attachProfile || '2. quality optimized';
    if (ap === PROFILE_REMOVE || ap === PROFILE_NOTHING) return;
    const file = await browser.compose.getAttachmentFile(attachment.id);
    const bytes = new Uint8Array(await file.arrayBuffer());
    await precalcImageBytes(tab.id, bytes, getFormat(attachment.name), 'attach');
  } catch (e) {
    console.warn('[PPP] Pre-Kalkulation Anhang:', e.message);
  }
});
