'use strict';
// ═══════════════════════════════════════════════
// js/encryption.js — At-rest encryption
// Phase 0: Safety net + schema versioning
// Phase 1: Key derivation + password setup / lock screen
// ═══════════════════════════════════════════════

// ── Session key (memory only — never persisted) ──────────────────────────────
let _sessionKey = null;
let _encLocked  = false; // true when encryption is enabled but session not unlocked
function getSessionKey() { return _sessionKey; }
function isEncryptionLocked() { return _encLocked; }
function encryptionLock() {
  _sessionKey = null;
  _frjEncCache = null;   // clear decrypted data from memory
  _frjBlobSchemaV2 = false;
  _encLocked = true;
}

// ── Schema version ────────────────────────────────────────────────────────────
async function getSchemaVersion() { return await idbMetaGet('schemaVersion') ?? null; }
async function setSchemaVersion(v) { await idbMetaPut('schemaVersion', v); }

// ── Phase 0: Emergency backup ─────────────────────────────────────────────────
function _ab2b64(ab) {
  const bytes = new Uint8Array(ab);
  let s = '';
  for (let i = 0; i < bytes.byteLength; i++) s += String.fromCharCode(bytes[i]);
  return btoa(s);
}

async function writeEmergencyBackup() {
  try {
    const lsData = {};
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      if (key && key.startsWith('frj_')) lsData[key] = localStorage.getItem(key);
    }
    const idbData = {};
    const keys = await idbGetAllKeys();
    for (const k of keys) {
      const val = await idbGet(k);
      if (val instanceof Blob) {
        const ab = await val.arrayBuffer();
        idbData[k] = { type: 'blob', mime: val.type, b64: _ab2b64(ab) };
      } else if (val instanceof ArrayBuffer) {
        idbData[k] = { type: 'arraybuffer', b64: _ab2b64(val) };
      }
    }
    await idbEmergencyPut('snapshot', {
      created: new Date().toISOString(), schemaVersion: 1, ls: lsData, idb: idbData
    });
  } catch(e) { console.warn('[encryption] writeEmergencyBackup failed:', e); }
}

async function _phase0() {
  try {
    const v = await getSchemaVersion();
    if (v === null) await setSchemaVersion(1);
    if (v === null || v === 1) await writeEmergencyBackup();
  } catch(e) { console.warn('[encryption] phase0 failed:', e); }
}

// ── Crypto helpers ─────────────────────────────────────────────────────────────
async function deriveKey(password, salt, iterations) {
  const keyMaterial = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: iterations || 600000, hash: 'SHA-256' },
    keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
  );
}

async function _encryptStr(key, str) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ct = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv }, key, new TextEncoder().encode(str)
  );
  return { iv: Array.from(iv), ciphertext: Array.from(new Uint8Array(ct)) };
}

async function _decryptStr(key, { iv, ciphertext }) {
  const pt = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: new Uint8Array(iv) }, key, new Uint8Array(ciphertext)
  );
  return new TextDecoder().decode(pt);
}

const _SENTINEL = 'forestorers-v1-ok';

// ── Password strength ──────────────────────────────────────────────────────────
function _pwStrength(pw) {
  if (!pw || pw.length < 4) return 0;
  let s = 0;
  if (pw.length >= 8)  s++;
  if (pw.length >= 12) s++;
  if (/[A-Z]/.test(pw) && /[a-z]/.test(pw)) s++;
  if (/[0-9]/.test(pw)) s++;
  if (/[^A-Za-z0-9]/.test(pw)) s++;
  return Math.min(4, s);
}
const _STR_LABELS = ['', 'Weak', 'Fair', 'Good', 'Strong'];
const _STR_COLORS = ['', 'var(--red)', '#f59e0b', '#10b981', '#10b981'];

// ── First-run setup screen ─────────────────────────────────────────────────────
function _showSetupScreen() {
  return new Promise(resolve => {
    const overlay = document.getElementById('enc-setup');
    overlay.classList.add('active');
    const pw1  = document.getElementById('enc-setup-pw1');
    const pw2  = document.getElementById('enc-setup-pw2');
    const fill = document.getElementById('enc-setup-bar-fill');
    const lbl  = document.getElementById('enc-setup-bar-label');
    const bar  = document.getElementById('enc-setup-bar');
    const chk  = document.getElementById('enc-setup-check');
    const btn  = document.getElementById('enc-setup-btn');
    const err  = document.getElementById('enc-setup-err');
    pw1.value = ''; pw2.value = ''; chk.checked = false; err.textContent = '';

    pw1.addEventListener('input', () => {
      const s = _pwStrength(pw1.value);
      fill.style.width = (s * 25) + '%';
      fill.style.background = _STR_COLORS[s] || '';
      lbl.textContent = _STR_LABELS[s] || '';
      bar.style.display = pw1.value ? 'flex' : 'none';
    });

    btn.onclick = async () => {
      err.textContent = '';
      if (!pw1.value || pw1.value.length < 8) { err.textContent = 'Password must be at least 8 characters.'; return; }
      if (pw1.value !== pw2.value) { err.textContent = 'Passwords do not match.'; return; }
      if (!chk.checked) { err.textContent = 'Please check the box to confirm.'; return; }
      btn.disabled = true; btn.textContent = 'Setting up…';
      try {
        const salt      = crypto.getRandomValues(new Uint8Array(16));
        const _iters    = 600000;
        const key       = await deriveKey(pw1.value, salt, _iters);
        const verifier  = await _encryptStr(key, _SENTINEL);
        await idbMetaPut('encryptionSalt',     Array.from(salt));
        await idbMetaPut('encryptionVerifier', verifier);
        await idbMetaPut('encryptionIterations', _iters);
        await idbMetaPut('encryptionEnabled',  true);
        _sessionKey = key;
        overlay.classList.remove('active');
        resolve();
      } catch(e) {
        err.textContent = 'Setup failed. Please try again.';
        btn.disabled = false; btn.textContent = 'Set Password';
      }
    };

    document.getElementById('enc-setup-skip').onclick = async () => {
      const ok = confirm(
        'Leave data unprotected?\n\n' +
        'This app stores your journal entries, progress photos, and body measurements.\n\n' +
        'Without encryption, anyone with access to this device or browser (including browser extensions, ' +
        'shared accounts, or physical access) can read everything, including photos.\n\n' +
        'You will not be able to export backups or use cross-device sync without encryption.\n\n' +
        'Are you sure you want to skip?'
      );
      if (!ok) return;
      await idbMetaPut('encryptionEnabled', false);
      overlay.classList.remove('active');
      resolve();
    };
  });
}

