Source: priceHistory.js

// ITEM PRICE HISTORY (STACK-43)
// =============================================================================
// Silent per-item price history recording.
// Mirrors the spotHistory pattern in spot.js: save/load/record/purge with dedup.
// Data structure: { "uuid": [{ ts, retail, spot, melt }, ...], ... }
// =============================================================================

/**
 * Saves item price history to localStorage.
 */
const saveItemPriceHistory = () => {
  try {
    saveDataSync(ITEM_PRICE_HISTORY_KEY, itemPriceHistory);
  } catch (error) {
    console.error('Error saving item price history:', error);
  }
};

/**
 * Loads item price history from localStorage into the global state variable.
 */
const loadItemPriceHistory = () => {
  try {
    const data = loadDataSync(ITEM_PRICE_HISTORY_KEY, {});
    itemPriceHistory = (data && typeof data === 'object' && !Array.isArray(data)) ? data : {};
  } catch (error) {
    console.error('Error loading item price history:', error);
    itemPriceHistory = {};
  }
};

/**
 * Removes item price history entries older than the specified number of days.
 * Not called automatically — available for future settings UI.
 *
 * @param {number} days - Number of days to retain (default: 365)
 */
const purgeItemPriceHistory = (days = 365) => {
  const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
  let changed = false;

  for (const uuid of Object.keys(itemPriceHistory)) {
    const before = itemPriceHistory[uuid].length;
    itemPriceHistory[uuid] = itemPriceHistory[uuid].filter(e => e.ts >= cutoff);
    if (itemPriceHistory[uuid].length === 0) {
      delete itemPriceHistory[uuid];
      changed = true;
    } else if (itemPriceHistory[uuid].length !== before) {
      changed = true;
    }
  }

  if (changed) saveItemPriceHistory();
};

/**
 * Records a single price data point for an inventory item.
 * Applies deduplication rules:
 *   - add/edit/bulk: skip exact duplicates only (same retail + spot + melt)
 *   - spot-sync: 24h throttle if retail unchanged; beyond 24h, require >1% delta
 *
 * Does NOT save to localStorage — caller must batch saves.
 *
 * @param {Object} item - Inventory item (must have uuid, metal, weight, qty, purity, marketValue)
 * @param {string} trigger - Recording trigger: 'add' | 'edit' | 'bulk' | 'spot-sync'
 * @returns {boolean} True if a data point was recorded
 */
const recordItemPrice = (item, trigger = 'spot-sync') => {
  if (!item || !item.uuid) return false;

  const uuid = item.uuid;
  const metalKey = (item.metal || 'Silver').toLowerCase();
  const spot = spotPrices[metalKey] || 0;
  const melt = parseFloat(computeMeltValue(item, spot).toFixed(2));

  // Retail hierarchy: (1) Goldback denomination price, (2) manual marketValue, (3) 0
  // Must match the 3-tier lookup used by table renderer, CSV/PDF export, and details modal
  const gbDenomPrice = (typeof getGoldbackRetailPrice === 'function') ? getGoldbackRetailPrice(item) : null;
  const rawMarket = (item.marketValue && item.marketValue > 0) ? parseFloat(item.marketValue) : 0;
  const retail = gbDenomPrice ? parseFloat(gbDenomPrice.toFixed(2)) : rawMarket;
  const now = Date.now();

  // Initialize array for this UUID if needed
  if (!itemPriceHistory[uuid]) {
    itemPriceHistory[uuid] = [];
  }

  const entries = itemPriceHistory[uuid];
  const last = entries.length > 0 ? entries[entries.length - 1] : null;

  // Dedup for spot-sync trigger (aggressive throttling)
  if (last && trigger === 'spot-sync') {
    const timeSinceLast = now - last.ts;
    const within24h = timeSinceLast < 24 * 60 * 60 * 1000;
    const retailUnchanged = last.retail === retail;

    if (retailUnchanged) {
      // Hard 24h throttle: never record more than once per 24h if retail unchanged
      if (within24h) return false;

      // Beyond 24h: only record if spot or melt changed meaningfully (> 1%)
      const meltDelta = last.melt > 0 ? Math.abs(melt - last.melt) / last.melt : 0;
      const spotDelta = last.spot > 0 ? Math.abs(spot - last.spot) / last.spot : 0;
      if (meltDelta <= 0.01 && spotDelta <= 0.01) return false;
    }
    // If retail changed, always record (falls through)
  }

  // Dedup for add/edit/bulk: skip exact duplicates only
  if (last && trigger !== 'spot-sync') {
    if (last.retail === retail &&
        last.spot === spot &&
        Math.abs(last.melt - melt) < 0.005) {
      return false;
    }
  }

  entries.push({
    ts: now,
    retail: retail,
    spot: spot,
    melt: melt
  });

  return true;
};

