Source: constants.js

// CONFIGURATION & GLOBAL CONSTANTS
/**
 * API Provider configurations for metals pricing services
 *
 * Each provider configuration contains:
 * @property {string} name - Display name for the provider
 * @property {string} baseUrl - Base API endpoint URL
 * @property {Object} endpoints - API endpoints for different metals
 * @property {function} parseResponse - Function to parse API response into standard format
 * @property {string} documentation - URL to provider's API documentation
 * @property {boolean} batchSupported - Whether provider supports batch requests
 * @property {string} batchEndpoint - Batch request endpoint pattern
 * @property {function} parseBatchResponse - Function to parse batch API response
 */
const API_PROVIDERS = {
  STAKTRAKR: {
    name: "StakTrakr",
    baseUrl: "https://api.staktrakr.com/data",
    requiresKey: false,
    documentation: "https://www.staktrakr.com",
    hourlyBaseUrl: "https://api.staktrakr.com/data/hourly",
    endpoints: { silver: "", gold: "", platinum: "", palladium: "" },
    getEndpoint: () => "",
    parseResponse: () => null,
    parseBatchResponse: (data) => {
      const current = {};
      if (Array.isArray(data)) {
        data.forEach(e => {
          if (e.metal && e.spot > 0) current[e.metal.toLowerCase()] = e.spot;
        });
      }
      return { current, history: {} };
    },
    batchSupported: false,
    maxHistoryDays: 365,
    symbolsPerRequest: "all",
  },
  METALS_DEV: {
    name: "Metals.dev",
    requiresKey: true,
    baseUrl: "https://api.metals.dev/v1",
    endpoints: {
      silver: "/metal/spot?api_key={API_KEY}&metal=silver&currency=USD",
      gold: "/metal/spot?api_key={API_KEY}&metal=gold&currency=USD",
      platinum: "/metal/spot?api_key={API_KEY}&metal=platinum&currency=USD",
      palladium: "/metal/spot?api_key={API_KEY}&metal=palladium&currency=USD",
    },
    latestBatchEndpoint: "/latest?api_key={API_KEY}&currency=USD&unit=toz",
    parseResponse: (data) => data.rate?.price || null,
    parseLatestBatchResponse: (data) => {
      const current = {};
      if (data?.metals) {
        Object.entries(data.metals).forEach(([metal, price]) => {
          if (typeof price === "number" && price > 0) {
            current[metal.toLowerCase()] = price;
          }
        });
      }
      return current;
    },
    documentation: "https://www.metals.dev/docs",
    maxHistoryDays: 30,
    symbolsPerRequest: "all",
    docUrl: "https://metals.dev/documentation",
    batchSupported: true,
    batchEndpoint: "/timeseries?api_key={API_KEY}&start_date={START_DATE}&end_date={END_DATE}",
    parseBatchResponse: (data) => {
      // Metals.dev /timeseries response:
      // { status, currency, unit, rates: { "YYYY-MM-DD": { metals: { gold: N, silver: N, ... } } } }
      const current = {};
      const history = {};
      if (data?.rates && typeof data.rates === "object") {
        const dates = Object.keys(data.rates).sort();
        dates.forEach((dateStr) => {
          const entry = data.rates[dateStr];
          const metals = entry.metals || entry;
          Object.entries(metals).forEach(([metal, price]) => {
            if (typeof price !== "number" || price <= 0) return;
            const key = metal.toLowerCase();
            if (!history[key]) history[key] = [];
            history[key].push({
              timestamp: `${dateStr} 00:00:00`,
              price,
            });
            // Latest date becomes current price
            current[key] = price;
          });
        });
      }
      return { current, history };
    },
  },
  METALS_API: {
    name: "Metals-API.com",
    requiresKey: true,
    baseUrl: "https://metals-api.com/api",
    endpoints: {
      silver: "/latest?access_key={API_KEY}&base=USD&symbols=XAG",
      gold: "/latest?access_key={API_KEY}&base=USD&symbols=XAU",
      platinum: "/latest?access_key={API_KEY}&base=USD&symbols=XPT",
      palladium: "/latest?access_key={API_KEY}&base=USD&symbols=XPD",
    },
    parseResponse: (data, metal) => {
      // Expected format: { "success": true, "rates": { "XAG": 0.04 } }
      const metalCode =
        metal === "silver"
          ? "XAG"
          : metal === "gold"
            ? "XAU"
            : metal === "platinum"
              ? "XPT"
              : "XPD";
      const rate = data.rates?.[metalCode];
      return rate ? 1 / rate : null; // Convert from metal per USD to USD per ounce
    },
    documentation: "https://metals-api.com/documentation",
    maxHistoryDays: 30,
    symbolsPerRequest: 1,
    docUrl: "https://metals-api.com/documentation",
    batchSupported: true,
    batchEndpoint: "/timeseries?access_key={API_KEY}&start_date={START_DATE}&end_date={END_DATE}&base=USD&symbols={SYMBOLS}",
    parseBatchResponse: (data) => {
      const current = {};
      const history = {};
      const symbolMap = {
        XAG: "silver",
        XAU: "gold",
        XPT: "platinum",
        XPD: "palladium",
      };
      if (data.rates) {
        const firstKey = Object.keys(data.rates)[0];
        if (
          firstKey &&
          typeof data.rates[firstKey] === "object" &&
          /^\d{4}-\d{2}-\d{2}$/.test(firstKey)
        ) {
          // Timeseries format: { date: { symbol: rate } }
          Object.entries(data.rates).forEach(([date, symbols]) => {
            Object.entries(symbols).forEach(([symbol, rate]) => {
              const metal = symbolMap[symbol];
              if (metal && rate) {
                const price = 1 / rate;
                if (!history[metal]) history[metal] = [];
                history[metal].push({
                  timestamp: `${date} 00:00:00`,
                  price,
                });
                current[metal] = price;
              }
            });
          });
        } else {
          Object.entries(data.rates).forEach(([symbol, rate]) => {
            const metal = symbolMap[symbol];
            if (metal && rate) current[metal] = 1 / rate;
          });
        }
      }
      return { current, history };
    },
  },
  METAL_PRICE_API: {
    name: "MetalPriceAPI.com",
    requiresKey: true,
    baseUrl: "https://api.metalpriceapi.com/v1",
    endpoints: {
      silver: "/latest?api_key={API_KEY}&base=USD&currencies=XAG",
      gold: "/latest?api_key={API_KEY}&base=USD&currencies=XAU",
      platinum: "/latest?api_key={API_KEY}&base=USD&currencies=XPT",
      palladium: "/latest?api_key={API_KEY}&base=USD&currencies=XPD",
    },
    parseResponse: (data, metal) => {
      // Expected format: { "success": true, "rates": { "XAG": 0.04 } }
      const metalCode =
        metal === "silver"
          ? "XAG"
          : metal === "gold"
            ? "XAU"
            : metal === "platinum"
              ? "XPT"
              : "XPD";
      const rate = data.rates?.[metalCode];
      return rate ? 1 / rate : null; // Convert from metal per USD to USD per ounce
    },
    documentation: "https://metalpriceapi.com/documentation",
    maxHistoryDays: 365,
    maxHourlyDays: 7,
    symbolsPerRequest: "all",
    docUrl: "https://metalpriceapi.com/documentation",
    batchSupported: true,
    batchEndpoint: "/timeframe?api_key={API_KEY}&start_date={START_DATE}&end_date={END_DATE}&base=USD&currencies={CURRENCIES}",
    hourlyEndpoint: "/hourly?api_key={API_KEY}&base=USD&currency={CURRENCY}&start_date={START_DATE}&end_date={END_DATE}",
    parseBatchResponse: (data) => {
      const current = {};
      const history = {};
      const symbolMap = {
        XAG: "silver",
        XAU: "gold",
        XPT: "platinum",
        XPD: "palladium",
      };
      if (data.rates) {
        const firstKey = Object.keys(data.rates)[0];
        if (
          firstKey &&
          typeof data.rates[firstKey] === "object" &&
          /^\d{4}-\d{2}-\d{2}$/.test(firstKey)
        ) {
          Object.entries(data.rates).forEach(([date, symbols]) => {
            Object.entries(symbols).forEach(([symbol, rate]) => {
              const metal = symbolMap[symbol];
              if (metal && rate) {
                const price = 1 / rate;
                if (!history[metal]) history[metal] = [];
                history[metal].push({
                  timestamp: `${date} 00:00:00`,
                  price,
                });
                current[metal] = price;
              }
            });
          });
        } else {
          Object.entries(data.rates).forEach(([symbol, rate]) => {
            const metal = symbolMap[symbol];
            if (metal && rate) current[metal] = 1 / rate;
          });
        }
      }
      return { current, history };
    },
  },
  CUSTOM: {
    name: "Custom Provider",
    requiresKey: true,
    baseUrl: "",
    endpoints: {
      silver: "",
      gold: "",
      platinum: "",
      palladium: "",
    },
    parseResponse: (data) => {
      if (typeof data === "number") return data;
      if (data.price) return data.price;
      if (data.rate && typeof data.rate === "number") return data.rate;
      return null;
    },
    documentation: "",
    custom: true,
    batchSupported: false, // Custom providers will use individual requests by default
    batchEndpoint: "",
    parseBatchResponse: (data) => {
      // Custom batch parsing would depend on the provider's API format
      return { current: {}, history: {} };
    },
  },
};

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

