Source: settings-listeners.js

/**
 * Settings modal listener binders (STAK-135)
 *
 * Keeps listener wiring split by concern while preserving existing behavior.
 */

let _patternMode = 'keywords';

/**
 * Helper to safely get an element by ID, returning null if not found.
 *
 * @param {string} id - The DOM element ID
 * @returns {HTMLElement|null} The element or null
 */
const getExistingElement = (id) => {
  const el = safeGetElement(id);
  return el && el.id ? el : null;
};

/**
 * Binds listeners for settings modal navigation (sidebar, provider tabs, log tabs).
 */
const bindSettingsNavigationListeners = () => {
  // Sidebar navigation.
  document.querySelectorAll('.settings-nav-item').forEach(item => {
    item.addEventListener('click', () => {
      switchSettingsSection(item.dataset.section);
    });
  });

  // Provider tabs.
  document.querySelectorAll('.settings-provider-tab').forEach(tab => {
    tab.addEventListener('click', () => {
      switchProviderTab(tab.dataset.provider);
    });
  });

  // Log sub-tabs.
  document.querySelectorAll('[data-log-tab]').forEach(tab => {
    tab.addEventListener('click', () => {
      switchLogTab(tab.dataset.logTab);
    });
  });
};

/**
 * Binds listeners for appearance settings (theme, display currency, timezone, header toggles).
 */
const bindAppearanceAndHeaderListeners = () => {
  // Theme picker buttons.
  document.querySelectorAll('.theme-option').forEach(btn => {
    btn.addEventListener('click', () => {
      const theme = btn.dataset.theme;
      if (typeof setTheme === 'function') {
        setTheme(theme);
      }
      if (typeof updateThemeButton === 'function') {
        updateThemeButton();
      }
      document.querySelectorAll('.theme-option').forEach((b) => {
        b.classList.toggle('active', b.dataset.theme === theme);
      });
    });
  });

  // Display currency (STACK-50).
  const currencySelect = getExistingElement('settingsDisplayCurrency');
  if (currencySelect) {
    currencySelect.addEventListener('change', () => {
      saveDisplayCurrency(currencySelect.value);
      if (typeof renderTable === 'function') renderTable();
      if (typeof updateSummary === 'function') updateSummary();
      if (typeof updateAllSparklines === 'function') updateAllSparklines();
      if (typeof syncGoldbackSettingsUI === 'function') syncGoldbackSettingsUI();
    });
  }

  // Display timezone (STACK-63).
  const tzSelect = getExistingElement('settingsTimezone');
  if (tzSelect) {
    tzSelect.addEventListener('change', () => {
      localStorage.setItem(TIMEZONE_KEY, tzSelect.value);
      window.location.reload();
    });
  }

  wireStorageToggle('settingsHeaderThemeBtn', 'headerThemeBtnVisible', {
    defaultVal: false,
    onApply: () => applyHeaderToggleVisibility(),
  });
  wireStorageToggle('settingsHeaderThemeBtn_hdr', 'headerThemeBtnVisible', {
    defaultVal: false,
    onApply: () => applyHeaderToggleVisibility(),
  });

  wireStorageToggle('settingsHeaderCurrencyBtn', 'headerCurrencyBtnVisible', {
    defaultVal: false,
    onApply: () => applyHeaderToggleVisibility(),
  });
  wireStorageToggle('settingsHeaderCurrencyBtn_hdr', 'headerCurrencyBtnVisible', {
    defaultVal: false,
    onApply: () => applyHeaderToggleVisibility(),
  });

  wireStorageToggle('settingsHeaderTrendBtn', HEADER_TREND_BTN_KEY, {
    defaultVal: true,
    onApply: () => applyHeaderToggleVisibility(),
  });
  wireStorageToggle('settingsHeaderTrendBtn_hdr', HEADER_TREND_BTN_KEY, {
    defaultVal: true,
    onApply: () => applyHeaderToggleVisibility(),
  });

  wireStorageToggle('settingsHeaderSyncBtn', HEADER_SYNC_BTN_KEY, {
    defaultVal: true,
    onApply: () => applyHeaderToggleVisibility(),
  });
  wireStorageToggle('settingsHeaderSyncBtn_hdr', HEADER_SYNC_BTN_KEY, {
    defaultVal: true,
    onApply: () => applyHeaderToggleVisibility(),
  });

  // Trend cycle header button.
  const headerTrendBtn = safeGetElement('headerTrendBtn');
  if (headerTrendBtn) {
    headerTrendBtn.addEventListener('click', () => {
      if (typeof window.cycleSpotTrend === 'function') window.cycleSpotTrend();
    });
  }

  // Sync all spot prices header button.
  const headerSyncBtn = safeGetElement('headerSyncBtn');
  if (headerSyncBtn) {
    headerSyncBtn.addEventListener('click', () => {
      ['Silver', 'Gold', 'Platinum', 'Palladium'].forEach(m => {
        const btn = document.getElementById(`syncIcon${m}`);
        if (btn && !btn.disabled) btn.click();
      });
    });
  }

  // Theme cycle header button (STACK-54).
  if (elements.headerThemeBtn) {
    elements.headerThemeBtn.addEventListener('click', () => {
      if (typeof toggleTheme === 'function') toggleTheme();
      if (typeof updateThemeButton === 'function') updateThemeButton();
      const currentTheme = localStorage.getItem(THEME_KEY) || 'light';
      document.querySelectorAll('.theme-option').forEach((btn) => {
        btn.classList.toggle('active', btn.dataset.theme === currentTheme);
      });
    });
  }

  // Currency picker header button (STACK-54).
  if (elements.headerCurrencyBtn) {
    elements.headerCurrencyBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      toggleCurrencyDropdown();
    });
  }

  const ippSelect = getExistingElement('settingsItemsPerPage');
  if (ippSelect) {
    ippSelect.addEventListener('change', () => {
      const ippVal = ippSelect.value;
      itemsPerPage = ippVal === 'all' ? Infinity : parseInt(ippVal, 10);
      try { localStorage.setItem(ITEMS_PER_PAGE_KEY, ippVal); } catch (e) { /* ignore */ }
      if (elements.itemsPerPage) elements.itemsPerPage.value = ippVal;
      renderTable();
    });
  }

  const spotCompareSetting = getExistingElement('settingsSpotCompareMode');
  if (spotCompareSetting) {
    spotCompareSetting.addEventListener('change', () => {
      try { localStorage.setItem(SPOT_COMPARE_MODE_KEY, spotCompareSetting.value); } catch (e) { /* ignore */ }
      if (typeof updateAllSparklines === 'function') updateAllSparklines();
    });
  }
};

