Source: events.js

/**
 * EVENTS MODULE - FIXED VERSION
 *
 * Handles all DOM event listeners with proper null checking and error handling.
 * Includes file protocol compatibility fixes and fallback event attachment methods.
 */

// EVENT UTILITIES
// =============================================================================

/**
 * Safely attaches event listener with fallback methods
 * @param {HTMLElement|Window|Document} element - Element to attach listener to
 * @param {string} event - Event type
 * @param {Function} handler - Event handler function
 * @param {string} [description=""] - Description for logging
 * @returns {boolean} Success status
 */
const safeAttachListener = (element, event, handler, description = "") => {
  if (!element) {
    console.warn(
      `Cannot attach ${event} listener: element not found (${description})`,
    );
    return false;
  }

  try {
    // Method 1: Standard addEventListener
    element.addEventListener(event, handler);
    return true;
  } catch (error) {
    console.warn(`Standard addEventListener failed for ${description}:`, error);

    try {
      // Method 2: Legacy event handler
      element["on" + event] = handler;
      debugLog(`✓ Fallback event handler attached: ${description}`);
      return true;
    } catch (fallbackError) {
      console.error(
        `All event attachment methods failed for ${description}:`,
        fallbackError,
      );
      return false;
    }
  }
};

/**
 * Attaches a listener only if the element exists; silent no-op otherwise.
 * Avoids console.warn spam for intentionally optional UI elements.
 * @param {HTMLElement|null} el - Element (may be null)
 * @param {string} event - Event type
 * @param {Function} handler - Event handler
 * @param {string} label - Description for logging
 */
const optionalListener = (el, event, handler, label) => {
  if (el) safeAttachListener(el, event, handler, label);
};

// =============================================================================
// IMAGE UPLOAD STATE (STACK-32) — Dual obverse/reverse support
// =============================================================================

/** @type {Blob|null} Pending obverse upload blob — saved on item commit */
let _pendingObverseBlob = null;
/** @type {Blob|null} Pending reverse upload blob — saved on item commit */
let _pendingReverseBlob = null;

/** @type {string|null} Preview object URL for obverse — revoked on modal close */
let _pendingObversePreviewUrl = null;
/** @type {string|null} Preview object URL for reverse — revoked on modal close */
let _pendingReversePreviewUrl = null;

/** @type {boolean} User clicked Remove on obverse — delete on save */
let _deleteObverseOnSave = false;
/** @type {boolean} User clicked Remove on reverse — delete on save */
let _deleteReverseOnSave = false;

/**
 * Process a user-selected image file and show preview for a specific side.
 * @param {File} file
 * @param {'obverse'|'reverse'} [side='obverse']
 */
const processUploadedImage = async (file, side = 'obverse') => {
  if (!file || typeof imageProcessor === 'undefined') return;

  const result = await imageProcessor.processFile(file, {
    maxDim: typeof IMAGE_MAX_DIM !== 'undefined' ? IMAGE_MAX_DIM : 600,
    maxBytes: typeof IMAGE_MAX_BYTES !== 'undefined' ? IMAGE_MAX_BYTES : 512000,
  });

  if (!result?.blob) {
    debugLog(`Image processing failed for ${side}`);
    return;
  }

  const suffix = side === 'reverse' ? 'Rev' : 'Obv';

  if (side === 'reverse') {
    _pendingReverseBlob = result.blob;
    if (_pendingReversePreviewUrl) URL.revokeObjectURL(_pendingReversePreviewUrl);
    _pendingReversePreviewUrl = imageProcessor.createPreview(result.blob);
  } else {
    _pendingObverseBlob = result.blob;
    if (_pendingObversePreviewUrl) URL.revokeObjectURL(_pendingObversePreviewUrl);
    _pendingObversePreviewUrl = imageProcessor.createPreview(result.blob);
  }

  const previewUrl = side === 'reverse' ? _pendingReversePreviewUrl : _pendingObversePreviewUrl;

  // Show preview in the appropriate side's elements
  const previewContainer = document.getElementById('itemImagePreview' + suffix);
  const previewImg = document.getElementById('itemImagePreviewImg' + suffix);
  const sizeInfo = document.getElementById('itemImageSizeInfo' + suffix);
  const removeBtn = document.getElementById('itemImageRemoveBtn' + suffix);

  if (previewImg && previewUrl) {
    previewImg.src = previewUrl;
    if (previewContainer) previewContainer.style.display = 'block';
  }
  if (sizeInfo) {
    const origKB = (result.originalSize / 1024).toFixed(0);
    const compKB = (result.compressedSize / 1024).toFixed(0);
    sizeInfo.textContent = `${origKB} KB → ${compKB} KB (${result.format.split('/')[1]})`;
  }
  if (removeBtn) removeBtn.style.display = '';
};

/**
 * Track an externally-created preview object URL so it gets revoked
 * when clearUploadState() runs (prevents memory leaks in editItem preview).
 * @param {string} url - Object URL to track
 * @param {'obverse'|'reverse'} [side='obverse']
 */
const setEditPreviewUrl = (url, side = 'obverse') => {
  if (side === 'reverse') {
    if (_pendingReversePreviewUrl) URL.revokeObjectURL(_pendingReversePreviewUrl);
    _pendingReversePreviewUrl = url;
  } else {
    if (_pendingObversePreviewUrl) URL.revokeObjectURL(_pendingObversePreviewUrl);
    _pendingObversePreviewUrl = url;
  }
};

/**
 * Clear the pending upload state and previews for both sides.
 */
const clearUploadState = () => {
  _pendingObverseBlob = null;
  _pendingReverseBlob = null;
  _deleteObverseOnSave = false;
  _deleteReverseOnSave = false;

  if (_pendingObversePreviewUrl) {
    URL.revokeObjectURL(_pendingObversePreviewUrl);
    _pendingObversePreviewUrl = null;
  }
  if (_pendingReversePreviewUrl) {
    URL.revokeObjectURL(_pendingReversePreviewUrl);
    _pendingReversePreviewUrl = null;
  }

  // Clear obverse side UI
  const previewObv = document.getElementById('itemImagePreviewObv');
  const imgObv = document.getElementById('itemImagePreviewImgObv');
  const sizeObv = document.getElementById('itemImageSizeInfoObv');
  const removeObv = document.getElementById('itemImageRemoveBtnObv');
  const fileObv = document.getElementById('itemImageFileObv');

  if (previewObv) previewObv.style.display = 'none';
  if (imgObv) imgObv.src = '';
  if (sizeObv) sizeObv.textContent = '';
  if (removeObv) removeObv.style.display = 'none';
  if (fileObv) fileObv.value = '';

  // Clear reverse side UI
  const previewRev = document.getElementById('itemImagePreviewRev');
  const imgRev = document.getElementById('itemImagePreviewImgRev');
  const sizeRev = document.getElementById('itemImageSizeInfoRev');
  const removeRev = document.getElementById('itemImageRemoveBtnRev');
  const fileRev = document.getElementById('itemImageFileRev');

  if (previewRev) previewRev.style.display = 'none';
  if (imgRev) imgRev.src = '';
  if (sizeRev) sizeRev.textContent = '';
  if (removeRev) removeRev.style.display = 'none';
  if (fileRev) fileRev.value = '';

  // Reset pattern toggle state
  const patternToggle = document.getElementById('imagePatternToggle');
  const patternKeywordsGroup = document.getElementById('imagePatternKeywordsGroup');
  const patternKeywords = document.getElementById('imagePatternKeywords');
  if (patternToggle) patternToggle.checked = false;
  if (patternKeywordsGroup) patternKeywordsGroup.style.display = 'none';
  if (patternKeywords) patternKeywords.value = '';
};

/**
 * Save the pending upload blob(s) to IndexedDB for the given item UUID.
 * @param {string} uuid
 * @returns {Promise<boolean>}
 */
const saveUserImageForItem = async (uuid) => {
  if (!uuid || !window.imageCache?.isAvailable()) {
    debugLog('saveUserImageForItem: invalid uuid or cache unavailable');
    return false;
  }

  // Priority 1: Handle deletions first
  const hasDeleteIntent = _deleteObverseOnSave || _deleteReverseOnSave;
  const hasNewImages = _pendingObverseBlob || _pendingReverseBlob;

  if (hasDeleteIntent && !hasNewImages) {
    // Pure deletion case: user removed images without uploading new ones
    await handleImageDeletion(uuid);
    clearUploadState();
    return true;
  }

  if (!hasNewImages) {
    // No changes at all
    debugLog('saveUserImageForItem: no changes to save');
    clearUploadState();
    return false;
  }

  // Priority 2: New uploads - merge with existing or replace deleted sides
  debugLog(`saveUserImageForItem: saving images for ${uuid}`);

  let obvBlob = _pendingObverseBlob;
  let revBlob = _pendingReverseBlob;

  // Merge with existing images if only one side uploaded
  if (!obvBlob || !revBlob) {
    try {
      const existing = await window.imageCache.getUserImage(uuid);
      if (existing) {
        // Only merge if not marked for deletion
        if (!obvBlob && existing.obverse && !_deleteObverseOnSave) {
          obvBlob = existing.obverse;
        }
        if (!revBlob && existing.reverse && !_deleteReverseOnSave) {
          revBlob = existing.reverse;
        }
      }
    } catch { /* ignore */ }
  }

  // cacheUserImage requires at least obverse; pass reverse as optional
  if (!obvBlob && revBlob) {
    // Only reverse uploaded — store as obverse (API requirement)
    obvBlob = revBlob;
    revBlob = null;
  }

  const saved = await window.imageCache.cacheUserImage(uuid, obvBlob, revBlob);
  debugLog(`saveUserImageForItem: saved=${saved}`);
  clearUploadState();
  return saved;
};

/**
 * Handle image deletion based on deletion flags.
 * Supports partial deletion (one side only) or full deletion (both sides).
 * @param {string} uuid - Item UUID
 * @returns {Promise<void>}
 */
const handleImageDeletion = async (uuid) => {
  if (!uuid || !window.imageCache?.isAvailable()) return;

  const deleteBoth = _deleteObverseOnSave && _deleteReverseOnSave;
  const deleteNeither = !_deleteObverseOnSave && !_deleteReverseOnSave;

  if (deleteNeither) return;

  if (deleteBoth) {
    // Delete entire record
    debugLog(`handleImageDeletion: deleting both sides for ${uuid}`);
    await window.imageCache.deleteUserImage(uuid);
  } else {
    // Partial deletion: keep one side, delete the other
    debugLog(`handleImageDeletion: partial deletion for ${uuid}`);

    try {
      const existing = await window.imageCache.getUserImage(uuid);
      if (!existing) return; // Nothing to delete

      // Nullify the deleted side, keep the other
      const newObverse = _deleteObverseOnSave ? null : existing.obverse;
      const newReverse = _deleteReverseOnSave ? null : existing.reverse;

      // If both would be null, delete entire record
      if (!newObverse && !newReverse) {
        await window.imageCache.deleteUserImage(uuid);
      } else {
        // Save updated record with one side nullified
        // cacheUserImage requires obverse, so if only reverse remains, store it as obverse
        const obvToSave = newObverse || newReverse;
        const revToSave = newObverse ? newReverse : null;
        await window.imageCache.cacheUserImage(uuid, obvToSave, revToSave);
      }
    } catch (err) {
      debugLog(`Failed to handle partial deletion: ${err}`, 'warn');
    }
  }
};