/**
 * Cert verification lookup URLs for grading authorities.
 * {certNumber} is replaced with the actual certification number.
 * URLs without {certNumber} open the generic verification page.
 *
 * @constant {Object.<string, string>}
 */
const CERT_LOOKUP_URLS = {
  PCGS: 'https://www.pcgs.com/cert/{certNumber}',
  NGC: 'https://www.ngccoin.com/certlookup/{certNumber}/?CertNum={certNumber}&Grade={grade}&lookup=',
  ANACS: 'https://anacs.com/verify/',
  ICG: 'https://www.icgcoin.com/verification/',
};

/**
 * @constant {string} APP_VERSION - Application version
 * Follows BRANCH.RELEASE.PATCH.state format
 * State codes: a=alpha, b=beta, rc=release candidate
 * Example: 3.03.02a → branch 3, release 03, patch 02, alpha
 * Updated: 2026-02-12 - STACK-38/STACK-31: Responsive card view + mobile layout
 */

const APP_VERSION = "3.31.5";

/**
 * Numista metadata cache TTL: 30 days in milliseconds.
 * Used by viewModal.js to skip re-fetching recently cached metadata.
 * @constant {number}
 */
const VIEW_METADATA_TTL = 30 * 24 * 60 * 60 * 1000;

/**
 * @constant {string} DEFAULT_CURRENCY - Default currency code for monetary formatting
 */
const DEFAULT_CURRENCY = "USD";

/**
 * Supported display currencies for the currency selector (STACK-50)
 * @constant {Array<{code: string, name: string}>}
 */
const SUPPORTED_CURRENCIES = [
  { code: "USD", name: "US Dollar" },
  { code: "EUR", name: "Euro" },
  { code: "GBP", name: "British Pound" },
  { code: "CAD", name: "Canadian Dollar" },
  { code: "AUD", name: "Australian Dollar" },
  { code: "CHF", name: "Swiss Franc" },
  { code: "JPY", name: "Japanese Yen" },
  { code: "CNY", name: "Chinese Yuan" },
  { code: "INR", name: "Indian Rupee" },
  { code: "MXN", name: "Mexican Peso" },
  { code: "SEK", name: "Swedish Krona" },
  { code: "NOK", name: "Norwegian Krone" },
  { code: "NZD", name: "New Zealand Dollar" },
  { code: "SGD", name: "Singapore Dollar" },
  { code: "HKD", name: "Hong Kong Dollar" },
  { code: "ZAR", name: "South African Rand" },
  { code: "RUB", name: "Russian Ruble" },
];

/** @constant {string} DISPLAY_CURRENCY_KEY - LocalStorage key for display currency preference (STACK-50) */
const DISPLAY_CURRENCY_KEY = "displayCurrency";

/** @constant {string} EXCHANGE_RATES_KEY - LocalStorage key for cached exchange rates (STACK-50) */
const EXCHANGE_RATES_KEY = "exchangeRates";

/** @constant {string} EXCHANGE_RATE_API_URL - Free exchange rate API (no key required) */
const EXCHANGE_RATE_API_URL = "https://open.er-api.com/v6/latest/USD";

/**
 * Fallback exchange rates (USD-based) for offline/file:// use (STACK-50)
 * Updated Feb 2026. Used only when API fetch fails and no cached rates exist.
 * @constant {Object<string, number>}
 */
const FALLBACK_EXCHANGE_RATES = {
  USD: 1,
  EUR: 0.92,
  GBP: 0.79,
  CAD: 1.36,
  AUD: 1.53,
  CHF: 0.89,
  JPY: 149.5,
  CNY: 7.24,
  INR: 83.1,
  MXN: 17.15,
  SEK: 10.42,
  NOK: 10.55,
  NZD: 1.64,
  SGD: 1.34,
  HKD: 7.82,
  ZAR: 18.65,
  RUB: 92.5,
};

/**
 * Returns formatted version string
 *
 * @param {string} [prefix="v"] - Prefix to add before version
 * @returns {string} Formatted version string (e.g., "v3.03.07b")
 */
const getVersionString = (prefix = "v") => `${prefix}${APP_VERSION}`;

/**
 * Template replacement functions for documentation
 * Used by build process to replace {{TEMPLATE}} variables
 */
const getTemplateVariables = () => ({
  VERSION: APP_VERSION,
  VERSION_WITH_V: `v${APP_VERSION}`,
  VERSION_TITLE: `StakTrakr v${APP_VERSION}`,
  VERSION_BRANCH: APP_VERSION.split('.').slice(0, 2).join('.') + '.x',
  BRANDING_NAME: BRANDING_TITLE
});

/**
 * Replaces template variables in text
 * @param {string} text - Text containing {{VARIABLE}} placeholders
 * @returns {string} Text with variables replaced
 */
const replaceTemplateVariables = (text) => {
  const variables = getTemplateVariables();
  return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
    return variables[key] || match;
  });
};

/** Maximum upload size in bytes for local imports (2MB) */
const MAX_LOCAL_FILE_SIZE = 2 * 1024 * 1024;

/** Flag indicating whether cloud backup is enabled */
let cloudBackupEnabled = false;

/**
 * Inserts formatted version string into a target element
 *
 * @param {string} elementId - ID of the element to update
 * @param {string} [prefix="v"] - Prefix to add before version
 */
const injectVersionString = (elementId, prefix = "v") => {
  const el = document.getElementById(elementId);
  if (el) {
    el.textContent = getVersionString(prefix);
  }
};

/** @constant {string} BRANDING_TITLE - Optional custom application title */
const BRANDING_TITLE = "StakTrakr";

/**
 * Domain-based branding configuration
 *
 * @property {Object.<string,string>} domainMap - Map of domain keywords to
 * custom display names. Keys are compared in lowercase and may omit the
 * domain extension when `removeExtension` is true.
 * @property {boolean} removeExtension - When true, strips the domain
 * extension (e.g. ".com") before lookup
 * @property {boolean} alwaysOverride - When true, domain mapping overrides
 * `BRANDING_TITLE` across the entire application
 */