// ── Curtain lock/unlock ────────────────────────────────────────────────────────
// Wires up the curtain's password field. Returns a Promise that resolves on
// successful unlock (or immediately if encryption is not enabled).
function _encLockAwaitUnlock() {
  return new Promise(resolve => {
    const _wire = (enabled) => {
      const pwRow       = document.getElementById('curtain-pw-row');
      const pwInput     = document.getElementById('curtain-pw');
      const unlockBtn   = document.getElementById('curtain-unlock-btn');
      const err         = document.getElementById('curtain-err');
      const forgotLink  = document.getElementById('curtain-forgot');
      const forgotPanel = document.getElementById('curtain-forgot-panel');

      if (err)         err.textContent = '';
      if (pwInput)     pwInput.value = '';
      if (forgotPanel) forgotPanel.style.display = 'none';

      if (enabled) {
        if (pwRow)      pwRow.style.display      = 'block';
        if (forgotLink) forgotLink.style.display = 'inline';
        if (unlockBtn) { unlockBtn.textContent = 'Unlock'; unlockBtn.disabled = false; }
        setTimeout(() => pwInput && pwInput.focus(), 60);

        const doUnlock = async () => {
          if (err) err.textContent = '';
          if (!pwInput.value) { if (err) err.textContent = 'Enter your password.'; return; }
          unlockBtn.disabled = true; unlockBtn.textContent = 'Unlocking…';
          try {
            const saltArr  = await idbMetaGet('encryptionSalt');
            if (!saltArr) throw new Error('no-salt');
            const verifier = await idbMetaGet('encryptionVerifier');
            if (!verifier) throw new Error('no-verifier');
            const iters    = await idbMetaGet('encryptionIterations') || 200000;
            const key      = await deriveKey(pwInput.value, new Uint8Array(saltArr), iters);
            const result   = await _decryptStr(key, verifier);
            if (result !== _SENTINEL) throw new Error('wrong-password');
            _sessionKey = key;
            resolve();
          } catch(e) {
            console.error('[encryption] unlock failed:', e.message, e);
            if (err) err.textContent = (e.message === 'no-salt' || e.message === 'no-verifier')
              ? 'Encryption data missing — your browser may have cleared storage. Try re-pairing with your other device, or use "Forgot password" below.'
              : 'Incorrect password.';
            unlockBtn.disabled = false; unlockBtn.textContent = 'Unlock';
            if (pwInput) { pwInput.value = ''; pwInput.focus(); }
          }
        };

        if (unlockBtn) unlockBtn.onclick = doUnlock;
        if (pwInput)   pwInput.onkeydown = e => { if (e.key === 'Enter') doUnlock(); };

        if (forgotLink) forgotLink.onclick = e => {
          e.preventDefault();
          if (forgotPanel) forgotPanel.style.display = forgotPanel.style.display === 'none' ? 'block' : 'none';
        };

        const clearBtn = document.getElementById('curtain-clear-data');
        if (clearBtn) clearBtn.onclick = async () => {
          if (!confirm('This will permanently erase all your journal data. This cannot be undone.')) return;
          localStorage.clear();
          // Close our own IDB connection first to prevent "blocked" state
          if (typeof closeIDB === 'function') closeIDB();
          await new Promise(resolve => {
            const r = indexedDB.deleteDatabase('frj_db');
            r.onsuccess = resolve;
            r.onerror = resolve;
            r.onblocked = () => {
              // Another tab still holds the connection — delete will complete when it closes.
              // Proceed anyway; the reload will clear this tab's reference.
              resolve();
            };
          });
          location.reload();
        };

      } else {
        // No encryption — just a "Continue" tap
        if (pwRow)      pwRow.style.display      = 'none';
        if (forgotLink) forgotLink.style.display = 'none';
        if (unlockBtn) {
          unlockBtn.textContent = 'Continue';
          unlockBtn.disabled    = false;
          unlockBtn.onclick     = () => resolve();
        }
      }
    };

    idbMetaGet('encryptionEnabled').then(_wire).catch(idbErr => {
      console.error('[encryption] idbMetaGet(encryptionEnabled) failed — skipping curtain:', idbErr);
      resolve(); // IDB is broken — can't verify encryption; let user through rather than locking them out
    });
  });
}

// Set the curtain's avatar + name from profile / localStorage
function _setCurtainIdentity() {
  const profile  = typeof getProfile === 'function' ? getProfile() : {};
  const name     = profile.displayName || lsGet('frj_displayname', '') || '';
  const avatar   = profile.avatar || '🌿';
  const avatarEl = document.getElementById('curtain-avatar');
  const nameEl   = document.getElementById('curtain-name');
  const dotEl    = document.getElementById('curtain-p2p-dot');
  if (avatarEl) avatarEl.textContent = avatar;
  if (nameEl)   nameEl.textContent   = name || 'Forestorers';
  if (dotEl) {
    const offline = localStorage.getItem('frj_p2p_offline') !== '0';
    dotEl.style.background = offline ? '#555' : '#22c55e';
    dotEl.title            = offline ? 'Offline' : 'Online';
  }
}

// ── Main init — called from app.js init() after openIDB() ─────────────────────
async function encryptionInit() {
  await _phase0();

  // First-load unlock: show curtain before app renders (no DOM to clear yet)
  const enabled = await idbMetaGet('encryptionEnabled');
  if (enabled && !_sessionKey) {
    // Session key not yet set (i.e. not just created in setup) — require unlock
    _encLocked = true;
    const curtain = document.getElementById('privacyCurtain');
    if (curtain) {
      _setCurtainIdentity();
      curtain.style.display = 'flex';
      await _encLockAwaitUnlock();
      curtain.style.display = 'none';
    }
    _encLocked = false;
    // If schema v2, load decrypted data cache so lsGet/idbGet work transparently
    await _loadDecryptedCache();
    // Apply any pending password change delivered from another device
    await _checkPendingPwChangeOnUnlock();
  }
}

// ═══════════════════════════════════════════════
// Phase 2 — Migration & Encrypted Data Layer
// ═══════════════════════════════════════════════