/**
 * Records a price data point for a single item and saves immediately.
 * Used after item add, edit, or inline edit.
 *
 * @param {Object} item - Inventory item
 * @param {string} trigger - 'add' | 'edit' | 'bulk'
 */
const recordSingleItemPrice = (item, trigger = 'edit') => {
  if (recordItemPrice(item, trigger)) {
    saveItemPriceHistory();
  }
};

/**
 * Snapshots all inventory items after a spot price change.
 * Applies spot-sync dedup rules (24h throttle, 1% delta).
 * Called after API sync, manual spot update, or spot reset.
 */
const recordAllItemPriceSnapshots = () => {
  if (!inventory || inventory.length === 0) return;

  let anyRecorded = false;
  for (const item of inventory) {
    if (recordItemPrice(item, 'spot-sync')) {
      anyRecorded = true;
    }
  }

  if (anyRecorded) {
    saveItemPriceHistory();
  }
};

/**
 * Merges imported item price history with existing data.
 * Union by UUID + timestamp — deduplicates, sorts ascending.
 * Used during ZIP restore (unlike spot history which does full replace).
 *
 * @param {Object} importedHistory - Object keyed by UUID with arrays of entries
 */
const mergeItemPriceHistory = (importedHistory) => {
  if (!importedHistory || typeof importedHistory !== 'object') return;

  for (const [uuid, entries] of Object.entries(importedHistory)) {
    if (!Array.isArray(entries)) continue;

    if (!itemPriceHistory[uuid]) {
      // New UUID — copy as-is
      itemPriceHistory[uuid] = entries;
    } else {
      // Merge: combine, deduplicate by timestamp, sort ascending
      const combined = [...itemPriceHistory[uuid], ...entries];
      const seen = new Set();
      const deduped = [];
      for (const entry of combined) {
        if (!seen.has(entry.ts)) {
          seen.add(entry.ts);
          deduped.push(entry);
        }
      }
      deduped.sort((a, b) => a.ts - b.ts);
      itemPriceHistory[uuid] = deduped;
    }
  }

  saveItemPriceHistory();
};

/**
 * Removes history for UUIDs not present in the current inventory.
 * Not called automatically — available for storage optimization.
 *
 * @returns {number} Number of orphaned UUIDs removed
 */
const cleanOrphanedItemPriceHistory = () => {
  const activeUuids = new Set(inventory.map(i => i.uuid).filter(Boolean));
  let removed = 0;

  for (const uuid of Object.keys(itemPriceHistory)) {
    if (!activeUuids.has(uuid)) {
      delete itemPriceHistory[uuid];
      removed++;
    }
  }

  if (removed > 0) saveItemPriceHistory();
  return removed;
};

// =============================================================================
// ITEM PRICE HISTORY — SETTINGS LOG TABLE
// =============================================================================

/** @type {string} Filter text for the settings price history table */
let priceHistoryFilterText = '';
/** @type {string} Sort column for settings price history table */
let settingsPriceSortColumn = '';
/** @type {boolean} Sort ascending for settings price history table */
let settingsPriceSortAsc = true;

/** Column keys for the price history table, matching <th> order. */
const PRICE_HISTORY_COLS = ['ts', 'name', 'retail', 'spot', 'melt'];

/**
 * Flattens, filters, and sorts item price history into renderable rows.
 * @param {string} [filterUuid] - If provided, only return rows for this UUID
 * @returns {Array<Object>} Sorted row objects (includes uuid for delete support)
 */
