'use strict';
// ═══════════════════════════════════════════════
// js/compare.js — compare two entries, side-by-side, loupe
// ═══════════════════════════════════════════════

let _cmpLoupeReady = false;
let _cmpImgTimer = null;

// Per-side transform state — persists across re-renders within the session
const _cmpTransform = {
  A: { scale: 1, rot: 0, ox: 0, oy: 0 },
  B: { scale: 1, rot: 0, ox: 0, oy: 0 },
};

// Authoritative photo source selections — prevents DOM rebuild race conditions
let _cmpSrcA = 'entry';
let _cmpSrcB = 'entry';
let _cmpPopulating = false; // guard against synthetic change events during rebuild
let _cmpLoadedKeyA = null; // stored IDB key from loaded comparison (fallback)
let _cmpLoadedKeyB = null;
let _cmpLoadedComp = null; // full loaded comparison object for cross-device resolution
let _cmpLoadedTransformA = null; // saved transform with refW/refH for initial CSS render
let _cmpLoadedTransformB = null;

// Called by imgURL() in storage.js when a lazy-loaded image arrives from IDB.
// Debounced re-render so the compare view refreshes once photos are cached.
function _cmpImgReady(key) {
  if (!document.getElementById('page-compare')?.classList.contains('active')) return;
  if (_cmpImgTimer) return;
  _cmpImgTimer = setTimeout(() => { _cmpImgTimer = null; renderCompare(); }, 100);
}

function populateCompareDrops() {
  const entries = getEntries().sort((a, b) => b.date.localeCompare(a.date));
  function buildOpts(placeholder) {
    let html = `<option value="">${placeholder}</option>`;
    // Group by year
    const byYear = {};
    entries.forEach(e => {
      const yr = e.date.slice(0, 4);
      if (!byYear[yr]) byYear[yr] = [];
      byYear[yr].push(e);
    });
    Object.keys(byYear).sort((a,b) => b-a).forEach(yr => {
      html += `<optgroup label="${yr}">`;
      byYear[yr].forEach(e => {
        html += `<option value="${e.id}">${esc(e.date)} - CI-${e.ci}${e.pctTugged != null ? ' · '+Math.round(e.pctTugged*7/100)+'d tugged':''}</option>`;
      });
      html += `</optgroup>`;
    });
    return html;
  }
  const la = document.getElementById('cmp-a'), lb = document.getElementById('cmp-b');
  if (!la || !lb) return;
  const va = la.value, vb = lb.value;
  la.innerHTML = buildOpts('- Entry A -');
  lb.innerHTML = buildOpts('- Entry B -');
  if (va) la.value = va;
  if (vb) lb.value = vb;
  _populateComparePhotoSrc('a');
  _populateComparePhotoSrc('b');
}

function _populateComparePhotoSrc(side) {
  const sel = document.getElementById(`cmp-${side}-src`);
  if (!sel) return;
  const prev = side === 'a' ? _cmpSrcA : _cmpSrcB;

  _cmpPopulating = true;

  while (sel.options.length) sel.remove(0);

  sel.add(new Option(`Photo: Entry ${side.toUpperCase()}`, 'entry'));

  // Own galleries
  const galleries = typeof getGalleries === 'function' ? getGalleries() : [];
  for (const g of galleries) {
    if (!g.items.length) continue;
    const grp = document.createElement('optgroup');
    grp.label = g.title || 'Gallery';
    for (const item of g.items) {
      grp.appendChild(new Option(
        `${g.title} — ${item.title || item.id.slice(0,8)}`,
        `gal:${g.id}:${item.id}`
      ));
    }
    sel.appendChild(grp);
  }

  // Trusted buddy photos (cached journal + received galleries)
  const contacts = typeof getContacts === 'function' ? getContacts() : [];
  const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
  for (const c of contacts) {
    if (!c.pubKeyB64 || c.removedAt || c.forgottenAt) continue;
    const mutualTrust = c.trusted && c.senderTrusts;
    if (!mutualTrust) continue;
    const name = c.journal?.displayName || c.nickname || 'Buddy';
    const sl16 = c.pubKeyB64.slice(0, 16);
    let hasItems = false;

    // Buddy journal photos
    const jEntries = (c.journal?.entries || []).filter(e => e.hasImgFront);
    if (jEntries.length) {
      hasItems = true;
      const grp = document.createElement('optgroup');
      grp.label = `${name} — Journal`;
      for (const e of jEntries.sort((a, b) => b.date.localeCompare(a.date))) {
        grp.appendChild(new Option(
          `${e.date}`,
          `buddy:${c.pubKeyB64}:${e.id}`
        ));
      }
      sel.appendChild(grp);
    }

    // Buddy received galleries
    const byGal = recv[sl16] || {};
    for (const [galId, galData] of Object.entries(byGal)) {
      const items = Object.entries(galData.items || {});
      if (!items.length) continue;
      hasItems = true;
      const grp = document.createElement('optgroup');
      grp.label = `${name} — ${galData.title || 'Gallery'}`;
      for (const [itemId, itemData] of items) {
        grp.appendChild(new Option(
          itemData.title || itemId.slice(0, 8),
          `brecv:${sl16}:${galId}:${itemId}`
        ));
      }
      sel.appendChild(grp);
    }
  }

  sel.value = prev;
  // If the source doesn't exist as a dropdown option (e.g. loaded from buddy chat),
  // keep the authoritative _cmpSrc variable intact — don't reset to 'entry'.
  // The resolver uses _cmpSrcA/_cmpSrcB directly, not the dropdown value.
  if (sel.value !== prev) {
    // Source not in dropdown — that's OK for chat comparisons, just leave dropdown on entry
    sel.value = 'entry';
    // But do NOT overwrite the authoritative source — it stays as the flipped value
  }

  _cmpPopulating = false;
}