// ── Globals (checked by storage.js lsGet/lsSet/idbGet/idbPut) ────────────────
let _frjEncCache    = null;   // null = inactive; object = decrypted in-memory store
let _frjBlobSchemaV2 = false; // true = route idbGet/idbPut through encrypted stores

// ── Sensitive LS keys (migrated to frj_data_enc) ─────────────────────────────
const SENSITIVE_LS_KEYS = [
  'frj_entries', 'frj_contacts', 'frj_ws_zones', 'frj_ws_settings',
  'frj_weekly_stats', 'frj_profile', 'frj_buddy_notes', 'frj_notes_unread',
  'frj_notes_sent', 'frj_notes_queue', 'frj_vanish_settings', 'frj_displayname',
  'frj_watermark_settings', 'frj_deleted', 'frj_public_profile',
  'frj_timer_sessions', 'frj_active_timer', 'frj_buddy_permissions',
  'frj_galleries', 'frj_gallery_notes', 'frj_gallery_received',
  'frj_gallery_notes_sent', 'frj_gallery_buddy_notes',
  'frj_gallery_kots', 'frj_gallery_kots_sent', 'frj_gallery_kot_queue'
];

// ── Blob record encryption (binary format stored in IDB) ─────────────────────
// Layout:  [12b iv][1b type: 0=Blob, 1=ArrayBuffer][payload]
// Blob:    [...][4b mimeLen][mimeBytes][ciphertext]
// AB:      [...][ciphertext]

async function _frjEncryptBlobRecord(val) {
  if (!_sessionKey) throw new Error('No session key');
  const iv = crypto.getRandomValues(new Uint8Array(12));
  if (val instanceof Blob) {
    const ab       = await val.arrayBuffer();
    const mimeB    = new TextEncoder().encode(val.type || '');
    const ct       = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, _sessionKey, ab);
    const out      = new Uint8Array(12 + 1 + 4 + mimeB.length + ct.byteLength);
    out.set(iv, 0);
    out[12] = 0;
    new DataView(out.buffer).setUint32(13, mimeB.length, true);
    out.set(mimeB, 17);
    out.set(new Uint8Array(ct), 17 + mimeB.length);
    return out.buffer;
  }
  if (val instanceof ArrayBuffer) {
    const ct  = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, _sessionKey, val);
    const out = new Uint8Array(12 + 1 + ct.byteLength);
    out.set(iv, 0); out[12] = 1;
    out.set(new Uint8Array(ct), 13);
    return out.buffer;
  }
  throw new Error('unsupported blob type');
}

async function _frjDecryptBlobRecord(encBuffer) {
  if (!_sessionKey) throw new Error('No session key');
  const buf  = encBuffer instanceof ArrayBuffer ? encBuffer : encBuffer;
  const view = new DataView(buf);
  const iv   = new Uint8Array(buf, 0, 12);
  const type = new Uint8Array(buf)[12];
  if (type === 0) {
    const mimeLen = view.getUint32(13, true);
    const mime    = new TextDecoder().decode(new Uint8Array(buf, 17, mimeLen));
    const ct      = new Uint8Array(buf, 17 + mimeLen);
    const pt      = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, _sessionKey, ct);
    return new Blob([pt], { type: mime });
  }
  const ct = new Uint8Array(buf, 13);
  return crypto.subtle.decrypt({ name: 'AES-GCM', iv }, _sessionKey, ct);
}

// ── Async flush — called fire-and-forget by lsSet when cache is active ────────
async function _frjEncFlushKey(k, v) {
  if (!_sessionKey || !_frjEncCache || _rekeyInProgress) return;
  try {
    const enc = await _encryptStr(_sessionKey, JSON.stringify(v));
    await idbDataEncPut(k, enc);
  } catch(e) {
    console.warn('[encryption] flush failed:', k, e);
    _dirtyFlushKeys.add(k);
    _scheduleFlushRetry();
  }
}

// ── Dirty key retry for failed flushes ────────────────────────────────────────
const _dirtyFlushKeys = new Set();
let _flushRetryTimer = null;

function _scheduleFlushRetry() {
  if (_flushRetryTimer) return;
  _flushRetryTimer = setTimeout(async () => {
    _flushRetryTimer = null;
    if (!_sessionKey || !_frjEncCache || _rekeyInProgress) return;
    const keys = [..._dirtyFlushKeys];
    for (const k of keys) {
      try {
        const v = _frjEncCache[k];
        if (v === undefined) { _dirtyFlushKeys.delete(k); continue; }
        const enc = await _encryptStr(_sessionKey, JSON.stringify(v));
        await idbDataEncPut(k, enc);
        _dirtyFlushKeys.delete(k);
      } catch(e) { console.warn('[encryption] flush retry failed:', k, e); }
    }
    if (_dirtyFlushKeys.size) {
      console.warn(`[encryption] ${_dirtyFlushKeys.size} keys still dirty after retry`);
      if (typeof toast === 'function') toast('Warning: some data may not be saved. Check storage space.', true);
    }
  }, 5000);
}

// ── Load decrypted cache (called after unlock when schema v2) ─────────────────
async function _loadDecryptedCache() {
  const v = await getSchemaVersion();
  if (v !== 2 || !_sessionKey) return;
  try {
    const keys = await idbDataEncGetAllKeys();
    const cache = {};
    let decryptFailures = 0;
    for (const k of keys) {
      const enc = await idbDataEncGet(k);
      if (!enc) continue;
      try {
        const str = await _decryptStr(_sessionKey, enc);
        try {
          cache[k] = JSON.parse(str);
        } catch(_) {
          // Some keys (e.g. frj_displayname) may be stored as plain strings, not JSON
          cache[k] = str;
        }
      } catch(e) {
        console.warn('[encryption] cache decrypt failed:', k, e);
        decryptFailures++;
      }
    }
    if (decryptFailures > 0) {
      console.error(`[encryption] ${decryptFailures} records failed to decrypt`);
      if (typeof toast === 'function') toast(`Warning: ${decryptFailures} record(s) could not be decrypted. Data may be partially corrupted.`, true);
    }

    // Fallback: pick up any sensitive keys still in localStorage that weren't
    // migrated yet (e.g. written during a session when _frjEncCache was null).
    // This handles the first-sync-on-fresh-device scenario.
    const lsFallbackKeys = [...SENSITIVE_LS_KEYS];
    for (let i = 0; i < localStorage.length; i++) {
      const k = localStorage.key(i);
      if (k && k.startsWith('frj_chat_') && !lsFallbackKeys.includes(k)) lsFallbackKeys.push(k);
    }
    for (const k of lsFallbackKeys) {
      if (k in cache) continue; // already loaded from encrypted store
      const raw = localStorage.getItem(k);
      if (raw === null) continue;
      try {
        let parsed;
        try { parsed = JSON.parse(raw); } catch(_) { parsed = raw; }
        cache[k] = parsed;
        // Migrate it into the encrypted store so it won't be in LS next time
        const enc = await _encryptStr(_sessionKey, raw);
        await idbDataEncPut(k, enc);
        localStorage.removeItem(k);
        console.log('[encryption] fallback-migrated LS key:', k);
      } catch(e) { console.warn('[encryption] fallback LS key failed:', k, e); }
    }

    _frjEncCache    = cache;
    _frjBlobSchemaV2 = true;
  } catch(e) {
    _frjEncCache    = null;
    _frjBlobSchemaV2 = false;
    console.warn('[encryption] _loadDecryptedCache failed:', e);
  }
}