/**
 * Sets up the override/merge/file-input triad for a single import format.
 * @param {HTMLElement|null} overrideBtn - "Override" button element
 * @param {HTMLElement|null} mergeBtn - "Merge" button element
 * @param {HTMLElement|null} fileInput - Hidden file input element
 * @param {Function} importFn - Import function (file, isOverride) => void
 * @param {string} formatName - Human label (e.g. "CSV", "JSON", "Numista CSV")
 */
const setupFormatImport = (overrideBtn, mergeBtn, fileInput, importFn, formatName) => {
  let isOverride = false;

  if (overrideBtn && fileInput) {
    safeAttachListener(overrideBtn, "click", () => {
      if (confirm(`Importing ${formatName} will overwrite all existing data. To combine data, choose Merge instead. Press OK to continue.`)) {
        isOverride = true;
        fileInput.click();
      }
    }, `${formatName} override button`);
  }

  if (mergeBtn && fileInput) {
    safeAttachListener(mergeBtn, "click", () => {
      isOverride = false;
      fileInput.click();
    }, `${formatName} merge button`);
  }

  optionalListener(fileInput, "change", function (e) {
    if (e.target.files.length > 0) {
      const file = e.target.files[0];
      if (!checkFileSize(file)) {
        alert("File exceeds 2MB limit. Enable cloud backup for larger uploads.");
      } else {
        importFn(file, isOverride);
      }
    }
    this.value = "";
  }, `${formatName} import`);
};

/**
 * Implements dynamic column resizing for the inventory table
 */
const setupColumnResizing = () => {
  const table = document.getElementById("inventoryTable");
  if (!table) {
    console.warn("Inventory table not found for column resizing");
    return;
  }

  // Clear any existing resize handles
  const existingHandles = table.querySelectorAll(".resize-handle");
  existingHandles.forEach((handle) => handle.remove());

  let isResizing = false;
  let currentColumn = null;
  let startX = 0;
  let startWidth = 0;

  // Add resize handles to table headers
  const headers = table.querySelectorAll("th");
  headers.forEach((header, index) => {
    // Ensure header text is wrapped in .header-text span
    let headerTextSpan = header.querySelector('.header-text');
    if (!headerTextSpan) {
      // Create new header-text span
      headerTextSpan = document.createElement('span');
      headerTextSpan.className = 'header-text';
    }

    // Check if the span is empty or needs text
    if (!headerTextSpan.textContent.trim()) {
      // Find the text content (excluding SVG and existing elements)
      const textNodes = Array.from(header.childNodes).filter(node =>
        node.nodeType === Node.TEXT_NODE && node.textContent.trim()
      );

      if (textNodes.length > 0) {
        // Move text content into the span
        headerTextSpan.textContent = textNodes.map(node => node.textContent.trim()).join(' ');

        // Remove original text nodes
        textNodes.forEach(node => node.remove());

        // Insert the span after the SVG icon (if present) if it's not already in the DOM
        if (!header.contains(headerTextSpan)) {
          const svg = header.querySelector('svg');
          if (svg) {
            svg.insertAdjacentElement('afterend', headerTextSpan);
          } else {
            header.insertBefore(headerTextSpan, header.firstChild);
          }
        }
      }
    }

    // Skip adding resize handle to the Actions column (last column)
    if (index >= headers.length - 1) return;

    const resizeHandle = document.createElement("div");
    resizeHandle.className = "resize-handle";

    /* position:sticky (set via CSS on #inventoryTable thead th) already
       provides a containing block for the absolutely-positioned resize
       handle — no inline position:relative needed. */
    header.appendChild(resizeHandle);

    safeAttachListener(
      resizeHandle,
      "mousedown",
      (e) => {
        isResizing = true;
        currentColumn = header;
        startX = e.clientX;
        startWidth = parseInt(
          document.defaultView.getComputedStyle(header).width,
          10,
        );

        e.preventDefault();
        e.stopPropagation();

        // Prevent header click event from firing
        header.style.pointerEvents = "none";
        setTimeout(() => {
          header.style.pointerEvents = "auto";
        }, 100);
      },
      "Column resize handle",
    );
  });

  // Handle mouse move for resizing
  safeAttachListener(
    document,
    "mousemove",
    (e) => {
      if (!isResizing || !currentColumn) return;

      const width = startWidth + e.clientX - startX;
      const minWidth = 40;
      const maxWidth = 300;

      if (width >= minWidth && width <= maxWidth) {
        currentColumn.style.width = width + "px";
      }
    },
    "Document mousemove for resizing",
  );

  // Handle mouse up to stop resizing
  safeAttachListener(
    document,
    "mouseup",
    () => {
      if (isResizing) {
        isResizing = false;
        currentColumn = null;
      }
    },
    "Document mouseup for resizing",
  );

  // Prevent text selection during resize
  safeAttachListener(
    document,
    "selectstart",
    (e) => {
      if (isResizing) {
        e.preventDefault();
      }
    },
    "Document selectstart for resizing",
  );
};

// RESPONSIVE TABLE HANDLING
// =============================================================================

/**
 * Updates column visibility based on current viewport width
 */
const updateColumnVisibility = () => {
  const width = window.innerWidth;
  const isTouch = window.matchMedia('(pointer: coarse)').matches;
  const desktopCardView = localStorage.getItem(DESKTOP_CARD_VIEW_KEY) === 'true';
  const forceCards = desktopCardView || (isTouch && width > 1350 && width <= 1600);

  document.body.classList.toggle('force-card-view', forceCards);

  // Card view handles all column visibility via CSS at ≤1350px (STACK-70)
  // or via .force-card-view for large touch tablets (STACK-70)
  if (width <= 1350 || forceCards) return;
  const hidden = new Set();

  const breakpoints = [
    { width: 1400, hide: ["notes"] },
    { width: 1200, hide: ["notes"] },
    { width: 992, hide: ["notes", "premium"] },
    { width: 768, hide: ["notes", "premium", "spot"] },
    {
      width: 640,
      hide: ["notes", "premium", "spot", "weight"],
    },
    {
      width: 576,
      hide: [
        "notes",
        "premium",
        "spot",
        "weight",
        "purchaseLocation",
        "storageLocation",
        "numista",
        "type",
        "metal",
        "actions",
      ],
    },
  ];

  breakpoints.forEach((bp) => {
    if (width < bp.width) bp.hide.forEach((c) => hidden.add(c));
  });

  // Hide image column when table thumbnails are off or COIN_IMAGES disabled
  const _imgOn = localStorage.getItem('tableImagesEnabled') !== 'false'
    && typeof featureFlags !== 'undefined' && featureFlags.isEnabled('COIN_IMAGES');
  if (!_imgOn) hidden.add('image');

  const allColumns = [
    "date",
    "type",
    "metal",
    "image",
    "qty",
    "name",
    "weight",
    "purchasePrice",
    "spot",
    "premium",
    "purchaseLocation",
    "storageLocation",
    "numista",
    "notes",
    "actions",
  ];

  allColumns.forEach((col) => {
    document.querySelectorAll(`[data-column="${col}"]`).forEach((el) => {
      el.classList.toggle("hidden", hidden.has(col));
    });
  });
};

/**
 * Sets up responsive column visibility handling
 */
const setupResponsiveColumns = () => {
  updateColumnVisibility();
  safeAttachListener(
    window,
    "resize",
    updateColumnVisibility,
    "Window resize for column visibility",
  );
};

// SUB-FUNCTIONS FOR EVENT LISTENER SETUP
// =============================================================================

/**
 * Sets up search input and chip-related listeners
 */
const setupSearchAndChipListeners = () => {
  // Search Input
  if (elements.searchInput) {
    const debouncedSearch = debounce(() => {
      searchQuery = elements.searchInput.value.replace(/[<>]/g, "").trim();
      renderTable();
      if (typeof renderActiveFilters === "function") {
        renderActiveFilters();
      }
    }, 300);
    safeAttachListener(elements.searchInput, "input", debouncedSearch, "Search Input");
  }

  // Chip minimum count dropdown (inline)
  const chipMinCountEl = document.getElementById('chipMinCount');
  if (chipMinCountEl) {
    safeAttachListener(
      chipMinCountEl,
      'change',
      (e) => {
        const minCount = parseInt(e.target.value, 10);
        localStorage.setItem('chipMinCount', minCount.toString());
        // Sync settings modal control
        const settingsChipMin = document.getElementById('settingsChipMinCount');
        if (settingsChipMin) settingsChipMin.value = minCount.toString();
        if (typeof renderActiveFilters === 'function') {
          renderActiveFilters();
        }
      },
      'Chip minimum count dropdown'
    );
  }

  // Grouped name chips toggle (inline) — uses global helper from settings.js
  const groupNameChipsEl = document.getElementById('groupNameChips');
  if (groupNameChipsEl && window.featureFlags) {
    // Set initial state from feature flag
    const initVal = window.featureFlags.isEnabled('GROUPED_NAME_CHIPS') ? 'yes' : 'no';
    groupNameChipsEl.querySelectorAll('.chip-sort-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.val === initVal);
    });
  }
  if (typeof wireFeatureFlagToggle === 'function') {
    wireFeatureFlagToggle('groupNameChips', 'GROUPED_NAME_CHIPS', {
      syncId: 'settingsGroupNameChips',
      onApply: () => { if (typeof renderActiveFilters === 'function') renderActiveFilters(); },
    });
  }

  // Chip sort order inline toggle — uses global helper from settings.js
  const chipSortEl = document.getElementById('chipSortOrder');
  if (chipSortEl) {
    // Restore saved value on setup (migrate 'default' → 'alpha')
    const savedSort = localStorage.getItem('chipSortOrder');
    const activeSort = (savedSort === 'count') ? 'count' : 'alpha';
    chipSortEl.querySelectorAll('.chip-sort-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.sort === activeSort);
    });
  }
  if (typeof wireChipSortToggle === 'function') {
    wireChipSortToggle('chipSortOrder', 'settingsChipSortOrder');
  }
};

/**
 * Sets up header button listeners (logo, settings, about, details)
 */