function _resolveComparePhotoKey(srcVal, entry) {
  if (srcVal?.startsWith('_loaded:')) {
    // Virtual source for buddy's comparison — try to find the photo in any cached buddy journal
    const side = srcVal.split(':')[1];
    const _entryId = side === 'a' ? _cmpLoadedComp?.entryA : _cmpLoadedComp?.entryB;
    if (_entryId) {
      // Check own journal first
      const myEntries = typeof getEntries === 'function' ? getEntries() : [];
      const myE = myEntries.find(e => e.id === _entryId);
      if (myE?.imgFront) return myE.imgFront;
      // Check all buddy journals for this entry ID
      const contacts = typeof getContacts === 'function' ? getContacts() : [];
      for (const c of contacts) {
        if (!c.journal?.entries) continue;
        const be = c.journal.entries.find(e => e.id === _entryId);
        if (be?.hasImgFront) {
          const cKey = `c_${c.pubKeyB64.slice(0, 16)}_${_entryId}_front`;
          if (typeof imgURL === 'function') imgURL(cKey);
          return cKey;
        }
      }
    }
    return null; // will fall through to stored keyA/keyB
  }
  if (!srcVal || srcVal === 'entry') {
    if (entry?.imgFront) return entry.imgFront;
    // Entry not in our journal — might be a buddy's entry. Check cached copies.
    if (entry?.id) {
      const contacts = typeof getContacts === 'function' ? getContacts() : [];
      for (const c of contacts) {
        if (!c.pubKeyB64 || !c.journal?.entries) continue;
        const bEntry = c.journal.entries.find(e => e.id === entry.id);
        if (bEntry?.hasImgFront) {
          const cacheKey = `c_${c.pubKeyB64.slice(0, 16)}_${entry.id}_front`;
          if (typeof imgURL === 'function') imgURL(cacheKey);
          return cacheKey;
        }
      }
    }
    return null;
  }
  const parts = srcVal.split(':');
  if (parts[0] === 'gal') {
    const [, galId, itemId] = parts;
    // Check own galleries first
    const galleries = typeof getGalleries === 'function' ? getGalleries() : [];
    const g = galleries.find(x => x.id === galId);
    if (g) {
      const item = g.items.find(i => i.id === itemId);
      if (item?.imgKey) return item.imgKey;
    }
    // Not our gallery — check received galleries
    const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
    for (const [sl16, byGal] of Object.entries(recv)) {
      if (byGal[galId]?.items?.[itemId]?.idbKey) {
        const key = byGal[galId].items[itemId].idbKey;
        if (typeof imgURL === 'function') imgURL(key);
        return key;
      }
    }
    // Use creator's pubKey slice to construct the cache key directly
    const cSl16 = _cmpLoadedComp?.creatorSl16;
    if (cSl16) {
      const key = `gal_${cSl16}_${galId}_${itemId}`;
      if (typeof imgURL === 'function') imgURL(key);
      return key;
    }
    return null;
  }
  if (parts[0] === 'buddy') {
    const [, pubKeyB64, entryId] = parts;
    // Check if this is MY photo (buddy loaded a comparison that references my journal)
    const myEntries = typeof getEntries === 'function' ? getEntries() : [];
    const myEntry = myEntries.find(e => e.id === entryId);
    if (myEntry?.imgFront) return myEntry.imgFront;
    // Check buddy's cached journal
    const cacheKey = `c_${pubKeyB64.slice(0, 16)}_${entryId}_front`;
    if (typeof imgCache !== 'undefined' && imgCache[cacheKey]) return cacheKey;
    // Trigger lazy load even if not cached yet
    if (typeof imgURL === 'function') imgURL(cacheKey);
    return cacheKey;
  }
  if (parts[0] === 'brecv') {
    const [, sl16, galId, itemId] = parts;
    // Check if this is MY gallery
    const myGalleries = typeof getGalleries === 'function' ? getGalleries() : [];
    const myGal = myGalleries.find(g => g.id === galId);
    if (myGal) {
      const myItem = myGal.items.find(i => i.id === itemId);
      if (myItem?.imgKey) return myItem.imgKey;
    }
    // Check received gallery
    const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
    const idbKey = recv[sl16]?.[galId]?.items?.[itemId]?.idbKey;
    if (idbKey) {
      if (typeof imgURL === 'function') imgURL(idbKey); // trigger lazy load
      return idbKey;
    }
    return null;
  }
  return entry?.imgFront || null;
}