const BRANDING_DOMAIN_OPTIONS = {
  domainMap: {
    staktrakr: "StakTrakr",
    stackrtrackr: "StackrTrackr",
    stackertrackr: "Stacker Tracker",
  },
  /** Logo split: [silverPart, goldPart, viewBoxWidth] for the inline SVG tspan elements */
  logoSplit: {
    "StakTrakr": ["Stak", "Trakr", 480],
    "StackrTrackr": ["Stackr", "Trackr", 560],
    "Stacker Tracker": ["Stacker ", "Tracker", 680],
  },
  removeExtension: true,
  alwaysOverride: false,
};

/**
 * Title detected from the current domain or null if no mapping found
 *
 * Falls back to `BRANDING_TITLE` when running under file:// or no domain is
 * detected.
 * @constant {string|null}
 */
const BRANDING_DOMAIN_OVERRIDE =
  typeof window !== "undefined" && window.location && window.location.hostname
    ? (() => {
        let host = window.location.hostname.toLowerCase();
        if (BRANDING_DOMAIN_OPTIONS.removeExtension) {
          const parts = host.split(".");
          // Strip www prefix and TLD to get the core domain name
          // "www.stackrtrackr.com" → ["www","stackrtrackr","com"] → "stackrtrackr"
          // "stackrtrackr.com" → ["stackrtrackr","com"] → "stackrtrackr"
          // "staktrakr.pages.dev" → ["staktrakr","pages","dev"] → "staktrakr"
          const filtered = parts.filter(p => p !== "www");
          host = filtered.length > 1 ? filtered[0] : parts[0];
        }
        return BRANDING_DOMAIN_OPTIONS.domainMap[host] || null;
      })()
    : null;

/** @constant {string} LS_KEY - LocalStorage key for inventory data */
const LS_KEY = "metalInventory";

/** @constant {string} SERIAL_KEY - LocalStorage key for inventory serial counter */
const SERIAL_KEY = "inventorySerial";

/** @constant {string} CATALOG_MAP_KEY - LocalStorage key for S#/N# associations */
const CATALOG_MAP_KEY = "catalogMap";

/** @constant {string} SPOT_HISTORY_KEY - LocalStorage key for spot price history */
const SPOT_HISTORY_KEY = "metalSpotHistory";

/** @constant {string} ITEM_PRICE_HISTORY_KEY - LocalStorage key for per-item price history (STACK-43) */
const ITEM_PRICE_HISTORY_KEY = "item-price-history";

/** @constant {string} GOLDBACK_PRICES_KEY - LocalStorage key for Goldback denomination prices (STACK-45) */
const GOLDBACK_PRICES_KEY = "goldback-prices";

/** @constant {string} GOLDBACK_PRICE_HISTORY_KEY - LocalStorage key for Goldback price history (STACK-45) */
const GOLDBACK_PRICE_HISTORY_KEY = "goldback-price-history";

/** @constant {string} GOLDBACK_ENABLED_KEY - LocalStorage key for Goldback pricing toggle (STACK-45) */
const GOLDBACK_ENABLED_KEY = "goldback-enabled";

/** @constant {string} GOLDBACK_ESTIMATE_ENABLED_KEY - LocalStorage key for Goldback estimation toggle (STACK-52) */
const GOLDBACK_ESTIMATE_ENABLED_KEY = "goldback-estimate-enabled";

/** @constant {number} GB_ESTIMATE_PREMIUM - Default estimation premium multiplier (1.0 = no premium, pure 2x spot) */
const GB_ESTIMATE_PREMIUM = 1.0;

/** @constant {string} GB_ESTIMATE_MODIFIER_KEY - LocalStorage key for user-configurable premium modifier */
const GB_ESTIMATE_MODIFIER_KEY = "goldback-estimate-modifier";

/** @constant {number} GB_TO_OZT - Conversion factor: 1 Goldback = 0.001 troy oz 24K gold */
const GB_TO_OZT = 0.001;

/**
 * Standard Goldback denominations with gold content.
 * weight = denomination value (used as item.weight when weightUnit='gb')
 * goldOz = troy oz of 24K gold per note
 * @constant {Array<{weight: number, label: string, goldOz: number}>}
 */
const GOLDBACK_DENOMINATIONS = [
  { weight: 0.5,  label: '½ Goldback',   goldOz: 0.0005 },
  { weight: 1,    label: '1 Goldback',   goldOz: 0.001 },
  { weight: 2,    label: '2 Goldback',   goldOz: 0.002 },
  { weight: 5,    label: '5 Goldback',   goldOz: 0.005 },
  { weight: 10,   label: '10 Goldback',  goldOz: 0.01 },
  { weight: 25,   label: '25 Goldback',  goldOz: 0.025 },
  { weight: 50,   label: '50 Goldback',  goldOz: 0.05 },
  { weight: 100,  label: '100 Goldback', goldOz: 0.1 },
];

/** @constant {string} ITEM_TAGS_KEY - LocalStorage key for item tags mapping (STAK-126) */
const ITEM_TAGS_KEY = "itemTags";

/** @constant {number} MAX_TAGS_PER_ITEM - Maximum number of tags allowed per item (STAK-126) */
const MAX_TAGS_PER_ITEM = 20;

/** @constant {number} MAX_TAG_LENGTH - Maximum characters per tag name (STAK-126) */
const MAX_TAG_LENGTH = 50;

/** @constant {string} CATALOG_HISTORY_KEY - LocalStorage key for catalog API call history */
const CATALOG_HISTORY_KEY = "staktrakr.catalog.history";

/** @constant {string} THEME_KEY - LocalStorage key for theme preference */
const THEME_KEY = "appTheme";

/** @constant {string} ACK_DISMISSED_KEY - LocalStorage key for acknowledgment dismissal */
const ACK_DISMISSED_KEY = "ackDismissed";

/** @constant {string} API_KEY_STORAGE_KEY - LocalStorage key for API provider information */
const API_KEY_STORAGE_KEY = "metalApiConfig";

/** @constant {string} API_CACHE_KEY - LocalStorage key for cached API data */
const API_CACHE_KEY = "metalApiCache";

/** @constant {string} LAST_CACHE_REFRESH_KEY - LocalStorage key for last cache refresh timestamp */
const LAST_CACHE_REFRESH_KEY = "lastCacheRefresh";

/** @constant {string} LAST_API_SYNC_KEY - LocalStorage key for last API sync timestamp */
const LAST_API_SYNC_KEY = "lastApiSync";

/** @constant {string} APP_VERSION_KEY - LocalStorage key for current app version */
const APP_VERSION_KEY = "currentAppVersion";

/** @constant {string} VERSION_ACK_KEY - LocalStorage key for acknowledged app version */
const VERSION_ACK_KEY = "ackVersion";

/** @constant {string} LAST_VERSION_CHECK_KEY - LocalStorage key for last remote version check timestamp (STACK-67) */
const LAST_VERSION_CHECK_KEY = "lastVersionCheck";

/** @constant {string} LATEST_REMOTE_VERSION_KEY - LocalStorage key for cached latest remote version (STACK-67) */
const LATEST_REMOTE_VERSION_KEY = "latestRemoteVersion";

/** @constant {string} LATEST_REMOTE_URL_KEY - LocalStorage key for cached latest remote release URL (STACK-67) */
const LATEST_REMOTE_URL_KEY = "latestRemoteUrl";

/** @constant {string} VERSION_CHECK_URL - Remote endpoint for latest version info (STACK-67) */
const VERSION_CHECK_URL = "https://www.staktrakr.com/version.json";

/** @constant {number} VERSION_CHECK_TTL - Cache TTL for remote version check in ms (24 hours) */
const VERSION_CHECK_TTL = 24 * 60 * 60 * 1000;

/** @constant {string} FEATURE_FLAGS_KEY - LocalStorage key for feature flags */
const FEATURE_FLAGS_KEY = "featureFlags";