const setupHeaderButtonListeners = () => {
  // CRITICAL HEADER BUTTONS
  debugLog("Setting up header buttons...");

  // App Logo
  if (elements.appLogo) {
    safeAttachListener(
      elements.appLogo,
      "click",
      () => window.location.reload(),
      "App Logo",
    );
  }

  // Settings Button
  if (elements.settingsBtn) {
    safeAttachListener(
      elements.settingsBtn,
      "click",
      (e) => {
        e.preventDefault();
        debugLog("Settings button clicked");
        if (typeof showSettingsModal === "function") {
          showSettingsModal();
        }
      },
      "Settings Button",
    );
  }

  // About Button
  if (elements.aboutBtn) {
    safeAttachListener(
      elements.aboutBtn,
      "click",
      (e) => {
        e.preventDefault();
        if (typeof showAboutModal === "function") {
          showAboutModal();
        }
      },
      "About Button",
    );
  }


  // Details modal triggers
  if (elements.totalTitles && elements.totalTitles.length) {
    elements.totalTitles.forEach((title) => {
      safeAttachListener(
        title,
        "click",
        () => {
          const metal = title.dataset.metal;
          if (typeof showDetailsModal === "function") {
            showDetailsModal(metal);
          }
        },
        `Totals title (${title.dataset.metal})`,
      );
    });
  }

  if (elements.detailsCloseBtn) {
    safeAttachListener(
      elements.detailsCloseBtn,
      "click",
      () => {
        if (typeof closeDetailsModal === "function") {
          closeDetailsModal();
        }
      },
      "Close details modal",
    );
  }
};

/**
 * Sets up table header sorting and Goldback denomination picker
 */
const setupTableSortListeners = () => {
  // TABLE HEADER SORTING
  debugLog("Setting up table sorting...");
  const inventoryTable = document.getElementById("inventoryTable");
  if (inventoryTable) {
    const headers = inventoryTable.querySelectorAll("th");
    headers.forEach((header, index) => {
      // Skip the Actions column (last column)
      if (index >= headers.length - 1) {
        return;
      }

      header.style.cursor = "pointer";

      safeAttachListener(
        header,
        "click",
        (e) => {
          if (e.shiftKey) return;
          // Toggle sort direction if same column, otherwise set to new column with asc
          if (sortColumn === index) {
            sortDirection = sortDirection === "asc" ? "desc" : "asc";
          } else {
            sortColumn = index;
            sortDirection = "asc";
          }

          renderTable();
        },
        `Table header ${index}`,
      );
    });
  } else {
    console.error("Inventory table not found for sorting setup!");
  }

  // GOLDBACK DENOMINATION PICKER TOGGLE (STACK-45)
  // Swaps weight text input ↔ denomination select when unit changes to/from 'gb'.
  // Auto-fills hidden weight value from the selected denomination.
  const showEl = (el, visible) => { if (el) el.style.display = visible ? '' : 'none'; };
  /**
   * Toggles the visible input between weight and goldback denomination.
   * Auto-fills hidden weight value from the selected denomination when in 'gb' mode.
   */
  window.toggleGbDenomPicker = () => {
    const isGb = elements.itemWeightUnit?.value === 'gb';
    const denomSelect = elements.itemGbDenom;
    const weightInput = elements.itemWeight;
    const weightLabel = document.getElementById('itemWeightLabel');

    showEl(denomSelect, isGb);
    showEl(weightInput, !isGb);
    if (isGb && weightInput && denomSelect) weightInput.value = denomSelect.value;
    if (weightLabel) weightLabel.textContent = isGb ? 'Denomination' : 'Weight';
  };
};

// FORM SUBMIT HELPERS (STACK-61)
// =============================================================================

/**
 * Parses weight from form input, handling Goldback denominations,
 * fractions, and gram-to-troy-oz conversion.
 * @param {string} weightRaw - Raw weight input value
 * @param {string} weightUnit - Unit: 'oz', 'g', or 'gb'
 * @param {boolean} isEditing - Whether in edit mode
 * @param {Object} existingItem - Existing item (edit mode)
 * @returns {number} Weight in troy ounces (or denomination value for gb)
 */
const parseWeight = (weightRaw, weightUnit, isEditing, existingItem) => {
  if (isEditing && weightRaw === '') {
    return typeof existingItem.weight !== 'undefined' ? existingItem.weight : 0;
  }
  let weight = parseFraction(weightRaw);
  if (weightUnit === 'g') {
    weight = gramsToOzt(weight);
  }
  // gb: weight stays as raw denomination value (conversion happens in computeMeltValue)
  return isNaN(weight) ? 0 : parseFloat(weight.toFixed(6));
};

/**
 * Converts a user-entered price from display currency to USD.
 * @param {string} rawValue - Raw price input value
 * @param {number} fxRate - Exchange rate (display currency per 1 USD)
 * @param {boolean} isEditing - Whether in edit mode
 * @param {number} existingValue - Existing price (edit mode)
 * @returns {number} Price in USD
 */
const parsePriceToUSD = (rawValue, fxRate, isEditing, existingValue) => {
  if (isEditing && rawValue === '') {
    return typeof existingValue !== 'undefined' ? existingValue : 0;
  }
  let entered = rawValue === '' ? 0 : parseFloat(rawValue);
  entered = isNaN(entered) || entered < 0 ? 0 : entered;
  return fxRate !== 1 ? entered / fxRate : entered;
};

/**
 * Reads purity from the select/custom input pair.
 * @param {boolean} isEditing - Whether in edit mode
 * @param {Object} existingItem - Existing item (edit mode)
 * @returns {number} Purity value (0–1)
 */
const parsePurity = (isEditing, existingItem) => {
  const puritySelect = elements.itemPuritySelect;
  if (puritySelect && puritySelect.value === 'custom') {
    return elements.itemPurity ? (parseFloat(elements.itemPurity.value) || 1.0) : 1.0;
  }
  if (puritySelect) {
    return parseFloat(puritySelect.value) || 1.0;
  }
  return isEditing ? (existingItem.purity || 1.0) : 1.0;
};

/**
 * Reads all form fields and returns a parsed fields object.
 * @param {boolean} isEditing - Whether in edit mode
 * @param {Object} existingItem - Existing item (edit mode)
 * @returns {Object} Parsed field values
 */
const parseItemFormFields = (isEditing, existingItem) => {
  const composition = getCompositionFirstWords(elements.itemMetal.value);
  const metal = parseNumistaMetal(composition);
  const fxRate = (typeof getExchangeRate === 'function') ? getExchangeRate() : 1;

  const nameInput = elements.itemName.value.trim();
  const qtyInput = elements.itemQty.value.trim();

  const weightUnit = elements.itemWeightUnit.value;
  const weightRaw = (weightUnit === 'gb' && elements.itemGbDenom)
    ? elements.itemGbDenom.value
    : elements.itemWeight.value;

  const marketValueInput = elements.itemMarketValue ? elements.itemMarketValue.value.trim() : '';
  let marketValue;
  if (marketValueInput && !isNaN(parseFloat(marketValueInput))) {
    const enteredMv = parseFloat(marketValueInput);
    marketValue = fxRate !== 1 ? enteredMv / fxRate : enteredMv;
  } else {
    marketValue = 0;
  }

  return {
    metal,
    composition,
    name: isEditing ? (nameInput || existingItem.name || '') : nameInput,
    qty: qtyInput === '' ? (isEditing ? (existingItem.qty || 1) : 1) : parseInt(qtyInput, 10),
    type: elements.itemType.value || (isEditing ? existingItem.type : ''),
    weight: parseWeight(weightRaw, weightUnit, isEditing, existingItem),
    weightUnit,
    price: parsePriceToUSD(elements.itemPrice.value.trim(), fxRate, isEditing, existingItem.price),
    purchaseLocation: elements.purchaseLocation.value.trim(),
    storageLocation: elements.storageLocation.value.trim(),
    serialNumber: elements.itemSerialNumber?.value?.trim() ?? '',
    notes: elements.itemNotes.value.trim(),
    date: elements.itemDate.value || (isEditing ? (existingItem.date || '') : todayStr()),
    catalog: elements.itemCatalog ? elements.itemCatalog.value.trim() : '',
    year: elements.itemYear?.value?.trim() ?? '',
    grade: elements.itemGrade?.value?.trim() ?? '',
    gradingAuthority: elements.itemGradingAuthority?.value?.trim() ?? '',
    certNumber: elements.itemCertNumber?.value?.trim() ?? '',
    pcgsNumber: elements.itemPcgsNumber?.value?.trim() ?? '',
    marketValue,
    purity: parsePurity(isEditing, existingItem),
    currency: displayCurrency,
    obverseImageUrl: elements.itemObverseImageUrl?.value?.trim() ?? '',
    reverseImageUrl: elements.itemReverseImageUrl?.value?.trim() ?? '',
  };
};

/**
 * Validates mandatory item fields.
 * @param {Object} f - Parsed fields from parseItemFormFields()
 * @returns {string|null} Error message or null if valid
 */
const validateItemFields = (f) => {
  if (
    !f.name || !f.date || !f.type || !f.metal ||
    isNaN(f.weight) || f.weight <= 0 ||
    isNaN(f.qty) || f.qty < 1 || !Number.isInteger(f.qty)
  ) {
    return "Please enter valid values for Name, Date, Type, Metal, Weight, and Quantity.";
  }
  return null;
};

/**
 * Builds the common field object shared by both add and edit paths.
 * @param {Object} f - Parsed fields from parseItemFormFields()
 * @returns {Object} Common item fields
 */
const buildItemFields = (f) => ({
  metal: f.metal, composition: f.composition, name: f.name, qty: f.qty,
  type: f.type, weight: f.weight, weightUnit: f.weightUnit, price: f.price,
  marketValue: f.marketValue, date: f.date, purchaseLocation: f.purchaseLocation,
  storageLocation: f.storageLocation, serialNumber: f.serialNumber, notes: f.notes,
  year: f.year, grade: f.grade, gradingAuthority: f.gradingAuthority,
  certNumber: f.certNumber, pcgsNumber: f.pcgsNumber, purity: f.purity,
});

/**
 * Commits a parsed item to inventory (add or edit mode).
 * @param {Object} f - Parsed fields from parseItemFormFields()
 * @param {boolean} isEditing - Whether in edit mode
 * @param {number|null} editIdx - Index being edited (null for add)
 */