/**
 * Binds listeners for filter settings and Numista integration options.
 */
const bindFilterAndNumistaListeners = () => {
  const chipMinSetting = getExistingElement('settingsChipMinCount');
  if (chipMinSetting) {
    chipMinSetting.addEventListener('change', () => {
      const val = chipMinSetting.value;
      localStorage.setItem('chipMinCount', val);
      const chipMinInline = getExistingElement('chipMinCount');
      if (chipMinInline) chipMinInline.value = val;
      if (typeof renderActiveFilters === 'function') renderActiveFilters();
    });
  }

  wireFeatureFlagToggle('settingsGroupNameChips', 'GROUPED_NAME_CHIPS', {
    syncId: 'groupNameChips',
    onApply: () => { if (typeof renderActiveFilters === 'function') renderActiveFilters(); },
  });

  wireFeatureFlagToggle('settingsDynamicChips', 'DYNAMIC_NAME_CHIPS', {
    onApply: () => { if (typeof renderActiveFilters === 'function') renderActiveFilters(); },
  });

  wireFeatureFlagToggle('settingsChipQtyBadge', 'CHIP_QTY_BADGE', {
    onApply: () => { if (typeof renderActiveFilters === 'function') renderActiveFilters(); },
  });

  wireFeatureFlagToggle('settingsFuzzyAutocomplete', 'FUZZY_AUTOCOMPLETE', {
    onApply: (isEnabled) => {
      if (isEnabled && typeof initializeAutocomplete === 'function') initializeAutocomplete(inventory);
    },
  });

  wireFeatureFlagToggle('settingsNumistaLookup', 'NUMISTA_SEARCH_LOOKUP');

  const numistaViewContainer = getExistingElement('numistaViewFieldToggles');
  if (numistaViewContainer) {
    const nfConfig = typeof getNumistaViewFieldConfig === 'function' ? getNumistaViewFieldConfig() : {};
    numistaViewContainer.querySelectorAll('input[data-nf]').forEach((cb) => {
      const field = cb.dataset.nf;
      if (nfConfig[field] !== undefined) cb.checked = nfConfig[field];
    });
    numistaViewContainer.addEventListener('change', () => {
      const config = {};
      numistaViewContainer.querySelectorAll('input[data-nf]').forEach((cb) => {
        config[cb.dataset.nf] = cb.checked;
      });
      if (typeof saveNumistaViewFieldConfig === 'function') saveNumistaViewFieldConfig(config);
    });
  }

  const addNumistaRuleBtn = getExistingElement('addNumistaRuleBtn');
  if (addNumistaRuleBtn) {
    addNumistaRuleBtn.addEventListener('click', () => {
      const patternInput = getExistingElement('numistaRulePatternInput');
      const replacementInput = getExistingElement('numistaRuleReplacementInput');
      const idInput = getExistingElement('numistaRuleIdInput');
      if (!patternInput || !replacementInput) return;

      const pattern = patternInput.value.trim();
      const replacement = replacementInput.value.trim();
      const numistaId = idInput ? idInput.value.trim() : '';

      if (!pattern || !replacement) {
        alert('Pattern and Numista query are required.');
        return;
      }

      if (!window.NumistaLookup) return;
      const result = NumistaLookup.addRule(pattern, replacement, numistaId || null);
      if (!result.success) {
        alert(result.error);
        return;
      }

      patternInput.value = '';
      replacementInput.value = '';
      if (idInput) idInput.value = '';
      renderCustomRuleTable();
    });
  }

  wireChipSortToggle('settingsChipSortOrder', 'chipSortOrder');
  if (typeof window.setupChipGroupingEvents === 'function') {
    window.setupChipGroupingEvents();
  }
};

/**
 * Binds listeners for Numista bulk sync operations.
 */
const bindNumistaBulkSyncListeners = () => {
  const nsStartBtn = getExistingElement('numistaSyncStartBtn');
  if (nsStartBtn) {
    nsStartBtn.addEventListener('click', () => {
      if (typeof startBulkSync === 'function') startBulkSync();
    });
  }

  const nsCancelBtn = getExistingElement('numistaSyncCancelBtn');
  if (nsCancelBtn) {
    nsCancelBtn.addEventListener('click', () => {
      if (window.BulkImageCache) BulkImageCache.abort();
      nsCancelBtn.style.display = 'none';
    });
  }

  const nsClearBtn = getExistingElement('numistaSyncClearBtn');
  if (nsClearBtn) {
    nsClearBtn.addEventListener('click', () => {
      if (typeof clearAllCachedData === 'function') clearAllCachedData();
    });
  }
};

/**
 * Binds listeners for the settings modal shell (close button, background click).
 */
const bindSettingsModalShellListeners = () => {
  const closeBtn = getExistingElement('settingsCloseBtn');
  if (closeBtn) {
    closeBtn.addEventListener('click', hideSettingsModal);
  }

  const modal = getExistingElement('settingsModal');
  if (modal) {
    modal.addEventListener('click', (e) => {
      if (e.target === modal) hideSettingsModal();
    });
  }

  // Provider priority dropdowns (STACK-90).
  setupProviderPriority();
};

/**
 * Binds listeners for Goldback feature toggles and estimation settings.
 */