// ── Migration ─────────────────────────────────────────────────────────────────
async function _getAllSensitiveLsKeys() {
  const keys = [...SENSITIVE_LS_KEYS];
  for (let i = 0; i < localStorage.length; i++) {
    const k = localStorage.key(i);
    if (k && k.startsWith('frj_chat_') && !keys.includes(k)) keys.push(k);
  }
  return keys.filter(k => localStorage.getItem(k) !== null);
}

async function _migrateLsRecord(k) {
  const raw = localStorage.getItem(k);
  if (raw === null) return; // already gone or not present
  const enc = await _encryptStr(_sessionKey, raw);
  await idbDataEncPut(k, enc);
  // Verify
  const check = await idbDataEncGet(k);
  const dec   = await _decryptStr(_sessionKey, check);
  if (dec !== raw) throw new Error(`LS verify failed: ${k}`);
  localStorage.removeItem(k);
  await idbMigLogPut('ls:' + k, { done: true, ts: Date.now() });
}

async function _migrateBlobRecord(k) {
  const val = await idbGet(k); // reads frj_blobs (v2 flag still false during migration)
  if (!val) return;
  const enc = await _frjEncryptBlobRecord(val);
  await idbBlobsEncPut(k, enc);
  // Verify: just decrypt and check it produced something non-null
  const check = await idbBlobsEncGet(k);
  const dec   = await _frjDecryptBlobRecord(check);
  const decSize = dec instanceof Blob ? dec.size : (dec?.byteLength ?? 0);
  if (!decSize) throw new Error(`Blob verify failed: ${k}`);
  await idbDel(k); // delete plaintext from frj_blobs
  await idbMigLogPut('blob:' + k, { done: true, ts: Date.now() });
}

// Main migration runner. onProgress({ total, done, current, failed, complete })
async function runMigration(onProgress) {
  if (!_sessionKey) throw new Error('Must unlock before migrating');

  const lsKeys   = await _getAllSensitiveLsKeys();
  const blobKeys = await idbGetAllKeys(); // frj_blobs keys
  const total    = lsKeys.length + blobKeys.length;
  let done = 0;

  const prog = (current, failed = false, complete = false) =>
    onProgress({ total, done, current, failed, complete });

  prog('Starting…');

  for (const k of lsKeys) {
    const already = await idbMigLogGet('ls:' + k);
    if (already?.done) { done++; continue; }
    prog(`Encrypting: ${k}`);
    try {
      await _migrateLsRecord(k);
      done++;
      prog(`✓ ${k}`);
    } catch(e) {
      onProgress({ total, done, current: `Failed: ${k} - ${e.message}`, failed: true });
      throw e; // stop migration, leave plaintext intact
    }
  }

  for (const k of blobKeys) {
    const already = await idbMigLogGet('blob:' + k);
    if (already?.done) { done++; continue; }
    const label = k.startsWith('img_') ? 'photo' : k.startsWith('c_') ? 'buddy photo' : k;
    prog(`Encrypting: ${label}`);
    try {
      await _migrateBlobRecord(k);
      done++;
      prog(`✓ ${label}`);
    } catch(e) {
      onProgress({ total, done, current: `Failed: ${label} - ${e.message}`, failed: true });
      throw e;
    }
  }

  // Set schema version to 2 — encrypted from now on
  await setSchemaVersion(2);
  try { await idbEmergencyDel('snapshot'); } catch(e) { console.warn('[encryption] emergency backup cleanup failed:', e); }
  _frjBlobSchemaV2 = true;
  await _loadDecryptedCache();
  onProgress({ total, done: total, current: 'Complete!', failed: false, complete: true });
}

// ── Migration modal UI ────────────────────────────────────────────────────────
let _migrationBackupDone = false;

function showMigrationModal() {
  const overlay = document.getElementById('enc-migrate');
  if (!overlay) return;
  overlay.classList.add('active');
  _migrationBackupDone = false;
  _updateMigrateBackupBtn();
  // Reset to pre-migration state
  document.getElementById('enc-migrate-progress').style.display = 'none';
  document.getElementById('enc-migrate-start').style.display = '';
}

function _updateMigrateBackupBtn() {
  const btn    = document.getElementById('enc-mig-backup-btn');
  const goBtn  = document.getElementById('enc-mig-go-btn');
  if (!btn || !goBtn) return;
  if (_migrationBackupDone) {
    btn.textContent = '✓ Backup downloaded';
    btn.disabled    = true;
  } else {
    btn.textContent = '↓ Download backup first (recommended)';
    btn.disabled    = false;
  }
  goBtn.disabled  = false;
}

async function _encMigDownloadBackup() {
  // Download a plaintext safety-net backup before encryption starts.
  // exportBackup() requires encryption to already be active, so we call
  // _exportPlaintext directly (warning already shown in the UI copy).
  if (typeof _exportPlaintext === 'function') await _exportPlaintext(true);
  _migrationBackupDone = true;
  _updateMigrateBackupBtn();
}