const commitItemToInventory = (f, isEditing, editIdx) => {
  if (isEditing) {
    const oldItem = { ...inventory[editIdx] };
    const serial = oldItem.serial;

    inventory[editIdx] = {
      ...oldItem,
      ...buildItemFields(f),
      numistaId: f.catalog,
      currency: f.currency,
      obverseImageUrl: f.obverseImageUrl || window.selectedNumistaResult?.imageUrl || oldItem.obverseImageUrl || '',
      reverseImageUrl: f.reverseImageUrl || window.selectedNumistaResult?.reverseImageUrl || oldItem.reverseImageUrl || '',
    };

    addCompositionOption(f.composition);

    try {
      if (window.catalogManager && inventory[editIdx].numistaId) {
        catalogManager.setCatalogId(serial, inventory[editIdx].numistaId);
      }
    } catch (catErr) {
      console.warn('Failed to update catalog mapping:', catErr);
    }

    // Apply spot lookup override if user selected a historical spot (STACK-49)
    const lookupSpotEdit = elements.itemSpotPrice ? parseFloat(elements.itemSpotPrice.value) : NaN;
    if (!isNaN(lookupSpotEdit) && lookupSpotEdit > 0) {
      inventory[editIdx].spotPriceAtPurchase = lookupSpotEdit;
    }

    saveInventory();

    // Record price data point if price-related fields changed (STACK-43)
    if (typeof recordSingleItemPrice === 'function') {
      const cur = inventory[editIdx];
      const priceChanged = oldItem.marketValue !== cur.marketValue
        || oldItem.price !== cur.price || oldItem.weight !== cur.weight
        || oldItem.qty !== cur.qty || oldItem.metal !== cur.metal
        || oldItem.purity !== cur.purity;
      if (priceChanged) recordSingleItemPrice(cur, 'edit');
    }

    renderTable();
    renderActiveFilters();
    logItemChanges(oldItem, inventory[editIdx]);

    editingIndex = null;
    editingChangeLogIndex = null;
  } else {
    const metalKey = f.metal.toLowerCase();
    // Prefer spot price from lookup modal, fall back to current spot (STACK-49)
    const lookupSpot = elements.itemSpotPrice ? parseFloat(elements.itemSpotPrice.value) : NaN;
    const spotPriceAtPurchase = !isNaN(lookupSpot) && lookupSpot > 0
      ? lookupSpot
      : (spotPrices[metalKey] ?? 0);
    const serial = getNextSerial();

    inventory.push({
      ...buildItemFields(f),
      pcgsVerified: false,
      spotPriceAtPurchase,
      premiumPerOz: 0,
      totalPremium: 0,
      serial,
      uuid: generateUUID(),
      numistaId: f.catalog,
      currency: f.currency,
      obverseImageUrl: f.obverseImageUrl || window.selectedNumistaResult?.imageUrl || '',
      reverseImageUrl: f.reverseImageUrl || window.selectedNumistaResult?.reverseImageUrl || '',
    });

    typeof registerName === "function" && registerName(f.name);
    addCompositionOption(f.composition);

    if (window.catalogManager && f.catalog) {
      catalogManager.setCatalogId(serial, f.catalog);
    }

    saveInventory();

    // Log the add action to the changelog (BUG-004)
    const addedItem = inventory[inventory.length - 1];
    const addSummary = [addedItem.metal, addedItem.type, addedItem.name,
      typeof formatWeight === 'function' ? formatWeight(addedItem.weight, addedItem.weightUnit) : addedItem.weight + ' oz',
      typeof formatCurrency === 'function' ? formatCurrency(addedItem.price) : '$' + Number(addedItem.price).toFixed(2)
    ].filter(Boolean).join(' · ');
    logChange(addedItem.name, 'Added', '', addSummary, inventory.length - 1);

    // STAK-126: Auto-apply Numista tags from the lookup result
    if (window.selectedNumistaResult?.tags && typeof applyNumistaTags === 'function') {
      const newUuid = addedItem.uuid;
      applyNumistaTags(newUuid, window.selectedNumistaResult.tags);
    }

    // Record initial price data point (STACK-43)
    if (typeof recordSingleItemPrice === 'function') {
      recordSingleItemPrice(addedItem, 'add');
    }

    renderTable();

    // Success toast (UX-002)
    if (typeof showToast === 'function') {
      showToast('\u2713 ' + addedItem.name + ' added to inventory');
    }
  }
};

/**
 * Builds a Numista search query, optionally rewriting via NumistaLookup patterns.
 * @param {string} nameVal - Item name input value
 * @param {string} metalVal - Metal composition value
 * @returns {{ query: string, numistaId: string|null, matched: boolean }}
 */
const buildNumistaSearchQuery = (nameVal, metalVal) => {
  const combined = (metalVal && !nameVal.toLowerCase().includes(metalVal.toLowerCase()))
    ? `${metalVal} ${nameVal}` : nameVal;

  // Try pattern-based lookup if feature is enabled
  if (window.NumistaLookup && window.featureFlags && featureFlags.isEnabled('NUMISTA_SEARCH_LOOKUP')) {
    const match = NumistaLookup.matchQuery(combined);
    if (match) {
      return { query: match.replacement, numistaId: match.numistaId, matched: true };
    }
  }

  // Fallback: original behavior (raw query)
  return { query: combined, numistaId: null, matched: false };
};

/**
 * Sets up item form submission and related button listeners
 */
