'use strict';
// ═══════════════════════════════════════════════
// js/app.js — init, tab system, sidebar, ToS, toast, utilities
// ═══════════════════════════════════════════════

// ── Unicode sanitization ──
// Strips bidi overrides, zero-width chars, and control chars that can spoof UI
const _UNSAFE_UNICODE = /[\u202A-\u202E\u2066-\u2069\u200E\u200F\u200B-\u200D\uFEFF\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
function sanitizeUnicode(str) {
  if (typeof str !== 'string') return str;
  return str.replace(_UNSAFE_UNICODE, '');
}

// ── Escape HTML ──
function esc(s) {
  if (s == null) return '';
  return sanitizeUnicode(String(s))
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// ── Toast ──
let _toastTimer = null;
function toast(msg, isErr, _noMirror) {
  const el = document.getElementById('toast');
  if (!el) return;
  el.textContent = msg;
  el.className = 'toast show' + (isErr ? ' err' : '');
  clearTimeout(_toastTimer);
  _toastTimer = setTimeout(() => el.classList.remove('show'), 3000);
  // Mirror every toast (non-error) into the notification panel so nothing is lost
  // _noMirror=true when called from pushNotification to prevent circular loop
  if (!isErr && !_noMirror && typeof pushNotification === 'function') {
    pushNotification('system', msg, '', { ttl: 7 * 86400 * 1000 });
  }
}

// ── Notify (buddy events) ──
// Thin wrapper: delegates to notifications.js pushNotification so that all
// buddy-event notifications live in one unified store.

function notify(msg, opts = {}) {
  if (typeof pushNotification !== 'function') return;
  pushNotification('event', msg, '', {
    tag: opts.tag || msg,
    nav: opts.nav || null,
    ttl: 30 * 86400 * 1000,
  });
}

// ── Confirm modal ──
function confirm2(title, body, onOk) {
  const m = document.getElementById('confirmModal');
  if (!m) { if (confirm(body)) onOk(); return; }
  document.getElementById('confirm-title').textContent = title;
  document.getElementById('confirm-body').textContent = body;
  const okBtn = document.getElementById('confirm-ok');
  const newOk = okBtn.cloneNode(true);
  newOk.disabled = false;
  okBtn.parentNode.replaceChild(newOk, okBtn);
  newOk.addEventListener('click', () => {
    newOk.disabled = true;
    m.classList.remove('open');
    onOk();
  });
  m.classList.add('open');
}

function openModal(id) {
  document.getElementById(id)?.classList.add('open');
}

function closeModal(id) {
  document.getElementById(id)?.classList.remove('open');
}

// ── Lightbox ──
function openLb(src) {
  const lb = document.getElementById('lightbox');
  if (!lb) return;
  const img = lb.querySelector('img');
  if (img) img.src = src;
  lb.classList.add('open');
}

// ── Copy to clipboard ──
function copyToClipboard(text) {
  // Auto-clear clipboard after 60s for sensitive codes (identity, backup)
  const sensitive = /^FR[1PK]/.test(text);
  const onCopied = () => {
    toast('Copied ✓');
    if (sensitive) setTimeout(() => {
      navigator.clipboard?.writeText('').catch(() => {});
    }, 60000);
  };
  navigator.clipboard?.writeText(text).then(onCopied).catch(() => {
    // Fallback
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed'; ta.style.opacity = '0';
    document.body.appendChild(ta);
    ta.select();
    document.execCommand('copy');
    document.body.removeChild(ta);
    onCopied();
  });
}

// ── Tab system ──
const TABS = ['dash', 'journal', 'activity', 'contacts', 'messages', 'gallery', 'compare', 'timeline', 'data', 'settings'];
let _currentTab = 'dash';

function tab(name) {
  _currentTab = name;
  TABS.forEach(t => {
    const page    = document.getElementById('page-' + t);
    const navItem = document.querySelector(`.nav-item[data-tab="${t}"]`);
    const bnItem  = document.querySelector(`.bottom-nav-item[data-tab="${t}"]`);
    if (page)    page.classList.toggle('active', t === name);
    if (navItem) navItem.classList.toggle('active', t === name);
    if (bnItem)  bnItem.classList.toggle('active', t === name);
  });
  // Always close sidebar when navigating
  closeSidebar();
  // Render page content
  if (name === 'dash') renderDash();
  if (name === 'journal')  renderJournalPage();
  if (name === 'contacts') renderContactsPage();
  if (name === 'activity') { if (typeof renderActivityPage === 'function') renderActivityPage(); }
  if (name === 'messages') { if (typeof renderMessengerPage === 'function') renderMessengerPage(); }
  if (name === 'gallery')  { if (typeof renderGallery === 'function') renderGallery(); }
  if (name === 'compare')  { populateCompareDrops(); renderCompare(); }
  if (name === 'timeline') renderTimeline();
  if (name === 'data')     { if (typeof renderDataPage === 'function') renderDataPage(); }
  if (name === 'settings') { if (typeof renderSettingsPage === 'function') renderSettingsPage(); }
}

// ── Sidebar toggle ──
function toggleSidebar() {
  document.getElementById('sidebar')?.classList.toggle('open');
}

function closeSidebar() {
  document.getElementById('sidebar')?.classList.remove('open');
}

// ── Theme (light default; dark is opt-in via Settings > Appearance) ──
// The head bootstrap script applies the stored choice before first paint;
// these functions handle changes at runtime.
function getTheme() {
  try { return localStorage.getItem('frj_theme') || 'light'; } catch (e) { return 'light'; }
}

function applyTheme() {
  const t = getTheme();
  const dark = t === 'dark' || (t === 'system' && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches);
  if (dark) document.documentElement.setAttribute('data-theme', 'dark');
  else document.documentElement.removeAttribute('data-theme');
}

function setTheme(t) {
  try { localStorage.setItem('frj_theme', t); } catch (e) { /* private mode */ }
  applyTheme();
  if (_currentTab === 'settings' && typeof renderSettingsPage === 'function') renderSettingsPage();
}

// Follow OS changes while in system mode
if (window.matchMedia) {
  try {
    matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
      if (getTheme() === 'system') applyTheme();
    });
  } catch (e) { /* older Safari lacks addEventListener on MediaQueryList */ }
}