/** @constant {string} SPOT_TREND_RANGE_KEY - LocalStorage key for sparkline trend range preferences */
const SPOT_TREND_RANGE_KEY = "spotTrendRange";

/** @constant {string} SPOT_COMPARE_MODE_KEY - LocalStorage key for 24h % comparison mode (STACK-92) */
const SPOT_COMPARE_MODE_KEY = "spotCompareMode";

/** @constant {string} TIMEZONE_KEY - LocalStorage key for display timezone preference (STACK-63) */
const TIMEZONE_KEY = "appTimeZone";

/** @constant {string} ITEMS_PER_PAGE_KEY - LocalStorage key for items per page setting */
const ITEMS_PER_PAGE_KEY = "settingsItemsPerPage";

/** @constant {string} CARD_STYLE_KEY - LocalStorage key for card view style (A/B/C/D) (STAK-118) */
const CARD_STYLE_KEY = "cardViewStyle";

/** @constant {string} DESKTOP_CARD_VIEW_KEY - LocalStorage key for desktop card view toggle (STAK-118) */
const DESKTOP_CARD_VIEW_KEY = "desktopCardView";

/** @constant {string} DEFAULT_SORT_COL_KEY - LocalStorage key for default inventory sort column */
const DEFAULT_SORT_COL_KEY = "defaultSortColumn";

/** @constant {string} DEFAULT_SORT_DIR_KEY - LocalStorage key for default inventory sort direction */
const DEFAULT_SORT_DIR_KEY = "defaultSortDir";

/** @constant {string} METAL_ORDER_KEY - LocalStorage key for metal order/visibility config */
const METAL_ORDER_KEY = "metalOrderConfig";

/** @constant {string} SPOT_TREND_KEY - LocalStorage key for persisted spot trend period */
const SPOT_TREND_KEY = "spotTrendPeriod";

/** @constant {string} HEADER_TREND_BTN_KEY - LocalStorage key for header trend button visibility */
const HEADER_TREND_BTN_KEY = "headerTrendBtnVisible";

/** @constant {string} HEADER_SYNC_BTN_KEY - LocalStorage key for header sync button visibility */
const HEADER_SYNC_BTN_KEY = "headerSyncBtnVisible";

// =============================================================================
// IMAGE PROCESSOR DEFAULTS (STACK-95)
// =============================================================================

/** @constant {number} IMAGE_MAX_DIM - Max image dimension in px for resize */
const IMAGE_MAX_DIM = 500;

/** @constant {number} IMAGE_QUALITY - Default compression quality (0-1) */
const IMAGE_QUALITY = 0.75;

/** @constant {number} IMAGE_MAX_BYTES - Max output size per image side in bytes (500 KB) */
const IMAGE_MAX_BYTES = 512000;

/**
 * List of recognized localStorage keys for cleanup validation
 * @constant {string[]}
 */
const VAULT_FILE_EXTENSION = '.stvault';

// =============================================================================
// CLOUD AUTO-SYNC CONSTANTS (STAK-149)
// =============================================================================

/** Poll interval for remote change detection (10 minutes in ms) */
const SYNC_POLL_INTERVAL = 600000;

/** Debounce delay before pushing after a saveInventory() call (2 seconds) */
const SYNC_PUSH_DEBOUNCE = 2000;

/** Dropbox path for the rolling sync vault file */
const SYNC_FILE_PATH = '/StakTrakr/staktrakr-sync.stvault';

/** Dropbox path for the lightweight sync metadata pointer */
const SYNC_META_PATH = '/StakTrakr/staktrakr-sync.json';

/**
 * Keys included in a sync vault (excludes API keys, tokens, spot history).
 * Only inventory data + display preferences that are meaningful across devices.
 */
const SYNC_SCOPE_KEYS = [
  'metalInventory',   // LS_KEY — inventory items
  'itemTags',         // ITEM_TAGS_KEY — per-item tags
  'displayCurrency',  // DISPLAY_CURRENCY_KEY — active display currency
  'appTheme',         // THEME_KEY — light/dark/sepia/system theme
  'inlineChipConfig', // inline chip config (grade, year, etc.)
  'filterChipCategoryConfig', // filter chip category config
  'viewModalSectionConfig',   // view modal section visibility
  'chipMinCount',     // minimum count for filter chips
];

const ALLOWED_STORAGE_KEYS = [
  LS_KEY,
  SERIAL_KEY,
  CATALOG_MAP_KEY,
  SPOT_HISTORY_KEY,
  ITEM_PRICE_HISTORY_KEY,
  THEME_KEY,
  ACK_DISMISSED_KEY,
  API_KEY_STORAGE_KEY,
  API_CACHE_KEY,
  LAST_CACHE_REFRESH_KEY,
  LAST_API_SYNC_KEY,
  APP_VERSION_KEY,
  VERSION_ACK_KEY,
  FEATURE_FLAGS_KEY,
  "spotSilver",
  "spotGold",
  "spotPlatinum",
  "spotPalladium",
  "chipMinCount",
  "changeLog",
  "autocomplete_lookup_cache",
  "autocomplete_cache_timestamp",
  "staktrakr.debug",
  "stackrtrackr.debug",
  "catalog_api_config",
  "staktrakr.catalog.cache",
  "staktrakr.catalog.settings",
  CATALOG_HISTORY_KEY,
  SPOT_TREND_RANGE_KEY,
  SPOT_COMPARE_MODE_KEY,
  ITEMS_PER_PAGE_KEY,
  "chipCustomGroups",
  "chipBlacklist",
  "inlineChipConfig",
  "apiProviderOrder",
  "providerPriority",
  "filterChipCategoryConfig",
  "chipSortOrder",
  GOLDBACK_PRICES_KEY,
  GOLDBACK_PRICE_HISTORY_KEY,
  GOLDBACK_ENABLED_KEY,
  GOLDBACK_ESTIMATE_ENABLED_KEY,
  GB_ESTIMATE_MODIFIER_KEY,
  DISPLAY_CURRENCY_KEY,
  EXCHANGE_RATES_KEY,
  "headerThemeBtnVisible",    // boolean string: "true"/"false" (STACK-54)
  "headerCurrencyBtnVisible", // boolean string: "true"/"false" (STACK-54)
  SPOT_TREND_KEY,             // string: trend period ("1"|"7"|"30"|"90"|"365"|"1095")
  HEADER_TREND_BTN_KEY,       // boolean string: "true"/"false" — header trend button visibility
  HEADER_SYNC_BTN_KEY,        // boolean string: "true"/"false" — header sync button visibility
  "layoutVisibility",         // JSON object: { spotPrices, totals, search, table } (STACK-54) — legacy, migrated to layoutSectionConfig
  "layoutSectionConfig",      // JSON array: ordered section config [{ id, label, enabled }] (STACK-54)
  LAST_VERSION_CHECK_KEY,     // timestamp: last remote version check (STACK-67)
  LATEST_REMOTE_VERSION_KEY,  // string: cached latest remote version (STACK-67)
  LATEST_REMOTE_URL_KEY,      // string: cached latest remote release URL (STACK-67)
  "ff_migration_fuzzy_autocomplete", // one-time migration flag (v3.26.01)
  "migration_hourlySource",          // one-time migration flag: re-tag StakTrakr hourly entries
  "numistaLookupRules",              // custom Numista search lookup rules (JSON array)
  "numistaViewFields",               // view modal Numista field visibility config (JSON object)
  TIMEZONE_KEY,                        // string: "auto" | "UTC" | IANA zone (STACK-63)
  "viewModalSectionConfig",            // JSON array: ordered view modal section config [{ id, label, enabled }]
  "numistaOverridePersonal",           // boolean string: "true"/"false" — Numista API overrides user pattern images
  "tableImagesEnabled",                // boolean string: "true"/"false" — show thumbnail images in table rows
  "tableImageSides",                   // string: "both"|"obverse"|"reverse" — which sides to show in table (STAK-118)
  CARD_STYLE_KEY,                        // string: "A"|"B"|"C" — card view style (STAK-118)
  DESKTOP_CARD_VIEW_KEY,                 // boolean string: "true"/"false" — desktop card view (STAK-118)
  DEFAULT_SORT_COL_KEY,                  // number string: default sort column index
  DEFAULT_SORT_DIR_KEY,                  // string: "asc"|"desc" — default sort direction
  METAL_ORDER_KEY,                       // JSON array: metal order/visibility config
  ITEM_TAGS_KEY,                           // JSON object: per-item tags keyed by UUID (STAK-126)
  "enabledSeedRules",                        // JSON array: enabled built-in Numista lookup rule IDs
  "seedImagesVer",                             // string: current seed images version for cache invalidation
  "cloud_token_dropbox",                       // JSON: Dropbox OAuth token data
  "cloud_token_pcloud",                        // JSON: pCloud OAuth token data
  "cloud_token_box",                           // JSON: Box OAuth token data
  "cloud_last_backup",                         // JSON: { provider, timestamp } last cloud backup info
  "cloud_kraken_seen",                         // boolean string: easter egg flag
  "staktrakr_oauth_result",                    // JSON: transient OAuth callback relay (cleared after read)
  "cloud_activity_log",                        // JSON: cloud sync activity log entries
  // STAK-149: Auto-sync keys
  "cloud_sync_enabled",                        // boolean string: "true"/"false" — master auto-sync toggle
  "cloud_sync_last_push",                      // JSON: { syncId, timestamp, rev, itemCount } — last push from this device
  "cloud_sync_last_pull",                      // JSON: { syncId, timestamp, rev } — last pull on this device
  "cloud_sync_device_id",                      // UUID string: stable per-device identifier
  "cloud_sync_cursor",                         // Dropbox rev string: for efficient change detection
  "cloud_sync_override_backup",                // JSON: { timestamp, itemCount, appVersion, data: {...} } — pre-pull local snapshot
];