function renderCompare() {
  const idA = document.getElementById('cmp-a')?.value;
  const idB = document.getElementById('cmp-b')?.value;
  const _srcAVal = _cmpSrcA;
  const _srcBVal = _cmpSrcB;
  const galOnlyA = _srcAVal !== 'entry';
  const galOnlyB = _srcBVal !== 'entry';
  const el = document.getElementById('compare-content');
  if (!el) return;
  // Need at least one selection per side: an entry OR a gallery photo
  if ((!idA && !galOnlyA) || (!idB && !galOnlyB)) {
    el.innerHTML = `<div class="empty"><div class="icon">⇔</div><p>Pick two entries or gallery photos to compare, or load a saved comparison below.</p></div>`;
    _renderSavedComparisons();
    return;
  }
  const eA = idA ? getEntry(idA) : null;
  const eB = idB ? getEntry(idB) : null;
  if (!galOnlyA && !eA) { el.innerHTML = `<div class="empty"><p>Entry A not found</p></div>`; _renderSavedComparisons(); return; }
  if (!galOnlyB && !eB) { el.innerHTML = `<div class="empty"><p>Entry B not found</p></div>`; return; }
  const diff = document.getElementById('cmp-diff')?.checked;

  function entryHeader(e, label) {
    return `<div style="background:var(--text);color:#fff;padding:10px 14px;border-radius:8px;margin-bottom:10px">
      <div style="font-family:var(--mono);font-size:11px;font-weight:500">${label}: ${esc(e.date)}</div>
      <div style="font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:2px">
        <span class="ci-badge ${ciBadgeClass(e.ci)}" style="opacity:1">CI-${e.ci}</span>
      </div>
    </div>`;
  }

  function fieldBlock(label, valA, valB, isDiff) {
    const hl = diff && isDiff ? 'border-color:var(--red)' : '';
    const diffBadge = diff && isDiff ? `<span class="oor-badge" style="margin:0;margin-left:4px">diff</span>` : '';
    function renderVal(v) {
      if (v == null || v === '') return `<span style="color:var(--muted2);font-size:12px">-</span>`;
      if (Array.isArray(v)) {
        if (!v.length) return `<span style="color:var(--muted2);font-size:12px">-</span>`;
        return `<div style="display:flex;flex-wrap:wrap;gap:4px">${v.map(x => `<span style="background:var(--accent2);border:1px solid #c0d4ff;color:var(--accent);font-family:var(--mono);font-size:11px;padding:3px 8px;border-radius:4px">${esc(x)}</span>`).join('')}</div>`;
      }
      return `<span style="font-size:13px">${esc(String(v))}</span>`;
    }
    return { a: `<div style="background:var(--surface);border:1px solid var(--border);${hl};border-radius:7px;padding:10px 12px;margin-bottom:7px">
      <div style="font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted2);margin-bottom:5px">${label}${diffBadge}</div>
      ${renderVal(valA)}</div>`,
    b: `<div style="background:var(--surface);border:1px solid var(--border);${hl};border-radius:7px;padding:10px 12px;margin-bottom:7px">
      <div style="font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted2);margin-bottom:5px">${label}${diffBadge}</div>
      ${renderVal(valB)}</div>` };
  }

  function photoBlock(label, keyA, keyB) {
    const srcA = keyA ? imgURL(keyA) : null;
    const srcB = keyB ? imgURL(keyB) : null;
    const isDiff = !srcA !== !srcB;
    const hl = diff && isDiff ? 'border-color:var(--red)' : '';
    function slot(src, side, imgKey) {
      if (!src && !imgKey) return `<div style="background:var(--surface2);border:1px solid var(--border);border-radius:5px;aspect-ratio:3/4;display:flex;align-items:center;justify-content:center;color:var(--muted2);font-family:var(--mono);font-size:11px">no photo</div>`;
      if (!src && imgKey) {
        // Key exists but blob not cached yet — show canvas for lazy loading
        return `<div class="cmp-img-viewport" data-side="${side}" style="overflow:hidden;border-radius:5px;aspect-ratio:3/4;position:relative;background:var(--surface3)">
          <canvas class="photo-canvas" data-img-key="${esc(imgKey)}" style="width:100%;height:100%;display:block"></canvas>
        </div>
        <div style="font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:4px;text-align:center">Loading...</div>`;
      }
      const t = _cmpTransform[side];
      // Use percentage-based CSS from saved comparison (avoids single-frame position flash).
      // After rAF in loadComparison(), _applyCmpTransform overwrites with scaled pixel values.
      const savedT = side === 'A' ? _cmpLoadedTransformA : _cmpLoadedTransformB;
      const cssTransform = savedT?.refW
        ? _scaleCmpTransformCSS(savedT)
        : `translate(${t.ox}px,${t.oy}px) scale(${t.scale}) rotate(${t.rot}deg)`;
      return `<div class="cmp-img-viewport" data-side="${side}" style="overflow:hidden;border-radius:5px;aspect-ratio:3/4;position:relative;cursor:grab;touch-action:none;background:var(--surface3)">
        <img src="${src}" data-cmp-img="${side}" style="display:block;width:100%;height:100%;object-fit:contain;transform:${cssTransform};transform-origin:center center;pointer-events:none;user-select:none" alt="${label}">
      </div>
      <div style="display:flex;flex-direction:column;gap:4px;margin-top:6px">
        <div style="display:flex;align-items:center;gap:6px">
          <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);width:28px">Size</span>
          <input type="range" data-cmp-ctrl="scale" data-side="${side}" min="0.3" max="3" step="0.05" value="${t.scale}" style="flex:1;height:2px">
        </div>
        <div style="display:flex;align-items:center;gap:6px">
          <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);width:28px">Rot</span>
          <input type="range" data-cmp-ctrl="rot" data-side="${side}" min="-180" max="180" step="1" value="${t.rot}" style="flex:1;height:2px">
        </div>
        <div style="display:flex;gap:4px;justify-content:flex-end">
          <button class="btn btn-ghost btn-sm" style="font-size:10px;padding:2px 6px" data-action="cmpResetTransform" data-a0="${side}">Reset</button>
        </div>
      </div>`;
    }
    return {
      a: `<div style="background:var(--surface);border:1px solid var(--border);${hl};border-radius:7px;padding:10px 12px;margin-bottom:7px">
        <div style="font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted2);margin-bottom:5px">${label}</div>
        ${slot(srcA, 'A', keyA)}</div>`,
      b: `<div style="background:var(--surface);border:1px solid var(--border);${hl};border-radius:7px;padding:10px 12px;margin-bottom:7px">
        <div style="font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted2);margin-bottom:5px">${label}</div>
        ${slot(srcB, 'B', keyB)}</div>`
    };
  }

  // Build columns
  const fields = [
    { key: 'ci', label: 'Coverage Index' },
    { key: 'pctTugged', label: 'Days Tugged', fmt: v => v != null ? Math.round(v*7/100)+' / 7' : null },
    { key: 'pctRetained', label: 'Days Retained', fmt: v => v != null ? Math.round(v*7/100)+' / 7' : null },
    { key: 'avgHrsTugged', label: 'Hrs/day Tugged', fmt: v => v != null ? v+'h' : null },
    { key: 'avgHrsRetained', label: 'Hrs/day Retained', fmt: v => v != null ? v+'h' : null },
    { key: 'tuggingDevices', label: 'Tugging Devices' },
    { key: 'retainingDevices', label: 'Retaining Devices' },
    { key: 'notes', label: 'Notes' }
  ];

  let colA = eA ? entryHeader(eA, 'Entry A') : `<div style="background:var(--surface2);padding:10px 14px;border-radius:8px;margin-bottom:10px;font-family:var(--mono);font-size:11px;color:var(--muted2)">Gallery photo</div>`;
  let colB = eB ? entryHeader(eB, 'Entry B') : `<div style="background:var(--surface2);padding:10px 14px;border-radius:8px;margin-bottom:10px;font-family:var(--mono);font-size:11px;color:var(--muted2)">Gallery photo</div>`;

  if (eA && eB) {
    fields.forEach(f => {
      const vA = f.fmt ? f.fmt(eA[f.key]) : eA[f.key];
      const vB = f.fmt ? f.fmt(eB[f.key]) : eB[f.key];
      const isDiff = JSON.stringify(vA) !== JSON.stringify(vB);
      const bl = fieldBlock(f.label, vA, vB, isDiff);
      colA += bl.a; colB += bl.b;
    });
  }

  // Photo — resolve from source selectors, with multiple fallbacks
  const _keyA = _resolveComparePhotoKey(_srcAVal, eA) || _cmpLoadedKeyA || _srcFallbackKey(_srcAVal);
  const _keyB = _resolveComparePhotoKey(_srcBVal, eB) || _cmpLoadedKeyB || _srcFallbackKey(_srcBVal);
  const ph = photoBlock('Photo', _keyA, _keyB);
  colA += ph.a; colB += ph.b;

  el.innerHTML = `<div class="cmp-grid"><div>${colA}</div><div>${colB}</div></div>`;

  _setupCmpTransformControls(el);

  // Show save button when a comparison is active
  const saveBtn = document.getElementById('cmp-save-btn');
  if (saveBtn) saveBtn.style.display = '';

  // Render saved comparisons list
  _renderSavedComparisons();

  if (!_cmpLoupeReady) {
    _cmpLoupeReady = true;
    setupCmpLoupe();
  }
}