const bindGoldbackToggleListeners = () => {
  const gbToggle = getExistingElement('settingsGoldbackEnabled');
  if (gbToggle) {
    gbToggle.addEventListener('click', (e) => {
      const btn = e.target.closest('.chip-sort-btn');
      if (!btn) return;
      const isEnabled = btn.dataset.val === 'on';
      if (typeof saveGoldbackEnabled === 'function') saveGoldbackEnabled(isEnabled);
      gbToggle.querySelectorAll('.chip-sort-btn').forEach((b) => b.classList.toggle('active', b === btn));
      if (typeof renderTable === 'function') renderTable();
    });
  }

  const gbEstToggle = getExistingElement('settingsGoldbackEstimateEnabled');
  if (gbEstToggle) {
    gbEstToggle.addEventListener('click', (e) => {
      const btn = e.target.closest('.chip-sort-btn');
      if (!btn) return;
      const isEnabled = btn.dataset.val === 'on';
      if (typeof saveGoldbackEstimateEnabled === 'function') saveGoldbackEstimateEnabled(isEnabled);
      gbEstToggle.querySelectorAll('.chip-sort-btn').forEach((b) => b.classList.toggle('active', b === btn));
      if (isEnabled && typeof onGoldSpotPriceChanged === 'function') onGoldSpotPriceChanged();
      if (typeof syncGoldbackSettingsUI === 'function') syncGoldbackSettingsUI();
      if (typeof renderTable === 'function') renderTable();
    });
  }

  const gbEstRefreshBtn = getExistingElement('goldbackEstimateRefreshBtn');
  if (gbEstRefreshBtn) {
    gbEstRefreshBtn.addEventListener('click', async () => {
      if (typeof syncProviderChain !== 'function') return;
      const origText = gbEstRefreshBtn.textContent;
      gbEstRefreshBtn.textContent = 'Refreshing...';
      gbEstRefreshBtn.disabled = true;
      try {
        await syncProviderChain({ showProgress: false, forceSync: true });
      } catch (err) {
        console.warn('Goldback estimate refresh failed:', err);
      } finally {
        gbEstRefreshBtn.textContent = origText;
        gbEstRefreshBtn.disabled = false;
      }
    });
  }

  const gbModifierInput = getExistingElement('goldbackEstimateModifierInput');
  if (gbModifierInput) {
    gbModifierInput.addEventListener('change', () => {
      const val = parseFloat(gbModifierInput.value);
      if (isNaN(val) || val <= 0) {
        gbModifierInput.value = goldbackEstimateModifier.toFixed(2);
        return;
      }
      if (typeof saveGoldbackEstimateModifier === 'function') saveGoldbackEstimateModifier(val);
      if (goldbackEstimateEnabled && typeof onGoldSpotPriceChanged === 'function') onGoldSpotPriceChanged();
      if (typeof recordAllItemPriceSnapshots === 'function') recordAllItemPriceSnapshots();
      if (typeof syncGoldbackSettingsUI === 'function') syncGoldbackSettingsUI();
      if (typeof renderTable === 'function') renderTable();
    });
  }
};

/**
 * Binds listeners for Goldback price entry and history actions.
 */
const bindGoldbackActionListeners = () => {
  const gbSaveBtn = getExistingElement('goldbackSavePricesBtn');
  if (gbSaveBtn) {
    gbSaveBtn.addEventListener('click', () => {
      const tbody = getExistingElement('goldbackPriceTableBody');
      if (!tbody) return;
      const now = Date.now();
      const fxRate = (typeof getExchangeRate === 'function') ? getExchangeRate() : 1;
      tbody.querySelectorAll('tr[data-denom]').forEach((row) => {
        const denom = row.dataset.denom;
        const input = row.querySelector('input[type="number"]');
        if (!input) return;
        const displayVal = parseFloat(input.value);
        if (!isNaN(displayVal) && displayVal > 0) {
          const usdVal = fxRate !== 1 ? displayVal / fxRate : displayVal;
          goldbackPrices[denom] = { price: usdVal, updatedAt: now };
        }
      });
      if (typeof saveGoldbackPrices === 'function') saveGoldbackPrices();
      if (typeof recordGoldbackPrices === 'function') recordGoldbackPrices();
      if (typeof recordAllItemPriceSnapshots === 'function') recordAllItemPriceSnapshots();
      if (typeof syncGoldbackSettingsUI === 'function') syncGoldbackSettingsUI();
      if (typeof renderTable === 'function') renderTable();
    });
  }

  const gbQuickFillBtn = getExistingElement('goldbackQuickFillBtn');
  if (gbQuickFillBtn) {
    gbQuickFillBtn.addEventListener('click', () => {
      const input = getExistingElement('goldbackQuickFillInput');
      if (!input) return;
      const rate = parseFloat(input.value);
      if (isNaN(rate) || rate <= 0) {
        alert('Enter a valid 1 Goldback rate.');
        return;
      }
      const tbody = getExistingElement('goldbackPriceTableBody');
      if (!tbody || typeof GOLDBACK_DENOMINATIONS === 'undefined') return;
      tbody.querySelectorAll('tr[data-denom]').forEach((row) => {
        const denom = parseFloat(row.dataset.denom);
        const priceInput = row.querySelector('input[type="number"]');
        if (priceInput) {
          priceInput.value = (Math.round(rate * denom * 100) / 100).toFixed(2);
        }
      });
    });
  }

  const gbHistoryBtn = getExistingElement('goldbackHistoryBtn');
  if (gbHistoryBtn) {
    gbHistoryBtn.addEventListener('click', () => {
      if (typeof showGoldbackHistoryModal === 'function') showGoldbackHistoryModal();
    });
  }

  const gbHistoryCloseBtn = getExistingElement('goldbackHistoryCloseBtn');
  if (gbHistoryCloseBtn) {
    gbHistoryCloseBtn.addEventListener('click', () => {
      if (typeof hideGoldbackHistoryModal === 'function') hideGoldbackHistoryModal();
    });
  }

  const gbHistoryModal = getExistingElement('goldbackHistoryModal');
  if (gbHistoryModal) {
    gbHistoryModal.addEventListener('click', (e) => {
      if (e.target === gbHistoryModal) {
        if (typeof hideGoldbackHistoryModal === 'function') hideGoldbackHistoryModal();
      }
    });
  }

  const gbExportBtn = getExistingElement('exportGoldbackHistoryBtn');
  if (gbExportBtn) {
    gbExportBtn.addEventListener('click', () => {
      if (typeof exportGoldbackHistory === 'function') exportGoldbackHistory();
    });
  }
};

/**
 * Binds listeners for image sync and cache clearing operations.
 */