// =============================================================================
// INLINE CHIP CONFIG — controls which chips appear in the Name cell and order
// =============================================================================

/**
 * Default inline chip configuration. Order determines display order.
 * @constant {Array<{id: string, label: string, enabled: boolean}>}
 */
const INLINE_CHIP_DEFAULTS = [
  { id: 'grade',   label: 'Grade',           enabled: true },
  { id: 'numista', label: 'Numista (N#)',     enabled: true },
  { id: 'pcgs',    label: 'PCGS #',          enabled: false },
  { id: 'year',    label: 'Year',            enabled: true },
  { id: 'serial',  label: 'Serial #',         enabled: false },
  { id: 'storage', label: 'Storage Location', enabled: false },
  { id: 'notes',   label: 'Notes Indicator',  enabled: false },
  { id: 'purity',  label: 'Purity',           enabled: false },
  { id: 'tags',    label: 'Tags',             enabled: false },
];

/**
 * Loads the inline chip config from localStorage, merging with defaults
 * so new chip types added in future versions appear automatically.
 * @returns {Array<{id: string, label: string, enabled: boolean}>}
 */
const getInlineChipConfig = () => {
  try {
    const raw = localStorage.getItem('inlineChipConfig');
    if (raw) {
      const saved = JSON.parse(raw);
      // Build a map of saved chips for quick lookup
      const savedMap = new Map(saved.map(c => [c.id, c]));
      // Start with saved order, preserving user's arrangement
      const merged = saved.filter(c => INLINE_CHIP_DEFAULTS.some(d => d.id === c.id));
      // Append any new defaults not in saved config
      for (const def of INLINE_CHIP_DEFAULTS) {
        if (!savedMap.has(def.id)) {
          merged.push({ ...def });
        }
      }
      return merged;
    }
  } catch (e) {
    console.warn('Failed to load inline chip config:', e);
  }
  return INLINE_CHIP_DEFAULTS.map(d => ({ ...d }));
};

/**
 * Saves the inline chip config to localStorage.
 * @param {Array<{id: string, label: string, enabled: boolean}>} config
 */
const saveInlineChipConfig = (config) => {
  try {
    localStorage.setItem('inlineChipConfig', JSON.stringify(config));
  } catch (e) {
    console.warn('Failed to save inline chip config:', e);
  }
};

// =============================================================================
// FILTER CHIP CATEGORY CONFIG — controls which chip categories appear and order
// =============================================================================

/**
 * Default filter chip category configuration. Order determines display order.
 * @constant {Array<{id: string, label: string, enabled: boolean}>}
 */
const FILTER_CHIP_CATEGORY_DEFAULTS = [
  { id: 'metal',            label: 'Metals',            enabled: true, group: null },
  { id: 'type',             label: 'Types',             enabled: true, group: null },
  { id: 'name',             label: 'Names',             enabled: true, group: null },
  { id: 'customGroup',      label: 'Custom Groups',     enabled: true, group: null },
  { id: 'dynamicName',      label: 'Dynamic Names',     enabled: true, group: null },
  { id: 'purchaseLocation', label: 'Purchase Location', enabled: true, group: null },
  { id: 'storageLocation',  label: 'Storage Location',  enabled: true, group: null },
  { id: 'year',             label: 'Years',             enabled: true, group: null },
  { id: 'grade',            label: 'Grades',            enabled: true, group: null },
  { id: 'numistaId',        label: 'Numista IDs',       enabled: true, group: null },
  { id: 'purity',           label: 'Purity',            enabled: true, group: null },
  { id: 'tags',             label: 'Tags',              enabled: true, group: null },
];

/**
 * Loads the filter chip category config from localStorage, merging with defaults
 * so new categories added in future versions appear automatically.
 * @returns {Array<{id: string, label: string, enabled: boolean}>}
 */
const getFilterChipCategoryConfig = () => {
  try {
    const raw = localStorage.getItem('filterChipCategoryConfig');
    if (raw) {
      const saved = JSON.parse(raw);
      const savedMap = new Map(saved.map(c => [c.id, c]));
      // Start with saved order, preserving user's arrangement
      const merged = saved.filter(c => FILTER_CHIP_CATEGORY_DEFAULTS.some(d => d.id === c.id));
      // Append any new defaults not in saved config
      for (const def of FILTER_CHIP_CATEGORY_DEFAULTS) {
        if (!savedMap.has(def.id)) {
          merged.push({ ...def });
        }
      }
      return merged;
    }
  } catch (e) {
    console.warn('Failed to load filter chip category config:', e);
  }
  return FILTER_CHIP_CATEGORY_DEFAULTS.map(d => ({ ...d }));
};

/**
 * Saves the filter chip category config to localStorage.
 * @param {Array<{id: string, label: string, enabled: boolean}>} config
 */
const saveFilterChipCategoryConfig = (config) => {
  try {
    localStorage.setItem('filterChipCategoryConfig', JSON.stringify(config));
  } catch (e) {
    console.warn('Failed to save filter chip category config:', e);
  }
};

// =============================================================================
// LAYOUT SECTION CONFIG — controls visibility and order of major page sections
// =============================================================================

/**
 * Default layout section configuration. Order determines display order.
 * @constant {Array<{id: string, label: string, enabled: boolean}>}
 */
const LAYOUT_SECTION_DEFAULTS = [
  { id: 'spotPrices', label: 'Spot price cards',    enabled: true },
  { id: 'totals',     label: 'Summary totals',      enabled: true },
  { id: 'search',     label: 'Search & filter bar', enabled: true },
  { id: 'table',      label: 'Inventory table',     enabled: true },
];