async function _encMigStart() {
  if (!_migrationBackupDone) {
    if (!confirm('You haven\'t downloaded a backup yet. If encryption fails or your password is lost, data may be unrecoverable.\n\nProceed without a backup?')) return;
  }

  // Session key must be set before migrating. If it isn't, run the password setup
  // screen first (user may have skipped on first launch).
  if (!_sessionKey) {
    // enc-setup and enc-migrate share the same z-index; enc-migrate is later in
    // the DOM so it paints on top — hide it while password setup runs.
    const migEl = document.getElementById('enc-migrate');
    if (migEl) migEl.classList.remove('active');
    await _showSetupScreen();
    if (!_sessionKey) return; // user skipped — abort
    if (migEl) migEl.classList.add('active');
  }

  const startEl = document.getElementById('enc-migrate-start');
  const progEl  = document.getElementById('enc-migrate-progress');
  const barFill = document.getElementById('enc-mig-bar-fill');
  const status  = document.getElementById('enc-mig-status');
  const goBtn   = document.getElementById('enc-mig-go-btn');

  startEl.style.display = 'none';
  progEl.style.display  = '';
  goBtn.disabled        = true;

  try {
    await runMigration(({ total, done, current, failed, complete }) => {
      const pct = total > 0 ? Math.round((done / total) * 100) : 0;
      if (barFill) barFill.style.width = pct + '%';
      if (status)  status.textContent  = current;
      if (complete) {
        if (barFill) barFill.style.background = '#10b981';
        document.getElementById('enc-mig-done').style.display = '';
        const closeBtn = document.getElementById('enc-mig-close-btn');
        if (closeBtn) {
          closeBtn.style.display = '';
          // Override close to lock screen immediately so user can test their password
          closeBtn.onclick = async () => {
            closeMigrationModal();
            if (typeof _refreshEncryptBtn === 'function') _refreshEncryptBtn();
            if (typeof updateDataEncBadge === 'function') updateDataEncBadge();
            if (typeof updateBackupWarning === 'function') updateBackupWarning();
            const curtain = document.getElementById('privacyCurtain');
            if (curtain) {
              _setCurtainIdentity();
              curtain.style.display = 'flex';
              await _encLockAwaitUnlock();
              curtain.style.display = 'none';
              if (typeof _loadDecryptedCache === 'function') await _loadDecryptedCache();
              if (typeof renderDash === 'function') renderDash();
            }
          };
        }
      }
    });
  } catch(e) {
    if (status) status.textContent = 'Migration stopped. Your data is safe. You can retry.';
    if (barFill) barFill.style.background = 'var(--red)';
    document.getElementById('enc-mig-retry-btn').style.display = '';
  }
}

function closeMigrationModal() {
  const overlay = document.getElementById('enc-migrate');
  if (overlay) overlay.classList.remove('active');
  // Refresh sidebar encrypt button + data badge
  if (typeof _refreshEncryptBtn === 'function') _refreshEncryptBtn();
  if (typeof updateDataEncBadge === 'function') updateDataEncBadge();
}

// ── Require encryption before allowing image upload/capture ───────────────────
// Returns true if encryption is enabled, false if user declined setup.
async function requireEncryptionForPhotos() {
  const enabled = await idbMetaGet('encryptionEnabled');
  if (enabled && _sessionKey) return true;
  if (enabled && !_sessionKey) return false; // locked — shouldn't reach here, but guard
  // Not set up — show setup screen
  await _showSetupScreen();
  if (_sessionKey) {
    await idbMetaPut('encryptionSetupDone', true);
    if (typeof _refreshEncryptBtn === 'function') _refreshEncryptBtn();
    if (typeof updateDataEncBadge === 'function') updateDataEncBadge();
    return true;
  }
  return false; // user skipped
}

// ── Open encryption settings (called from profile Settings section) ───────────
// Handles all three cases: not set up, set up but skipped, already enabled.
async function openEncryptionSettings() {
  const setupDone = await idbMetaGet('encryptionSetupDone');
  const enabled   = await idbMetaGet('encryptionEnabled');
  if (!setupDone || !enabled) {
    // Re-show first-run setup screen
    await _showSetupScreen();
    await idbMetaPut('encryptionSetupDone', true);
    if (typeof _refreshEncryptBtn === 'function') _refreshEncryptBtn();
    if (typeof updateDataEncBadge === 'function') updateDataEncBadge();
    if (typeof updateBackupWarning === 'function') updateBackupWarning();
    if (typeof openProfile === 'function') openProfile();
  } else {
    showChangePasswordModal();
  }
}

// ── Pending password change ───────────────────────────────────────────────────
// When other devices are paired, a password change is staged here first and only
// applied once the other device(s) have synced and confirmed they have the new key.
// Record stored in IDB meta key 'pending_pw_change':
// {
//   stagedAt:    ISO string
//   stagedBy:    deviceId of initiating device
//   newSalt:     Array<number>  (16 bytes)
//   newVerifier: { iv, ciphertext }  (encrypted _SENTINEL with new key)
//   encPw:       { iv, ciphertext }  (new password encrypted with CURRENT session key)
// }

async function _hasPairedDevices() {
  // Sprint 1 will populate 'paired_devices' in IDB meta.
  // Until then, this always returns false → password changes apply immediately.
  const paired = await idbMetaGet('paired_devices');
  return Array.isArray(paired) && paired.length > 0;
}

// Stage a pending password change. Does NOT modify encryption yet.
async function _stagePwChange(currentPw, newPw) {
  const saltArr = await idbMetaGet('encryptionSalt');
  if (!saltArr) throw new Error('No encryption salt found.');
  const currentIters = await idbMetaGet('encryptionIterations') || 200000;
  const currentKey = await deriveKey(currentPw, new Uint8Array(saltArr), currentIters);
  const verifier   = await idbMetaGet('encryptionVerifier');
  const result     = await _decryptStr(currentKey, verifier);
  if (result !== _SENTINEL) throw new Error('Incorrect current password.');

  const newSalt     = crypto.getRandomValues(new Uint8Array(16));
  const newKey      = await deriveKey(newPw, newSalt, 600000);
  const newVerifier = await _encryptStr(newKey, _SENTINEL);

  // Encrypt the new password with the CURRENT session key so device B can read it
  // after unlocking with the current password.
  const encPw = await _encryptStr(currentKey, newPw);

  const deviceId = typeof getDeviceId === 'function' ? await getDeviceId() : null;

  const record = {
    stagedAt:    new Date().toISOString(),
    stagedBy:    deviceId,
    newSalt:     Array.from(newSalt),
    newIterations: 600000,
    newVerifier,
    encPw
  };
  await idbMetaPut('pending_pw_change', record);
  return record;
}