const bindImageSyncListeners = () => {
  const clearImagesBtn = getExistingElement('clearAllImagesBtn');
  if (clearImagesBtn) {
    clearImagesBtn.addEventListener('click', async () => {
      if (!confirm('Clear all cached images, pattern rules, user uploads, AND image URLs from inventory items?')) return;
      if (window.imageCache?.isAvailable()) await imageCache.clearAll();
      let cleared = 0;
      for (const item of inventory) {
        if (item.obverseImageUrl || item.reverseImageUrl) {
          item.obverseImageUrl = '';
          item.reverseImageUrl = '';
          cleared++;
        }
      }
      if (cleared > 0 && typeof saveInventory === 'function') saveInventory();
      populateImagesSection();
      if (typeof renderTable === 'function') renderTable();
      alert(`Cleared all image data. ${cleared} item URL(s) reset.`);
    });
  }

  const syncImageUrlsBtn = getExistingElement('syncImageUrlsBtn');
  if (syncImageUrlsBtn) {
    syncImageUrlsBtn.addEventListener('click', async () => {
      const config = typeof catalogConfig !== 'undefined' ? catalogConfig.getNumistaConfig() : null;
      if (!config?.apiKey) {
        alert('Numista API key not configured.');
        return;
      }
      const eligible = inventory.filter(i => i.numistaId);
      if (!eligible.length) {
        alert('No items with Numista IDs found.');
        return;
      }
      if (!confirm(`Sync image URLs for ${eligible.length} items from Numista API?\nThis bypasses cache and uses your API quota.`)) {
        return;
      }

      syncImageUrlsBtn.disabled = true;
      syncImageUrlsBtn.textContent = 'Syncing…';
      let synced = 0;
      let failed = 0;
      let skipped = 0;
      const seen = new Set();
      const urlByCatId = new Map();
      try {
        for (const item of eligible) {
          const catId = item.numistaId;
          if (seen.has(catId)) {
            const donor = urlByCatId.get(catId);
            if (donor) {
              item.obverseImageUrl = donor.obverseImageUrl;
              item.reverseImageUrl = donor.reverseImageUrl;
              synced++;
            } else {
              skipped++;
            }
            continue;
          }
          seen.add(catId);
          try {
            const url = `https://api.numista.com/v3/types/${catId}?lang=en`;
            const resp = await fetch(url, {
              headers: { 'Numista-API-Key': config.apiKey, 'Content-Type': 'application/json' },
              cache: 'no-cache',
            });
            if (!resp.ok) {
              failed++;
              continue;
            }
            const data = await resp.json();
            const obv = data.obverse_thumbnail || data.obverse?.thumbnail || '';
            const rev = data.reverse_thumbnail || data.reverse?.thumbnail || '';
            urlByCatId.set(catId, { obverseImageUrl: obv, reverseImageUrl: rev });
            for (const inv of eligible) {
              if (inv.numistaId === catId) {
                inv.obverseImageUrl = obv;
                inv.reverseImageUrl = rev;
              }
            }
            synced++;
            await new Promise((resolve) => setTimeout(resolve, 200));
          } catch {
            failed++;
          }
        }
        if (typeof saveInventory === 'function') saveInventory();
        populateImagesSection();
        alert(`Image URL sync complete.\n${synced} synced, ${failed} failed, ${skipped} skipped (dupes).`);
      } finally {
        syncImageUrlsBtn.disabled = false;
        syncImageUrlsBtn.textContent = 'Sync Image URLs from Numista';
      }
    });
  }
};

/**
 * Binds listeners for pattern rule mode switching and creation.
 */
const bindPatternRuleModeListeners = () => {
  const patternModeKeywords = getExistingElement('patternModeKeywords');
  const patternModeRegex = getExistingElement('patternModeRegex');
  const patternInput = getExistingElement('patternRulePattern');
  const patternTip = getExistingElement('patternRuleTip');

  if (patternModeKeywords && patternModeRegex) {
    patternModeKeywords.addEventListener('click', () => {
      _patternMode = 'keywords';
      patternModeKeywords.classList.add('active');
      patternModeRegex.classList.remove('active');
      if (patternInput) patternInput.placeholder = 'e.g. morgan, peace, walking liberty';
      if (patternTip) patternTip.textContent = 'Separate keywords with commas or semicolons. Matches item names containing any keyword.';
    });
    patternModeRegex.addEventListener('click', () => {
      _patternMode = 'regex';
      patternModeRegex.classList.add('active');
      patternModeKeywords.classList.remove('active');
      if (patternInput) patternInput.placeholder = 'e.g. \\bmorgan\\b|\\bpeace\\b';
      if (patternTip) patternTip.textContent = 'Case-insensitive regex. Use \\b for word boundaries, | for OR, .* for wildcards.';
    });
  }

  if (patternInput && typeof attachAutocomplete === 'function') {
    attachAutocomplete(patternInput, 'names');
  }

  // Camera capture buttons — bridge capture input → main file input via DataTransfer
  [['patternRuleObverseCamera', 'patternRuleObverseCapture', 'patternRuleObverse'],
   ['patternRuleReverseCamera', 'patternRuleReverseCapture', 'patternRuleReverse']].forEach(([btnId, captureId, mainId]) => {
    const btn = getExistingElement(btnId);
    const captureInput = getExistingElement(captureId);
    const mainInput = getExistingElement(mainId);
    if (btn && captureInput && mainInput) {
      btn.addEventListener('click', () => captureInput.click());
      captureInput.addEventListener('change', () => {
        if (!captureInput.files?.length) return;
        const dt = new DataTransfer();
        dt.items.add(captureInput.files[0]);
        mainInput.files = dt.files;
        mainInput.dispatchEvent(new Event('change'));
      });
    }
  });

  const addPatternRuleBtn = getExistingElement('addPatternRuleBtn');
  if (addPatternRuleBtn) {
    addPatternRuleBtn.addEventListener('click', async () => {
      const obverseInput = getExistingElement('patternRuleObverse');
      const reverseInput = getExistingElement('patternRuleReverse');

      const rawPattern = patternInput?.value?.trim();
      const replacement = rawPattern || '';

      if (!rawPattern) {
        alert('Pattern is required.');
        return;
      }

      let pattern = rawPattern;
      if (_patternMode === 'keywords') {
        const terms = rawPattern.split(/[,;]/).map(t => t.trim()).filter(t => t.length > 0);
        if (terms.length === 0) {
          alert('Enter at least one keyword.');
          return;
        }
        pattern = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
      }

      try {
        new RegExp(pattern, 'i');
      } catch (e) {
        alert('Invalid pattern: ' + e.message);
        return;
      }

      if (!obverseInput?.files?.[0] && !reverseInput?.files?.[0]) {
        alert('Please select at least one image (obverse or reverse).');
        return;
      }

      let obverseBlob = null;
      let reverseBlob = null;
      const processor = typeof imageProcessor !== 'undefined' ? imageProcessor : null;

      try {
        if (obverseInput?.files?.[0]) {
          if (processor) {
            const result = await processor.processFile(obverseInput.files[0]);
            obverseBlob = result?.blob || null;
          } else {
            obverseBlob = obverseInput.files[0];
          }
        }

        if (reverseInput?.files?.[0]) {
          if (processor) {
            const result = await processor.processFile(reverseInput.files[0]);
            reverseBlob = result?.blob || null;
          } else {
            reverseBlob = reverseInput.files[0];
          }
        }
      } catch (err) {
        console.error('Image processing failed:', err);
        alert('Failed to process image: ' + err.message);
        return;
      }

      const ruleId = 'custom-img-' + Date.now();
      const addResult = NumistaLookup.addRule(pattern, replacement, null, ruleId);
      if (!addResult.success) {
        alert(addResult.error || 'Failed to add rule.');
        return;
      }

      if ((obverseBlob || reverseBlob) && window.imageCache?.isAvailable()) {
        await imageCache.cachePatternImage(ruleId, obverseBlob, reverseBlob);
      }

      if (patternInput) patternInput.value = '';
      if (obverseInput) obverseInput.value = '';
      if (reverseInput) reverseInput.value = '';

      renderCustomPatternRules();
      renderImageStorageStats();
    });
  }
};