const setupItemFormListeners = () => {
  // UNIFIED FORM SUBMISSION (Add + Edit via single #itemModal)
  debugLog("Setting up unified item form...");
  if (elements.inventoryForm) {
    safeAttachListener(
      elements.inventoryForm,
      "submit",
      async function (e) {
        e.preventDefault();

        const isEditing = editingIndex !== null;
        const existingItem = isEditing ? { ...inventory[editingIndex] } : {};

        const fields = parseItemFormFields(isEditing, existingItem);
        const error = validateItemFields(fields);
        if (error) { alert(error); return; }

        // Capture index before commit — commitItemToInventory nulls editingIndex
        const savedEditIdx = editingIndex;
        commitItemToInventory(fields, isEditing, editingIndex);

        // Save user-uploaded image if pending (STACK-32)
        // Pattern toggle: create a pattern rule instead of per-item image
        let patternRuleSaved = false;
        const patternToggle = document.getElementById('imagePatternToggle');
        if ((_pendingObverseBlob || _pendingReverseBlob) && patternToggle?.checked) {
          try {
            const rawKeywords = (document.getElementById('imagePatternKeywords')?.value || '').trim();
            if (rawKeywords) {
              // Convert keywords to regex: "morgan, peace" → "morgan|peace"
              const terms = rawKeywords.split(/[,;]/).map(t => t.trim()).filter(t => t.length > 0);
              const pattern = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
              // Pre-generate ruleId and pass as seedImageId — the image lookup
              // chain resolves via rule.seedImageId, not rule.id
              const ruleId = 'custom-img-' + Date.now();
              const result = NumistaLookup.addRule(pattern, rawKeywords, null, ruleId);
              if (result?.success && window.imageCache?.isAvailable()) {
                await window.imageCache.cachePatternImage(ruleId, _pendingObverseBlob, _pendingReverseBlob);
                debugLog(`Pattern rule created: ${result.id} (images: ${ruleId}) for "${rawKeywords}"`);
              } else {
                debugLog(`Failed to create pattern rule: ${result?.error}`, 'warn');
              }
              clearUploadState();
              patternRuleSaved = true;
            }
          } catch (err) {
            console.warn('Failed to create pattern rule from modal:', err);
            clearUploadState();
            patternRuleSaved = true; // prevent double-save on error
          }
        }
        if (!patternRuleSaved && (_pendingObverseBlob || _pendingReverseBlob || _deleteObverseOnSave || _deleteReverseOnSave)) {
          // Per-item save: save blobs against the item's UUID
          const savedItem = isEditing ? inventory[savedEditIdx] : inventory[inventory.length - 1];
          if (savedItem?.uuid) {
            try {
              const saved = await saveUserImageForItem(savedItem.uuid);
              if (!saved) {
                debugLog('Image save returned false — image may not have been stored');
              }
            } catch (err) {
              console.warn('Failed to save user image:', err);
            }
          }
        } else if (!patternRuleSaved) {
          clearUploadState();
        }

        // Clear spot lookup hidden field after commit (STACK-49)
        if (elements.itemSpotPrice) elements.itemSpotPrice.value = '';

        if (!isEditing) {
          this.reset();
          elements.itemWeightUnit.value = "oz";
          elements.itemDate.value = todayStr();
        }

        // Close modal
        try {
          if (typeof closeModalById === 'function') {
            closeModalById('itemModal');
          } else if (elements.itemModal) {
            elements.itemModal.style.display = 'none';
            document.body.style.overflow = '';
          }
        } catch (closeErr) {
          console.warn('Failed to close item modal:', closeErr);
        }

        // Update filter chips after inventory mutation
        if (typeof renderActiveFilters === 'function') {
          renderActiveFilters();
        }
      },
      "Unified item form",
    );
  } else {
    console.error("Main inventory form not found!");
  }

  // UNDO CHANGE BUTTON
  if (elements.undoChangeBtn) {
    safeAttachListener(
      elements.undoChangeBtn,
      "click",
      (e) => {
        if (e && typeof e.preventDefault === 'function') e.preventDefault();
        if (editingChangeLogIndex !== null) {
          toggleChange(editingChangeLogIndex);
          try { if (typeof closeModalById === 'function') closeModalById('itemModal'); } catch(undoErr) {}
          editingIndex = null;
          editingChangeLogIndex = null;
          renderChangeLog();
        }
      },
      "Undo change button",
    );
  }

  // ITEM MODAL CLOSE / CANCEL BUTTONS
  const closeItemModal = (e) => {
    if (e && typeof e.preventDefault === 'function') e.preventDefault();
    if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
    // Dismiss any open autocomplete dropdowns (BUG-002/003)
    if (typeof dismissAllAutocompletes === 'function') dismissAllAutocompletes();
    try { if (typeof closeModalById === 'function') closeModalById('itemModal'); } catch(closeErr) {}
    editingIndex = null;
    editingChangeLogIndex = null;
  };

  optionalListener(elements.cancelItemBtn, "click", closeItemModal, "Cancel item button");
  optionalListener(elements.itemCloseBtn, "click", closeItemModal, "Item modal close button");

  // RETAIL PRICE HISTORY LINK — opens per-item price history modal (STAK-109)
  const retailHistoryLink = document.getElementById('retailPriceHistoryLink');
  if (retailHistoryLink) {
    retailHistoryLink.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      if (editingIndex === null) return;
      const item = inventory[editingIndex];
      if (!item || !item.uuid) return;
      if (typeof openItemPriceHistoryModal === 'function') {
        openItemPriceHistoryModal(item.uuid, item.name || 'Unnamed');
      }
    });
  }

  // ITEM PRICE HISTORY MODAL — close & filter handlers (STAK-109)
  const itemPriceHistoryModal = document.getElementById('itemPriceHistoryModal');
  const itemPriceHistoryCloseBtn = document.getElementById('itemPriceHistoryCloseBtn');
  const itemPriceHistoryFilter = document.getElementById('itemPriceHistoryFilter');
  const itemPriceHistoryClearFilterBtn = document.getElementById('itemPriceHistoryClearFilterBtn');

  if (itemPriceHistoryCloseBtn) {
    itemPriceHistoryCloseBtn.addEventListener('click', () => {
      if (itemPriceHistoryModal) itemPriceHistoryModal.style.display = 'none';
    });
  }
  if (itemPriceHistoryModal) {
    itemPriceHistoryModal.addEventListener('click', (e) => {
      if (e.target === itemPriceHistoryModal) {
        itemPriceHistoryModal.style.display = 'none';
      }
    });
  }
  if (itemPriceHistoryFilter) {
    itemPriceHistoryFilter.addEventListener('input', () => {
      if (typeof window._setItemPriceModalFilter === 'function') {
        window._setItemPriceModalFilter(itemPriceHistoryFilter.value);
      }
    });
  }
  if (itemPriceHistoryClearFilterBtn) {
    itemPriceHistoryClearFilterBtn.addEventListener('click', () => {
      if (itemPriceHistoryFilter) itemPriceHistoryFilter.value = '';
      if (typeof window._setItemPriceModalFilter === 'function') {
        window._setItemPriceModalFilter('');
      }
    });
  }

  // IMAGE URL FIELDS — show when COIN_IMAGES enabled
  const imageUrlGroup = document.getElementById('imageUrlGroup');
  if (imageUrlGroup && featureFlags.isEnabled('COIN_IMAGES')) {
    imageUrlGroup.style.display = '';
  }

  // Refresh image URLs button — bypasses SW cache, fetches direct from Numista
  const refreshImageUrlsBtn = document.getElementById('refreshImageUrlsBtn');
  if (refreshImageUrlsBtn) {
    safeAttachListener(refreshImageUrlsBtn, 'click', async () => {
      const catalogId = (elements.itemCatalog?.value || '').trim();
      if (!catalogId) {
        alert('Enter a Numista # first.');
        return;
      }
      refreshImageUrlsBtn.disabled = true;
      refreshImageUrlsBtn.title = 'Fetching…';
      try {
        const config = typeof catalogConfig !== 'undefined' ? catalogConfig.getNumistaConfig() : null;
        if (!config?.apiKey) {
          alert('Numista API key not configured.');
          return;
        }
        const url = `https://api.numista.com/v3/types/${catalogId}?lang=en`;
        const resp = await fetch(url, {
          headers: { 'Numista-API-Key': config.apiKey, 'Content-Type': 'application/json' },
          cache: 'no-cache',
        });
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        const data = await resp.json();
        const obv = data.obverse_thumbnail || data.obverse?.thumbnail || '';
        const rev = data.reverse_thumbnail || data.reverse?.thumbnail || '';
        if (elements.itemObverseImageUrl) elements.itemObverseImageUrl.value = obv;
        if (elements.itemReverseImageUrl) elements.itemReverseImageUrl.value = rev;
        if (!obv && !rev) alert('No image URLs returned by Numista for this item.');
      } catch (err) {
        console.error('Image URL refresh failed:', err);
        alert('Failed to fetch image URLs: ' + err.message);
      } finally {
        refreshImageUrlsBtn.disabled = false;
        refreshImageUrlsBtn.title = 'Fetch image URLs from Numista API (bypasses cache)';
      }
    }, 'Refresh image URLs button');
  }

  // IMAGE UPLOAD BUTTONS — Obverse + Reverse (STACK-32/33)
  const imageUploadGroup = document.getElementById('imageUploadGroup');

  if (imageUploadGroup && featureFlags.isEnabled('COIN_IMAGES')) {
    imageUploadGroup.style.display = '';

    const isSecure = location.protocol === 'https:' || location.hostname === 'localhost';
    const isMobile = /Mobi|Android/i.test(navigator.userAgent);

    // Wire each side: Obv and Rev
    ['Obv', 'Rev'].forEach(suffix => {
      const side = suffix === 'Rev' ? 'reverse' : 'obverse';
      const fileInput = document.getElementById('itemImageFile' + suffix);
      const uploadBtn = document.getElementById('itemImageUploadBtn' + suffix);
      const cameraBtn = document.getElementById('itemImageCameraBtn' + suffix);
      const removeBtn = document.getElementById('itemImageRemoveBtn' + suffix);

      if (isMobile && isSecure && cameraBtn && fileInput) {
        cameraBtn.style.display = '';
        cameraBtn.addEventListener('click', () => {
          fileInput.setAttribute('capture', 'environment');
          fileInput.click();
        });
      }

      if (uploadBtn && fileInput) {
        uploadBtn.addEventListener('click', () => {
          fileInput.removeAttribute('capture');
          fileInput.click();
        });
      }

      if (fileInput) {
        fileInput.addEventListener('change', async () => {
          const file = fileInput.files?.[0];
          if (file) await processUploadedImage(file, side);
        });
      }

      if (removeBtn) {
        removeBtn.addEventListener('click', () => {
          // Clear just this side
          if (side === 'reverse') {
            _pendingReverseBlob = null;
            _deleteReverseOnSave = true;
            if (_pendingReversePreviewUrl) { URL.revokeObjectURL(_pendingReversePreviewUrl); _pendingReversePreviewUrl = null; }
          } else {
            _pendingObverseBlob = null;
            _deleteObverseOnSave = true;
            if (_pendingObversePreviewUrl) { URL.revokeObjectURL(_pendingObversePreviewUrl); _pendingObversePreviewUrl = null; }
          }
          const preview = document.getElementById('itemImagePreview' + suffix);
          const img = document.getElementById('itemImagePreviewImg' + suffix);
          const sizeInfo = document.getElementById('itemImageSizeInfo' + suffix);
          if (preview) preview.style.display = 'none';
          if (img) img.src = '';
          if (sizeInfo) sizeInfo.textContent = '';
          if (removeBtn) removeBtn.style.display = 'none';
          if (fileInput) fileInput.value = '';
        });
      }
    });

    // PATTERN TOGGLE — "Apply to all matching items" checkbox
    const patternToggleGroup = document.getElementById('imagePatternToggleGroup');
    const patternToggleCheckbox = document.getElementById('imagePatternToggle');
    const patternKeywordsGroup = document.getElementById('imagePatternKeywordsGroup');
    const patternKeywordsInput = document.getElementById('imagePatternKeywords');

    if (patternToggleGroup) {
      patternToggleGroup.style.display = '';
    }
    if (patternToggleCheckbox) {
      patternToggleCheckbox.addEventListener('change', () => {
        if (patternKeywordsGroup) {
          patternKeywordsGroup.style.display = patternToggleCheckbox.checked ? '' : 'none';
        }
        if (patternToggleCheckbox.checked && patternKeywordsInput) {
          const itemName = document.getElementById('itemName')?.value?.trim() || '';
          if (itemName && !patternKeywordsInput.value.trim()) {
            patternKeywordsInput.value = itemName;
          }
        }
      });
    }
  }

  // SEARCH NUMISTA BUTTON — lookup by N# or search by name
  if (elements.searchNumistaBtn) {
    safeAttachListener(
      elements.searchNumistaBtn,
      "click",
      async () => {
        const catalogVal = elements.itemCatalog?.value.trim() || '';
        const nameVal = elements.itemName?.value.trim() || '';

        if (!catalogVal && !nameVal) {
          alert('Enter a Name or Catalog N# to search.');
          return;
        }

        if (!catalogAPI || !catalogAPI.activeProvider) {
          alert('Configure Numista API key in Settings first.');
          return;
        }

        const btn = elements.searchNumistaBtn;
        const originalHTML = btn.innerHTML;
        btn.textContent = 'Searching...';
        btn.disabled = true;

        // Type → Numista category mapping for smarter search results
        const TYPE_TO_NUMISTA_CATEGORY = {
          'Coin': 'coin',
          'Bar': 'exonumia',
          'Round': 'exonumia',
          'Note': 'banknote',
        };

        try {
          if (catalogVal) {
            const result = await catalogAPI.lookupItem(catalogVal);
            showNumistaResults(result ? [result] : [], true, catalogVal);
          } else {
            const typeVal = elements.itemType?.value || '';
            const metalVal = elements.itemMetal?.value || '';

            const searchFilters = { limit: 20 };
            const numistaCategory = TYPE_TO_NUMISTA_CATEGORY[typeVal];
            if (numistaCategory) searchFilters.category = numistaCategory;

            const searchResult = buildNumistaSearchQuery(nameVal, metalVal);

            if (searchResult.matched) {
              // Pattern matched — build raw query for fallback results
              const rawQuery = (metalVal && !nameVal.toLowerCase().includes(metalVal.toLowerCase()))
                ? `${metalVal} ${nameVal}` : nameVal;

              // Fire all requests in parallel: direct N# + rewritten + raw fallback
              const promises = [
                searchResult.numistaId
                  ? catalogAPI.lookupItem(searchResult.numistaId).catch(() => null)
                  : Promise.resolve(null),
                catalogAPI.searchItems(searchResult.query, searchFilters),
                catalogAPI.searchItems(rawQuery, searchFilters),
              ];
              const [directResult, rewrittenResults, rawResults] = await Promise.all(promises);

              // Layer results: pinned direct → rewritten → raw fallback (deduped)
              const seen = new Set();
              const merged = [];
              const addUnique = (item) => {
                if (item && item.catalogId && !seen.has(item.catalogId)) {
                  seen.add(item.catalogId);
                  merged.push(item);
                }
              };
              if (directResult) addUnique(directResult);
              for (const r of rewrittenResults) addUnique(r);
              for (const r of rawResults) addUnique(r);

              showNumistaResults(merged, false, searchResult.query);
            } else {
              const results = await catalogAPI.searchItems(searchResult.query, searchFilters);
              showNumistaResults(results, false, searchResult.query);
            }
          }
        } catch (error) {
          console.error('Numista search error:', error);
          alert('Search failed: ' + error.message);
        } finally {
          // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
          btn.innerHTML = originalHTML;
          btn.disabled = false;
        }
      },
      "Search Numista button",
    );
  }

  // LOOKUP PCGS BUTTON — verify by Cert# or look up by PCGS#
  if (elements.lookupPcgsBtn) {
    safeAttachListener(
      elements.lookupPcgsBtn,
      "click",
      async () => {
        if (typeof lookupPcgsFromForm !== 'function') {
          alert('PCGS lookup is not available.');
          return;
        }

        const btn = elements.lookupPcgsBtn;
        const originalHTML = btn.innerHTML;
        btn.textContent = 'Looking up...';
        btn.disabled = true;

        try {
          const result = await lookupPcgsFromForm();

          if (!result.verified) {
            alert(result.error || 'PCGS lookup failed.');
            return;
          }

          // Show field picker modal instead of auto-filling
          if (typeof showPcgsFieldPicker === 'function') {
            showPcgsFieldPicker(result);
          } else {
            alert('PCGS field picker not available.');
          }
        } catch (error) {
          console.error('PCGS lookup error:', error);
          alert('PCGS lookup failed: ' + error.message);
        } finally {
          btn.innerHTML = originalHTML;
          btn.disabled = false;
        }
      },
      "Lookup PCGS button",
    );
  }

  // SPOT LOOKUP BUTTON — search historical spot prices by date (STACK-49)
  if (elements.spotLookupBtn) {
    safeAttachListener(
      elements.spotLookupBtn,
      "click",
      () => {
        if (typeof openSpotLookupModal === 'function') openSpotLookupModal();
      },
      "Spot lookup button",
    );
  }

  // DATE FIELD — enable/disable spot lookup button based on date value (STACK-49)
  if (elements.itemDate) {
    const updateSpotBtnState = () => {
      if (elements.spotLookupBtn) {
        elements.spotLookupBtn.disabled = !elements.itemDate.value;
      }
      if (elements.itemSpotPrice) elements.itemSpotPrice.value = '';
    };
    safeAttachListener(elements.itemDate, "change", updateSpotBtnState, "Date field for spot btn");
    safeAttachListener(elements.itemDate, "input", updateSpotBtnState, "Date field input for spot btn");
  }

  // METAL CHANGE — clear stale spot lookup value (STACK-49)
  if (elements.itemMetal) {
    safeAttachListener(
      elements.itemMetal,
      "change",
      () => {
        if (elements.itemSpotPrice) elements.itemSpotPrice.value = '';
      },
      "Metal change clears spot lookup",
    );
  }
};