const preparePriceHistoryRows = (filterUuid) => {
  loadItemPriceHistory();

  const rows = [];
  const uuids = filterUuid ? [[filterUuid, itemPriceHistory[filterUuid] || []]] : Object.entries(itemPriceHistory);
  for (const [uuid, entries] of uuids) {
    const item = inventory.find(i => i.uuid === uuid);
    const name = item ? (item.name || 'Unnamed') : `(deleted: ${uuid.slice(0, 8)})`;
    for (const e of entries) {
      rows.push({ ts: e.ts, name, uuid, retail: e.retail, spot: e.spot, melt: e.melt });
    }
  }

  let data = rows;
  if (priceHistoryFilterText) {
    const f = priceHistoryFilterText.toLowerCase();
    data = data.filter(r => r.name.toLowerCase().includes(f));
  }

  if (settingsPriceSortColumn) {
    data.sort((a, b) => {
      const valA = a[settingsPriceSortColumn];
      const valB = b[settingsPriceSortColumn];
      if (valA < valB) return settingsPriceSortAsc ? -1 : 1;
      if (valA > valB) return settingsPriceSortAsc ? 1 : -1;
      return 0;
    });
  } else {
    data.sort((a, b) => b.ts - a.ts);
  }

  return data;
};

/**
 * Attaches click-to-sort handlers to price history table headers.
 * @param {HTMLTableElement} table
 */
const attachPriceHistorySortHeaders = (table) => {
  table.querySelectorAll('th').forEach(th => {
    const idx = Array.from(th.parentNode.children).indexOf(th);
    const col = PRICE_HISTORY_COLS[idx];
    if (!col) return; // Skip non-data columns (e.g. Actions)
    th.style.cursor = 'pointer';
    th.onclick = () => {
      if (settingsPriceSortColumn === col) {
        settingsPriceSortAsc = !settingsPriceSortAsc;
      } else {
        settingsPriceSortColumn = col;
        settingsPriceSortAsc = true;
      }
      renderItemPriceHistoryTable();
    };
  });
};

/**
 * Renders the item price history table in the Settings > Activity Log > Price History sub-tab.
 */
const renderItemPriceHistoryTable = () => {
  const table = document.getElementById('settingsPriceHistoryTable');
  if (!table) return;

  const data = preparePriceHistoryRows();
  const tbody = table.querySelector('tbody');
  if (!tbody) return;

  if (data.length === 0) {
    const msg = priceHistoryFilterText ? 'No items match the current filter.' : 'No item price history recorded yet.';
    // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
    tbody.innerHTML = `<tr class="settings-log-empty"><td colspan="6">${msg}</td></tr>`;
    return;
  }

  const fmt = (v) => typeof formatCurrency === 'function' ? formatCurrency(v) : `$${Number(v).toFixed(2)}`;
  const htmlRows = data.map(r => {
    const ts = typeof formatTimestamp === 'function' ? formatTimestamp(r.ts) : new Date(r.ts).toLocaleString();
    const deleteBtn = `<button class="price-history-delete-btn" title="Delete entry" onclick="event.stopPropagation(); deleteItemPriceEntry('${escapeHtml(r.uuid)}', ${r.ts})">&times;</button>`;
    return `<tr><td>${ts}</td><td>${escapeHtml(r.name)}</td><td>${fmt(r.retail)}</td><td>${fmt(r.spot)}</td><td>${fmt(r.melt)}</td><td class="action-cell">${deleteBtn}</td></tr>`;
  });

  // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
  tbody.innerHTML = htmlRows.join('');
  attachPriceHistorySortHeaders(table);
};

/**
 * Reads the filter input value and re-renders the price history table.
 */
const filterItemPriceHistoryTable = () => {
  const input = document.getElementById('priceHistoryFilterInput');
  priceHistoryFilterText = input ? input.value : '';
  renderItemPriceHistoryTable();
};

/**
 * Clears all item price history after user confirmation.
 */
const clearItemPriceHistory = () => {
  if (!confirm('Clear all item price history? This cannot be undone.')) return;
  itemPriceHistory = {};
  saveItemPriceHistory();
  const panel = document.getElementById('logPanel_pricehistory');
  if (panel) delete panel.dataset.rendered;
  renderItemPriceHistoryTable();
};

// =============================================================================
// ITEM PRICE HISTORY — PER-ITEM MODAL (STAK-109)
// =============================================================================

/** @type {string} UUID of the item currently displayed in the per-item modal */
let _itemPriceModalUuid = '';
/** @type {string} Filter text for the per-item modal table */
let _itemPriceModalFilterText = '';

/**
 * Opens the per-item price history modal for a specific inventory item.
 * @param {string} uuid - Item UUID
 * @param {string} itemName - Item name for the modal title
 */