/**
 * Binds listeners for card style and table image toggles.
 */
const bindCardAndTableImageListeners = () => {
  // Card style toggle (A/B/C/D chip buttons in Appearance > Inventory)
  const cardStyleToggleEl = getExistingElement('settingsCardStyleToggle');
  if (cardStyleToggleEl) {
    const savedStyle = localStorage.getItem(CARD_STYLE_KEY) || 'A';
    cardStyleToggleEl.querySelectorAll('.chip-sort-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.style === savedStyle);
    });
    cardStyleToggleEl.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-style]');
      if (!btn) return;
      const val = btn.dataset.style;
      localStorage.setItem(CARD_STYLE_KEY, val);
      cardStyleToggleEl.querySelectorAll('.chip-sort-btn').forEach(b => b.classList.toggle('active', b === btn));
      // Sync live sort bar toggle
      const liveSortToggle = document.getElementById('cardStyleToggle');
      if (liveSortToggle) {
        liveSortToggle.querySelectorAll('[data-style]').forEach(b => b.classList.toggle('active', b.dataset.style === val));
      }
      if (typeof renderTable === 'function') renderTable();
    });
  }

  // Default sort column
  const defaultSortColEl = getExistingElement('settingsDefaultSortColumn');
  if (defaultSortColEl) {
    const savedCol = localStorage.getItem(DEFAULT_SORT_COL_KEY);
    if (savedCol !== null) defaultSortColEl.value = savedCol;
    defaultSortColEl.addEventListener('change', () => {
      const val = parseInt(defaultSortColEl.value, 10);
      localStorage.setItem(DEFAULT_SORT_COL_KEY, String(val));
      sortColumn = val;
      if (typeof updateCardSortBar === 'function') updateCardSortBar();
      if (typeof renderTable === 'function') renderTable();
    });
  }

  // Default sort direction
  const defaultSortDirEl = getExistingElement('settingsDefaultSortDir');
  if (defaultSortDirEl) {
    const savedDir = localStorage.getItem(DEFAULT_SORT_DIR_KEY) || 'asc';
    defaultSortDirEl.querySelectorAll('.chip-sort-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.val === savedDir);
    });
    defaultSortDirEl.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-val]');
      if (!btn) return;
      const val = btn.dataset.val;
      localStorage.setItem(DEFAULT_SORT_DIR_KEY, val);
      sortDirection = val;
      defaultSortDirEl.querySelectorAll('.chip-sort-btn').forEach(b => b.classList.toggle('active', b === btn));
      if (typeof updateCardSortBar === 'function') updateCardSortBar();
      if (typeof renderTable === 'function') renderTable();
    });
  }

  wireStorageToggle('settingsDesktopCardView', DESKTOP_CARD_VIEW_KEY, {
    defaultVal: false,
    onApply: (isEnabled) => {
      document.body.classList.toggle('force-card-view', isEnabled);
      if (typeof renderTable === 'function') renderTable();
    },
  });

  wireStorageToggle('tableImagesToggle', 'tableImagesEnabled', {
    defaultVal: true,
    onApply: () => { if (typeof renderTable === 'function') renderTable(); },
  });

  const sidesEl = getExistingElement('tableImageSidesToggle');
  if (sidesEl) {
    const curSides = localStorage.getItem('tableImageSides') || 'both';
    sidesEl.querySelectorAll('.chip-sort-btn').forEach((btn) => {
      btn.classList.toggle('active', btn.dataset.val === curSides);
    });
    sidesEl.addEventListener('click', (e) => {
      const btn = e.target.closest('.chip-sort-btn');
      if (!btn) return;
      localStorage.setItem('tableImageSides', btn.dataset.val);
      sidesEl.querySelectorAll('.chip-sort-btn').forEach((b) => b.classList.toggle('active', b === btn));
      if (typeof renderTable === 'function') renderTable();
    });
  }

  wireStorageToggle('numistaOverrideToggle', 'numistaOverridePersonal', {
    defaultVal: false,
  });
};

// ---------------------------------------------------------------------------
// Image Export/Import helpers
// ---------------------------------------------------------------------------

/**
 * Converts a Blob to WebP format.
 * @param {Blob} blob - The source image blob.
 * @param {number} [quality=0.85] - WebP quality (0 to 1).
 * @returns {Promise<Blob|null>} WebP blob or null if failed.
 */
const blobToWebP = (blob, quality = 0.85) => new Promise((resolve) => {
  if (!blob) return resolve(null);
  const url = URL.createObjectURL(blob);
  const img = new Image();
  img.onload = () => {
    URL.revokeObjectURL(url);
    const canvas = document.createElement('canvas');
    canvas.width = img.naturalWidth;
    canvas.height = img.naturalHeight;
    canvas.getContext('2d').drawImage(img, 0, 0);
    canvas.toBlob(resolve, 'image/webp', quality);
  };
  img.onerror = () => { URL.revokeObjectURL(url); resolve(blob); };
  img.src = url;
});

/**
 * Builds a ZIP archive of all exported images.
 * @param {Object} options - Export options.
 * @param {boolean} [options.includeCdn=false] - Whether to include CDN images.
 * @param {Function} [options.onProgress=null] - Progress callback.
 * @returns {Promise<JSZip>} The generated ZIP object.
 */