// Apply a staged pending change. Caller must ensure session is unlocked (current key in memory).
// If record is null, reads from IDB.
let _rekeyInProgress = false; // suppress flushes during re-encryption

async function applyPendingPwChange(record) {
  const r = record || await idbMetaGet('pending_pw_change');
  if (!r) return;
  if (!_sessionKey) throw new Error('Session not unlocked.');

  // Recover the new password from the record (encrypted with the current key)
  let newPw;
  try {
    newPw = await _decryptStr(_sessionKey, r.encPw);
  } catch(e) {
    throw new Error('Could not decrypt staged password. Session key mismatch.');
  }

  const newSalt = new Uint8Array(r.newSalt);
  const newIters = r.newIterations || 600000;
  const newKey  = await deriveKey(newPw, newSalt, newIters);

  // Verify the new key produces the expected verifier (data integrity check)
  try {
    const check = await _decryptStr(newKey, r.newVerifier);
    if (check !== _SENTINEL) throw new Error('Verifier mismatch');
  } catch(e) {
    throw new Error('Pending change verification failed. Record may be corrupted.');
  }

  const oldKey = _sessionKey;
  const v = await getSchemaVersion();
  if (v === 2) {
    // Mark re-key in progress BEFORE modifying any records — crash recovery marker
    _rekeyInProgress = true;
    await idbMetaPut('rekey_in_progress', { newSalt: r.newSalt, newVerifier: r.newVerifier, newIterations: newIters });

    try {
      await _rekeyAllRecords(oldKey, newKey);

      // All records re-encrypted — now atomically swap salt/verifier and clear markers
      await idbMetaPut('encryptionSalt',     r.newSalt);
      await idbMetaPut('encryptionVerifier', r.newVerifier);
      await idbMetaPut('encryptionIterations', newIters);
      _sessionKey = newKey;
      await idbMetaPut('rekey_in_progress', null);
      await idbMetaPut('pending_pw_change', null);
    } finally {
      // Always clear the in-memory flag so flush operations are never permanently blocked.
      // The IDB rekey_in_progress marker remains set on failure, enabling resume on next unlock.
      _rekeyInProgress = false;
    }
  } else {
    // Schema v1 — just swap the metadata, no records to re-encrypt
    await idbMetaPut('encryptionSalt',     r.newSalt);
    await idbMetaPut('encryptionVerifier', r.newVerifier);
    await idbMetaPut('encryptionIterations', newIters);
    _sessionKey = newKey;
    await idbMetaPut('pending_pw_change', null);
  }
}

// Re-encrypt all data + blob records from oldKey to newKey.
// Each record is dual-tried: if it already has newKey (from a previous interrupted attempt), skip it.
async function _rekeyAllRecords(oldKey, newKey) {
  const dataKeys = await idbDataEncGetAllKeys();
  for (const k of dataKeys) {
    const enc = await idbDataEncGet(k);
    if (!enc) continue;
    // Try new key first — if this record was already re-encrypted in a prior attempt, skip it
    try { await _decryptStr(newKey, enc); continue; } catch(_) { /* needs re-encryption */ }
    const str    = await _decryptStr(oldKey, enc);
    const newEnc = await _encryptStr(newKey, str);
    await idbDataEncPut(k, newEnc);
  }
  const blobKeys = await idbBlobsEncGetAllKeys();
  for (const k of blobKeys) {
    const encBuf = await idbBlobsEncGet(k);
    if (!encBuf) continue;
    const buf = encBuf instanceof ArrayBuffer ? encBuf : encBuf;
    const bIv  = new Uint8Array(buf, 0, 12);
    const bType = new Uint8Array(buf)[12];
    // Try new key first — skip already-re-encrypted
    try {
      if (bType === 0) {
        const mimeLen = new DataView(buf).getUint32(13, true);
        const ct = new Uint8Array(buf, 17 + mimeLen);
        await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bIv }, newKey, ct);
      } else {
        const ct = new Uint8Array(buf, 13);
        await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bIv }, newKey, ct);
      }
      continue; // already on new key
    } catch(_) { /* needs re-encryption */ }
    // Decrypt with old key
    let plain;
    if (bType === 0) {
      const mimeLen = new DataView(buf).getUint32(13, true);
      const mime    = new TextDecoder().decode(new Uint8Array(buf, 17, mimeLen));
      const ct      = new Uint8Array(buf, 17 + mimeLen);
      const pt      = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bIv }, oldKey, ct);
      plain = new Blob([pt], { type: mime });
    } else {
      const ct = new Uint8Array(buf, 13);
      plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bIv }, oldKey, ct);
    }
    // Encrypt with new key
    const nIv = crypto.getRandomValues(new Uint8Array(12));
    if (plain instanceof Blob) {
      const ab    = await plain.arrayBuffer();
      const mimeB = new TextEncoder().encode(plain.type || '');
      const ct    = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nIv }, newKey, ab);
      const out   = new Uint8Array(12 + 1 + 4 + mimeB.length + ct.byteLength);
      out.set(nIv, 0); out[12] = 0;
      new DataView(out.buffer).setUint32(13, mimeB.length, true);
      out.set(mimeB, 17);
      out.set(new Uint8Array(ct), 17 + mimeB.length);
      await idbBlobsEncPut(k, out.buffer);
    } else {
      const ct  = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nIv }, newKey, plain);
      const out = new Uint8Array(12 + 1 + ct.byteLength);
      out.set(nIv, 0); out[12] = 1;
      out.set(new Uint8Array(ct), 13);
      await idbBlobsEncPut(k, out.buffer);
    }
  }
}

// Resume an interrupted re-key on unlock. Called from _checkPendingPwChangeOnUnlock.
async function _resumeInterruptedRekey() {
  const marker = await idbMetaGet('rekey_in_progress');
  if (!marker) return false;
  const pending = await idbMetaGet('pending_pw_change');
  if (!pending || !_sessionKey) { await idbMetaPut('rekey_in_progress', null); return false; }
  console.log('[encryption] resuming interrupted re-key…');
  try {
    const newPw   = await _decryptStr(_sessionKey, pending.encPw);
    const newIters = marker.newIterations || pending.newIterations || 600000;
    const newKey  = await deriveKey(newPw, new Uint8Array(marker.newSalt), newIters);
    _rekeyInProgress = true;
    await _rekeyAllRecords(_sessionKey, newKey);
    await idbMetaPut('encryptionSalt',     marker.newSalt);
    await idbMetaPut('encryptionVerifier', marker.newVerifier);
    await idbMetaPut('encryptionIterations', newIters);
    _sessionKey = newKey;
    await idbMetaPut('rekey_in_progress', null);
    await idbMetaPut('pending_pw_change', null);
    _rekeyInProgress = false;
    console.log('[encryption] interrupted re-key completed');
    if (typeof toast === 'function') toast('Password change completed after interruption');
    return true;
  } catch(e) {
    console.error('[encryption] could not resume re-key:', e);
    _rekeyInProgress = false;
    return false;
  }
}