/**
 * Migrates old layoutVisibility object to new ordered array format.
 * @param {Object} obj - Old format: { spotPrices: bool, totals: bool, ... }
 * @returns {Array<{id: string, label: string, enabled: boolean}>}
 */
const migrateLayoutVisibility = (obj) => {
  const config = LAYOUT_SECTION_DEFAULTS.map(d => ({
    ...d,
    enabled: obj[d.id] !== undefined ? !!obj[d.id] : d.enabled,
  }));
  try {
    localStorage.setItem('layoutSectionConfig', JSON.stringify(config));
    localStorage.removeItem('layoutVisibility');
  } catch (e) {
    console.warn('Failed to migrate layout visibility:', e);
  }
  return config;
};

/**
 * Generic loader for section config arrays from localStorage.
 * Merges saved user config with defaults so new sections are auto-appended.
 * @param {string} key - localStorage key
 * @param {Array<{id: string, label: string, enabled: boolean}>} defaults
 * @param {string} [legacyKey] - Optional legacy key to migrate from
 * @param {function} [migrateFn] - Migration function for legacy data
 * @returns {Array<{id: string, label: string, enabled: boolean}>}
 */
const _loadSectionConfig = (key, defaults, legacyKey, migrateFn) => {
  try {
    const raw = localStorage.getItem(key);
    if (raw) {
      const saved = JSON.parse(raw);
      const savedMap = new Map(saved.map(c => [c.id, c]));
      const merged = saved.filter(c => defaults.some(d => d.id === c.id));
      for (const def of defaults) {
        if (!savedMap.has(def.id)) merged.push({ ...def });
      }
      return merged;
    }
    if (legacyKey && migrateFn) {
      const legacy = localStorage.getItem(legacyKey);
      if (legacy) return migrateFn(JSON.parse(legacy));
    }
  } catch (e) {
    console.warn(`Failed to load ${key}:`, e);
  }
  return defaults.map(d => ({ ...d }));
};

/**
 * Generic saver for section config arrays to localStorage.
 * @param {string} key - localStorage key
 * @param {Array<{id: string, label: string, enabled: boolean}>} config
 */
const _saveSectionConfig = (key, config) => {
  try {
    localStorage.setItem(key, JSON.stringify(config));
  } catch (e) {
    console.warn(`Failed to save ${key}:`, e);
  }
};

/** Loads the layout section config, with migration from old layoutVisibility format. */
const getLayoutSectionConfig = () =>
  _loadSectionConfig('layoutSectionConfig', LAYOUT_SECTION_DEFAULTS, 'layoutVisibility', migrateLayoutVisibility);

/** Saves the layout section config to localStorage. */
const saveLayoutSectionConfig = (config) =>
  _saveSectionConfig('layoutSectionConfig', config);

// =============================================================================
// VIEW MODAL SECTION CONFIG — controls order/visibility of view modal sections
// =============================================================================

/**
 * Default view modal section configuration. Order determines display order.
 * @constant {Array<{id: string, label: string, enabled: boolean}>}
 */
const VIEW_MODAL_SECTION_DEFAULTS = [
  { id: 'images',       label: 'Coin images',       enabled: true },
  { id: 'valuation',    label: 'Valuation',          enabled: true },
  { id: 'priceHistory', label: 'Price history',      enabled: true },
  { id: 'inventory',    label: 'Inventory details',  enabled: true },
  { id: 'grading',      label: 'Grading',            enabled: true },
  { id: 'numista',      label: 'Numista data',       enabled: true },
  { id: 'notes',        label: 'Notes',              enabled: true },
  { id: 'tags',         label: 'Tags',               enabled: true },
];

/** Loads the view modal section config from localStorage, merged with defaults. */
const getViewModalSectionConfig = () =>
  _loadSectionConfig('viewModalSectionConfig', VIEW_MODAL_SECTION_DEFAULTS);

/** Saves the view modal section config to localStorage. */
const saveViewModalSectionConfig = (config) =>
  _saveSectionConfig('viewModalSectionConfig', config);

// =============================================================================
// NUMISTA VIEW FIELD CONFIG — controls which fields appear in view modal
// =============================================================================

/**
 * Default Numista view field visibility. All enabled by default.
 * @constant {Object<string, boolean>}
 */
const NUMISTA_VIEW_FIELD_DEFAULTS = {
  denomination: true,
  shape: true,
  diameter: true,
  thickness: true,
  orientation: true,
  composition: true,
  country: true,
  technique: true,
  references: true,
  edge: true,
  tags: true,
  commemorative: true,
  rarity: true,
  mintage: true,
  imageTooltips: true,
};

/**
 * Load Numista view field config from localStorage, merged with defaults.
 * @returns {Object<string, boolean>}
 */
const getNumistaViewFieldConfig = () => {
  try {
    const raw = localStorage.getItem('numistaViewFields');
    if (raw) {
      const saved = JSON.parse(raw);
      return { ...NUMISTA_VIEW_FIELD_DEFAULTS, ...saved };
    }
  } catch (e) {
    console.warn('Failed to load Numista view field config:', e);
  }
  return { ...NUMISTA_VIEW_FIELD_DEFAULTS };
};

/**
 * Save Numista view field config to localStorage.
 * @param {Object<string, boolean>} config
 */
const saveNumistaViewFieldConfig = (config) => {
  try {
    localStorage.setItem('numistaViewFields', JSON.stringify(config));
  } catch (e) {
    console.warn('Failed to save Numista view field config:', e);
  }
};

// Persist current application version for comparison on future loads
try {
  localStorage.setItem(APP_VERSION_KEY, APP_VERSION);
} catch (e) {
  console.warn("Unable to store app version", e);
}

/**
 * @constant {number} DEFAULT_API_CACHE_DURATION - Default cache duration in milliseconds (24 hours)
 */
const DEFAULT_API_CACHE_DURATION = 24 * 60 * 60 * 1000;

/** @constant {number} DEFAULT_API_QUOTA - Default monthly API call quota */
const DEFAULT_API_QUOTA = 100;

/** @constant {boolean} DEV_MODE - Enables verbose debug logging when true */
const DEV_MODE = false;

/**
 * Global debug flag, can be toggled via DEV_MODE or `?debug` query parameter
 * @constant {boolean}
 */
let DEBUG = DEV_MODE;

if (typeof window !== "undefined") {
  const params = new URLSearchParams(window.location.search);
  if (params.has("debug")) {
    const value = params.get("debug");
    DEBUG = value === null || value === "" || value === "1" || value === "true";
  }
}

// =============================================================================
// FEATURE FLAGS SYSTEM
// =============================================================================

/**
 * Feature flags configuration
 * Controls experimental features and gradual rollouts
 * 
 * Each feature flag contains:
 * @property {boolean} enabled - Default enabled state
 * @property {boolean} urlOverride - Allow URL parameter override
 * @property {boolean} userToggle - Allow user preference toggle
 * @property {string} description - Human-readable description
 * @property {string} phase - Development phase (dev/testing/beta/stable)
 */
const FEATURE_FLAGS = {
  FUZZY_AUTOCOMPLETE: {
    enabled: true,
    urlOverride: true,
    userToggle: true,
    description: "Fuzzy search autocomplete for item names and locations",
    phase: "stable"
  },
  DEBUG_UI: {
    enabled: false,
    urlOverride: true,
    userToggle: false,
    description: "Debug UI indicators and development tools",
    phase: "dev"
  },
  GROUPED_NAME_CHIPS: {
    enabled: true,
    urlOverride: true,
    userToggle: true,
    description: "Group item names by base name (e.g., 'American Silver Eagle (3)' instead of separate year chips)",
    phase: "beta"
  },
  DYNAMIC_NAME_CHIPS: {
    enabled: false,
    urlOverride: true,
    userToggle: true,
    description: "Auto-extract text from parentheses and quotes in item names as additional filter chips",
    phase: "beta"
  },
  CHIP_QTY_BADGE: {
    enabled: true,
    urlOverride: true,
    userToggle: true,
    description: "Show item count badge on filter chips",
    phase: "stable"
  },
  NUMISTA_SEARCH_LOOKUP: {
    enabled: false,
    urlOverride: true,
    userToggle: true,
    description: "Pattern-based Numista search improvement",
    phase: "beta"
  },
  COIN_IMAGES: {
    enabled: true,
    urlOverride: true,
    userToggle: true,
    description: "Coin image caching and item view modal",
    phase: "beta"
  }
};