/** Closes the notes modal and resets the notes index. */
const dismissNotesModal = () => {
  if (elements.notesModal) elements.notesModal.style.display = "none";
  notesIndex = null;
};

/**
 * Sets up notes modal, debug modal, bulk edit, changelog, and settings clear button listeners
 */
const setupNoteAndModalListeners = () => {
  // NOTES MODAL BUTTONS
  optionalListener(elements.saveNotesBtn, "click", () => {
    if (notesIndex === null) return;
    const text = elements.notesTextarea ? elements.notesTextarea.value.trim() : "";

    const oldItem = { ...inventory[notesIndex] };
    inventory[notesIndex].notes = text;
    saveInventory();
    renderTable();
    logItemChanges(oldItem, inventory[notesIndex]);
    dismissNotesModal();
  }, "Save notes button");

  optionalListener(elements.cancelNotesBtn, "click", dismissNotesModal, "Cancel notes button");
  optionalListener(elements.notesCloseBtn, "click", dismissNotesModal, "Notes modal close button");
  optionalListener(document.getElementById('notesViewCloseBtn'), "click", () => {
    if (typeof closeModalById === 'function') closeModalById('notesViewModal');
  }, "Notes view modal close button");

  optionalListener(document.getElementById('goldbackExchangeRateLink'), "click", (e) => {
    e.preventDefault();
    window.open(
      'https://www.goldback.com/exchange-rates/',
      'goldback_rates',
      'width=1250,height=800,scrollbars=yes,resizable=yes,toolbar=no,location=no,menubar=no,status=no',
    );
  }, "Goldback exchange rates link");

  optionalListener(document.getElementById('spotLookupCloseBtn'), "click", () => {
    if (typeof closeSpotLookupModal === 'function') closeSpotLookupModal();
  }, "Spot lookup modal close button");

  optionalListener(elements.debugCloseBtn, "click",
    () => { if (typeof hideDebugModal === "function") hideDebugModal(); },
    "Debug modal close button");

  // Bulk Edit modal open/close
  optionalListener(elements.bulkEditBtn, "click",
    () => { if (typeof openBulkEdit === "function") openBulkEdit(); },
    "Bulk edit open button");
  optionalListener(elements.bulkEditCloseBtn, "click",
    () => { if (typeof closeBulkEdit === "function") closeBulkEdit(); },
    "Bulk edit close button");

  optionalListener(elements.changeLogBtn, "click", (e) => {
    e.preventDefault();
    if (typeof showSettingsModal === "function") showSettingsModal("changelog");
  }, "Change log button");

  // Settings panel clear buttons (STACK-44)
  optionalListener(elements.settingsChangeLogClearBtn, "click",
    () => { if (typeof clearChangeLog === "function") clearChangeLog(); },
    "Settings change log clear button");
  optionalListener(elements.settingsSpotHistoryClearBtn, "click",
    () => { if (typeof clearSpotHistory === "function") clearSpotHistory(); },
    "Settings spot history clear button");
  optionalListener(elements.settingsCatalogHistoryClearBtn, "click",
    () => { if (typeof clearCatalogHistory === "function") clearCatalogHistory(); },
    "Settings catalog history clear button");
  optionalListener(elements.settingsPriceHistoryClearBtn, "click",
    () => { if (typeof clearItemPriceHistory === "function") clearItemPriceHistory(); },
    "Settings price history clear button");
  optionalListener(elements.settingsCloudActivityClearBtn, "click",
    () => { if (typeof clearCloudActivityLog === "function") clearCloudActivityLog(); },
    "Settings cloud activity clear button");

  // Price History filter input (STACK-44)
  optionalListener(elements.priceHistoryFilterInput, "input",
    () => { if (typeof filterItemPriceHistoryTable === "function") filterItemPriceHistoryTable(); },
    "Price history filter input");

  optionalListener(elements.backupReminder, "click", (e) => {
    e.preventDefault();
    if (typeof showSettingsModal === "function") showSettingsModal('system');
  }, "Backup reminder link");

  optionalListener(elements.storageReportLink, "click", (e) => {
    e.preventDefault();
    if (typeof showSettingsModal === "function") showSettingsModal("storage");
  }, "Storage report link");

  optionalListener(elements.changeLogCloseBtn, "click", () => {
    if (elements.changeLogModal) {
      if (window.closeModalById) closeModalById('changeLogModal');
      else {
        elements.changeLogModal.style.display = "none";
        document.body.style.overflow = "";
      }
    }
  }, "Change log close button");

  optionalListener(elements.changeLogClearBtn, "click",
    () => { if (typeof clearChangeLog === "function") clearChangeLog(); },
    "Change log clear button");

};

/**
 * Sets up spot price sync icons, range dropdowns, and inline editing
 */
const setupSpotPriceListeners = () => {
  // SPOT PRICE EVENT LISTENERS — Sparkline card redesign
  debugLog("Setting up spot price listeners...");
  Object.values(METALS).forEach((metalConfig) => {
    const metalKey = metalConfig.key;
    const metalName = metalConfig.name;

    // Sync icon button
    const syncIcon = document.getElementById(`syncIcon${metalName}`);
    if (syncIcon) {
      safeAttachListener(
        syncIcon,
        "click",
        () => {
          debugLog(`Sync icon clicked for ${metalName}`);
          if (typeof syncSpotPricesFromApi === "function") {
            syncSpotPricesFromApi(true);
          } else {
            alert(
              "API sync functionality requires Metals API configuration. Please configure an API provider first.",
            );
          }
        },
        `Sync spot price for ${metalName}`,
      );
    }

    // Range dropdown change → re-render sparkline + save preference
    const rangeSelect = document.getElementById(`spotRange${metalName}`);
    if (rangeSelect) {
      // Restore saved preference
      const saved = typeof loadTrendRanges === "function" ? loadTrendRanges() : {};
      if (saved[metalKey]) {
        rangeSelect.value = String(saved[metalKey]);
      }

      safeAttachListener(
        rangeSelect,
        "change",
        () => {
          const days = parseInt(rangeSelect.value, 10);
          if (typeof saveTrendRange === "function") saveTrendRange(metalKey, days);
          if (typeof updateSparkline === "function") updateSparkline(metalKey);
        },
        `Trend range for ${metalName}`,
      );
    }
  });

  // Shift+click capture handler for inline spot price editing
  document.addEventListener(
    "click",
    (e) => {
      if (!e.shiftKey) return;
      const valueEl = e.target.closest(".spot-card-value");
      if (!valueEl) return;

      e.preventDefault();
      e.stopPropagation();

      const card = valueEl.closest(".spot-card");
      if (!card || !card.dataset.metal) return;

      if (typeof startSpotInlineEdit === "function") {
        startSpotInlineEdit(valueEl, card.dataset.metal);
      }
    },
    true,
  );

};

/**
 * Sets up vault backup/restore listeners and password strength UI.
 */
const setupVaultListeners = () => {
  const vaultCloseBtn = document.getElementById('vaultCloseBtn');
  const vaultActionBtn = document.getElementById('vaultActionBtn');
  const vaultCancelBtn = document.getElementById('vaultCancelBtn');
  const vaultPasswordToggle = document.getElementById('vaultPasswordToggle');
  const vaultConfirmToggle = document.getElementById('vaultConfirmToggle');

  optionalListener(elements.vaultExportBtn, "click",
    () => { openVaultModal("export"); },
    "Vault export button");

  optionalListener(elements.vaultImportBtn, "click",
    () => { if (elements.vaultImportFile) elements.vaultImportFile.click(); },
    "Vault import button");

  optionalListener(elements.vaultImportFile, "change", function (e) {
    var file = e.target.files && e.target.files[0];
    if (file) {
      openVaultModal("import", file);
      e.target.value = "";
    }
  }, "Vault import file input");

  optionalListener(vaultCloseBtn, "click", () => {
    if (typeof closeVaultModal === 'function') closeVaultModal();
  }, "Vault modal close button");

  optionalListener(vaultActionBtn, "click", () => {
    if (typeof handleVaultAction === 'function') handleVaultAction();
  }, "Vault modal action button");

  optionalListener(vaultCancelBtn, "click", () => {
    if (typeof closeVaultModal === 'function') closeVaultModal();
  }, "Vault modal cancel button");

  optionalListener(vaultPasswordToggle, "click", () => {
    if (typeof toggleVaultPasswordVisibility === 'function') {
      toggleVaultPasswordVisibility('vaultPassword', vaultPasswordToggle);
    }
  }, "Vault password toggle");

  optionalListener(vaultConfirmToggle, "click", () => {
    if (typeof toggleVaultPasswordVisibility === 'function') {
      toggleVaultPasswordVisibility('vaultConfirmPassword', vaultConfirmToggle);
    }
  }, "Vault confirm password toggle");

  // Vault modal live password events
  const pw = document.getElementById("vaultPassword");
  const cpw = document.getElementById("vaultConfirmPassword");
  optionalListener(pw, "input", () => {
    updateStrengthBar(pw.value);
    if (cpw) updateMatchIndicator(pw.value, cpw.value);
  }, "Vault password input");
  optionalListener(cpw, "input", () => {
    if (pw) updateMatchIndicator(pw.value, cpw.value);
  }, "Vault confirm password input");
};

/**
 * Sets up data-destructive action listeners (remove data, boating accident).
 */
const setupDataManagementListeners = () => {
  optionalListener(elements.removeInventoryDataBtn, "click", async () => {
    const confirmed = typeof showAppConfirm === "function"
      ? await showAppConfirm("Remove all inventory items? This cannot be undone.", "Data Management")
      : confirm("Remove all inventory items? This cannot be undone.");
    if (confirmed) {
      localStorage.removeItem(LS_KEY);
      // STACK-62: Clear stale autocomplete cache so it rebuilds from fresh inventory
      if (typeof clearLookupCache === 'function') clearLookupCache();
      loadInventory();
      renderTable();
      renderActiveFilters();
      if (typeof showAppAlert === "function") await showAppAlert("Inventory data cleared.", "Data Management");
      else alert("Inventory data cleared.");
    }
  }, "Remove inventory data button");

  optionalListener(elements.boatingAccidentBtn, "click", async () => {
    const confirmed = typeof showAppConfirm === "function"
      ? await showAppConfirm("Did you really lose it all in a boating accident? This will wipe all local data.", "Data Management")
      : confirm("Did you really lose it all in a boating accident? This will wipe all local data.");
    if (confirmed) {
      // Nuclear wipe: clear every allowed localStorage key
      ALLOWED_STORAGE_KEYS.forEach((key) => {
        localStorage.removeItem(key);
      });
      sessionStorage.clear();

      // Clear IndexedDB image cache
      if (window.imageCache && typeof imageCache.clearAll === 'function') {
        imageCache.clearAll().catch(() => {});
      }

      // Reset in-memory log/history arrays
      if (typeof changeLog !== 'undefined') changeLog = [];
      if (typeof catalogHistory !== 'undefined') catalogHistory = [];
      if (typeof spotHistory !== 'undefined') spotHistory = {};

      // Disconnect cloud providers (UI reset)
      if (typeof syncCloudUI === 'function') syncCloudUI();

      loadInventory();
      renderTable();
      renderActiveFilters();
      loadSpotHistory();
      fetchSpotPrice();

      apiConfig = { provider: "", keys: {} };
      apiCache = null;
      updateSyncButtonStates();

      if (typeof showAppAlert === "function") await showAppAlert("All data has been erased. Hope your scuba gear is ready!", "Data Management");
      else alert("All data has been erased. Hope your scuba gear is ready!");
    }
  }, "Boating accident button");
};