async function cancelPendingPwChange() {
  await idbMetaPut('pending_pw_change', null);
  if (typeof toast === 'function') toast('Password change cancelled');
  _refreshPwChangeStatus();
}

// Show/hide the "pending" banner in the change-pw modal and settings
async function _refreshPwChangeStatus() {
  const pending = await idbMetaGet('pending_pw_change');
  const banner  = document.getElementById('enc-chpw-pending-banner');
  if (!banner) return;
  if (pending) {
    const dt = new Date(pending.stagedAt);
    const when = dt.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
    banner.style.display = '';
    banner.innerHTML = `
      <div style="background:var(--surface2);border:1px solid var(--yellow,#f59e0b);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:12px;color:var(--text);margin-bottom:12px">
        <div style="color:var(--yellow,#f59e0b);font-weight:600;margin-bottom:4px">⏳ Password change pending</div>
        <div style="color:var(--muted2);margin-bottom:8px">Staged ${when}. Waiting for your other device to come online and sync.</div>
        <button class="btn btn-ghost btn-sm" style="font-size:11px" data-action="cancelPendingPwChange">Cancel this change</button>
      </div>`;
  } else {
    banner.style.display = 'none';
    banner.innerHTML = '';
  }
}

// ── Change password modal ─────────────────────────────────────────────────────
function showChangePasswordModal() {
  const overlay = document.getElementById('enc-change-pw');
  if (!overlay) return;
  overlay.classList.add('active');
  const cur    = document.getElementById('enc-chpw-current');
  const pw1    = document.getElementById('enc-chpw-new1');
  const pw2    = document.getElementById('enc-chpw-new2');
  const fill   = document.getElementById('enc-chpw-bar-fill');
  const lbl    = document.getElementById('enc-chpw-bar-label');
  const bar    = document.getElementById('enc-chpw-bar');
  const btn    = document.getElementById('enc-chpw-btn');
  const err    = document.getElementById('enc-chpw-err');
  const cancel = document.getElementById('enc-chpw-cancel');
  cur.value = ''; pw1.value = ''; pw2.value = '';
  err.textContent = '';
  btn.disabled = false; btn.textContent = 'Change Password';
  bar.style.display = 'none';

  _refreshPwChangeStatus();

  // Clone to remove old listeners
  const pw1c = pw1.cloneNode(true); pw1.parentNode.replaceChild(pw1c, pw1);
  pw1c.addEventListener('input', () => {
    const s = _pwStrength(pw1c.value);
    fill.style.width = (s * 25) + '%';
    fill.style.background = _STR_COLORS[s] || '';
    lbl.textContent = _STR_LABELS[s] || '';
    bar.style.display = pw1c.value ? 'flex' : 'none';
  });

  btn.onclick = async () => {
    err.textContent = '';
    if (!cur.value) { err.textContent = 'Enter your current password.'; return; }
    if (!pw1c.value || pw1c.value.length < 8) { err.textContent = 'New password must be at least 8 characters.'; return; }
    if (pw1c.value !== pw2.value) { err.textContent = 'New passwords do not match.'; return; }
    btn.disabled = true; btn.textContent = 'Applying…';
    try {
      await _doChangePassword(cur.value, pw1c.value);
      overlay.classList.remove('active');
      if (typeof openProfile === 'function') openProfile();
    } catch(e) {
      err.textContent = e.message || 'Change failed. Please try again.';
      btn.disabled = false; btn.textContent = 'Change Password';
    }
  };
  cancel.onclick = () => overlay.classList.remove('active');
  setTimeout(() => cur.focus(), 60);
}

async function _doChangePassword(currentPw, newPw) {
  // Stage the change first (builds the IDB record, verifies current password)
  await _stagePwChange(currentPw, newPw);

  const hasPeers = await _hasPairedDevices();
  if (hasPeers) {
    // Other devices registered — leave the change staged.
    // The sync engine (Sprint 2) will deliver the pending change record to peers.
    // Once a peer confirms, applyPendingPwChange() will be called here too.
    if (typeof toast === 'function') toast('Password change staged. Will apply when your other device syncs.');
    _refreshPwChangeStatus();
    // Broadcast to any currently-connected peers
    if (typeof p2pBroadcastPwChangePending === 'function') p2pBroadcastPwChangePending();
  } else {
    // No paired devices — apply immediately (behaviour unchanged from pre-sync era)
    await applyPendingPwChange();
    if (typeof toast === 'function') toast('Password changed');
  }
}

// ── Called by sync layer when THIS device receives a pending pw change from a peer ──
// (Sprint 2 will call this after syncing the pending_pw_change record from the other device)
async function receivePeerPwChange(record) {
  if (!_sessionKey) return; // not unlocked, skip — will retry on next unlock
  try {
    await applyPendingPwChange(record);
    if (typeof toast === 'function') toast('Password change synced from another device ✓');
  } catch(e) {
    console.error('[encryption] receivePeerPwChange failed:', e);
  }
}

// ── On unlock: check for interrupted re-key or pending change ─────────────────
async function _checkPendingPwChangeOnUnlock() {
  // First: resume any interrupted re-key from a prior crash
  if (await _resumeInterruptedRekey()) return;

  const record = await idbMetaGet('pending_pw_change');
  if (!record) return;
  const myDeviceId = typeof getDeviceId === 'function' ? await getDeviceId() : null;
  if (record.stagedBy && record.stagedBy !== myDeviceId) {
    // This change was staged by a different device and delivered to us via sync —
    // apply it now automatically (the user has already unlocked with their current password)
    try {
      await applyPendingPwChange(record);
      if (typeof toast === 'function') toast('Password change from your other device applied ✓');
    } catch(e) {
      console.error('[encryption] auto-apply pending pw change failed:', e);
    }
  }
  // If stagedBy === myDeviceId, we staged it ourselves and are waiting for peer confirmation.
  // Leave it pending.
}