// ── Compare loupe ──
function setupCmpLoupe() {
  const area = document.getElementById('compare-content');
  if (!area) return;
  area.addEventListener('mousemove', onCmpLoupeMove);
  area.addEventListener('mouseleave', () => {
    document.getElementById('loupe-cmp')?.classList.remove('show');
  });
}

function onCmpLoupeMove(e) {
  const lcEl = document.getElementById('loupe-cmp');
  if (!lcEl) return;
  if (document.body.classList.contains('blur-images')) { lcEl.classList.remove('show'); return; }
  const wrap = e.target.closest('.cmp-img-wrap[data-photo]');
  if (!wrap) { lcEl.classList.remove('show'); return; }

  const W = 500, H = 380, footH = 28, paneW = Math.floor((W - 2) / 2);
  const zoom = 2;
  const img = wrap.querySelector('img'); if (!img) return;
  const pR = img.getBoundingClientRect();
  const nx = Math.max(0, Math.min(1, (e.clientX - pR.left) / pR.width));
  const ny = Math.max(0, Math.min(1, (e.clientY - pR.top) / pR.height));
  let lx = e.clientX + 28, ly = e.clientY - H / 2;
  if (lx + W > window.innerWidth - 8) lx = e.clientX - 28 - W;
  if (ly < 8) ly = 8;
  if (ly + H > window.innerHeight - 8) ly = window.innerHeight - H - 8;
  lcEl.style.left = lx + 'px'; lcEl.style.top = ly + 'px';
  lcEl.style.width = W + 'px'; lcEl.style.height = H + 'px';

  const lcCVA = document.getElementById('lc-cv-a');
  const lcCVB = document.getElementById('lc-cv-b');
  if (!lcCVA || !lcCVB) return;
  lcCVA.width = paneW; lcCVA.height = H - footH;
  lcCVB.width = paneW; lcCVB.height = H - footH;

  // Find the pair: same data-slot, look for both A/B wraps
  const mySlot = wrap.dataset.slot;
  const pairSlot = mySlot === 'A' ? 'B' : 'A';
  const myRow = wrap.dataset.row;
  const allWraps = document.querySelectorAll('.cmp-img-wrap[data-photo]');
  // Find matching wrap from the opposite column with same photo type
  const pairWrap = [...allWraps].find(w => w.dataset.slot === pairSlot && w.dataset.row === myRow);
  const srcA = mySlot === 'A' ? wrap.dataset.photo : pairWrap?.dataset.photo || '';
  const ovA  = mySlot === 'A' ? (wrap.dataset.overlay || '') : (pairWrap?.dataset.overlay || '');
  const srcB = mySlot === 'B' ? wrap.dataset.photo : pairWrap?.dataset.photo || '';
  const ovB  = mySlot === 'B' ? (wrap.dataset.overlay || '') : (pairWrap?.dataset.overlay || '');

  const selA = document.getElementById('cmp-a');
  const selB = document.getElementById('cmp-b');
  const footA = document.getElementById('lc-foot-a');
  const footB = document.getElementById('lc-foot-b');
  if (footA) footA.textContent = 'A: ' + (selA?.options[selA.selectedIndex]?.text || 'Entry A');
  if (footB) footB.textContent = 'B: ' + (selB?.options[selB.selectedIndex]?.text || 'Entry B');

  drawLoupePane(lcCVA, srcA, ovA, nx, ny, zoom, paneW, H - footH);
  drawLoupePane(lcCVB, srcB, ovB, nx, ny, zoom, paneW, H - footH);
  lcEl.classList.add('show');
}

function drawLoupePane(cv_, photoSrc, ovSrc, nx, ny, zoom, pw, ph) {
  const ctx = cv_.getContext('2d'); ctx.clearRect(0, 0, pw, ph);
  if (!photoSrc) {
    ctx.fillStyle = '#e8eaf0'; ctx.fillRect(0, 0, pw, ph);
    ctx.fillStyle = 'rgba(0,0,0,.3)'; ctx.font = '10px "DM Mono"';
    ctx.textAlign = 'center'; ctx.fillText('No image', pw / 2, ph / 2); ctx.textAlign = 'left'; return;
  }
  const imgEl = new Image(); imgEl.src = photoSrc;
  const nw = imgEl.naturalWidth || 800, nh = imgEl.naturalHeight || 600;
  const srcW = pw / zoom, srcH = ph / zoom;
  const sx = Math.max(0, Math.min(nx * nw - srcW / 2, nw - srcW));
  const sy = Math.max(0, Math.min(ny * nh - srcH / 2, nh - srcH));
  try { ctx.drawImage(imgEl, sx, sy, srcW, srcH, 0, 0, pw, ph); } catch(e) {}
  if (ovSrc) {
    const ov = new Image(); ov.src = ovSrc;
    try { ctx.drawImage(ov, sx, sy, srcW, srcH, 0, 0, pw, ph); } catch(e) {}
  }
  ctx.strokeStyle = 'rgba(255,255,255,.3)'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(pw / 2, 0); ctx.lineTo(pw / 2, ph);
  ctx.moveTo(0, ph / 2); ctx.lineTo(pw, ph / 2); ctx.stroke();
}

// ── Transform scaling ─────────────────────────────────────────────────────────

// Scale a saved transform's ox/oy from its reference viewport size to a target size.
// Returns a new object — does not mutate the input.
function _scaleCmpTransform(t, targetW, targetH) {
  if (!t) return { scale: 1, rot: 0, ox: 0, oy: 0 };
  const refW = t.refW || 0;
  const refH = t.refH || 0;
  const ratioX = refW > 0 && targetW > 0 ? targetW / refW : 1;
  const ratioY = refH > 0 && targetH > 0 ? targetH / refH : 1;
  return {
    scale: t.scale || 1,
    rot:   t.rot   || 0,
    ox:    (t.ox || 0) * ratioX,
    oy:    (t.oy || 0) * ratioY,
  };
}