const buildImageExportZip = async ({ includeCdn = false, onProgress = null } = {}) => {
  const zip = new JSZip();
  const manifest = [];
  const addedCatalogIds = new Set();

  // 1. User images
  const userImages = await imageCache.exportAllUserImages();
  for (const rec of userImages) {
    if (rec.obverse) zip.file(`user/${rec.uuid}_obverse.webp`, rec.obverse);
    if (rec.reverse) zip.file(`user/${rec.uuid}_reverse.webp`, rec.reverse);
  }

  // 2. Pattern images + rules
  const patternImages = await imageCache.exportAllPatternImages();
  for (const rec of patternImages) {
    if (rec.obverse) zip.file(`pattern/${rec.ruleId}_obverse.webp`, rec.obverse);
    if (rec.reverse) zip.file(`pattern/${rec.ruleId}_reverse.webp`, rec.reverse);
  }
  const customRules = NumistaLookup.listCustomRules();
  zip.file('pattern_rules.json', JSON.stringify(customRules, null, 2));

  // 3. Existing coinImages from IndexedDB (always included)
  const coinImages = await imageCache.exportAllCoinImages();
  for (const rec of coinImages) {
    const { catalogId } = rec;
    addedCatalogIds.add(catalogId);
    const obvWebP = await blobToWebP(rec.obverse);
    const revWebP = await blobToWebP(rec.reverse);
    if (obvWebP) zip.file(`cdn/${catalogId}_obverse.webp`, obvWebP);
    if (revWebP) zip.file(`cdn/${catalogId}_reverse.webp`, revWebP);
    const item = typeof inventory !== 'undefined'
      ? inventory.find(i => BulkImageCache.resolveCatalogId(i) === catalogId)
      : null;
    manifest.push({
      catalogId,
      uuid: item?.uuid || '',
      name: item?.name || '',
      numistaId: item?.numistaId || '',
      obverseFile: obvWebP ? `cdn/${catalogId}_obverse.webp` : null,
      reverseFile: revWebP ? `cdn/${catalogId}_reverse.webp` : null,
    });
  }

  // 4. Optionally fetch CDN images not yet in IndexedDB
  if (includeCdn) {
    const eligible = BulkImageCache.buildEligibleList();
    const toFetch = eligible.filter(e => !addedCatalogIds.has(e.catalogId));
    for (let i = 0; i < toFetch.length; i++) {
      const { item, catalogId } = toFetch[i];
      if (onProgress) onProgress(i + 1, toFetch.length, catalogId);
      if (item.obverseImageUrl || item.reverseImageUrl) {
        await imageCache.cacheImages(catalogId, item.obverseImageUrl || '', item.reverseImageUrl || '');
      }
      const rec = await imageCache.getImages(catalogId);
      if (rec) {
        const obvWebP = await blobToWebP(rec.obverse);
        const revWebP = await blobToWebP(rec.reverse);
        if (obvWebP) zip.file(`cdn/${catalogId}_obverse.webp`, obvWebP);
        if (revWebP) zip.file(`cdn/${catalogId}_reverse.webp`, revWebP);
        manifest.push({
          catalogId,
          uuid: item.uuid || '',
          name: item.name || '',
          numistaId: item.numistaId || '',
          obverseFile: obvWebP ? `cdn/${catalogId}_obverse.webp` : null,
          reverseFile: revWebP ? `cdn/${catalogId}_reverse.webp` : null,
        });
      }
      await new Promise(r => setTimeout(r, 1000));
    }
  }

  if (manifest.length > 0) {
    zip.file('manifest.json', JSON.stringify(manifest, null, 2));
  }

  return zip;
};

/**
 * Restores CDN images from an imported ZIP file.
 * @param {JSZip} zip - The loaded ZIP object.
 */
const _restoreCdnFolderFromZip = async (zip) => {
  const cdnFolder = zip.folder('cdn');
  if (!cdnFolder) return;
  const cdnMap = new Map();
  cdnFolder.forEach((relativePath, zipEntry) => {
    const match = relativePath.match(/^(.+)_(obverse|reverse)\.webp$/);
    if (match) {
      const [, catalogId, side] = match;
      if (!cdnMap.has(catalogId)) cdnMap.set(catalogId, {});
      cdnMap.get(catalogId)[side] = zipEntry;
    }
  });
  for (const [catalogId, sides] of cdnMap) {
    const alreadyCached = await imageCache.hasImages(catalogId);
    if (alreadyCached) continue;
    const obverse = sides.obverse ? await sides.obverse.async('blob') : null;
    const reverse = sides.reverse ? await sides.reverse.async('blob') : null;
    const item = typeof inventory !== 'undefined'
      ? inventory.find(i => BulkImageCache.resolveCatalogId(i) === catalogId)
      : null;
    await imageCache._put('coinImages', {
      catalogId,
      obverse,
      reverse,
      obverseUrl: item?.obverseImageUrl || '',
      reverseUrl: item?.reverseImageUrl || '',
      cachedAt: Date.now(),
      size: (obverse?.size || 0) + (reverse?.size || 0),
    });
  }
};

/**
 * Binds listeners for image import/export buttons.
 */