// ── Pairing helpers ───────────────────────────────────────────────────────────

// Returns true if pw matches the current at-rest encryption password.
async function verifyEncryptionPassword(pw) {
  try {
    const saltArr  = await idbMetaGet('encryptionSalt');
    const verifier = await idbMetaGet('encryptionVerifier');
    if (!saltArr || !verifier) return false;
    const iters  = await idbMetaGet('encryptionIterations') || 200000;
    const key    = await deriveKey(pw, new Uint8Array(saltArr), iters);
    const result = await _decryptStr(key, verifier);
    return result === _SENTINEL;
  } catch(e) {
    return false;
  }
}

// Set up at-rest encryption using a password received via the pairing code.
// Called on device B immediately after importPairingCode succeeds.
// Idempotent: if the same password is already configured, keeps the existing salt
// so previously encrypted data remains readable.
async function setupEncryptionFromPairing(password) {
  const alreadySetup   = await idbMetaGet('encryptionSetupDone');
  const alreadyEnabled = await idbMetaGet('encryptionEnabled');

  if (alreadySetup && alreadyEnabled) {
    const ok = await verifyEncryptionPassword(password);
    if (ok) {
      // Same password already configured — check if salt needs alignment with peer
      const peerSalt = await idbMetaGet('peerEncSalt').catch(() => null);
      const localSalt = await idbMetaGet('encryptionSalt');
      if (peerSalt && Array.isArray(peerSalt) && peerSalt.length === 16 &&
          JSON.stringify(peerSalt) !== JSON.stringify(localSalt)) {
        // Same password, different salt — re-key to peer's salt so password changes sync
        console.log('[encryption] re-keying to peer salt for cross-device sync');
        await idbMetaPut('peerEncSalt', null);
        const localIters = await idbMetaGet('encryptionIterations') || 200000;
        const oldKey = _sessionKey || await deriveKey(password, new Uint8Array(localSalt), localIters);
        const newSalt = new Uint8Array(peerSalt);
        const newKey = await deriveKey(password, newSalt, localIters);
        const newVerifier = await _encryptStr(newKey, _SENTINEL);
        // Re-encrypt all data AND blobs with the new key
        const v = await getSchemaVersion();
        if (v === 2) {
          const dataKeys = await idbDataEncGetAllKeys();
          for (const k of dataKeys) {
            const enc = await idbDataEncGet(k);
            if (!enc) continue;
            const str    = await _decryptStr(oldKey, enc);
            const newEnc = await _encryptStr(newKey, str);
            await idbDataEncPut(k, newEnc);
          }
          // Re-encrypt blobs (photos, keypair) — same format as applyPendingPwChange
          const blobKeys = await idbBlobsEncGetAllKeys();
          for (const k of blobKeys) {
            const encBuf = await idbBlobsEncGet(k);
            if (!encBuf) continue;
            const buf = encBuf instanceof ArrayBuffer ? encBuf : encBuf;
            const bIv  = new Uint8Array(buf, 0, 12);
            const bType = new Uint8Array(buf)[12];
            let plain;
            if (bType === 0) {
              const mimeLen = new DataView(buf).getUint32(13, true);
              const mime    = new TextDecoder().decode(new Uint8Array(buf, 17, mimeLen));
              const ct      = new Uint8Array(buf, 17 + mimeLen);
              const pt      = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bIv }, oldKey, ct);
              plain = new Blob([pt], { type: mime });
            } else {
              const ct = new Uint8Array(buf, 13);
              const ab = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bIv }, oldKey, ct);
              plain = ab;
            }
            const nIv = crypto.getRandomValues(new Uint8Array(12));
            if (plain instanceof Blob) {
              const ab    = await plain.arrayBuffer();
              const mimeB = new TextEncoder().encode(plain.type || '');
              const ct    = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nIv }, newKey, ab);
              const out   = new Uint8Array(12 + 1 + 4 + mimeB.length + ct.byteLength);
              out.set(nIv, 0); out[12] = 0;
              new DataView(out.buffer).setUint32(13, mimeB.length, true);
              out.set(mimeB, 17);
              out.set(new Uint8Array(ct), 17 + mimeB.length);
              await idbBlobsEncPut(k, out.buffer);
            } else {
              const ct  = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nIv }, newKey, plain);
              const out = new Uint8Array(12 + 1 + ct.byteLength);
              out.set(nIv, 0); out[12] = 1;
              out.set(new Uint8Array(ct), 13);
              await idbBlobsEncPut(k, out.buffer);
            }
          }
        }
        await idbMetaPut('encryptionSalt', Array.from(newSalt));
        await idbMetaPut('encryptionVerifier', newVerifier);
        _sessionKey = newKey;
        console.log('[encryption] re-keyed to peer salt successfully');
        return;
      }
      await idbMetaPut('peerEncSalt', null); // clean up even if salts match
      if (!_sessionKey) {
        const existingIters = await idbMetaGet('encryptionIterations') || 200000;
      _sessionKey = await deriveKey(password, new Uint8Array(localSalt), existingIters);
      }
      console.log('[encryption] same password already configured — keeping existing salt');
      return;
    }
    // Different password: fall through to re-key (password was changed on the other device)
  }

  // Use peer's salt if available (stashed by importPairingCode) so both devices
  // derive the same session key — required for password change sync to work
  let salt;
  const peerSalt = await idbMetaGet('peerEncSalt').catch(() => null);
  if (peerSalt && Array.isArray(peerSalt) && peerSalt.length === 16) {
    salt = new Uint8Array(peerSalt);
    await idbMetaPut('peerEncSalt', null); // consumed
    console.log('[encryption] using peer salt for key derivation');
  } else {
    salt = crypto.getRandomValues(new Uint8Array(16));
  }
  const _pIters  = 600000;
  const key      = await deriveKey(password, salt, _pIters);
  const verifier = await _encryptStr(key, _SENTINEL);
  await idbMetaPut('encryptionSalt',      Array.from(salt));
  await idbMetaPut('encryptionVerifier',  verifier);
  await idbMetaPut('encryptionIterations', _pIters);
  await idbMetaPut('encryptionEnabled',   true);
  await idbMetaPut('encryptionSetupDone', true);
  _sessionKey = key;
  console.log('[encryption] at-rest encryption configured from pairing');
}