// Convert a saved transform to a CSS transform string scaled for a given container size.
// Useful for thumbnails where we can't measure the DOM element yet.
// ox/oy are converted to percentages of the element's own size (CSS translate %).
function _scaleCmpTransformCSS(t) {
  if (!t) return 'none';
  const refW = t.refW || 0;
  const refH = t.refH || 0;
  // If reference dimensions are available, convert px to % (scales naturally to any container).
  // If refW is 0 (legacy comparison or save-time failure), fall back to raw pixel translate
  // to match _scaleCmpTransform's passthrough behaviour for the same edge case.
  if (refW > 0 && refH > 0) {
    const pctX = ((t.ox || 0) / refW * 100);
    const pctY = ((t.oy || 0) / refH * 100);
    return `translate(${pctX.toFixed(2)}%,${pctY.toFixed(2)}%) scale(${t.scale||1}) rotate(${t.rot||0}deg)`;
  }
  return `translate(${t.ox||0}px,${t.oy||0}px) scale(${t.scale||1}) rotate(${t.rot||0}deg)`;
}

// ── Compare photo transform controls (scale, rotate, pan) ────────────────────

function _applyCmpTransform(side) {
  const t = _cmpTransform[side];
  const img = document.querySelector(`img[data-cmp-img="${side}"]`);
  if (img) img.style.transform = `translate(${t.ox}px,${t.oy}px) scale(${t.scale}) rotate(${t.rot}deg)`;
}

function _setupCmpTransformControls(root) {
  // Slider inputs (scale + rotation)
  root.querySelectorAll('input[data-cmp-ctrl]').forEach(inp => {
    inp.addEventListener('input', () => {
      const side = inp.dataset.side;
      const ctrl = inp.dataset.cmpCtrl;
      const t = _cmpTransform[side];
      if (ctrl === 'scale') t.scale = parseFloat(inp.value);
      if (ctrl === 'rot') t.rot = parseFloat(inp.value);
      _applyCmpTransform(side);
    });
  });

  // Drag-to-pan on viewports
  root.querySelectorAll('.cmp-img-viewport').forEach(vp => {
    let dragging = false, startX = 0, startY = 0, startOx = 0, startOy = 0;
    const side = vp.dataset.side;

    function onDown(e) {
      dragging = true;
      const pt = e.touches ? e.touches[0] : e;
      startX = pt.clientX; startY = pt.clientY;
      startOx = _cmpTransform[side].ox; startOy = _cmpTransform[side].oy;
      vp.style.cursor = 'grabbing';
      e.preventDefault();
    }
    function onMove(e) {
      if (!dragging) return;
      const pt = e.touches ? e.touches[0] : e;
      _cmpTransform[side].ox = startOx + (pt.clientX - startX);
      _cmpTransform[side].oy = startOy + (pt.clientY - startY);
      _applyCmpTransform(side);
    }
    function onUp() {
      dragging = false;
      vp.style.cursor = 'grab';
    }

    vp.addEventListener('mousedown', onDown);
    vp.addEventListener('touchstart', onDown, { passive: false });
    window.addEventListener('mousemove', onMove);
    window.addEventListener('touchmove', onMove, { passive: true });
    window.addEventListener('mouseup', onUp);
    window.addEventListener('touchend', onUp);

    // Scroll-wheel zoom
    vp.addEventListener('wheel', e => {
      e.preventDefault();
      const t = _cmpTransform[side];
      const delta = e.deltaY > 0 ? -0.1 : 0.1;
      t.scale = Math.max(0.3, Math.min(3, t.scale + delta));
      _applyCmpTransform(side);
      const slider = root.querySelector(`input[data-cmp-ctrl="scale"][data-side="${side}"]`);
      if (slider) slider.value = t.scale;
    }, { passive: false });
  });
}

function _cmpResetTransform(side) {
  _cmpTransform[side] = { scale: 1, rot: 0, ox: 0, oy: 0 };
  _applyCmpTransform(side);
  const root = document.getElementById('compare-content');
  if (!root) return;
  const scaleSlider = root.querySelector(`input[data-cmp-ctrl="scale"][data-side="${side}"]`);
  const rotSlider = root.querySelector(`input[data-cmp-ctrl="rot"][data-side="${side}"]`);
  if (scaleSlider) scaleSlider.value = '1';
  if (rotSlider) rotSlider.value = '0';
}

// ── Saved comparisons ────────────────────────────────────────────────────────

// Last-resort: construct a plausible IDB key from the source selector.
// Triggers imgURL for lazy load even if the blob isn't there yet.
function _srcFallbackKey(srcVal) {
  if (!srcVal || srcVal === 'entry' || srcVal.startsWith('_loaded:')) return null;
  const parts = srcVal.split(':');
  if (parts[0] === 'buddy') {
    // buddy:pubKeyB64:entryId → c_sl16_entryId_front
    const key = `c_${parts[1].slice(0, 16)}_${parts[2]}_front`;
    if (typeof imgURL === 'function') imgURL(key);
    return key;
  }
  if (parts[0] === 'brecv') {
    // brecv:sl16:galId:itemId → gal_sl16_galId_itemId
    const key = `gal_${parts[1]}_${parts[2]}_${parts[3]}`;
    if (typeof imgURL === 'function') imgURL(key);
    return key;
  }
  if (parts[0] === 'gal') {
    // gal:galId:itemId — try all senders in received galleries
    const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
    for (const [sl16, byGal] of Object.entries(recv)) {
      const idbKey = byGal[parts[1]]?.items?.[parts[2]]?.idbKey;
      if (idbKey) { if (typeof imgURL === 'function') imgURL(idbKey); return idbKey; }
      if (byGal[parts[1]]) {
        const key = `gal_${sl16}_${parts[1]}_${parts[2]}`;
        if (typeof imgURL === 'function') imgURL(key);
        return key;
      }
    }
    // Gallery not in received data — try using creator's pubKey slice from loaded comparison
    const cSl16 = _cmpLoadedComp?.creatorSl16;
    if (cSl16) {
      const key = `gal_${cSl16}_${parts[1]}_${parts[2]}`;
      if (typeof imgURL === 'function') imgURL(key);
      return key;
    }
    // No creator info — try all contacts
    const contacts = typeof getContacts === 'function' ? getContacts() : [];
    for (const c of contacts) {
      if (!c.pubKeyB64 || c.removedAt || c.forgottenAt) continue;
      const sl16 = c.pubKeyB64.slice(0, 16);
      const key = `gal_${sl16}_${parts[1]}_${parts[2]}`;
      if (typeof imgCache !== 'undefined' && imgCache[key]) return key;
      if (typeof imgURL === 'function') imgURL(key);
    }
  }
  return null;
}