// ── Sidebar accordion ──
function initAccordions() {
  document.querySelectorAll('.sb-section-hdr').forEach(hdr => {
    hdr.addEventListener('click', () => {
      const section = hdr.closest('.sb-section');
      if (!section) return;
      section.classList.toggle('collapsed');
    });
  });
}

// ── Dev note banner ──
const _DEV_NOTE = 'Some issues with the buddy system are being fixed.';

function _checkDevNote() {
  const el = document.getElementById('dev-note');
  const txt = document.getElementById('dev-note-text');
  const btn = document.getElementById('dev-note-close');
  if (!el || !txt || !_DEV_NOTE) return;
  // Key by message content so a new message reappears even if previous was dismissed
  const noteKey = 'frj_devnote_' + _DEV_NOTE.length + '_' + _DEV_NOTE.slice(0, 20).replace(/\W/g, '');
  if (localStorage.getItem(noteKey)) return;
  txt.textContent = _DEV_NOTE;
  el.style.display = '';
  if (btn) btn.onclick = () => { el.style.display = 'none'; localStorage.setItem(noteKey, '1'); };
}

// ── ToS modal ──
function checkTos() {
  const accepted = localStorage.getItem('frj_accepted');
  if (!accepted) {
    document.getElementById('tosModal')?.classList.add('open');
  }
}

function acceptTos() {
  const ageCheck = document.getElementById('tos-age-check');
  if (ageCheck && !ageCheck.checked) return; // guard against keyboard shortcuts bypassing the checkbox
  localStorage.setItem('frj_accepted', '1');
  document.getElementById('tosModal')?.classList.remove('open');
}
// Wire up ToS checkbox → button enable (CSP blocks inline onchange)
document.getElementById('tos-age-check')?.addEventListener('change', function() {
  const btn = document.getElementById('tos-accept-btn');
  if (btn) btn.disabled = !this.checked;
});

// ── ESC key handler ──
document.addEventListener('keydown', e => {
  if (e.key !== 'Escape') return;
  // Close open modals in reverse priority
  ['contactEntryModal', 'lightbox', 'webcamModal', 'confirmModal', 'profileModal'].forEach(id => {
    const m = document.getElementById(id);
    if (m && m.classList.contains('open')) {
      if (id === 'webcamModal') webcamClose();
      else m.classList.remove('open');
    }
  });
});

// ── Block right-click and drag on all images ──
document.addEventListener('contextmenu', e => { if (e.target.tagName === 'IMG') e.preventDefault(); });
document.addEventListener('dragstart',   e => { if (e.target.tagName === 'IMG') e.preventDefault(); });