/**
 * Feature state management class
 * Handles URL parameters, localStorage persistence, and runtime toggles
 */
class FeatureFlags {
  constructor() {
    this.state = this.loadFeatureState();
    this.listeners = new Map();
    this.initializeFromUrl();
  }

  /**
   * Load feature state from localStorage with defaults
   * @returns {Object} Current feature state
   */
  loadFeatureState() {
    try {
      const stored = localStorage.getItem(FEATURE_FLAGS_KEY);
      const parsed = stored ? JSON.parse(stored) : {};

      // Merge with defaults
      const state = {};
      for (const [key, config] of Object.entries(FEATURE_FLAGS)) {
        state[key] = parsed[key] !== undefined ? parsed[key] : config.enabled;
      }

      // One-time migration (v3.26.01): re-enable FUZZY_AUTOCOMPLETE for users
      // who had it silently disabled before the settings toggle existed
      const migrationKey = 'ff_migration_fuzzy_autocomplete';
      if (!localStorage.getItem(migrationKey) && parsed.FUZZY_AUTOCOMPLETE === false) {
        state.FUZZY_AUTOCOMPLETE = true;
        localStorage.setItem(migrationKey, '1');
        localStorage.setItem(FEATURE_FLAGS_KEY, JSON.stringify(state));
      }

      return state;
    } catch (e) {
      console.warn('Failed to load feature flags from localStorage:', e);
      return this.getDefaultState();
    }
  }

  /**
   * Get default feature state from configuration
   * @returns {Object} Default feature state
   */
  getDefaultState() {
    const state = {};
    for (const [key, config] of Object.entries(FEATURE_FLAGS)) {
      state[key] = config.enabled;
    }
    return state;
  }

  /**
   * Initialize feature flags from URL parameters
   */
  initializeFromUrl() {
    if (typeof window === "undefined") return;
    
    const params = new URLSearchParams(window.location.search);
    let changed = false;
    
    for (const [key, config] of Object.entries(FEATURE_FLAGS)) {
      if (!config.urlOverride) continue;
      
      const paramName = key.toLowerCase();
      if (params.has(paramName)) {
        const value = params.get(paramName);
        const enabled = value === null || value === "" || value === "1" || value === "true";
        
        if (this.state[key] !== enabled) {
          this.state[key] = enabled;
          changed = true;
          
          if (DEBUG) {
            console.log(`Feature flag ${key} set to ${enabled} via URL parameter`);
          }
        }
      }
    }
    
    if (changed) {
      this.saveFeatureState();
      this.notifyListeners();
    }
  }

  /**
   * Save current feature state to localStorage
   */
  saveFeatureState() {
    try {
      localStorage.setItem(FEATURE_FLAGS_KEY, JSON.stringify(this.state));
    } catch (e) {
      console.warn('Failed to save feature flags to localStorage:', e);
    }
  }

  /**
   * Check if a feature is enabled
   * @param {string} feature - Feature flag key
   * @returns {boolean} Whether the feature is enabled
   */
  isEnabled(feature) {
    return this.state[feature] === true;
  }

  /**
   * Enable a feature
   * @param {string} feature - Feature flag key
   * @param {boolean} [persist=true] - Whether to save to localStorage
   */
  enable(feature, persist = true) {
    if (this.state[feature] !== true) {
      this.state[feature] = true;
      if (persist) this.saveFeatureState();
      this.notifyListeners(feature);
      
      if (DEBUG) {
        console.log(`Feature ${feature} enabled`);
      }
    }
  }

  /**
   * Disable a feature
   * @param {string} feature - Feature flag key
   * @param {boolean} [persist=true] - Whether to save to localStorage
   */
  disable(feature, persist = true) {
    if (this.state[feature] !== false) {
      this.state[feature] = false;
      if (persist) this.saveFeatureState();
      this.notifyListeners(feature);
      
      if (DEBUG) {
        console.log(`Feature ${feature} disabled`);
      }
    }
  }

  /**
   * Toggle a feature on/off
   * @param {string} feature - Feature flag key
   * @param {boolean} [persist=true] - Whether to save to localStorage
   * @returns {boolean} New state after toggle
   */
  toggle(feature, persist = true) {
    const config = FEATURE_FLAGS[feature];
    if (!config || !config.userToggle) {
      console.warn(`Feature ${feature} cannot be toggled by user`);
      return this.state[feature];
    }
    
    if (this.state[feature]) {
      this.disable(feature, persist);
    } else {
      this.enable(feature, persist);
    }
    
    return this.state[feature];
  }

  /**
   * Reset all features to default state
   */
  reset() {
    this.state = this.getDefaultState();
    this.saveFeatureState();
    this.notifyListeners();
    
    if (DEBUG) {
      console.log('All feature flags reset to defaults');
    }
  }

  /**
   * Get current state of all features
   * @returns {Object} Current feature state
   */
  getState() {
    return { ...this.state };
  }

  /**
   * Get feature configuration
   * @param {string} feature - Feature flag key
   * @returns {Object|null} Feature configuration or null if not found
   */
  getConfig(feature) {
    return FEATURE_FLAGS[feature] || null;
  }

  /**
   * Get all feature configurations
   * @returns {Object} All feature configurations
   */
  getAllConfigs() {
    return { ...FEATURE_FLAGS };
  }

  /**
   * Add a listener for feature state changes
   * @param {string} feature - Feature to listen for (or 'all' for all changes)
   * @param {Function} callback - Callback function (feature, enabled, oldEnabled)
   */
  addListener(feature, callback) {
    if (!this.listeners.has(feature)) {
      this.listeners.set(feature, new Set());
    }
    this.listeners.get(feature).add(callback);
  }

  /**
   * Remove a listener for feature state changes
   * @param {string} feature - Feature to stop listening for
   * @param {Function} callback - Callback function to remove
   */
  removeListener(feature, callback) {
    const featureListeners = this.listeners.get(feature);
    if (featureListeners) {
      featureListeners.delete(callback);
      if (featureListeners.size === 0) {
        this.listeners.delete(feature);
      }
    }
  }

  /**
   * Notify listeners of feature state changes
   * @param {string} [changedFeature] - Specific feature that changed (optional)
   */
  notifyListeners(changedFeature = null) {
    // Notify 'all' listeners
    const allListeners = this.listeners.get('all');
    if (allListeners) {
      allListeners.forEach(callback => {
        try {
          callback(changedFeature, this.state);
        } catch (e) {
          console.error('Error in feature flag listener:', e);
        }
      });
    }
    
    // Notify specific feature listeners
    if (changedFeature) {
      const featureListeners = this.listeners.get(changedFeature);
      if (featureListeners) {
        const enabled = this.state[changedFeature];
        featureListeners.forEach(callback => {
          try {
            callback(changedFeature, enabled);
          } catch (e) {
            console.error('Error in feature flag listener:', e);
          }
        });
      }
    }
  }

  /**
   * Get debug information about feature flags
   * @returns {Object} Debug information
   */
  getDebugInfo() {
    return {
      state: this.getState(),
      configs: this.getAllConfigs(),
      urlParams: typeof window !== "undefined" ? 
        Object.fromEntries(new URLSearchParams(window.location.search)) : {},
      localStorage: (() => {
        try {
          return JSON.parse(localStorage.getItem(FEATURE_FLAGS_KEY) || '{}');
        } catch (e) {
          return null;
        }
      })()
    };
  }
}