function _cmpDescribeSource(src, entryId) {
  if (!src || src === 'entry') {
    if (!entryId) return 'Me';
    const e = typeof getEntry === 'function' ? getEntry(entryId) : null;
    return e ? `Me · ${e.date}` : 'Me';
  }
  const parts = src.split(':');
  if (parts[0] === 'gal') {
    const galleries = typeof getGalleries === 'function' ? getGalleries() : [];
    const g = galleries.find(x => x.id === parts[1]);
    const item = g?.items?.find(i => i.id === parts[2]);
    return `Me · ${g?.title || 'Gallery'}${item?.title ? ' · ' + item.title : ''}`;
  }
  if (parts[0] === 'buddy') {
    const contacts = typeof getContacts === 'function' ? getContacts() : [];
    const c = contacts.find(x => x.pubKeyB64 === parts[1]);
    return c?.journal?.displayName || c?.nickname || 'Buddy';
  }
  if (parts[0] === 'brecv') {
    const contacts = typeof getContacts === 'function' ? getContacts() : [];
    const c = contacts.find(x => x.pubKeyB64?.startsWith(parts[1]));
    const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
    const galTitle = recv[parts[1]]?.[parts[2]]?.title || 'Gallery';
    return `${c?.journal?.displayName || c?.nickname || 'Buddy'} · ${galTitle}`;
  }
  return 'Photo';
}

async function saveComparison() {
  const idA = document.getElementById('cmp-a')?.value || '';
  const idB = document.getElementById('cmp-b')?.value || '';
  if (!_cmpSrcA && !idA && !_cmpSrcB && !idB) { toast('Nothing to save', true); return; }

  // Auto-generate descriptive name
  const descA = _cmpDescribeSource(_cmpSrcA, idA);
  const descB = _cmpDescribeSource(_cmpSrcB, idB);
  const autoName = `${descA} vs ${descB}`;
  const name = prompt('Name this comparison:', autoName);
  if (!name) return;

  // Resolve IDB keys
  const eA = idA ? (typeof getEntry === 'function' ? getEntry(idA) : null) : null;
  const eB = idB ? (typeof getEntry === 'function' ? getEntry(idB) : null) : null;
  const resolvedKeyA = _resolveComparePhotoKey(_cmpSrcA, eA);
  const resolvedKeyB = _resolveComparePhotoKey(_cmpSrcB, eB);

  // Store creator's pubKey slice so buddies can resolve gallery photos
  let creatorSl16 = null;
  try {
    const identity = typeof getIdentity === 'function' ? await getIdentity() : null;
    if (identity?.pubKeyB64) creatorSl16 = identity.pubKeyB64.slice(0, 16);
  } catch(e) {}

  // Capture reference viewport dimensions so transforms can be scaled proportionally
  const vpA = document.querySelector('.cmp-img-viewport[data-side="A"]');
  const vpB = document.querySelector('.cmp-img-viewport[data-side="B"]');

  const comparison = {
    id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
    name: name.trim().slice(0, 80),
    descA, descB,
    createdAt: new Date().toISOString(),
    creatorSl16,
    entryA: idA || null,
    entryB: idB || null,
    srcA: _cmpSrcA,
    srcB: _cmpSrcB,
    keyA: resolvedKeyA,
    keyB: resolvedKeyB,
    transformA: { ..._cmpTransform.A, refW: vpA?.clientWidth || 0, refH: vpA?.clientHeight || 0 },
    transformB: { ..._cmpTransform.B, refW: vpB?.clientWidth || 0, refH: vpB?.clientHeight || 0 },
  };

  const saved = typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
  saved.unshift(comparison);
  if (saved.length > 50) saved.length = 50;
  if (typeof saveSavedComparisons === 'function') saveSavedComparisons(saved);

  // Broadcast to trusted buddies + notify any buddy whose photos are used
  _broadcastComparisons();
  _notifyBuddiesOfComparison(comparison);

  toast('Comparison saved');
  _renderSavedComparisons();
}

function loadComparison(compIdOrObj) {
  let comp;
  if (typeof compIdOrObj === 'object' && compIdOrObj) {
    comp = compIdOrObj; // direct comparison object (e.g. from chat reference)
  } else {
    const saved = typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
    comp = saved.find(c => c.id === compIdOrObj);
  }
  if (!comp) return;

  // Set entry dropdowns — if the entry exists locally, select it
  const la = document.getElementById('cmp-a');
  const lb = document.getElementById('cmp-b');
  if (la && comp.entryA) { la.value = comp.entryA; }
  if (lb && comp.entryB) { lb.value = comp.entryB; }

  // Set photo sources + stored key fallbacks
  // If source is 'entry' but the entry doesn't exist locally (buddy's comparison),
  // switch to a virtual source so renderCompare doesn't bail
  _cmpSrcA = comp.srcA || 'entry';
  _cmpSrcB = comp.srcB || 'entry';
  if (_cmpSrcA === 'entry' && comp.entryA && la && !la.value) {
    _cmpSrcA = `_loaded:a`; // virtual source — will use fallback keyA
  }
  if (_cmpSrcB === 'entry' && comp.entryB && lb && !lb.value) {
    _cmpSrcB = `_loaded:b`; // virtual source — will use fallback keyB
  }
  _cmpLoadedKeyA = comp.keyA || null;
  _cmpLoadedKeyB = comp.keyB || null;
  _cmpLoadedComp = comp;
  _populateComparePhotoSrc('a');
  _populateComparePhotoSrc('b');

  // Restore transforms — strip refW/refH (save-time metadata, not runtime state).
  // The initial render uses _scaleCmpTransformCSS for percentage-based positioning,
  // so the first frame is correct without measuring the viewport.
  function _pickTransform(saved) {
    if (!saved) return { scale: 1, rot: 0, ox: 0, oy: 0 };
    return { scale: saved.scale || 1, rot: saved.rot || 0, ox: saved.ox || 0, oy: saved.oy || 0 };
  }
  _cmpTransform.A = _pickTransform(comp.transformA);
  _cmpTransform.B = _pickTransform(comp.transformB);

  // Stash full saved transforms (with refW/refH) so renderCompare can use _scaleCmpTransformCSS
  _cmpLoadedTransformA = comp.transformA || null;
  _cmpLoadedTransformB = comp.transformB || null;

  renderCompare();

  // After render, measure actual viewport and convert percentage-based initial position
  // to pixel values for the live transform (so drag/zoom work correctly in current viewport)
  requestAnimationFrame(() => {
    let migrated = false;
    for (const side of ['A', 'B']) {
      const saved = side === 'A' ? comp.transformA : comp.transformB;
      const vp = document.querySelector(`.cmp-img-viewport[data-side="${side}"]`);
      if (!vp || vp.clientWidth <= 0) continue;

      if (saved?.refW) {
        // Modern comparison — scale from reference to current viewport
        const scaled = _scaleCmpTransform(saved, vp.clientWidth, vp.clientHeight);
        _cmpTransform[side].ox = scaled.ox;
        _cmpTransform[side].oy = scaled.oy;
        _applyCmpTransform(side);
      } else if (saved) {
        // Legacy comparison — no refW/refH. Backfill with current viewport dimensions
        // so future thumbnail/chat/cross-device renders can scale proportionally.
        saved.refW = vp.clientWidth;
        saved.refH = vp.clientHeight;
        migrated = true;
      }
    }
    // Persist backfilled reference dimensions for legacy comparisons
    if (migrated && typeof getSavedComparisons === 'function' && typeof saveSavedComparisons === 'function') {
      const all = getSavedComparisons();
      const idx = all.findIndex(c => c.id === comp.id);
      if (idx !== -1) {
        all[idx] = comp;
        saveSavedComparisons(all);
      }
    }
    // Clear loaded transforms — no longer needed after initial render
    _cmpLoadedTransformA = null;
    _cmpLoadedTransformB = null;
  });
}