/**
 * Sets up import/export event listeners (CSV, JSON, Numista, PDF, Vault, etc.)
 */
const setupImportExportListeners = () => {
  debugLog("Setting up import/export listeners...");

  // Import triads: Override / Merge / File-input for each format
  setupFormatImport(elements.importCsvOverride, elements.importCsvMerge, elements.importCsvFile, importCsv, "CSV");
  setupFormatImport(elements.importJsonOverride, elements.importJsonMerge, elements.importJsonFile, importJson, "JSON");
  setupFormatImport(
    document.getElementById("importNumistaBtn"),
    document.getElementById("mergeNumistaBtn"),
    elements.numistaImportFile, importNumistaCsv, "Numista CSV"
  );

  // Export buttons
  optionalListener(elements.exportCsvBtn, "click", exportCsv, "CSV export");
  optionalListener(elements.exportJsonBtn, "click", exportJson, "JSON export");
  optionalListener(elements.exportPdfBtn, "click", exportPdf, "PDF export");
  optionalListener(document.getElementById("exportZipBtn"), "click", () => {
    if (typeof createBackupZip === "function") createBackupZip();
  }, "ZIP export");

  // ZIP import
  const importZipBtn = document.getElementById("importZipBtn");
  const importZipFile = document.getElementById("importZipFile");
  if (importZipBtn && importZipFile) {
    importZipBtn.addEventListener("click", () => importZipFile.click());
    importZipFile.addEventListener("change", (e) => {
      const file = e.target.files && e.target.files[0];
      if (file && typeof restoreBackupZip === "function") {
        restoreBackupZip(file);
        importZipFile.value = "";
      }
    });
  }

  // Cloud Sync modal
  optionalListener(elements.cloudSyncBtn, "click", () => {
    if (elements.cloudSyncModal) {
      if (window.openModalById) openModalById('cloudSyncModal');
      else elements.cloudSyncModal.style.display = "flex";
    }
  }, "Cloud Sync button");
  const cloudSyncCloseBtn = document.getElementById("cloudSyncCloseBtn");
  if (cloudSyncCloseBtn && elements.cloudSyncModal) {
    safeAttachListener(cloudSyncCloseBtn, "click", () => {
      if (window.closeModalById) closeModalById('cloudSyncModal');
      else elements.cloudSyncModal.style.display = "none";
    }, "Cloud Sync close");
  }

  setupVaultListeners();
  setupDataManagementListeners();
};

// MAIN EVENT LISTENERS SETUP
// =============================================================================

/**
 * Sets up all primary event listeners for the application
 */
const setupEventListeners = () => {
  console.log(`Setting up event listeners (v${APP_VERSION})...`);

  try {
    setupSearchAndChipListeners();
    setupResponsiveColumns();
    setupHeaderButtonListeners();
    setupTableSortListeners();
    setupItemFormListeners();
    setupNoteAndModalListeners();
    setupSpotPriceListeners();
    setupImportExportListeners();

    // API MODAL EVENT LISTENERS
    debugLog("Setting up API modal listeners...");
    setupApiEvents();

    // ABOUT MODAL EVENT LISTENERS
    debugLog("Setting up about modal listeners...");
    if (typeof setupAboutModalEvents === "function") {
      setupAboutModalEvents();
    }

    debugLog("✓ All event listeners setup complete");
  } catch (error) {
    console.error("❌ Error setting up event listeners:", error);
    throw error;
  }
};

/**
 * Sets up visible-rows (portal view) event listener
 */
const setupPagination = () => {
  debugLog("Setting up visible-rows listener...");

  try {
    if (elements.itemsPerPage) {
      safeAttachListener(
        elements.itemsPerPage,
        "change",
        function () {
          const ippVal = this.value;
          itemsPerPage = ippVal === 'all' ? Infinity : parseInt(ippVal, 10);
          // Persist setting
          try { localStorage.setItem(ITEMS_PER_PAGE_KEY, ippVal); } catch (e) { /* ignore */ }
          // Sync settings modal control
          const settingsIpp = document.getElementById('settingsItemsPerPage');
          if (settingsIpp) settingsIpp.value = ippVal;
          renderTable();
        },
        "Visible rows select",
      );
    }

    debugLog("✓ Visible-rows listener setup complete");
  } catch (error) {
    console.error("❌ Error setting up visible-rows listener:", error);
  }

  // Back to top floating button
  const backToTopBtn = document.getElementById('backToTopBtn');
  if (backToTopBtn) {
    if (!window._backToTopInitialized) {
      window.addEventListener('scroll', () => {
        backToTopBtn.classList.toggle('visible', window.scrollY > 300);
      }, { passive: true });
      backToTopBtn.addEventListener('click', () => {
        window.scrollTo({ top: 0, behavior: 'smooth' });
      });
      window._backToTopInitialized = true;
    }
  }
};

/**
 * Sets up bulk edit control panel event listeners
 */
const setupBulkEditControls = () => {
  debugLog("Setting up bulk edit control listeners...");

  try {
    // Bulk toggle all edit mode
    const bulkToggleAll = document.getElementById('bulkToggleAll');
    if (bulkToggleAll) {
      safeAttachListener(
        bulkToggleAll,
        "click",
        function () {
          if (typeof window.toggleAllItemsEdit === 'function') {
            window.toggleAllItemsEdit();
          }
        },
        "Bulk toggle all edit mode",
      );
    }

    // Bulk save all changes
    const bulkSaveAll = document.getElementById('bulkSaveAll');
    if (bulkSaveAll) {
      safeAttachListener(
        bulkSaveAll,
        "click",
        function () {
          if (typeof window.saveAllEdits === 'function') {
            window.saveAllEdits();
          }
        },
        "Bulk save all changes",
      );
    }

    // Bulk cancel all changes
    const bulkCancelAll = document.getElementById('bulkCancelAll');
    if (bulkCancelAll) {
      safeAttachListener(
        bulkCancelAll,
        "click",
        function () {
          if (typeof window.cancelAllEdits === 'function') {
            window.cancelAllEdits();
          }
        },
        "Bulk cancel all changes",
      );
    }

    debugLog("✓ Bulk edit control listeners setup complete");
  } catch (error) {
    console.error("❌ Error setting up bulk edit control listeners:", error);
  }
};

/**
 * Sets up search event listeners
 */
const setupSearch = () => {
  debugLog("Setting up search listeners...");

  try {
    if (elements.searchInput) {
      const handleSearchInput = debounce(function () {
        searchQuery = this.value.replace(/[<>]/g, '').trim();
        renderTable();
      }, 300);
      safeAttachListener(
        elements.searchInput,
        "input",
        handleSearchInput,
        "Search input",
      );
    }

    if (elements.typeFilter) {
      safeAttachListener(
        elements.typeFilter,
        "change",
        function () {
          const value = this.value;
          if (value) {
            activeFilters.type = { values: [value], exclude: false };
          } else {
            delete activeFilters.type;
          }
          searchQuery = "";
          if (elements.searchInput) elements.searchInput.value = "";
          renderTable();
          renderActiveFilters();
        },
        "Type filter select",
      );
    }

    if (elements.metalFilter) {
      safeAttachListener(
        elements.metalFilter,
        "change",
        function () {
          const value = this.value;
          if (value) {
            activeFilters.metal = { values: [value], exclude: false };
          } else {
            delete activeFilters.metal;
          }
          searchQuery = "";
          if (elements.searchInput) elements.searchInput.value = "";
          renderTable();
          renderActiveFilters();
        },
        "Metal filter select",
      );
    }

    if (elements.clearBtn) {
      safeAttachListener(
        elements.clearBtn,
        "click",
        clearAllFilters,
        "Clear search button",
      );
    }

    if (elements.newItemBtn) {
      safeAttachListener(
        elements.newItemBtn,
        "click",
        () => {
          // Clear editing state (ensures add mode)
          editingIndex = null;
          editingChangeLogIndex = null;
          // Reset form and set defaults
          if (elements.inventoryForm) {
            elements.inventoryForm.reset();
            elements.itemWeightUnit.value = "oz";
            elements.itemDate.value = todayStr();
          }
          if (elements.itemSerial) elements.itemSerial.value = '';
          // Reset spot lookup state (STACK-49)
          if (elements.itemSpotPrice) elements.itemSpotPrice.value = '';
          if (elements.spotLookupBtn) elements.spotLookupBtn.disabled = !elements.itemDate.value;
          // Set modal to add mode
          if (elements.itemModalTitle) elements.itemModalTitle.textContent = "Add Inventory Item";
          if (elements.itemModalSubmit) elements.itemModalSubmit.textContent = "Add to Inventory";
          if (elements.undoChangeBtn) elements.undoChangeBtn.style.display = "none";
          // Reset purity to default (form.reset already sets select to first option)
          const purityCustom = elements.purityCustomWrapper;
          if (purityCustom) purityCustom.style.display = 'none';
          if (elements.itemPurity) elements.itemPurity.value = '';
          // Reset gb denomination picker (STACK-45)
          if (typeof toggleGbDenomPicker === 'function') toggleGbDenomPicker();
          // Hide PCGS verified icon in add mode
          const certVerifiedIcon = document.getElementById('certVerifiedIcon');
          if (certVerifiedIcon) certVerifiedIcon.style.display = 'none';
          // Hide price history link in add mode (STAK-109)
          const addRetailHistoryLink = document.getElementById('retailPriceHistoryLink');
          if (addRetailHistoryLink) addRetailHistoryLink.style.display = 'none';
          // Update currency symbols in modal (STACK-50)
          if (typeof updateModalCurrencyUI === 'function') updateModalCurrencyUI();
          // Clear image upload state for fresh add (STACK-32)
          if (typeof clearUploadState === 'function') clearUploadState();
          // Open modal
          if (elements.itemModal) {
            if (window.openModalById) openModalById('itemModal');
            else elements.itemModal.style.display = "flex";
          }
        },
        "New item button",
      );
    }

    // Chip minimum count control
    const chipMinCountEl = document.getElementById('chipMinCount');
    if (chipMinCountEl) {
      safeAttachListener(
        chipMinCountEl,
        "change",
        function() {
          localStorage.setItem('chipMinCount', this.value);
          if (typeof renderActiveFilters === "function") {
            renderActiveFilters();
          }
        },
        "Chip minimum count select",
      );
    }

    debugLog("✓ Search listeners setup complete");
  } catch (error) {
    console.error("❌ Error setting up search listeners:", error);
  }
};