// ── Click outside modal to close lightbox ──
document.addEventListener('click', e => {
  const lb = document.getElementById('lightbox');
  if (lb && lb.classList.contains('open') && e.target === lb) {
    lb.classList.remove('open');
  }
  // Close buddy dropdown if click is outside the status button/dropdown
  if (!e.target.closest('#buddyStatusBtn') && !e.target.closest('#buddyDropdown')) {
    const dd = document.getElementById('buddyDropdown');
    if (dd && dd.style.display !== 'none') { dd.style.display = 'none'; _hideDropdownBackdrop(); }
  }
  if (!e.target.closest('#deviceStatusBtn') && !e.target.closest('#deviceDropdown')) {
    const ddd = document.getElementById('deviceDropdown');
    if (ddd && ddd.style.display !== 'none') { ddd.style.display = 'none'; _hideDropdownBackdrop(); }
  }
  if (!e.target.closest('#hdrDotsPill') && !e.target.closest('#notifDropdown')) {
    const nd = document.getElementById('notifDropdown');
    if (nd) nd.style.display = 'none';
  }
  // Close profile modal when clicking on the overlay backdrop
  const pm = document.getElementById('profileModal');
  if (pm && pm.classList.contains('open') && e.target === pm) {
    pm.classList.remove('open');
  }
  // Close chat modal when clicking on the overlay backdrop
  const cm = document.getElementById('chatModal');
  if (cm && cm.classList.contains('open') && e.target === cm) {
    if (typeof closeChatPopup === 'function') closeChatPopup();
    else cm.classList.remove('open');
  }
  // Close gallery image modal when tapping the overlay backdrop. Tall photos
  // can push the X close button off-screen on small viewports, so backdrop-tap
  // is the only reliable close gesture there.
  const gm = document.getElementById('galImgModal');
  if (gm && gm.classList.contains('open') && e.target === gm) {
    if (typeof closeModal === 'function') closeModal('galImgModal');
    else gm.classList.remove('open');
  }
});

// ── Storage size — use updateStorageSize() / fmtBytes() from export.js ──
// ── Privacy curtain ──
const CURTAIN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
let _curtainTimer = null;

function _resetCurtainTimer() {
  clearTimeout(_curtainTimer);
  _curtainTimer = setTimeout(showCurtain, CURTAIN_TIMEOUT_MS);
}

function _clearAppDOM() {
  // Hide pages behind the curtain (curtain is z-index:99999 and covers everything visually).
  TABS.forEach(t => {
    const page = document.getElementById('page-' + t);
    if (page) page.style.visibility = 'hidden';
  });
  // Revoke all cached blob URLs so images are not accessible in memory
  if (typeof imgCache !== 'undefined') {
    Object.keys(imgCache).forEach(k => { URL.revokeObjectURL(imgCache[k]); delete imgCache[k]; });
  }
  // Wipe dynamic content containers so sensitive journal/contact data is not
  // readable from the DOM while the curtain is active (e.g. via DevTools or
  // a browser extension). tab() does a full re-render on unlock so these are
  // safe to clear — only the outer container elements need to survive.
  [
    'journal-list-pane', 'journal-entry-detail',
    'contacts-content',
    'timeline-content',
    'dash-timer', 'dash-welcome', 'dash-backup-nudge', 'dash-daily-chart',
    'dash-quick-log', 'dash-profile', 'dash-activity-feed'
  ].forEach(id => {
    const el = document.getElementById(id);
    if (el) el.innerHTML = '';
  });
}

// ── refreshImg — update DOM elements showing a specific IDB image key ──
function refreshImg(key) {
  if (!key || typeof imgCache === 'undefined') return;
  const url = imgCache[key];
  if (!url) return;
  document.querySelectorAll(`[data-img-key="${CSS.escape(key)}"]`).forEach(el => {
    if (el.tagName === 'IMG') {
      if (el.src !== url) el.src = url;
    } else {
      // placeholder div → swap in an img
      const img = document.createElement('img');
      img.className = el.className.replace('placeholder', '').trim();
      img.alt = '';
      img.src = url;
      img.dataset.imgKey = key;
      el.replaceWith(img);
    }
  });
}