function deleteComparison(compId) {
  const saved = typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
  const filtered = saved.filter(c => c.id !== compId);
  if (typeof saveSavedComparisons === 'function') saveSavedComparisons(filtered);
  _broadcastComparisons();
  _renderSavedComparisons();
  toast('Comparison deleted');
}

function _renderSavedComparisons() {
  const el = document.getElementById('cmp-saved-list');
  if (!el) return;
  const saved = typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
  if (!saved.length) { el.innerHTML = ''; return; }

  // Determine own sl16 for categorization
  const _mySl16 = (typeof getDisplayNameSync === 'function')
    ? null // can't get pubkey synchronously — use heuristic below
    : null;

  // Categorize: "mine" = no creatorSl16 (pre-existing) or creatorSl16 matches entries we own
  // "shared" = has creatorSl16 and we don't own any of the source photos
  const mine = [], shared = [];
  const bookmarks = typeof getBookmarks === 'function' ? getBookmarks() : {};
  for (const c of saved) {
    if (!_comparisonInvolvesMe(c)) continue; // never show comparisons that don't involve me
    const isShared = c.creatorSl16 && !_isMyComparison(c);
    (isShared ? shared : mine).push(c);
  }

  function _renderRow(c) {
    const ago = typeof timeAgo === 'function' ? timeAgo(c.createdAt) : c.createdAt;
    _cmpLoadedComp = c;
    const _eA = c.entryA ? (typeof getEntry === 'function' ? getEntry(c.entryA) : null) : null;
    const _eB = c.entryB ? (typeof getEntry === 'function' ? getEntry(c.entryB) : null) : null;
    const thumbKeyA = _resolveComparePhotoKey(c.srcA, _eA) || _srcFallbackKey(c.srcA) || c.keyA;
    const thumbKeyB = _resolveComparePhotoKey(c.srcB, _eB) || _srcFallbackKey(c.srcB) || c.keyB;
    const thumbSrcA = thumbKeyA ? imgURL(thumbKeyA) : null;
    const thumbSrcB = thumbKeyB ? imgURL(thumbKeyB) : null;
    const bmKey = `comparison:${c.id}`;
    const isBookmarked = !!bookmarks[bmKey];
    return `<div style="display:flex;align-items:center;gap:10px;padding:8px 14px;border-bottom:1px solid var(--border)">
      <div style="display:flex;gap:2px;flex-shrink:0;cursor:pointer" data-action="loadComparison" data-a0="${esc(c.id)}">
        ${thumbSrcA ? `<img src="${thumbSrcA}" style="width:32px;height:42px;object-fit:cover;border-radius:3px">` : `<div style="width:32px;height:42px;border-radius:3px;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-size:8px;color:var(--muted2)">A</div>`}
        ${thumbSrcB ? `<img src="${thumbSrcB}" style="width:32px;height:42px;object-fit:cover;border-radius:3px">` : `<div style="width:32px;height:42px;border-radius:3px;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-size:8px;color:var(--muted2)">B</div>`}
      </div>
      <div style="flex:1;min-width:0;cursor:pointer" data-action="loadComparison" data-a0="${esc(c.id)}">
        <div style="font-size:13px;font-weight:600">${esc(c.name)}</div>
        <div style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${esc(c.descA || '?')} vs ${esc(c.descB || '?')}</div>
        <div style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${esc(ago)}</div>
      </div>
      <button class="btn btn-sm ${isBookmarked ? '' : 'btn-ghost'}" data-action="toggleBookmark" data-a0="${esc(bmKey)}" style="${isBookmarked ? 'background:var(--accent2);border-color:var(--accent);color:var(--accent)' : ''}" title="${isBookmarked ? 'Bookmarked' : 'Bookmark'}">🔖</button>
      <button class="btn btn-ghost btn-sm" data-action="loadComparison" data-a0="${esc(c.id)}">Load</button>
      <button class="btn btn-ghost btn-sm" style="color:var(--red)" data-action="deleteComparison" data-a0="${esc(c.id)}">✕</button>
    </div>`;
  }

  let html = '';
  if (mine.length) {
    html += `<div class="panel" style="margin-top:1.5rem">
      <div class="ph"><h2>My Comparisons</h2><span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">${mine.length}</span></div>
      <div class="pb" style="padding:0">${mine.map(_renderRow).join('')}</div>
    </div>`;
  }
  if (shared.length) {
    html += `<div class="panel" style="margin-top:1rem">
      <div class="ph"><h2>Shared with me</h2><span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">${shared.length}</span></div>
      <div class="pb" style="padding:0">${shared.map(_renderRow).join('')}</div>
    </div>`;
  }
  el.innerHTML = html;
}