/**
 * Global feature flags instance
 * @constant {FeatureFlags}
 */
const featureFlags = new FeatureFlags();

/**
 * Convenience functions for common feature flag operations
 */
const isFeatureEnabled = (feature) => featureFlags.isEnabled(feature);
const enableFeature = (feature, persist = true) => featureFlags.enable(feature, persist);
const disableFeature = (feature, persist = true) => featureFlags.disable(feature, persist);
const toggleFeature = (feature, persist = true) => featureFlags.toggle(feature, persist);

/**
 * Log feature flag state on initialization (debug mode only)
 */
if (DEBUG && typeof window !== "undefined") {
  console.log('Feature Flags initialized:', featureFlags.getDebugInfo());
}

/**
 * Metal configuration object - Central registry for all metal-related information
 *
 * This configuration drives the entire application's metal handling by defining:
 * - Display names for user interface elements
 * - Key identifiers for data structures and calculations
 * - DOM element ID patterns for dynamic element selection
 * - LocalStorage keys for persistent data storage
 * - CSS color variables for styling and theming
 *
 * Each metal configuration contains:
 * @property {string} name - Display name used in UI elements and forms
 * @property {string} key - Lowercase identifier for data objects and calculations
 * @property {string} spotKey - DOM ID pattern for spot price input elements
 * @property {string} localStorageKey - Key for storing spot prices in localStorage
 * @property {string} color - CSS custom property name for metal-specific styling
 *
 * Adding a new metal type requires:
 * 1. Adding configuration here
 * 2. Adding corresponding HTML elements following the naming pattern
 * 3. Adding CSS custom properties for colors
 * 4. The application will automatically handle the rest through iteration
 */
const METALS = {
  SILVER: {
    name: "Silver",
    key: "silver",
    spotKey: "spotSilver",
    localStorageKey: "spotSilver",
    color: "silver",
    defaultPrice: 25.0,
  },
  GOLD: {
    name: "Gold",
    key: "gold",
    spotKey: "spotGold",
    localStorageKey: "spotGold",
    color: "gold",
    defaultPrice: 2500.0,
  },
  PLATINUM: {
    name: "Platinum",
    key: "platinum",
    spotKey: "spotPlatinum",
    localStorageKey: "spotPlatinum",
    color: "platinum",
    defaultPrice: 1000.0,
  },
  PALLADIUM: {
    name: "Palladium",
    key: "palladium",
    spotKey: "spotPalladium",
    localStorageKey: "spotPalladium",
    color: "palladium",
    defaultPrice: 1000.0,
  },
};

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

// Expose globals
if (typeof window !== "undefined") {
  window.API_PROVIDERS = API_PROVIDERS;
  window.METALS = METALS;
  window.DEBUG = DEBUG;
  window.DEFAULT_CURRENCY = DEFAULT_CURRENCY;
  window.MAX_LOCAL_FILE_SIZE = MAX_LOCAL_FILE_SIZE;
  window.BRANDING_DOMAIN_OPTIONS = BRANDING_DOMAIN_OPTIONS;
  window.BRANDING_DOMAIN_OVERRIDE = BRANDING_DOMAIN_OVERRIDE;
  window.getTemplateVariables = getTemplateVariables;
  window.replaceTemplateVariables = replaceTemplateVariables;
  
  // Feature flags system
  window.FEATURE_FLAGS = FEATURE_FLAGS;
  window.featureFlags = featureFlags;
  window.isFeatureEnabled = isFeatureEnabled;
  window.enableFeature = enableFeature;
  window.disableFeature = disableFeature;
  window.toggleFeature = toggleFeature;
  window.ALLOWED_STORAGE_KEYS = ALLOWED_STORAGE_KEYS;
  // STAK-149: Cloud auto-sync constants
  window.SYNC_POLL_INTERVAL = SYNC_POLL_INTERVAL;
  window.SYNC_PUSH_DEBOUNCE = SYNC_PUSH_DEBOUNCE;
  window.SYNC_FILE_PATH = SYNC_FILE_PATH;
  window.SYNC_META_PATH = SYNC_META_PATH;
  window.SYNC_SCOPE_KEYS = SYNC_SCOPE_KEYS;
  window.CERT_LOOKUP_URLS = CERT_LOOKUP_URLS;
  // Inline chip config
  window.INLINE_CHIP_DEFAULTS = INLINE_CHIP_DEFAULTS;
  window.getInlineChipConfig = getInlineChipConfig;
  window.saveInlineChipConfig = saveInlineChipConfig;
  // Filter chip category config
  window.FILTER_CHIP_CATEGORY_DEFAULTS = FILTER_CHIP_CATEGORY_DEFAULTS;
  window.getFilterChipCategoryConfig = getFilterChipCategoryConfig;
  window.saveFilterChipCategoryConfig = saveFilterChipCategoryConfig;
  // Layout section config
  window.LAYOUT_SECTION_DEFAULTS = LAYOUT_SECTION_DEFAULTS;
  window.getLayoutSectionConfig = getLayoutSectionConfig;
  window.saveLayoutSectionConfig = saveLayoutSectionConfig;
  // Goldback denomination pricing (STACK-45)
  window.GOLDBACK_PRICES_KEY = GOLDBACK_PRICES_KEY;
  window.GOLDBACK_PRICE_HISTORY_KEY = GOLDBACK_PRICE_HISTORY_KEY;
  window.GOLDBACK_ENABLED_KEY = GOLDBACK_ENABLED_KEY;
  window.GB_TO_OZT = GB_TO_OZT;
  window.GOLDBACK_DENOMINATIONS = GOLDBACK_DENOMINATIONS;
  window.GOLDBACK_ESTIMATE_ENABLED_KEY = GOLDBACK_ESTIMATE_ENABLED_KEY;
  window.GB_ESTIMATE_PREMIUM = GB_ESTIMATE_PREMIUM;
  window.GB_ESTIMATE_MODIFIER_KEY = GB_ESTIMATE_MODIFIER_KEY;
  // View modal section config
  window.VIEW_MODAL_SECTION_DEFAULTS = VIEW_MODAL_SECTION_DEFAULTS;
  window.getViewModalSectionConfig = getViewModalSectionConfig;
  window.saveViewModalSectionConfig = saveViewModalSectionConfig;
  // Numista view field config
  window.NUMISTA_VIEW_FIELD_DEFAULTS = NUMISTA_VIEW_FIELD_DEFAULTS;
  window.getNumistaViewFieldConfig = getNumistaViewFieldConfig;
  window.saveNumistaViewFieldConfig = saveNumistaViewFieldConfig;
  // Item tags (STAK-126)
  window.ITEM_TAGS_KEY = ITEM_TAGS_KEY;
  window.MAX_TAGS_PER_ITEM = MAX_TAGS_PER_ITEM;
  window.MAX_TAG_LENGTH = MAX_TAG_LENGTH;
  // Multi-currency support (STACK-50)
  window.SUPPORTED_CURRENCIES = SUPPORTED_CURRENCIES;
  window.DISPLAY_CURRENCY_KEY = DISPLAY_CURRENCY_KEY;
  window.EXCHANGE_RATES_KEY = EXCHANGE_RATES_KEY;
  window.EXCHANGE_RATE_API_URL = EXCHANGE_RATE_API_URL;
  window.FALLBACK_EXCHANGE_RATES = FALLBACK_EXCHANGE_RATES;
}

// Expose APP_VERSION globally for non-module usage
(function() {
  if (typeof window !== 'undefined') {
    window.APP_VERSION = (typeof APP_VERSION !== 'undefined') ? APP_VERSION : "0.0.0";
  }
})();