// ── _pillRipple — append ripple ring to the pill container (not the button)
// so Safari flex reflow is never triggered by DOM mutations inside flex items.
function _pillRipple(dot, cls, delays) {
  const pill = document.getElementById('hdrStatusWrap') || document.getElementById('hdrDotsPill');
  if (!pill || !dot) return;
  const pillRect = pill.getBoundingClientRect();
  const dotRect  = dot.getBoundingClientRect();
  const l = (dotRect.left - pillRect.left).toFixed(1);
  const t = (dotRect.top  - pillRect.top).toFixed(1);
  const s = dot.offsetWidth || 12;
  delays.forEach(delay => {
    setTimeout(() => {
      const ring = document.createElement('span');
      ring.className = cls;
      ring.style.cssText = `width:${s}px;height:${s}px;left:${l}px;top:${t}px;`;
      pill.appendChild(ring);
      ring.addEventListener('animationend', () => ring.remove(), { once: true });
    }, delay);
  });
}

// ── syncRipple — pulse the devices orb after a successful sync ──
function syncRipple(_mergedCount) {
  const dot = document.getElementById('dotDevices');
  if (!dot) return;
  dot.classList.add('sync-pulse');
  setTimeout(() => dot.classList.remove('sync-pulse'), 1400);
  _pillRipple(dot, 'hdr-dot-ripple', [0, 320]);
}

// ── syncWarnRipple — orange pulse on devices orb for sync warnings (e.g. timeout) ──
function syncWarnRipple() {
  const dot = document.getElementById('dotDevices');
  if (!dot) return;
  dot.classList.add('sync-warn-pulse');
  setTimeout(() => dot.classList.remove('sync-warn-pulse'), 1800);
  _pillRipple(dot, 'hdr-dot-ripple-warn', [0]);
}

// ── buddyRipple — pulse the buddy network orb after syncing with a buddy ──
function buddyRipple() {
  const dot = document.getElementById('peerDot');
  if (!dot) return;
  dot.classList.add('sync-pulse');
  setTimeout(() => dot.classList.remove('sync-pulse'), 1400);
  _pillRipple(dot, 'hdr-dot-ripple', [0, 320]);
}

let _curtainShowing = false;

function showCurtain() {
  if (_curtainShowing) return; // prevent double-call (inactivity timer + visibilitychange)
  _curtainShowing = true;
  clearTimeout(_curtainTimer);
  const el = document.getElementById('privacyCurtain');
  if (!el) { _curtainShowing = false; return; }

  // Set avatar + name
  if (typeof _setCurtainIdentity === 'function') _setCurtainIdentity();

  // Timer state lives in _frjEncCache when schema v2 (not plain localStorage).
  // encryptionLock() clears the cache, so timer state is inaccessible while locked.
  // The timer tick restarts on unlock via _hideCurtainAndRestore → renderDash → _renderDashTimer.

  // Clear DOM and session key before showing curtain
  _clearAppDOM();
  if (typeof encryptionLock === 'function') encryptionLock();

  el.style.display = 'flex';
  if (typeof p2pBroadcastCurtain === 'function') p2pBroadcastCurtain(true);
  // Keep WS alive while curtain is up — confirm connection is healthy before going idle
  if (typeof p2pResumeAfterBackground === 'function') p2pResumeAfterBackground();

  // Wire up unlock — re-render on success
  if (typeof _encLockAwaitUnlock === 'function') {
    _encLockAwaitUnlock().then(() => _hideCurtainAndRestore()).catch(e => {
      console.error('[app] curtain unlock failed:', e);
      _curtainShowing = false;
      // Ensure UI is usable — re-render current tab even if cache reload failed
      try { tab(_currentTab); } catch(e2) {}
      _resetCurtainTimer();
    });
  } else {
    _curtainShowing = false;
  }
}

async function _hideCurtainAndRestore() {
  _curtainShowing = false;
  const el = document.getElementById('privacyCurtain');
  if (el) el.style.display = 'none';
  if (typeof p2pBroadcastCurtain === 'function') p2pBroadcastCurtain(false);
  // Reconnect immediately — WS may have dropped while curtain was up
  if (typeof p2pResumeAfterBackground === 'function') p2pResumeAfterBackground();
  // Restore page visibility (hidden by _clearAppDOM)
  TABS.forEach(t => {
    const page = document.getElementById('page-' + t);
    if (page) page.style.visibility = '';
  });
  // Clear lock flag — _encLockAwaitUnlock set _sessionKey but doesn't reset _encLocked
  if (typeof _encLocked !== 'undefined') _encLocked = false;
  // Schema v2: reload decrypted data cache (cleared by lock)
  if (typeof _loadDecryptedCache === 'function') await _loadDecryptedCache();
  // Repopulate image cache then re-render the active tab
  if (typeof preloadImgs === 'function') await preloadImgs();
  tab(_currentTab);
  _resetCurtainTimer();
}