const openItemPriceHistoryModal = (uuid, itemName) => {
  _itemPriceModalUuid = uuid;
  _itemPriceModalFilterText = '';

  const titleEl = document.getElementById('itemPriceHistoryTitle');
  if (titleEl) titleEl.textContent = `Price History — ${itemName}`;

  const filterInput = document.getElementById('itemPriceHistoryFilter');
  if (filterInput) filterInput.value = '';

  renderItemPriceHistoryModalTable();

  const modal = document.getElementById('itemPriceHistoryModal');
  if (modal) modal.style.display = 'flex';
};

/**
 * Renders the per-item price history modal table.
 * Shows entries only for _itemPriceModalUuid, newest first.
 */
const renderItemPriceHistoryModalTable = () => {
  const table = document.getElementById('itemPriceHistoryTable');
  if (!table) return;

  const tbody = table.querySelector('tbody');
  if (!tbody) return;

  const data = preparePriceHistoryRows(_itemPriceModalUuid);

  // Apply modal-specific filter
  let filtered = data;
  if (_itemPriceModalFilterText) {
    const f = _itemPriceModalFilterText.toLowerCase();
    filtered = data.filter(r => {
      const ts = typeof formatTimestamp === 'function' ? formatTimestamp(r.ts) : new Date(r.ts).toLocaleString();
      const fmt = (v) => typeof formatCurrency === 'function' ? formatCurrency(v) : `$${Number(v).toFixed(2)}`;
      return ts.toLowerCase().includes(f) || fmt(r.retail).includes(f) || fmt(r.spot).includes(f) || fmt(r.melt).includes(f);
    });
  }

  if (filtered.length === 0) {
    const msg = _itemPriceModalFilterText ? 'No entries match the filter.' : 'No price history for this item.';
    // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
    tbody.innerHTML = `<tr class="settings-log-empty"><td colspan="5">${msg}</td></tr>`;
    return;
  }

  const fmt = (v) => typeof formatCurrency === 'function' ? formatCurrency(v) : `$${Number(v).toFixed(2)}`;
  const htmlRows = filtered.map(r => {
    const ts = typeof formatTimestamp === 'function' ? formatTimestamp(r.ts) : new Date(r.ts).toLocaleString();
    const deleteBtn = `<button class="price-history-delete-btn" title="Delete entry" onclick="event.stopPropagation(); deleteItemPriceEntry('${escapeHtml(r.uuid)}', ${r.ts})">&times;</button>`;
    return `<tr><td>${ts}</td><td>${fmt(r.retail)}</td><td>${fmt(r.spot)}</td><td>${fmt(r.melt)}</td><td class="action-cell">${deleteBtn}</td></tr>`;
  });

  // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
  tbody.innerHTML = htmlRows.join('');
};

/**
 * Deletes a single price history entry by UUID and timestamp.
 * Logs the deletion to the change log for undo/redo support.
 * @param {string} uuid - Item UUID
 * @param {number} timestamp - Entry timestamp to delete
 */
const deleteItemPriceEntry = (uuid, timestamp) => {
  if (!itemPriceHistory[uuid]) return;

  const idx = itemPriceHistory[uuid].findIndex(e => e.ts === timestamp);
  if (idx === -1) return;

  const deletedEntry = itemPriceHistory[uuid][idx];
  itemPriceHistory[uuid].splice(idx, 1);

  // Clean up empty arrays
  if (itemPriceHistory[uuid].length === 0) {
    delete itemPriceHistory[uuid];
  }

  saveItemPriceHistory();

  // Log to change log for undo support
  const item = inventory.find(i => i.uuid === uuid);
  const itemName = item ? (item.name || 'Unnamed') : `(deleted: ${uuid.slice(0, 8)})`;
  if (typeof logChange === 'function') {
    logChange(itemName, 'priceHistoryDelete',
      JSON.stringify({ uuid, entry: deletedEntry }), null, -1);
    if (typeof renderChangeLog === 'function') renderChangeLog();
  }

  // Re-render both tables
  renderItemPriceHistoryModalTable();
  renderItemPriceHistoryTable();
};

// Ensure global availability
window.renderItemPriceHistoryTable = renderItemPriceHistoryTable;
window.filterItemPriceHistoryTable = filterItemPriceHistoryTable;
window.clearItemPriceHistory = clearItemPriceHistory;
window.openItemPriceHistoryModal = openItemPriceHistoryModal;
window.renderItemPriceHistoryModalTable = renderItemPriceHistoryModalTable;
window.deleteItemPriceEntry = deleteItemPriceEntry;
window._setItemPriceModalFilter = (val) => {
  _itemPriceModalFilterText = val;
  renderItemPriceHistoryModalTable();
};