const bindImageImportExportListeners = () => {
  const exportBtn = getExistingElement('exportAllImagesBtn');
  if (exportBtn) {
    exportBtn.addEventListener('click', async () => {
      if (!window.imageCache?.isAvailable()) {
        alert('IndexedDB unavailable.');
        return;
      }
      if (typeof JSZip === 'undefined') {
        alert('JSZip not loaded.');
        return;
      }

      const eligibleCount = typeof BulkImageCache !== 'undefined'
        ? BulkImageCache.buildEligibleList().length
        : 0;
      // nosemgrep: javascript.browser.security.insecure-document-method.insecure-document-method
      const includeCdn = eligibleCount > 0 && confirm(
        'Download CDN catalog images for offline use?\n\n' +
        'This fetches images for items with Numista IDs (1 second between each). ' +
        'Already-cached images are included automatically.'
      );

      let progressEl = null;
      if (includeCdn) {
        progressEl = document.createElement('div');
        progressEl.id = 'imageCdnProgress';
        progressEl.innerHTML = '<span id="imageCdnProgressText">Preparing\u2026</span>' +
          '<progress id="imageCdnProgressBar" value="0" max="1"></progress>';
        exportBtn.parentNode.insertBefore(progressEl, exportBtn.nextSibling);
      }

      exportBtn.disabled = true;
      exportBtn.textContent = 'Exporting\u2026';

      try {
        const zip = await buildImageExportZip({
          includeCdn,
          onProgress: (current, total, catalogId) => {
            if (!progressEl) return;
            const textEl = document.getElementById('imageCdnProgressText');
            const barEl = document.getElementById('imageCdnProgressBar');
            if (textEl) textEl.textContent = `Fetching ${catalogId} (${current} of ${total})\u2026`;
            if (barEl) { barEl.value = current; barEl.max = total; }
          },
        });

        const blob = await zip.generateAsync({ type: 'blob' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'staktrakr-images.zip';
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(url);
      } catch (err) {
        console.error('Image export failed:', err);
        alert('Export failed: ' + err.message);
      } finally {
        exportBtn.textContent = 'Export All Images';
        exportBtn.disabled = false;
        if (progressEl) progressEl.remove();
      }
    });
  }

  const importBtn = getExistingElement('importImagesBtn');
  const importFile = getExistingElement('importImagesFile');
  if (importBtn && importFile) {
    importBtn.addEventListener('click', () => importFile.click());
    importFile.addEventListener('change', async (e) => {
      const file = e.target.files?.[0];
      if (!file) return;
      if (typeof JSZip === 'undefined') {
        alert('JSZip not loaded.');
        return;
      }
      if (!window.imageCache?.isAvailable()) {
        alert('IndexedDB unavailable.');
        return;
      }

      importBtn.textContent = 'Importing...';
      importBtn.disabled = true;

      try {
        const zip = await JSZip.loadAsync(file);

        const rulesFile = zip.file('pattern_rules.json');
        if (rulesFile) {
          const rulesJson = await rulesFile.async('string');
          const rules = JSON.parse(rulesJson);
          const existingPatterns = new Set(NumistaLookup.listCustomRules().map(r => r.pattern));
          for (const rule of rules) {
            if (!rule.pattern || existingPatterns.has(rule.pattern)) continue;
            NumistaLookup.addRule(rule.pattern, rule.replacement || '', rule.numistaId || null, rule.seedImageId || null);
            existingPatterns.add(rule.pattern);
          }
        }

        const patternFiles = zip.folder('pattern');
        if (patternFiles) {
          const patternMap = new Map();
          patternFiles.forEach((relativePath, zipEntry) => {
            const match = relativePath.match(/^(.+)_(obverse|reverse)\.webp$/);
            if (match) {
              const [, ruleId, side] = match;
              if (!patternMap.has(ruleId)) patternMap.set(ruleId, {});
              patternMap.get(ruleId)[side] = zipEntry;
            }
          });
          for (const [ruleId, sides] of patternMap) {
            const obverse = sides.obverse ? await sides.obverse.async('blob') : null;
            const reverse = sides.reverse ? await sides.reverse.async('blob') : null;
            await imageCache.cachePatternImage(ruleId, obverse, reverse);
          }
        }

        const userFolder = zip.folder('user');
        if (userFolder) {
          const userMap = new Map();
          userFolder.forEach((relativePath, zipEntry) => {
            const match = relativePath.match(/^(.+)_(obverse|reverse)\.webp$/);
            if (match) {
              const [, uuid, side] = match;
              if (!userMap.has(uuid)) userMap.set(uuid, {});
              userMap.get(uuid)[side] = zipEntry;
            }
          });
          for (const [uuid, sides] of userMap) {
            const obverse = sides.obverse ? await sides.obverse.async('blob') : null;
            const reverse = sides.reverse ? await sides.reverse.async('blob') : null;
            if (obverse) await imageCache.cacheUserImage(uuid, obverse, reverse);
          }
        }

        // Restore CDN catalog images
        await _restoreCdnFolderFromZip(zip);

        populateImagesSection();
      } catch (err) {
        console.error('Image import failed:', err);
        alert('Import failed: ' + err.message);
      } finally {
        importBtn.textContent = 'Import Images';
        importBtn.disabled = false;
        importFile.value = '';
      }
    });
  }


};

/**
 * Aggregates all image-related settings listeners.
 */
const bindImageSettingsListeners = () => {
  bindImageSyncListeners();
  bindPatternRuleModeListeners();
  bindCardAndTableImageListeners();
  bindImageImportExportListeners();
};

/**
 * Render the backup list for a cloud provider.
 */
const renderCloudBackupList = (provider, backups) => {
  const listEl = document.getElementById('cloudBackupList_' + provider);
  if (!listEl) return;

  if (!backups || backups.length === 0) {
    listEl.style.display = '';
    listEl.innerHTML = '<div class="cloud-backup-empty">No backups found</div>';
    return;
  }

  listEl.style.display = '';
  // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
  listEl.innerHTML = backups.map(function (b) {
    const d = new Date(b.server_modified);
    const dateStr = d.toLocaleDateString([], { month: 'short', day: 'numeric' }) +
      ' ' + d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
    const sizeStr = b.size < 1024 ? b.size + ' B' :
      b.size < 1048576 ? (b.size / 1024).toFixed(0) + ' KB' :
        (b.size / 1048576).toFixed(1) + ' MB';
    const safeProvider = sanitizeHtml(provider);
    const safeFilename = sanitizeHtml(b.name);
    return '<div class="cloud-backup-row">' +
      '<button class="cloud-backup-entry" data-provider="' + safeProvider +
        '" data-filename="' + safeFilename + '" data-size="' + b.size + '">' +
        '<span class="cloud-backup-name" title="' + safeFilename + '">' + sanitizeHtml(dateStr) + '</span>' +
        '<span class="cloud-backup-size">' + sanitizeHtml(sizeStr) + '</span>' +
      '</button>' +
      '<button class="cloud-backup-delete-btn" data-provider="' + safeProvider +
        '" data-filename="' + safeFilename + '" title="Delete this backup from Dropbox" aria-label="Delete ' + safeFilename + '">' +
        '&times;' +
      '</button>' +
    '</div>';
  }).join('');
};

/**
 * Wires cloud storage connect/disconnect/backup/restore buttons.
 */
const bindCloudCacheListeners = () => {
  // Session-only password cache — no toggle needed, auto-caches on first use
};

/**
 * Run an async action while showing a loading state on a button.
 * Saves innerHTML, disables the button, runs the action, then restores.
 * @param {HTMLElement} btn
 * @param {string} label - loading text to show (e.g. 'Uploading…')
 * @param {Function} action - async function to execute
 * @param {string} errorPrefix - prefix for alert on failure
 * @param {Function} [finallyFn] - optional cleanup in finally block
 */
const _cloudBtnAction = async (btn, label, action, errorPrefix, finallyFn) => {
  var origHtml = btn.innerHTML;
  btn.disabled = true;
  btn.textContent = label;
  try {
    await action(btn);
  } catch (err) {
    alert(errorPrefix + err.message);
  } finally {
    btn.disabled = false;
    btn.innerHTML = origHtml;
    if (finallyFn) { try { await finallyFn(); } catch (_) { /* ignore */ } }
  }
};

/**
 * Perform a cached-password cloud backup (encrypt + upload, no vault modal).
 */
const _cloudBackupWithCachedPw = (provider, password, btn) =>
  _cloudBtnAction(btn, 'Encrypting\u2026', async (b) => {
    var fileBytes = await vaultEncryptToBytes(password);
    b.textContent = 'Uploading\u2026';
    await cloudUploadVault(provider, fileBytes);
    if (typeof showCloudToast === 'function') showCloudToast('Backup complete.');
    if (typeof showKrakenToastIfFirst === 'function') showKrakenToastIfFirst();
  }, 'Backup failed: ');

/**
 * Perform a cached-password cloud restore (decrypt + restore, no vault modal).
 */
const _cloudRestoreWithCachedPw = async (provider, password, fileBytes) => {
  try {
    await vaultDecryptAndRestore(fileBytes, password);
    if (typeof showCloudToast === 'function') showCloudToast('Restore complete. Reloading\u2026');
    setTimeout(function () { location.reload(); }, 1200);
  } catch (err) {
    alert('Decryption failed. Opening password prompt.');
    openVaultModal('cloud-import', {
      provider: provider,
      fileBytes: fileBytes,
    });
  }
};

const bindCloudStorageListeners = () => {
  var panel = document.getElementById('settingsPanel_cloud');
  if (!panel) return;

  bindCloudCacheListeners();

  panel.addEventListener('click', async function (e) {
    var btn = e.target.closest('button');
    if (!btn) return;
    var provider = btn.dataset.provider;
    if (!provider) return;

    if (btn.classList.contains('cloud-connect-btn')) {
      if (typeof cloudAuthStart === 'function') cloudAuthStart(provider);

    } else if (btn.classList.contains('cloud-disconnect-btn')) {
      if (typeof cloudDisconnect === 'function') cloudDisconnect(provider);

    } else if (btn.classList.contains('cloud-backup-btn')) {
      await _cloudBtnAction(btn, 'Checking\u2026', async () => {
        var conflict = await cloudCheckConflict(provider);
        if (conflict.conflict) {
          var remoteDate = new Date(conflict.remote.timestamp);
          var remoteItems = Number(conflict.remote.itemCount) || 0;
          var localItems = conflict.local && Number(conflict.local.itemCount) || 0;
          var remoteInfo = remoteDate.toLocaleDateString() + ' ' +
            remoteDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
          var localInfo = localItems.toLocaleString() + ' items';
          var remoteLine = 'Remote: ' + remoteItems.toLocaleString() + ' items (' + remoteInfo + ')';
          var localLine = 'Local: ' + localInfo;
          if (!confirm(
            'A newer remote backup exists.\n\n' +
            remoteLine + '\n' +
            localLine + '\n\n' +
            'Do you want to overwrite the remote backup with current local data?'
          )) {
            return;
          }
        }
        var cachedPw = typeof cloudGetCachedPassword === 'function' ? cloudGetCachedPassword(provider) : null;
        if (cachedPw) {
          await _cloudBackupWithCachedPw(provider, cachedPw, btn);
          return;
        }
        openVaultModal('cloud-export', { provider: provider });
        if (typeof showKrakenToastIfFirst === 'function') showKrakenToastIfFirst();
      }, 'Conflict check failed: ');

    } else if (btn.classList.contains('cloud-restore-btn')) {
      var listEl = document.getElementById('cloudBackupList_' + provider);
      if (listEl && listEl.style.display !== 'none' && listEl.innerHTML) {
        listEl.style.display = 'none';
        listEl.innerHTML = '';
        return;
      }
      await _cloudBtnAction(btn, 'Loading\u2026', async () => {
        var backups = await cloudListBackups(provider);
        renderCloudBackupList(provider, backups);
      }, 'Failed to list backups: ');

    } else if (btn.classList.contains('cloud-backup-entry')) {
      var filename = btn.dataset.filename;
      var size = parseInt(btn.dataset.size, 10) || 0;
      var sizeStr = size < 1024 ? size + ' B' : size < 1048576 ? (size / 1024).toFixed(0) + ' KB' : (size / 1048576).toFixed(1) + ' MB';
      if (!confirm('Restore "' + filename + '" (' + sizeStr + ')?\n\nThis will overwrite all local data.')) return;
      await _cloudBtnAction(btn, 'Downloading\u2026', async () => {
        var fileBytes = await cloudDownloadVaultByName(provider, filename);
        var savedPw = typeof cloudGetCachedPassword === 'function' ? cloudGetCachedPassword(provider) : null;
        if (savedPw) {
          await _cloudRestoreWithCachedPw(provider, savedPw, fileBytes);
          return;
        }
        openVaultModal('cloud-import', {
          provider: provider,
          fileBytes: fileBytes,
          filename: filename,
          size: size,
        });
      }, 'Download failed: ', async () => {
        var parentList = btn.closest('.cloud-backup-list');
        if (parentList) {
          var refreshed = await cloudListBackups(provider);
          renderCloudBackupList(provider, refreshed);
        }
      });

    } else if (btn.classList.contains('cloud-backup-delete-btn')) {
      var delFilename = btn.dataset.filename;
      if (!await showBulkConfirm('Delete "' + delFilename + '" from cloud storage?\n\nThis cannot be undone.')) return;
      await _cloudBtnAction(btn, '\u2026', async () => {
        await cloudDeleteBackup(provider, delFilename);
        if (typeof showCloudToast === 'function') showCloudToast('"' + delFilename + '" deleted.');
      }, 'Delete failed: ', async () => {
        var parentList = btn.closest('.cloud-backup-list');
        if (parentList) {
          var refreshed = await cloudListBackups(provider);
          renderCloudBackupList(provider, refreshed);
        }
      });
    }
  });
};

/**
 * Wires up Storage section listeners (Refresh button, tiny-key toggle).
 */
const bindStorageListeners = () => {
  // Refresh button
  const refreshBtn = document.getElementById('storageRefreshBtn');
  if (refreshBtn) {
    refreshBtn.addEventListener('click', () => {
      if (typeof renderStorageSection === 'function') renderStorageSection();
    });
  }

  // Top-level tiny-key toggle
  const topToggle = document.getElementById('storageToggleTiny');
  if (topToggle) {
    topToggle.addEventListener('click', () => {
      if (typeof _handleStorageTinyToggle === 'function') _handleStorageTinyToggle();
    });
  }
};

/**
 * Wires up all Settings modal event listeners.
 * Called once during initialization.
 */
const setupSettingsEventListeners = () => {
  bindSettingsNavigationListeners();
  bindAppearanceAndHeaderListeners();
  bindFilterAndNumistaListeners();
  bindNumistaBulkSyncListeners();
  bindSettingsModalShellListeners();
  bindGoldbackToggleListeners();
  bindGoldbackActionListeners();
  bindImageSettingsListeners();
  bindCloudStorageListeners();
  bindStorageListeners();
};

if (typeof window !== 'undefined') {
  window.setupSettingsEventListeners = setupSettingsEventListeners;
}