function _refreshEncryptBtn() {} // button removed

function updateDataEncBadge() {
  const badge = document.getElementById('data-enc-badge');
  if (!badge) return;
  Promise.all([
    typeof idbMetaGet === 'function' ? idbMetaGet('encryptionEnabled') : Promise.resolve(null),
    typeof getSchemaVersion === 'function' ? getSchemaVersion() : Promise.resolve(null)
  ]).then(([enabled, v]) => {
    if (enabled) {
      badge.style.display = '';
      badge.style.background = 'rgba(34,197,94,.15)';
      badge.style.border = '1px solid rgba(34,197,94,.3)';
      badge.style.color = '#22c55e';
      badge.textContent = v === 2 ? '🔒 Encrypted' : '🔒 Encrypting…';
    } else {
      badge.style.display = '';
      badge.style.background = 'rgba(245,158,11,.15)';
      badge.style.border = '1px solid rgba(245,158,11,.3)';
      badge.style.color = '#f59e0b';
      badge.textContent = '🔓 Unencrypted';
    }
  }).catch(() => { if (badge) badge.style.display = 'none'; });
}

function hideCurtain() {
  // Legacy: called externally (e.g. p2p curtain sync). Just hide without re-render.
  const el = document.getElementById('privacyCurtain');
  if (el) el.style.display = 'none';
  if (typeof p2pBroadcastCurtain === 'function') p2pBroadcastCurtain(false);
  _resetCurtainTimer();
}

function _initCurtain() {
  ['mousemove', 'keydown', 'click', 'touchstart', 'scroll'].forEach(ev => {
    document.addEventListener(ev, _resetCurtainTimer, { passive: true });
  });
  _resetCurtainTimer();
}

// ── Phase 5: Background lock — clear key + show curtain after 30 min hidden ──
const _BG_LOCK_MS = 30 * 60 * 1000;
let _bgHiddenAt = null;

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    _bgHiddenAt = Date.now();
  } else {
    if (_bgHiddenAt !== null && Date.now() - _bgHiddenAt >= _BG_LOCK_MS) {
      // Was hidden for 30+ min — lock and require re-auth
      showCurtain();
    }
    _bgHiddenAt = null;
    // Reconnect / re-register signal server immediately (mobile browsers
    // suspend timers and may kill the WebSocket while backgrounded)
    if (typeof p2pResumeAfterBackground === 'function') p2pResumeAfterBackground();
    // Sync on tab refocus to pick up changes from other devices
    if (typeof syncAfterSave === 'function') syncAfterSave();
  }
});

// Clear key from memory when tab is closed/navigated away
window.addEventListener('beforeunload', () => {
  if (typeof encryptionLock === 'function') encryptionLock();
});

// Detect bfcache restore (e.g. after computer sleep discarded the page).
// JS state (timers, promises, IDB handles) is dead — force a full reload.
window.addEventListener('pageshow', (e) => {
  if (e.persisted) location.reload();
});

// ── Storage / private-mode warning ──
function _initStorageWarning() {
  // Already dismissed in a normal (persistent) session — skip.
  // In private/incognito localStorage is blank on every load, so dismissed is never set.
  if (localStorage.getItem('frj_swDismissed')) return;

  const bar = document.getElementById('storage-warn');
  if (!bar) return;

  bar.innerHTML = `<div class="storage-warn-bar">
    <span class="storage-warn-icon">⚠</span>
    <div class="storage-warn-body">
      <div class="storage-warn-title">Your data lives only on this device</div>
      <div>This app stores everything locally — your journal never leaves your device unless you explicitly share or export it.
           Clearing your browser data or using a private / incognito window will erase it.</div>
      <div class="storage-warn-private" id="sw-private-note">
        ⚠ You appear to be in a private or incognito window. Your journal will not be saved when you close this tab.
      </div>
    </div>
    <button class="storage-warn-dismiss" id="sw-dismiss-btn">Got it</button>
  </div>`;
  bar.style.display = '';

  document.getElementById('sw-dismiss-btn')?.addEventListener('click', () => {
    try { localStorage.setItem('frj_swDismissed', '1'); } catch(e) {}
    bar.style.display = 'none';
  });

  // Async private-mode detection — best-effort, not 100% reliable across all browsers
  _detectPrivateMode().then(isPrivate => {
    if (isPrivate) {
      document.getElementById('sw-private-note')?.classList.add('visible');
    }
  });
}