// Check if a comparison was created by us (vs received from a buddy)
function _isMyComparison(c) {
  if (!c.creatorSl16) return true; // no creator info = assume mine (pre-existing)
  // Check if any of our own entries or galleries are used as the source
  const myEntries = typeof getEntries === 'function' ? getEntries() : [];
  if (c.entryA && myEntries.some(e => e.id === c.entryA)) return true;
  if (c.entryB && myEntries.some(e => e.id === c.entryB)) return true;
  // Check if srcA/srcB uses 'gal:' (own gallery) without 'brecv:' or 'buddy:' prefix
  if (c.srcA === 'entry' && c.entryA && myEntries.some(e => e.id === c.entryA)) return true;
  if (c.srcB === 'entry' && c.entryB && myEntries.some(e => e.id === c.entryB)) return true;
  // Check if own gallery is referenced
  const myGals = typeof getGalleries === 'function' ? getGalleries() : [];
  const myGalIds = new Set(myGals.map(g => g.id));
  if (c.srcA?.startsWith('gal:') && myGalIds.has(c.srcA.split(':')[1])) return true;
  if (c.srcB?.startsWith('gal:') && myGalIds.has(c.srcB.split(':')[1])) return true;
  return false;
}

// ── Comparison privacy: involvement check ───────────────────────────────────────

// Returns true if at least one side of the comparison uses the current user's own photos.
// A comparison of two OTHER users' photos that somehow synced should never be shown.
function _comparisonInvolvesMe(c) {
  if (!c) return false;
  if (_isMyComparison(c)) return true; // I created it — always show
  // Check if either side references my own entries or galleries
  const myEntries = typeof getEntries === 'function' ? getEntries() : [];
  const myEntryIds = new Set(myEntries.map(e => e.id));
  const myGals = typeof getGalleries === 'function' ? getGalleries() : [];
  const myGalIds = new Set(myGals.map(g => g.id));
  for (const src of [c.srcA, c.srcB]) {
    if (!src) continue;
    // My journal entry
    if (src === 'entry') continue; // checked via entryA/entryB below
    if (src.startsWith('gal:') && myGalIds.has(src.split(':')[1])) return true;
  }
  if (c.entryA && myEntryIds.has(c.entryA)) return true;
  if (c.entryB && myEntryIds.has(c.entryB)) return true;
  return false;
}

// Returns true if a comparison references a specific buddy's photos on either side.
// Static check on stored source/key fields — no async, no IDB lookups.
function _comparisonInvolvesBuddy(c, pubKeyB64) {
  if (!c || !pubKeyB64) return false;
  const sl16 = pubKeyB64.slice(0, 16);
  for (const src of [c.srcA, c.srcB]) {
    if (src?.startsWith('buddy:') && src.split(':')[1] === pubKeyB64) return true;
    if (src?.startsWith('brecv:') && src.split(':')[1] === sl16) return true;
  }
  for (const key of [c.keyA, c.keyB]) {
    if (key?.startsWith('c_' + sl16 + '_')) return true;
    if (key?.startsWith('gal_' + sl16 + '_')) return true;
  }
  return false;
}

// ── Sync comparisons with trusted buddies ──────────────────────────────────────

function _broadcastComparisons() {
  if (typeof syncAfterSave === 'function') syncAfterSave();
}

// Called by sync.js to get comparisons for device sync
function _getSyncComparisons() {
  return typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
}

// Called by sync.js / p2p.js to merge incoming comparisons.
// senderPubKeyB64: when provided (buddy sync), only accept comparisons involving the sender.
// When null/undefined (device sync), accept all — user's own devices see everything.
function _mergeComparisons(remote, senderPubKeyB64) {
  if (!Array.isArray(remote) || !remote.length) return false;
  const local = typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
  const localIds = new Set(local.map(c => c.id));
  let changed = false;
  for (const rc of remote) {
    if (!rc.id || localIds.has(rc.id)) continue;
    // Privacy gate: buddy sync only accepts comparisons involving the sender
    if (senderPubKeyB64 && !_comparisonInvolvesBuddy(rc, senderPubKeyB64)) continue;
    local.push(rc);
    changed = true;
  }
  if (changed) {
    local.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
    if (local.length > 50) local.length = 50;
    if (typeof saveSavedComparisons === 'function') saveSavedComparisons(local);
  }
  return changed;
}

// Notify buddies whose photos are used in a new comparison
function _notifyBuddiesOfComparison(comparison) {
  const notified = new Set();
  for (const src of [comparison.srcA, comparison.srcB]) {
    if (!src) continue;
    let pubKeyB64 = null;
    if (src.startsWith('buddy:')) {
      pubKeyB64 = src.split(':')[1];
    } else if (src.startsWith('brecv:')) {
      const sl16 = src.split(':')[1];
      const contacts = typeof getContacts === 'function' ? getContacts() : [];
      const c = contacts.find(x => x.pubKeyB64?.startsWith(sl16));
      if (c) pubKeyB64 = c.pubKeyB64;
    }
    if (!pubKeyB64 || notified.has(pubKeyB64)) continue;
    notified.add(pubKeyB64);
    // Only notify mutually trusted buddies
    const contacts = typeof getContacts === 'function' ? getContacts() : [];
    const c = contacts.find(x => x.pubKeyB64 === pubKeyB64);
    if (!c?.trusted || !c?.senderTrusts) continue;
    const dc = typeof _getDC === 'function' ? _getDC(pubKeyB64) : null;
    if (dc && dc.readyState === 'open' && typeof _dcSend === 'function') {
      const myName = typeof getDisplayNameSync === 'function' ? (getDisplayNameSync() || 'Me') : 'Me';
      _dcSend(dc, { type: 'comparison_notify', name: comparison.name, creatorName: myName });
    }
  }
}

// Send comparisons to a trusted buddy via DC — only those involving this buddy
function _sendComparisons(dc, contactPubKeyB64) {
  const contacts = typeof getContacts === 'function' ? getContacts() : [];
  const c = contacts.find(x => x.pubKeyB64 === contactPubKeyB64);
  if (!c?.trusted || !c?.senderTrusts) return; // mutual trust required
  const all = typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
  const comparisons = all.filter(cmp => _comparisonInvolvesBuddy(cmp, contactPubKeyB64));
  if (!comparisons.length) return;
  if (typeof _dcSend === 'function') {
    _dcSend(dc, { type: 'comparisons_sync', comparisons });
  }
}