/**
 * Sets up theme toggle event listeners
 */
const updateThemeButton = () => {
  const savedTheme = localStorage.getItem(THEME_KEY) || "light";

  // Apply theme classes to all theme buttons (header buttons)
  document.querySelectorAll(".theme-btn").forEach((btn) => {
    btn.classList.remove("dark", "light", "sepia");
    btn.classList.add(savedTheme);
  });

  // Update settings modal theme picker active state
  document.querySelectorAll('.theme-option').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.theme === savedTheme);
  });
};

window.updateThemeButton = updateThemeButton;

/**
 * Sets up the theme toggle logic and listeners.
 * Initializes the theme based on saved preference or system settings.
 */
const setupThemeToggle = () => {
  debugLog("Setting up theme toggle...");

  try {
    // Initialize theme with system preference detection
    if (typeof initTheme === "function") {
      initTheme();
    } else {
      const savedTheme = localStorage.getItem(THEME_KEY) || "system";
      setTheme(savedTheme);
    }

    updateThemeButton();

    // Set up system theme change listener
    if (typeof setupSystemThemeListener === "function") {
      setupSystemThemeListener();
    }

    if (window.matchMedia) {
      window
        .matchMedia("(prefers-color-scheme: dark)")
        .addEventListener("change", () => {
          // Update button if no explicit theme is set
          if (!localStorage.getItem(THEME_KEY)) {
            updateThemeButton();
          }
        });
    }

    // Theme is now controlled from the Settings modal theme picker
    debugLog("✓ Theme toggle setup complete");
  } catch (error) {
    console.error("❌ Error setting up theme toggle:", error);
  }
};

/**
 * Sets up API-related event listeners
 */
const setupApiEvents = () => {
  debugLog("Setting up API events...");

  try {
    let quotaProvider = null;
    const infoModal = document.getElementById("apiInfoModal");
    const infoCloseBtn = document.getElementById("apiInfoCloseBtn");

    if (infoModal) {
      safeAttachListener(
        infoModal,
        "click",
        (e) => {
          if (
            e.target === infoModal &&
            typeof hideProviderInfo === "function"
          ) {
            hideProviderInfo();
          }
        },
        "Provider info modal background",
      );
    }

    if (infoCloseBtn) {
      safeAttachListener(
        infoCloseBtn,
        "click",
        () => {
          if (typeof hideProviderInfo === "function") {
            hideProviderInfo();
          }
        },
        "Provider info close",
      );
    }

    document.querySelectorAll(".api-save-btn").forEach((btn) => {
      const provider = btn.getAttribute("data-provider");
      safeAttachListener(
        btn,
        "click",
        () => {
          if (typeof handleProviderSave === "function") {
            handleProviderSave(provider);
          }
        },
        "API save button",
      );
    });

    document.querySelectorAll(".api-sync-btn").forEach((btn) => {
      const provider = btn.getAttribute("data-provider");
      safeAttachListener(
        btn,
        "click",
        () => {
          if (typeof handleProviderSync === "function") {
            handleProviderSync(provider);
          }
        },
        "API sync button",
      );
    });

    document.querySelectorAll(".api-clear-btn").forEach((btn) => {
      const provider = btn.getAttribute("data-provider");
      safeAttachListener(
        btn,
        "click",
        () => {
          if (typeof clearApiKey === "function") {
            clearApiKey(provider);
          }
        },
        "API clear key button",
      );
    });

    const quotaClose = document.getElementById("apiQuotaCloseBtn");
    if (quotaClose && elements.apiQuotaModal) {
      safeAttachListener(
        quotaClose,
        "click",
        () => (elements.apiQuotaModal.style.display = "none"),
        "API quota close",
      );
    }
    const quotaSave = document.getElementById("apiQuotaSaveBtn");
    if (quotaSave && elements.apiQuotaModal) {
      safeAttachListener(
        quotaSave,
        "click",
        () => {
          const input = document.getElementById("apiQuotaInput");
          const val = parseInt(input.value, 10);
          const qp = elements.apiQuotaModal.dataset.quotaProvider || quotaProvider;
          if (!isNaN(val) && qp) {
            const cfg = loadApiConfig();
            if (!cfg.usage[qp])
              cfg.usage[qp] = { quota: val, used: 0 };
            cfg.usage[qp].quota = val;
            saveApiConfig(cfg);
            elements.apiQuotaModal.style.display = "none";
            updateProviderHistoryTables();
          }
        },
        "API quota save",
      );
    }
    const flushCacheBtn = document.getElementById("flushCacheBtn");
    if (flushCacheBtn) {
      safeAttachListener(
        flushCacheBtn,
        "click",
        () => {
          if (typeof clearApiCache === "function") {
            const warnMessage =
              "This will delete the API cache and history. Click OK to continue or Cancel to keep it.";
            if (confirm(warnMessage)) {
              clearApiCache();
            }
          }
        },
        "Flush cache button",
      );
    }

    const historyBtn = document.getElementById("apiHistoryBtn");
    if (historyBtn) {
      safeAttachListener(
        historyBtn,
        "click",
        () => {
          if (typeof showApiHistoryModal === "function") {
            showApiHistoryModal();
          }
        },
        "API history button",
      );
    }

    const catalogHistoryBtn = document.getElementById("catalogHistoryBtn");
    if (catalogHistoryBtn) {
      safeAttachListener(
        catalogHistoryBtn,
        "click",
        () => {
          if (typeof showCatalogHistoryModal === "function") {
            showCatalogHistoryModal();
          }
        },
        "Catalog history button",
      );
    }

    const syncAllBtn = document.getElementById("syncAllBtn");
    if (syncAllBtn) {
      safeAttachListener(
        syncAllBtn,
        "click",
        async () => {
          if (typeof syncProviderChain === "function") {
            const { updatedCount, results } = await syncProviderChain({ showProgress: true, forceSync: true });
            const summary = Object.entries(results)
              .filter(([_, status]) => status !== "skipped")
              .map(([prov, status]) => `${API_PROVIDERS[prov]?.name || prov}: ${status}`)
              .join("\n");
            alert(`Synced ${updatedCount} prices.\n\n${summary}`);
          }
        },
        "Sync all providers button",
      );
    }

    const historyModal = document.getElementById("apiHistoryModal");
    const historyCloseBtn = document.getElementById("apiHistoryCloseBtn");
    if (historyModal) {
      safeAttachListener(
        historyModal,
        "click",
        (e) => {
          if (e.target === historyModal && typeof hideApiHistoryModal === "function") {
            hideApiHistoryModal();
          }
        },
        "API history modal background",
      );
    }
    if (historyCloseBtn) {
      safeAttachListener(
        historyCloseBtn,
        "click",
        () => {
          if (typeof hideApiHistoryModal === "function") {
            hideApiHistoryModal();
          }
        },
        "API history close button",
      );
    }
    const catalogHistoryModal = document.getElementById("catalogHistoryModal");
    const catalogHistoryCloseBtn = document.getElementById("catalogHistoryCloseBtn");
    if (catalogHistoryModal) {
      safeAttachListener(
        catalogHistoryModal,
        "click",
        (e) => {
          if (e.target === catalogHistoryModal && typeof hideCatalogHistoryModal === "function") {
            hideCatalogHistoryModal();
          }
        },
        "Catalog history modal background",
      );
    }
    if (catalogHistoryCloseBtn) {
      safeAttachListener(
        catalogHistoryCloseBtn,
        "click",
        () => {
          if (typeof hideCatalogHistoryModal === "function") {
            hideCatalogHistoryModal();
          }
        },
        "Catalog history close button",
      );
    }

    // ESC key to close modals (sub-modals first, then settings, then others)
    safeAttachListener(
      document,
      "keydown",
      (e) => {
        if (e.key === "Escape") {
          const infoModal = document.getElementById("apiInfoModal");
          const historyModal = document.getElementById("apiHistoryModal");
          const catalogHistModal = document.getElementById("catalogHistoryModal");
          const quotaModal = document.getElementById("apiQuotaModal");
          const bulkEditModal = document.getElementById("bulkEditModal");
          const settingsModal = document.getElementById("settingsModal");
          const itemModal = document.getElementById("itemModal");
          const notesModal = document.getElementById("notesModal");
          const detailsModal = document.getElementById("detailsModal");
          const changeLogModal = document.getElementById("changeLogModal");
          // Close sub-modals (stacking overlays) before settings modal
          if (
            infoModal &&
            infoModal.style.display === "flex" &&
            typeof hideProviderInfo === "function"
          ) {
            hideProviderInfo();
          } else if (
            historyModal &&
            historyModal.style.display === "flex" &&
            typeof hideApiHistoryModal === "function"
          ) {
            hideApiHistoryModal();
          } else if (
            catalogHistModal &&
            catalogHistModal.style.display === "flex" &&
            typeof hideCatalogHistoryModal === "function"
          ) {
            hideCatalogHistoryModal();
          } else if (
            quotaModal &&
            quotaModal.style.display === "flex"
          ) {
            quotaModal.style.display = "none";
          } else if (
            bulkEditModal &&
            bulkEditModal.style.display !== "none" &&
            typeof closeBulkEdit === "function"
          ) {
            closeBulkEdit();
          } else if (
            settingsModal &&
            settingsModal.style.display === "flex" &&
            typeof hideSettingsModal === "function"
          ) {
            hideSettingsModal();
          } else if (
            document.getElementById("spotLookupModal")?.style.display === "flex" &&
            typeof closeSpotLookupModal === "function"
          ) {
            closeSpotLookupModal();
          } else if (itemModal && itemModal.style.display === "flex") {
            itemModal.style.display = "none";
            document.body.style.overflow = "";
            editingIndex = null;
            editingChangeLogIndex = null;
          } else if (notesModal && notesModal.style.display === "flex") {
            notesModal.style.display = "none";
            notesIndex = null;
          } else if (changeLogModal && changeLogModal.style.display === "flex") {
            changeLogModal.style.display = "none";
            document.body.style.overflow = "";
          } else if (
            detailsModal &&
            detailsModal.style.display === "flex" &&
            typeof closeDetailsModal === "function"
          ) {
            closeDetailsModal();
          }
        }
      },
      "ESC key modal close",
    );

    debugLog("✓ API events setup complete");
  } catch (error) {
    console.error("❌ Error setting up API events:", error);
  }
};

// =============================================================================

// Early cleanup of stray localStorage entries before application initialization
document.addEventListener('DOMContentLoaded', cleanupStorage);