async function _detectPrivateMode() {
  try {
    // Primary signal: dramatically reduced storage quota (reliable for Safari private mode).
    // Chrome/Firefox incognito: localStorage is blank so banner shows regardless.
    if (navigator.storage?.estimate) {
      const { quota } = await navigator.storage.estimate();
      if (quota && quota < 600 * 1024 * 1024) return true; // < 600 MB → almost certainly private
    }
  } catch(e) {}
  return false;
}

// ── Blur images ──
function _applyBlurImages() {
  const on = !!lsGet('frj_blur_images', false);
  document.body.classList.toggle('blur-images', on);
  const btn = document.getElementById('blurImagesBtn');
  if (btn) { btn.textContent = on ? 'Show Images' : 'Hide Images'; btn.classList.toggle('active', on); }
}
function toggleBlurImages() {
  lsSet('frj_blur_images', !lsGet('frj_blur_images', false));
  _applyBlurImages();
}

function _applyP2POffline() {
  const offline = typeof p2pIsOfflineMode === 'function' ? p2pIsOfflineMode() : localStorage.getItem('frj_p2p_offline') !== '0';
  document.body.classList.toggle('p2p-offline', offline);
  if (offline && typeof updatePresenceToggleUI === 'function') updatePresenceToggleUI('offline');
  if (typeof updateNetworkToggleUI === 'function') updateNetworkToggleUI();
  const killLabel = document.getElementById('killSwitchLabel');
  if (killLabel) killLabel.textContent = offline ? 'Offline' : 'Online';
}
function toggleP2POffline() {
  const goOffline = typeof p2pIsOfflineMode === 'function' ? !p2pIsOfflineMode() : localStorage.getItem('frj_p2p_offline') === '0';
  localStorage.setItem('frj_p2p_offline', goOffline ? '1' : '0');
  if (typeof p2pSetOffline === 'function') p2pSetOffline(goOffline);
  _applyP2POffline();
}

// ── Dropdown backdrop ──
function _showDropdownBackdrop() {
  const bd = document.getElementById('dropdown-backdrop');
  if (bd) bd.style.display = '';
}
function _hideDropdownBackdrop() {
  const bd = document.getElementById('dropdown-backdrop');
  if (bd) bd.style.display = 'none';
  // Also close any open dropdown when backdrop is dismissed
  const dd = document.getElementById('deviceDropdown');
  const bdd = document.getElementById('buddyDropdown');
  const nd = document.getElementById('notifDropdown');
  if (dd) dd.style.display = 'none';
  if (bdd) bdd.style.display = 'none';
  if (nd) nd.style.display = 'none';
}

// ── Watermark settings ──
function _applyWatermarkSettings() {
  const wm = (typeof getWatermarkSettings === 'function') ? getWatermarkSettings() : { style: 'none', opacity: 'medium' };
  const styleSel = document.getElementById('wmStyleSel');
  const opSel    = document.getElementById('wmOpacitySel');
  if (styleSel) styleSel.value = wm.style || 'none';
  if (opSel)    opSel.value    = wm.opacity || 'medium';
}
function saveWatermarkStyle(val) {
  const wm = (typeof getWatermarkSettings === 'function') ? getWatermarkSettings() : { style: 'none', opacity: 'medium' };
  wm.style = val;
  if (typeof saveWatermarkSettings === 'function') saveWatermarkSettings(wm);
  if (typeof stampSettingsTs === 'function') stampSettingsTs();
  if (typeof syncAfterSave   === 'function') syncAfterSave();
  // Re-render any live buddy canvases
  document.querySelectorAll('canvas[data-img-key][data-wm-name]').forEach(c => {
    delete c.dataset.rendered;
    if (typeof _drawPhotoCanvas === 'function') _drawPhotoCanvas(c);
  });
}
function saveWatermarkOpacity(val) {
  const wm = (typeof getWatermarkSettings === 'function') ? getWatermarkSettings() : { style: 'none', opacity: 'medium' };
  wm.opacity = val;
  if (typeof saveWatermarkSettings === 'function') saveWatermarkSettings(wm);
  if (typeof stampSettingsTs === 'function') stampSettingsTs();
  if (typeof syncAfterSave   === 'function') syncAfterSave();
  document.querySelectorAll('canvas[data-img-key][data-wm-name]').forEach(c => {
    delete c.dataset.rendered;
    if (typeof _drawPhotoCanvas === 'function') _drawPhotoCanvas(c);
  });
}

// Version baked into this JS bundle. MUST be bumped together with version.txt and
// index.html's ?v=... query strings on every deploy — if this constant and the
// server's version.txt disagree, every visitor reloads once per page load.
const APP_VERSION = '20260729ao';

// Auto-update check: fetch a tiny version stamp from the server, bypassing all caches.
// If the server is on a newer version than the JS we're running, force a cache-busted
// reload immediately. iOS PWAs are notoriously bad at picking up new service workers;
// this check makes the app self-healing — stale boots auto-refresh on next open.
async function _checkVersionAndAutoRefresh() {
  try {
    // Random query string defeats every layer of cache (browser HTTP cache, SW cache).
    const r = await fetch('./version.txt?nc=' + Date.now() + Math.random().toString(36).slice(2),
      { cache: 'no-store', credentials: 'omit' });
    if (!r.ok) return;
    const serverVersion = (await r.text()).trim();
    if (!serverVersion || serverVersion === APP_VERSION) {
      // Up to date: drop the one-shot reload marker from the address bar if present
      const u = new URL(window.location.href);
      if (u.searchParams.has('_autoupd')) {
        u.searchParams.delete('_autoupd');
        history.replaceState(null, '', u.toString());
      }
      return;
    }
    // Loop guard: if we already reloaded for this exact server version and the
    // running code is STILL stale, reloading again can't help (deploy desync,
    // e.g. version.txt bumped without APP_VERSION). Give up quietly instead of
    // refresh-looping the user forever.
    const url = new URL(window.location.href);
    if (url.searchParams.get('_autoupd') === serverVersion) {
      console.warn(`[autoupdate] still on ${APP_VERSION} after reloading for ${serverVersion} — deploy is out of sync, not reloading again`);
      return;
    }
    console.log(`[autoupdate] stale build detected: running ${APP_VERSION}, server has ${serverVersion} — reloading`);
    // Cache-bust reload. IDB / localStorage are origin-scoped, not touched by URL changes.
    url.searchParams.set('_autoupd', serverVersion);
    window.location.replace(url.toString());
  } catch(e) { /* offline or version.txt missing -- ignore */ }
}

// ── Init ──
async function init() {
  // Fire the version check immediately (non-blocking). If a newer build is on the
  // server, the page will reload before the rest of init even runs.
  _checkVersionAndAutoRefresh();

  // Register service worker for offline caching + PWA installability
  if ('serviceWorker' in navigator) {
    // updateViaCache:'none' ensures the browser always fetches sw.js from the network
    navigator.serviceWorker.register('./sw.js', { updateViaCache: 'none' })
      .then(reg => {
        // Aggressive update checks for iOS PWA (which rarely auto-checks)
        // 1. Immediately on page load
        reg.update().catch(() => {});
        // 2. Every time the page becomes visible (e.g. PWA brought to foreground).
        //    Also fires the version check — if a new build went up while the PWA was
        //    suspended, this catches it the moment the user opens it again.
        document.addEventListener('visibilitychange', () => {
          if (document.visibilityState === 'visible') {
            reg.update().catch(() => {});
            _checkVersionAndAutoRefresh();
          }
        });
        // 3. On window focus (catches more edge cases on iOS)
        window.addEventListener('focus', () => {
          reg.update().catch(() => {});
          _checkVersionAndAutoRefresh();
        });
        // 4. Every 5 minutes (in case the tab stays open and visible)
        setInterval(() => reg.update().catch(() => {}), 5 * 60 * 1000);
        // Expose to a manual "Check for updates" button in Settings
        window._frjSwReg = reg;
      })
      .catch(e => console.warn('[sw] registration failed:', e));

    // When a new SW activates and claims this client, reload to pick up new code.
    // controllerchange fires when clients.claim() runs in a new SW.
    // Guard: only if we already had a controller (skip first-ever install).
    if (navigator.serviceWorker.controller) {
      let reloading = false;
      navigator.serviceWorker.addEventListener('controllerchange', () => {
        if (!reloading) { reloading = true; window.location.reload(); }
      });
    }

    // Belt-and-suspenders: new SW posts SW_UPDATED during activate
    navigator.serviceWorker.addEventListener('message', evt => {
      if (evt.data?.type === 'SW_UPDATED') window.location.reload();
    });
  }

  try { await openIDB(); } catch(e) { throw new Error('IDB open failed: ' + e.message); }
  // Request persistent storage so browsers (especially Safari/iOS) don't evict IDB data
  if (navigator.storage?.persist) navigator.storage.persist().catch(() => {});
  try { await encryptionInit(); } catch(e) { throw new Error('Encryption init failed: ' + e.message); }
  try { await sprint0Init(); } catch(e) { throw new Error('Sprint0 init failed: ' + e.message); }

  // If sprint0Init just advanced schema to v2 (fresh device, first run after pairing),
  // _loadDecryptedCache ran before schema was v2 so the cache was never initialized.
  if (typeof _loadDecryptedCache === 'function' && typeof _frjEncCache !== 'undefined' && _frjEncCache === null) {
    try { await _loadDecryptedCache(); } catch(e) { throw new Error('Decrypt cache reload failed: ' + e.message); }
  }

  // Generate ECDH identity if none exists (identity.js)
  let existing;
  try { existing = await getIdentity(); } catch(e) { throw new Error('Identity load failed: ' + e.message); }
  if (!existing) {
    try { await generateIdentity(); } catch(e) { throw new Error('Identity generation failed: ' + e.message); }
  }

  // Start signaling client for auto-connect (p2p.js)
  try { initSignaling(); } catch(e) { console.error('initSignaling failed:', e); }
  if (typeof initNetworkToggles === 'function') try { initNetworkToggles(); } catch(e) { console.error('initNetworkToggles failed:', e); }

  // Sprint 2/3: start device sync engine (sync.js)
  if (typeof syncInit === 'function') { try { await syncInit(); } catch(e) { console.error('syncInit failed:', e); } }

  // Prune old tombstones on init (not just during sync) — benefits unpaired users too
  if (typeof _pruneTombstones === 'function') try { _pruneTombstones(); } catch(e) { console.error(e); }

  // Dissipate old timer sessions (>1yr) into weekly stats
  if (typeof _dissipateOldTimerSessions === 'function') try { _dissipateOldTimerSessions(); } catch(e) { console.error(e); }

  // Auto-aggregate completed weeks from timer sessions into weekly stats
  if (typeof _autoAggregateWeeks === 'function') try { _autoAggregateWeeks(); } catch(e) { console.error(e); }

  // Migrate old weekly stats to enriched format (daysTugged, totalHrsTugged, hrsPerDayTugged)
  if (typeof _migrateWeeklyStats === 'function') try { _migrateWeeklyStats(); } catch(e) { console.error(e); }

  // Preload cached image blobs into imgCache
  try { await preloadImgs(); } catch(e) { console.error('preloadImgs failed:', e); }

  // Startup consistency check — detect orphan blob references (non-blocking)
  if (typeof runConsistencyCheck === 'function') runConsistencyCheck().catch(e => console.warn(e));

  // Storage / private-mode warning
  _initStorageWarning();

  // Check ToS — show modal if not accepted
  checkTos();
  _checkDevNote();
  if (typeof storageHealthInit === 'function') storageHealthInit();

  // Sidebar accordion
  initAccordions();

  // Set up nav items
  document.querySelectorAll('.nav-item[data-tab]').forEach(el => {
    el.addEventListener('click', () => tab(el.dataset.tab));
  });

  // Start on dashboard
  tab('dash');

  // Storage size display
  updateStorageSize();
  updateBackupWarning();
  updateDataEncBadge();
  updatePeerStatus(); // peer-dot starts as offline (no connections yet)
  if (typeof initNotifications === 'function') initNotifications();

  // Privacy curtain
  _initCurtain();

  // Blur images setting + encryption button visibility
  _applyBlurImages();
  _applyP2POffline();
  _refreshEncryptBtn();
  _applyWatermarkSettings();

}

window.addEventListener('load', () => init().catch(e => {
  console.error('[app] init failed:', e);
  const el = document.getElementById('app') || document.body;
  el.textContent = '';
  const wrap = document.createElement('div');
  wrap.style.cssText = 'padding:2em;text-align:center;color:#c00';
  const h = document.createElement('h2'); h.textContent = 'Failed to start'; wrap.appendChild(h);
  const p1 = document.createElement('p'); p1.textContent = e.message || 'Unknown error'; wrap.appendChild(p1);
  const p2 = document.createElement('p'); p2.textContent = 'Try refreshing the page. If the problem persists, clear site data in browser settings.'; wrap.appendChild(p2);
  el.appendChild(wrap);
}));
