Source: catalog-api.js

// CATALOG API SYSTEM
// =============================================================================
// Provider-agnostic catalog API architecture for StakTrakr
// Provider-agnostic architecture for catalog lookups

/**
 * Catalog API Configuration with base64-encoded key storage
 * Matches the metals API key pattern in js/api.js
 */
class CatalogConfig {
  constructor() {
    this.storageKey = 'catalog_api_config';
    this.load();
  }

  load() {
    try {
      const stored = localStorage.getItem(this.storageKey);
      if (stored) {
        const parsed = JSON.parse(stored);
        // Decode base64 keys on load
        if (parsed.numista && parsed.numista.apiKey) {
          try {
            parsed.numista.apiKey = atob(parsed.numista.apiKey);
          } catch (e) {
            // Key wasn't base64 encoded (legacy or plain text) — keep as-is
          }
        }
        if (parsed.pcgs && parsed.pcgs.bearerToken) {
          try {
            parsed.pcgs.bearerToken = atob(parsed.pcgs.bearerToken);
          } catch (e) {
            // Token wasn't base64 encoded — keep as-is
          }
        }
        this.config = parsed;
      } else {
        this.config = this.getDefaultConfig();
      }
    } catch (error) {
      console.warn('Failed to load catalog config:', error);
      this.config = this.getDefaultConfig();
    }
  }

  getDefaultConfig() {
    const now = new Date();
    const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
    const today = now.toISOString().slice(0, 10);
    return {
      numista: {
        apiKey: '',
        quota: 2000
      },
      numistaUsage: {
        used: 0,
        month: month
      },
      pcgs: {
        bearerToken: ''
      },
      pcgsUsage: {
        used: 0,
        date: today
      },
      local: {
        enabled: true
      }
    };
  }

  save() {
    try {
      // Encode keys as base64 before writing to localStorage
      const toStore = JSON.parse(JSON.stringify(this.config));
      if (toStore.numista && toStore.numista.apiKey) {
        toStore.numista.apiKey = btoa(toStore.numista.apiKey);
      }
      if (toStore.pcgs && toStore.pcgs.bearerToken) {
        toStore.pcgs.bearerToken = btoa(toStore.pcgs.bearerToken);
      }
      localStorage.setItem(this.storageKey, JSON.stringify(toStore));
    } catch (error) {
      console.error('Failed to save catalog config:', error);
    }
  }

  /**
   * Set Numista API key
   * @param {string} apiKey - Plain text API key
   * @param {number} quota - API quota (default 2000)
   */
  setNumistaConfig(apiKey, quota = 2000) {
    this.config.numista = {
      apiKey: apiKey || '',
      quota
    };
    this.save();
    return true;
  }

  /**
   * Get current Numista configuration
   */
  getNumistaConfig() {
    return {
      ...this.config.numista,
      apiKey: this.config.numista.apiKey || ''
    };
  }

  /**
   * Check if Numista is configured with a valid key
   */
  isNumistaEnabled() {
    return !!this.config.numista.apiKey;
  }

  /**
   * Clear stored Numista key
   */
  clearNumistaKey() {
    this.config.numista = {
      apiKey: '',
      quota: 2000
    };
    this.save();
  }

  /**
   * Check if user has stored a key
   */
  hasNumistaKey() {
    return !!this.config.numista.apiKey;
  }

  /**
   * Increment Numista usage counter, auto-resetting if month changed
   */
  incrementNumistaUsage() {
    const now = new Date();
    const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
    if (!this.config.numistaUsage) {
      this.config.numistaUsage = { used: 0, month: currentMonth };
    }
    if (this.config.numistaUsage.month !== currentMonth) {
      this.config.numistaUsage.used = 0;
      this.config.numistaUsage.month = currentMonth;
    }
    this.config.numistaUsage.used++;
    this.save();
  }

  /**
   * Get current Numista usage stats
   * @returns {{ used: number, quota: number, month: string }}
   */
  getNumistaUsage() {
    const now = new Date();
    const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
    if (!this.config.numistaUsage) {
      this.config.numistaUsage = { used: 0, month: currentMonth };
    }
    if (this.config.numistaUsage.month !== currentMonth) {
      this.config.numistaUsage.used = 0;
      this.config.numistaUsage.month = currentMonth;
    }
    return {
      used: this.config.numistaUsage.used,
      quota: this.config.numista?.quota || 2000,
      month: this.config.numistaUsage.month
    };
  }

  // ─── PCGS Methods ───────────────────────────────────────────────────────

  /**
   * Set PCGS bearer token
   * @param {string} token - Bearer token from PCGS
   */
  setPcgsConfig(token) {
    if (!this.config.pcgs) this.config.pcgs = {};
    this.config.pcgs.bearerToken = token || '';
    this.save();
    return true;
  }

  /**
   * Get current PCGS configuration
   * @returns {{ bearerToken: string }}
   */
  getPcgsConfig() {
    if (!this.config.pcgs) this.config.pcgs = { bearerToken: '' };
    return { bearerToken: this.config.pcgs.bearerToken || '' };
  }

  /**
   * Check if PCGS is configured with a valid token
   * @returns {boolean}
   */
  isPcgsEnabled() {
    return !!(this.config.pcgs && this.config.pcgs.bearerToken);
  }

  /**
   * Clear stored PCGS token
   */
  clearPcgsToken() {
    this.config.pcgs = { bearerToken: '' };
    this.save();
  }

  /**
   * Increment PCGS usage counter, auto-resetting if date changed (daily limit)
   */
  incrementPcgsUsage() {
    const today = new Date().toISOString().slice(0, 10);
    if (!this.config.pcgsUsage) {
      this.config.pcgsUsage = { used: 0, date: today };
    }
    if (this.config.pcgsUsage.date !== today) {
      this.config.pcgsUsage.used = 0;
      this.config.pcgsUsage.date = today;
    }
    this.config.pcgsUsage.used++;
    this.save();
  }

  /**
   * Check if a PCGS API request can be made (under daily rate limit)
   * @returns {boolean}
   */
  canMakePcgsRequest() {
    const today = new Date().toISOString().slice(0, 10);
    if (!this.config.pcgsUsage || this.config.pcgsUsage.date !== today) {
      return true; // New day, counter resets
    }
    return this.config.pcgsUsage.used < 1000;
  }

  /**
   * Get current PCGS usage stats
   * @returns {{ used: number, limit: number, date: string }}
   */
  getPcgsUsage() {
    const today = new Date().toISOString().slice(0, 10);
    if (!this.config.pcgsUsage) {
      this.config.pcgsUsage = { used: 0, date: today };
    }
    if (this.config.pcgsUsage.date !== today) {
      this.config.pcgsUsage.used = 0;
      this.config.pcgsUsage.date = today;
    }
    return {
      used: this.config.pcgsUsage.used,
      limit: 1000,
      date: this.config.pcgsUsage.date
    };
  }
}

// Global catalog configuration instance
const catalogConfig = new CatalogConfig();

console.log('🔌 Catalog API system ready - configure API keys through settings');

/**
 * Base interface for all catalog providers
 * Ensures consistent API regardless of provider
 */
class CatalogProvider {
  constructor(config = {}) {
    this.name = config.name || 'Unknown';
    this.apiKey = config.apiKey || '';
    this.baseUrl = config.baseUrl || '';
    this.rateLimit = config.rateLimit || 60; // requests per minute
    this.timeout = config.timeout || 10000; // 10 seconds
    this.lastRequest = 0;
    this.requestCount = 0;
    this.requestWindow = 60000; // 1 minute window
  }

  /**
   * Check if we can make a request (rate limiting)
   * @returns {boolean} True if request is allowed
   */
  canMakeRequest() {
    const now = Date.now();
    if (now - this.lastRequest > this.requestWindow) {
      this.requestCount = 0;
      this.lastRequest = now;
    }
    return this.requestCount < this.rateLimit;
  }

  /**
   * Make rate-limited HTTP request
   * @param {string} url - Request URL
   * @param {Object} options - Fetch options
   * @returns {Promise} Fetch response
   */
  async request(url, options = {}) {
    if (!this.canMakeRequest()) {
      throw new Error(`Rate limit exceeded for ${this.name}. Try again later.`);
    }

    this.requestCount++;

    // Persist Numista usage across page reloads
    if (this instanceof NumistaProvider) {
      catalogConfig.incrementNumistaUsage();
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers
        }
      });
      
      clearTimeout(timeoutId);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      return response;
    } catch (error) {
      clearTimeout(timeoutId);
      if (error.name === 'AbortError') {
        throw new Error(`Request timeout for ${this.name}`);
      }
      throw error;
    }
  }

  /**
   * Lookup item by catalog ID - MUST be implemented by providers
   * @param {string} catalogId - Catalog identifier
   * @returns {Promise<Object>} Standardized item data
   */
  async lookupItem(catalogId) {
    throw new Error('lookupItem must be implemented by provider');
  }

  /**
   * Search for items by query - MUST be implemented by providers
   * @param {string} query - Search term
   * @param {Object} filters - Search filters
   * @returns {Promise<Array>} Array of standardized item data
   */
  async searchItems(query, filters = {}) {
    throw new Error('searchItems must be implemented by provider');
  }

  /**
   * Get current market value for item - MUST be implemented by providers
   * @param {string} catalogId - Catalog identifier
   * @returns {Promise<number>} Current market value in USD
   */
  async getMarketValue(catalogId) {
    throw new Error('getMarketValue must be implemented by provider');
  }
}

/**
 * Numista API Provider
 * Implements Numista-specific API calls
 */
class NumistaProvider extends CatalogProvider {
  constructor() {
    const config = catalogConfig.getNumistaConfig();
    super({
      name: 'Numista',
      apiKey: config.apiKey,
      baseUrl: 'https://api.numista.com/v3',
      rateLimit: 100, // Numista allows 100 requests per minute
      timeout: 15000
    });
    this.clientName = config.clientName;
    this.clientId = config.clientId;
    this.quota = config.quota;
  }

  /**
   * Lookup item by Numista catalog ID
   * @param {string} catalogId - Numista item ID
   * @returns {Promise<Object>} Standardized item data
   */
  async lookupItem(catalogId) {
    if (!catalogId) throw new Error('Catalog ID is required');

    const url = `${this.baseUrl}/types/${catalogId}?lang=en`;

    try {
      const response = await this.request(url, {
        headers: { 'Numista-API-Key': this.apiKey }
      });
      const data = await response.json();
      if (typeof window !== 'undefined' && typeof window.debugLog === 'function') {
        window.debugLog(`Numista lookup ${catalogId}: keys=${Object.keys(data).join(',')}`);
        if (data.obverse) window.debugLog(`  obverse keys: ${Object.keys(data.obverse).join(',')}`);
        if (data.reverse) window.debugLog(`  reverse keys: ${Object.keys(data.reverse).join(',')}`);
      }

      return this.normalizeItemData(data);
    } catch (error) {
      console.error(`Numista lookup failed for ID ${catalogId}:`, error);
      throw new Error(`Failed to lookup item ${catalogId} from Numista: ${error.message}`);
    }
  }

  /**
   * Search for items on Numista
   * @param {string} query - Search term
   * @param {Object} filters - Search filters
   * @returns {Promise<Array>} Array of standardized item data
   */
  async searchItems(query, filters = {}) {
    const params = new URLSearchParams({
      q: query,
      count: Math.min(filters.limit || 20, 50),
      lang: 'en'
    });

    if (filters.page) params.append('page', filters.page);
    if (filters.country) params.append('issuer', filters.country);
    if (filters.category) params.append('category', filters.category);
    if (filters.year) params.append('year', filters.year);

    const url = `${this.baseUrl}/types?${params.toString()}`;

    try {
      const response = await this.request(url, {
        headers: { 'Numista-API-Key': this.apiKey }
      });
      const data = await response.json();

      return data.types ? data.types.map(item => this.normalizeItemData(item)) : [];
    } catch (error) {
      console.error('Numista search failed:', error);
      throw new Error(`Numista search failed: ${error.message}`);
    }
  }

  /**
   * Get current market value from Numista
   * @param {string} catalogId - Numista item ID
   * @returns {Promise<number>} Current market value in USD
   */
  async getMarketValue(catalogId) {
    // Note: Numista doesn't provide real-time market values
    // This would need to be enhanced or combined with other sources
    try {
      const item = await this.lookupItem(catalogId);
      return item.estimatedValue || 0;
    } catch (error) {
      console.warn(`Could not get market value for ${catalogId}:`, error);
      return 0;
    }
  }

  /**
   * Normalize Numista data to standard format
   * @param {Object} numistaData - Raw Numista API response
   * @returns {Object} Standardized item data
   */
  normalizeItemData(numistaData) {
    // Compose year from min_year / max_year range
    const minY = numistaData.min_year;
    const maxY = numistaData.max_year;
    const year = minY && maxY && minY !== maxY ? `${minY}-${maxY}` : (minY || maxY || '');

    // Handle composition — can be a string or object with .text
    const rawComp = numistaData.composition;
    const composition = typeof rawComp === 'object' && rawComp !== null ? (rawComp.text || '') : (rawComp || '');

    // Image: prefer obverse_thumbnail with nested fallback
    const imageUrl = numistaData.obverse_thumbnail ||
      numistaData.obverse?.thumbnail ||
      numistaData.reverse_thumbnail ||
      '';

    // Reverse image: separate field for showing both sides
    const reverseImageUrl = numistaData.reverse_thumbnail ||
      numistaData.reverse?.thumbnail ||
      '';

    debugLog(`  imageUrl: ${imageUrl || '(empty)'}, reverseImageUrl: ${reverseImageUrl || '(empty)'}`);

    // Extract catalog references (KM#, Schon#, etc.)
    const kmReferences = [];
    if (Array.isArray(numistaData.references)) {
      numistaData.references.forEach(ref => {
        if (ref.catalogue?.code && ref.number) {
          kmReferences.push(`${ref.catalogue.code}# ${ref.number}`);
        }
      });
    }

    // Extract mintage data by year
    const mintageByYear = [];
    if (Array.isArray(numistaData.years)) {
      numistaData.years.forEach(y => {
        if (y.year) {
          mintageByYear.push({
            year: y.year,
            mintage: y.mintage || 0,
            remark: y.remark || '',
          });
        }
      });
    }

    // Denomination / face value
    const denomination = numistaData.value?.text || '';

    return {
      catalogId: numistaData.id?.toString() || '',
      name: numistaData.title || '',
      year: year.toString(),
      country: numistaData.issuer?.name || '',
      metal: this.normalizeMetal(composition),
      weight: numistaData.weight || 0,
      diameter: numistaData.size || 0,
      thickness: numistaData.thickness || 0,
      type: this.normalizeType(numistaData.category || ''),
      mintage: 0, // Mintage is per-issue, not per-type in Numista API
      estimatedValue: numistaData.value?.numeric_value || 0,
      imageUrl: imageUrl,
      reverseImageUrl: reverseImageUrl,
      description: numistaData.comments || '',
      provider: 'Numista',
      lastUpdated: new Date().toISOString(),
      // Enriched fields for view modal
      denomination: denomination,
      shape: numistaData.shape || '',
      composition: composition,
      orientation: numistaData.orientation || '',
      commemorative: !!numistaData.is_commemorative,
      commemorativeDesc: numistaData.commemorative_description || '',
      rarityIndex: numistaData.rarity_index || 0,
      kmReferences: kmReferences,
      mintageByYear: mintageByYear,
      tags: Array.isArray(numistaData.tags) ? numistaData.tags : [],
      technique: typeof numistaData.technique === 'object' ? (numistaData.technique?.text || '') : (numistaData.technique || ''),
      obverseDesc: numistaData.obverse?.description || '',
      reverseDesc: numistaData.reverse?.description || '',
      edgeDesc: numistaData.edge?.description || '',
    };
  }

  /**
   * Normalize metal composition from Numista format
   * @param {string} composition - Numista composition string
   * @returns {string} Standardized metal name
   */
  normalizeMetal(composition) {
    const comp = composition.toLowerCase();
    if (comp.includes('gold') || comp.includes('au')) return 'Gold';
    if (comp.includes('silver') || comp.includes('ag')) return 'Silver';
    if (comp.includes('platinum') || comp.includes('pt')) return 'Platinum';
    if (comp.includes('palladium') || comp.includes('pd')) return 'Palladium';
    if (comp.includes('copper') || comp.includes('bronze') || comp.includes('brass')) return 'Alloy/Other';
    return 'Alloy/Other';
  }

  /**
   * Normalize item type from Numista format
   * @param {string} type - Numista type string
   * @returns {string} Standardized type
   */
  normalizeType(type) {
    const t = type.toLowerCase();
    if (t.includes('coin') || t.includes('circulation')) return 'Coin';
    if (t.includes('bar') || t.includes('ingot')) return 'Bar';
    if (t.includes('round')) return 'Round';
    if (t.includes('note') || t.includes('bill')) return 'Note';
    return 'Other';
  }
}

/**
/**
 * Local Provider (Fallback)
 * Uses local data when external APIs are unavailable
 */
class LocalProvider extends CatalogProvider {
  constructor() {
    super({
      name: 'Local',
      rateLimit: 1000, // No real rate limit for local data
      timeout: 1000
    });
    this.localData = this.loadLocalData();
  }

  loadLocalData() {
    // Load any cached catalog data from localStorage
    try {
      const stored = localStorage.getItem('staktrakr.catalog.cache');
      return stored ? JSON.parse(stored) : {};
    } catch (error) {
      console.warn('Could not load local catalog cache:', error);
      return {};
    }
  }

  async lookupItem(catalogId) {
    const item = this.localData[catalogId];
    if (!item) {
      throw new Error(`Item ${catalogId} not found in local cache`);
    }
    return item;
  }

  async searchItems(query, filters = {}) {
    const results = Object.values(this.localData).filter(item => 
      item.name.toLowerCase().includes(query.toLowerCase()) ||
      item.description.toLowerCase().includes(query.toLowerCase())
    );
    return results.slice(0, filters.limit || 20);
  }

  async getMarketValue(catalogId) {
    const item = this.localData[catalogId];
    return item ? (item.estimatedValue || 0) : 0;
  }

  /**
   * Cache item data locally
   * @param {string} catalogId - Catalog identifier
   * @param {Object} itemData - Standardized item data
   */
  cacheItem(catalogId, itemData) {
    this.localData[catalogId] = itemData;
    try {
      localStorage.setItem('staktrakr.catalog.cache', JSON.stringify(this.localData));
    } catch (error) {
      console.warn('Could not cache item data:', error);
    }
  }
}

/**
 * Main Catalog API Manager
 * Coordinates multiple providers with fallback chain
 */
class CatalogAPI {
  constructor() {
    this.providers = [];
    this.localProvider = new LocalProvider();
    this.activeProvider = null;
    this.settings = this.loadSettings();
    
    this.initializeProviders();
  }

  /**
   * Load API settings from localStorage
   */
  loadSettings() {
    try {
      const stored = localStorage.getItem('staktrakr.catalog.settings');
      return stored ? JSON.parse(stored) : {
        activeProvider: 'numista',
        numistaApiKey: '',
        enableFallback: true,
        cacheDuration: 3600000 // 1 hour
      };
    } catch (error) {
      console.warn('Could not load catalog API settings:', error);
      return {};
    }
  }

  /**
   * Save API settings to localStorage
   */
  saveSettings() {
    try {
      localStorage.setItem('staktrakr.catalog.settings', JSON.stringify(this.settings));
    } catch (error) {
      console.warn('Could not save catalog API settings:', error);
    }
  }

  /**
   * Initialize available providers based on API keys
   */
  initializeProviders() {
    this.providers = [];

    // Add Numista provider if configured and enabled
    if (catalogConfig.isNumistaEnabled()) {
      try {
        const numista = new NumistaProvider();
        this.providers.push(numista);
        this.activeProvider = numista;
        console.log('✅ Numista provider initialized');
      } catch (error) {
        console.error('❌ Failed to initialize Numista provider:', error);
      }
    }

    // Default to first available provider if none set
    if (!this.activeProvider && this.providers.length > 0) {
      this.activeProvider = this.providers[0];
    }

    console.log(`🔌 Catalog API initialized with ${this.providers.length} provider(s)`);
  }

  /**
   * Set API key for a provider
   * @param {string} provider - Provider name ('numista')
   * @param {string} apiKey - API key
   */
  setApiKey(provider, apiKey) {
    if (provider === 'numista') {
      this.settings.numistaApiKey = apiKey;
    }
    
    this.saveSettings();
    this.initializeProviders();
  }

  /**
   * Switch active provider
   * @param {string} providerName - Provider name to switch to
   */
  switchProvider(providerName) {
    const provider = this.providers.find(p => p.name.toLowerCase() === providerName.toLowerCase());
    if (provider) {
      this.activeProvider = provider;
      this.settings.activeProvider = providerName.toLowerCase();
      this.saveSettings();
      console.log(`Switched to ${provider.name} catalog provider`);
    } else {
      throw new Error(`Provider ${providerName} not available`);
    }
  }

  /**
   * Lookup item with fallback chain
   * @param {string} catalogId - Catalog identifier
   * @param {Object} [options={}] - Options (e.g. { action: 'test' })
   * @returns {Promise<Object>} Standardized item data
   */
  async lookupItem(catalogId, options = {}) {
    const startTime = Date.now();
    const action = options.action || 'lookup';
    const providers = this.settings.enableFallback ?
      [this.activeProvider, ...this.providers.filter(p => p !== this.activeProvider), this.localProvider] :
      [this.activeProvider];

    let lastError;

    for (const provider of providers) {
      if (!provider) continue;

      try {
        console.log(`Attempting lookup with ${provider.name}...`);
        const result = await provider.lookupItem(catalogId);

        // Cache successful results locally
        if (provider !== this.localProvider) {
          this.localProvider.cacheItem(catalogId, result);
        }

        recordCatalogHistory({
          action,
          query: catalogId,
          result: 'success',
          itemCount: 1,
          provider: provider.name,
          duration: Date.now() - startTime,
        });

        return result;
      } catch (error) {
        console.warn(`${provider.name} lookup failed:`, error.message);
        lastError = error;
        continue;
      }
    }

    recordCatalogHistory({
      action,
      query: catalogId,
      result: 'fail',
      itemCount: 0,
      provider: '',
      duration: Date.now() - startTime,
      error: lastError ? lastError.message : 'All providers failed',
    });

    throw lastError || new Error('All catalog providers failed');
  }

  /**
   * Search items with active provider
   * @param {string} query - Search term
   * @param {Object} filters - Search filters
   * @returns {Promise<Array>} Array of standardized item data
   */
  async searchItems(query, filters = {}) {
    const startTime = Date.now();

    if (!this.activeProvider) {
      recordCatalogHistory({
        action: 'search',
        query,
        result: 'fail',
        itemCount: 0,
        provider: '',
        duration: Date.now() - startTime,
        error: 'No catalog provider available',
      });
      throw new Error('No catalog provider available');
    }

    try {
      const results = await this.activeProvider.searchItems(query, filters);
      recordCatalogHistory({
        action: 'search',
        query,
        result: 'success',
        itemCount: results.length,
        provider: this.activeProvider.name,
        duration: Date.now() - startTime,
      });
      return results;
    } catch (error) {
      recordCatalogHistory({
        action: 'search',
        query,
        result: 'fail',
        itemCount: 0,
        provider: this.activeProvider.name,
        duration: Date.now() - startTime,
        error: error.message,
      });
      throw error;
    }
  }

  /**
   * Get market value with fallback
   * @param {string} catalogId - Catalog identifier
   * @returns {Promise<number>} Current market value in USD
   */
  async getMarketValue(catalogId) {
    const startTime = Date.now();
    const providers = this.settings.enableFallback ?
      [this.activeProvider, ...this.providers.filter(p => p !== this.activeProvider)] :
      [this.activeProvider];

    let lastError;

    for (const provider of providers) {
      if (!provider) continue;

      try {
        const value = await provider.getMarketValue(catalogId);
        recordCatalogHistory({
          action: 'market_value',
          query: catalogId,
          result: 'success',
          itemCount: 1,
          provider: provider.name,
          duration: Date.now() - startTime,
        });
        return value;
      } catch (error) {
        console.warn(`${provider.name} market value lookup failed:`, error.message);
        lastError = error;
        continue;
      }
    }

    recordCatalogHistory({
      action: 'market_value',
      query: catalogId,
      result: 'fail',
      itemCount: 0,
      provider: '',
      duration: Date.now() - startTime,
      error: lastError ? lastError.message : 'All providers failed',
    });

    return 0; // Fallback to 0 if all providers fail
  }

  /**
   * Get provider status information
   * @returns {Object} Status of all providers
   */
  getProviderStatus() {
    return {
      active: this.activeProvider ? this.activeProvider.name : 'None',
      available: this.providers.map(p => p.name),
      settings: this.settings
    };
  }
}

// Global catalog API instance
let catalogAPI = new CatalogAPI();

// =============================================================================
// CATALOG HISTORY LOGGING
// =============================================================================

let catalogHistoryEntries = [];
let catalogHistorySortColumn = "";
let catalogHistorySortAsc = true;
let catalogHistoryFilterText = "";
let selectedNumistaResult = null;

/**
 * Save catalog history to localStorage
 */
const saveCatalogHistory = () => {
  try {
    saveDataSync(CATALOG_HISTORY_KEY, catalogHistory);
  } catch (e) {
    console.warn("Failed to save catalog history:", e);
  }
};

/**
 * Load catalog history from localStorage
 */
const loadCatalogHistory = () => {
  catalogHistory = loadDataSync(CATALOG_HISTORY_KEY, []);
};

/**
 * Purge catalog history entries older than given number of days
 * @param {number} days - Maximum age in days (default 180)
 */
const purgeCatalogHistory = (days = 180) => {
  const cutoff = new Date();
  cutoff.setDate(cutoff.getDate() - days);
  const cutoffStr = cutoff.toISOString().slice(0, 10).replace(/-/g, "-");
  catalogHistory = catalogHistory.filter(
    (e) => e.timestamp >= cutoffStr
  );
};

/**
 * Record a catalog API call to history
 * @param {Object} entry - History entry data
 */
const recordCatalogHistory = (entry) => {
  loadCatalogHistory();
  purgeCatalogHistory();

  const now = new Date();
  const pad = (n) => String(n).padStart(2, "0");
  const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;

  catalogHistory.push({
    timestamp,
    action: entry.action || "lookup",
    query: entry.query || "",
    result: entry.result || "success",
    itemCount: entry.itemCount || 0,
    provider: entry.provider || "",
    duration: entry.duration || 0,
    error: entry.error || null,
  });

  saveCatalogHistory();
};

/**
 * Renders catalog history table with filtering and sorting
 * Mirrors renderApiHistoryTable() in api.js
 */
const renderCatalogHistoryTable = () => {
  const table = document.getElementById("catalogHistoryTable");
  if (!table) return;

  let data = [...catalogHistoryEntries];
  if (catalogHistoryFilterText) {
    const f = catalogHistoryFilterText.toLowerCase();
    data = data.filter((e) =>
      Object.values(e).some((v) => String(v).toLowerCase().includes(f))
    );
  }
  if (catalogHistorySortColumn) {
    data.sort((a, b) => {
      const valA = a[catalogHistorySortColumn];
      const valB = b[catalogHistorySortColumn];
      if (valA < valB) return catalogHistorySortAsc ? -1 : 1;
      if (valA > valB) return catalogHistorySortAsc ? 1 : -1;
      return 0;
    });
  }
  if (!catalogHistorySortColumn) {
    data.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
  }

  let html =
    '<tr><th data-column="timestamp">Time</th><th data-column="action">Action</th><th data-column="query">Query</th><th data-column="result">Result</th><th data-column="itemCount">Items</th><th data-column="provider">Provider</th><th data-column="duration">Duration</th></tr>';
  data.forEach((e) => {
    const resultClass = e.result === "fail" ? ' style="color: var(--danger, #e74c3c);"' : "";
    const errorTitle = e.error ? ` title="${e.error.replace(/"/g, "&quot;")}"` : "";
    html += `<tr><td>${e.timestamp}</td><td>${e.action}</td><td>${e.query}</td><td${resultClass}${errorTitle}>${e.result}</td><td>${e.itemCount}</td><td>${e.provider || ""}</td><td>${e.duration}ms</td></tr>`;
  });
  // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
  table.innerHTML = html;

  table.querySelectorAll("th").forEach((th) => {
    th.addEventListener("click", () => {
      const col = th.dataset.column;
      if (catalogHistorySortColumn === col) {
        catalogHistorySortAsc = !catalogHistorySortAsc;
      } else {
        catalogHistorySortColumn = col;
        catalogHistorySortAsc = true;
      }
      renderCatalogHistoryTable();
    });
  });
};

/**
 * Shows catalog history modal
 */
const showCatalogHistoryModal = () => {
  const modal = document.getElementById("catalogHistoryModal");
  if (!modal) return;

  loadCatalogHistory();
  catalogHistoryEntries = [...catalogHistory];
  catalogHistorySortColumn = "";
  catalogHistorySortAsc = true;
  catalogHistoryFilterText = "";

  const filterInput = document.getElementById("catalogHistoryFilter");
  const clearFilterBtn = document.getElementById("catalogHistoryClearFilterBtn");
  if (filterInput) {
    filterInput.value = "";
    filterInput.oninput = (e) => {
      catalogHistoryFilterText = e.target.value;
      renderCatalogHistoryTable();
    };
  }
  if (clearFilterBtn) {
    clearFilterBtn.onclick = () => {
      catalogHistoryFilterText = "";
      if (filterInput) filterInput.value = "";
      renderCatalogHistoryTable();
    };
  }
  renderCatalogHistoryTable();
  modal.style.display = "flex";
};

/**
 * Hides catalog history modal
 */
const hideCatalogHistoryModal = () => {
  const modal = document.getElementById("catalogHistoryModal");
  if (modal) modal.style.display = "none";
};

// =============================================================================
// NUMISTA RESULTS MODAL — Search results + field picker UI
// =============================================================================

/**
 * Escape HTML for safe insertion into innerHTML
 * @param {string} str - Raw string
 * @returns {string} Escaped string
 */
const escapeHtmlCatalog = (str) =>
  String(str || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');

/**
 * Render a single result card HTML string
 * @param {Object} result - Normalized Numista item data
 * @param {number} index - Index in results array
 * @returns {string} HTML string
 */
const renderNumistaResultCard = (result, index) => {
  const placeholder = `<div class="numista-img-placeholder">🪙</div>`;
  const obverseImg = result.imageUrl
    ? `<img src="${escapeHtmlCatalog(result.imageUrl)}" alt="Obverse" loading="lazy">`
    : placeholder;
  const reverseImg = result.reverseImageUrl
    ? `<img src="${escapeHtmlCatalog(result.reverseImageUrl)}" alt="Reverse" loading="lazy">`
    : '';
  const meta = [
    result.year,
    result.country,
    result.metal,
    result.weight ? `${result.weight}g` : '',
    result.type
  ].filter(Boolean).join(' · ');

  return `<div class="numista-result-card" data-result-index="${index}">
    <div class="numista-result-images">${obverseImg}${reverseImg}</div>
    <div class="numista-result-info">
      <div class="numista-result-name">${escapeHtmlCatalog(result.name)}</div>
      <div class="numista-result-meta">${escapeHtmlCatalog(meta)}</div>
      <div class="numista-result-id">N#${escapeHtmlCatalog(result.catalogId)}</div>
    </div>
  </div>`;
};

/**
 * Render the selected item preview in field picker
 * @param {Object} result - Normalized Numista item data
 * @returns {string} HTML string
 */
const renderNumistaSelectedItem = (result) => {
  const placeholder = `<div class="numista-img-placeholder">🪙</div>`;
  const obverseImg = result.imageUrl
    ? `<img src="${escapeHtmlCatalog(result.imageUrl)}" alt="Obverse" loading="lazy">`
    : placeholder;
  const reverseImg = result.reverseImageUrl
    ? `<img src="${escapeHtmlCatalog(result.reverseImageUrl)}" alt="Reverse" loading="lazy">`
    : '';
  const meta = [
    result.year,
    result.country,
    result.metal,
    result.weight ? `${result.weight}g` : '',
    result.type
  ].filter(Boolean).join(' · ');

  return `<div class="numista-result-images">${obverseImg}${reverseImg}</div>
    <div class="numista-result-info">
      <div class="numista-result-name">${escapeHtmlCatalog(result.name)}</div>
      <div class="numista-result-meta">${escapeHtmlCatalog(meta)}</div>
      <div class="numista-result-id">N#${escapeHtmlCatalog(result.catalogId)}</div>
    </div>`;
};

/**
 * Check if a value matches a valid <select> option
 * @param {string} selectId - DOM id of the select element
 * @param {string} value - Value to check
 * @returns {boolean}
 */
const isValidSelectOption = (selectId, value) => {
  const el = document.getElementById(selectId);
  if (!el) return false;
  return Array.from(el.options).some(o => o.value === value);
};

/**
 * Render field checkboxes with editable input fields for the selected result.
 * Each row: [checkbox] [label] [editable text input]
 * User can tweak values before hitting "Fill Fields".
 * @param {Object} result - Normalized Numista item data
 */
const renderNumistaFieldCheckboxes = (result) => {
  const container = document.getElementById('numistaFieldCheckboxes');
  if (!container) return;

  const typeValid = result.type && isValidSelectOption('itemType', result.type);

  // Fields ordered: primary (checked by default) first, then optional (unchecked)
  const fields = [
    { key: 'name', label: 'Name', value: result.name || '', available: true, defaultOn: true },
    { key: 'catalog', label: 'Catalog N#', value: result.catalogId || '', available: true, defaultOn: true },
    { key: 'year', label: 'Year', value: result.year || '', available: !!result.year, defaultOn: false },
    {
      key: 'type', label: 'Type',
      value: result.type || '',
      available: typeValid,
      defaultOn: false,
      warn: result.type && !typeValid ? `"${result.type}" — not in form options` : ''
    },
    { key: 'weight', label: 'Weight (g)', value: result.weight ? String(result.weight) : '', available: result.weight > 0, defaultOn: result.weight > 0 },
  ];

  // Keep the heading, rebuild field rows
  const heading = container.querySelector('.numista-fields-heading');
  container.innerHTML = '';
  if (heading) {
    container.appendChild(heading);
  } else {
    const h = document.createElement('div');
    h.className = 'numista-fields-heading';
    h.textContent = 'Fields to fill:';
    container.appendChild(h);
  }

  // Map field keys to current form values for "Current:" hints
  const currentFormValues = {
    name: (elements.itemName || document.getElementById('itemName'))?.value?.trim() || '',
    catalog: (elements.itemCatalog || document.getElementById('itemCatalog'))?.value?.trim() || '',
    year: (elements.itemYear || document.getElementById('itemYear'))?.value?.trim() || '',
    type: (elements.itemType || document.getElementById('itemType'))?.value || '',
    weight: (elements.itemWeight || document.getElementById('itemWeight'))?.value?.trim() || '',
  };

  fields.forEach(f => {
    // Checkbox — grid column 1
    const cb = document.createElement('input');
    cb.type = 'checkbox';
    cb.name = 'numistaField';
    cb.value = f.key;
    cb.checked = f.available && !!f.value && f.defaultOn;
    if (!f.value) cb.disabled = true;

    // Label — grid column 2
    const label = document.createElement('span');
    label.className = 'numista-field-label';
    label.textContent = f.label + ':';

    // Editable text input — grid column 3
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'numista-field-input';
    input.name = 'numistaFieldValue_' + f.key;
    input.value = f.value;
    input.placeholder = f.available ? '' : 'N/A';
    if (!f.available && !f.value) input.disabled = true;

    // Toggle input enabled/disabled when checkbox changes
    cb.addEventListener('change', () => { input.disabled = !cb.checked; });
    if (!cb.checked) input.disabled = true;

    container.appendChild(cb);
    container.appendChild(label);
    container.appendChild(input);

    // "Current:" hint showing existing form value (helps user compare before filling)
    const currentVal = currentFormValues[f.key];
    if (currentVal) {
      const hint = document.createElement('div');
      hint.className = 'numista-field-current';
      hint.textContent = `Current: ${currentVal}`;
      hint.title = currentVal;
      container.appendChild(hint);
    }

    // Warning text spanning all columns (e.g. "Alloy/Other — not in form options")
    if (f.warn) {
      const warn = document.createElement('div');
      warn.className = 'numista-field-warn';
      warn.textContent = f.warn;
      container.appendChild(warn);
    }
  });
};

/**
 * Curated list of popular bullion items for quick-pick in the no-results modal.
 * Focused on items silver/gold stackers commonly own.
 * @returns {Array<{id: string, name: string, metal: string}>}
 */
const getPopularNumistaItems = () => [
  // Silver bullion
  { id: '1493',   name: 'American Silver Eagle',         metal: 'Silver' },
  { id: '298883', name: 'American Silver Eagle (New Rev)', metal: 'Silver' },
  { id: '9164',   name: 'Canadian Silver Maple Leaf',    metal: 'Silver' },
  { id: '13410',  name: 'British Silver Britannia',      metal: 'Silver' },
  { id: '9165',   name: 'Austrian Silver Philharmonic',  metal: 'Silver' },
  { id: '13855',  name: 'Mexican Silver Libertad',       metal: 'Silver' },
  { id: '143754', name: 'Silver Krugerrand',             metal: 'Silver' },
  // Gold bullion
  { id: '23134',  name: 'American Gold Eagle',           metal: 'Gold' },
  { id: '18451',  name: 'American Gold Buffalo',         metal: 'Gold' },
  { id: '6002',   name: 'Gold Krugerrand',               metal: 'Gold' },
  { id: '15150',  name: 'Austrian Gold Philharmonic',    metal: 'Gold' },
  // Classic silver
  { id: '1492',   name: 'Morgan Dollar',                 metal: 'Silver' },
  { id: '5580',   name: 'Peace Dollar',                  metal: 'Silver' },
];

/**
 * Show Numista results modal with search results
 * @param {Array} results - Array of normalized Numista item data
 * @param {boolean} directLookup - If true and single result, skip list and show field picker
 * @param {string} originalQuery - The search query used (for retry pre-fill)
 */
const showNumistaResults = (results, directLookup = false, originalQuery = '') => {
  const modal = document.getElementById('numistaResultsModal');
  const list = document.getElementById('numistaResultsList');
  const picker = document.getElementById('numistaFieldPicker');
  const title = document.getElementById('numistaResultsTitle');
  const preview = document.getElementById('numistaSelectedItem');
  if (!modal || !list || !picker) return;

  selectedNumistaResult = null;
  list.innerHTML = '';
  picker.style.display = 'none';

  if (!results || results.length === 0) {
    title.textContent = 'No Results';

    // Build quick-picks from curated popular items
    const popularItems = getPopularNumistaItems();
    const quickPicksHtml = `<div class="numista-quick-picks">
        <p class="numista-quick-picks-label">Popular bullion items:</p>
        <div class="numista-quick-picks-list">
          ${popularItems.map(item =>
            `<button type="button" class="numista-quick-pick" data-numista-id="${escapeHtmlCatalog(item.id)}">
              <span class="quick-pick-id">N#${escapeHtmlCatalog(item.id)}</span>
              <span class="quick-pick-name">${escapeHtmlCatalog(item.name)}</span>
              <span class="quick-pick-count">${escapeHtmlCatalog(item.metal)}</span>
            </button>`
          ).join('')}
        </div>
      </div>`;

    // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
    list.innerHTML = `<div class="numista-no-results-enhanced">
      <div class="numista-retry-search">
        <p>No matching items found on Numista.</p>
        <div class="numista-retry-row">
          <input type="text" id="numistaRetryInput" class="numista-retry-input"
                 placeholder="Refine your search..." value="${escapeHtmlCatalog(originalQuery)}">
          <button type="button" id="numistaRetryBtn" class="btn btn-primary numista-retry-btn">Search</button>
        </div>
      </div>
      ${quickPicksHtml}
    </div>`;
    list.style.display = 'block';
    modal.style.display = 'flex';

    // Wire up retry search
    const retryBtn = document.getElementById('numistaRetryBtn');
    const retryInput = document.getElementById('numistaRetryInput');
    if (retryBtn && retryInput) {
      const doRetry = async () => {
        const query = retryInput.value.trim();
        if (!query) return;
        retryBtn.disabled = true;
        retryBtn.textContent = 'Searching\u2026';
        try {
          const retryResults = await catalogAPI.searchItems(query, { limit: 20 });
          showNumistaResults(retryResults, false, query);
        } catch (err) {
          console.error('Numista retry search failed:', err);
          retryBtn.textContent = 'Failed';
          setTimeout(() => { retryBtn.textContent = 'Search'; retryBtn.disabled = false; }, 1500);
        }
      };
      retryBtn.addEventListener('click', doRetry);
      retryInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doRetry(); });
      // Auto-focus the search input for quick editing
      setTimeout(() => retryInput.select(), 50);
    }

    // Wire up quick-pick clicks
    list.querySelectorAll('.numista-quick-pick').forEach(pickBtn => {
      pickBtn.addEventListener('click', async () => {
        const nId = pickBtn.dataset.numistaId;
        if (!nId) return;
        pickBtn.style.opacity = '0.5';
        pickBtn.disabled = true;
        try {
          const result = await catalogAPI.lookupItem(nId);
          showNumistaResults(result ? [result] : [], true, nId);
        } catch (err) {
          console.error('Numista quick-pick lookup failed:', err);
          pickBtn.style.opacity = '1';
          pickBtn.disabled = false;
        }
      });
    });

    return;
  }

  // Direct lookup with single result → skip to field picker
  if (directLookup && results.length === 1) {
    title.textContent = 'Numista Item Found';
    list.style.display = 'none';
    selectedNumistaResult = results[0];
    // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
    preview.innerHTML = renderNumistaSelectedItem(results[0]);
    renderNumistaFieldCheckboxes(results[0]);
    picker.style.display = 'block';
    modal.style.display = 'flex';
    return;
  }

  // Multiple results → show selectable card list with search refinement
  title.textContent = `Numista Results (${results.length})`;
  // Stash results on the list element for delegated click retrieval
  list._numistaResults = results;

  // Build refinement search bar + result cards
  const searchBarHtml = `<div class="numista-refine-search">
    <input type="text" id="numistaRefineInput" class="numista-refine-input"
           placeholder="Refine search..." value="${escapeHtmlCatalog(originalQuery)}">
    <button type="button" id="numistaRefineBtn" class="btn btn-primary numista-refine-btn">Search</button>
  </div>`;
  const cardsHtml = results.slice(0, 20).map((r, i) => renderNumistaResultCard(r, i)).join('');
  // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
  list.innerHTML = searchBarHtml + cardsHtml;
  list.style.display = 'flex';
  modal.style.display = 'flex';

  // Wire up refinement search
  const refineBtn = document.getElementById('numistaRefineBtn');
  const refineInput = document.getElementById('numistaRefineInput');
  if (refineBtn && refineInput) {
    const doRefine = async () => {
      const query = refineInput.value.trim();
      if (!query) return;
      refineBtn.disabled = true;
      refineBtn.textContent = 'Searching\u2026';
      try {
        const refineResults = await catalogAPI.searchItems(query, { limit: 20 });
        showNumistaResults(refineResults, false, query);
      } catch (err) {
        console.error('Numista refine search failed:', err);
        refineBtn.textContent = 'Failed';
        setTimeout(() => { refineBtn.textContent = 'Search'; refineBtn.disabled = false; }, 1500);
      }
    };
    refineBtn.addEventListener('click', doRefine);
    refineInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doRefine(); });
    setTimeout(() => refineInput.select(), 50);
  }
};

/**
 * Fill form fields from the editable picker inputs.
 * Reads values from the numistaFieldValue_* text inputs (user may have edited them).
 */
const fillFormFromNumistaResult = () => {
  const container = document.getElementById('numistaFieldCheckboxes');
  if (!container) return;

  // Collect checked fields and their edited values from the picker inputs
  const checkboxes = container.querySelectorAll('input[name="numistaField"]');

  // Intercept for bulk edit — when callback is set, route field values there instead
  if (typeof window._bulkEditNumistaCallback === 'function') {
    const fieldMap = {};
    checkboxes.forEach(cb => {
      if (!cb.checked) return;
      const input = container.querySelector('input[name="numistaFieldValue_' + cb.value + '"]');
      if (input && input.value.trim()) fieldMap[cb.value] = input.value.trim();
    });
    window._bulkEditNumistaCallback(fieldMap);
    window._bulkEditNumistaCallback = null;
    closeNumistaResultsModal();
    return;
  }

  checkboxes.forEach(cb => {
    if (!cb.checked) return;
    const input = container.querySelector(`input[name="numistaFieldValue_${cb.value}"]`);
    if (!input) return;
    const val = input.value.trim();
    if (!val) return;

    switch (cb.value) {
      case 'name': {
        const el = elements.itemName || document.getElementById('itemName');
        if (el) el.value = val;
        break;
      }
      case 'year': {
        const el = elements.itemYear || document.getElementById('itemYear');
        if (el) el.value = val;
        break;
      }
      case 'type': {
        const el = elements.itemType || document.getElementById('itemType');
        if (el) {
          const valid = Array.from(el.options).map(o => o.value);
          if (valid.includes(val)) el.value = val;
        }
        break;
      }
      case 'weight': {
        const el = elements.itemWeight || document.getElementById('itemWeight');
        const unitEl = document.getElementById('itemWeightUnit');
        const num = parseFloat(val);
        if (el && !isNaN(num) && num > 0) {
          el.value = num;
          if (unitEl) unitEl.value = 'g';
        }
        break;
      }
      case 'catalog': {
        const el = elements.itemCatalog || document.getElementById('itemCatalog');
        if (el) el.value = val;
        break;
      }
    }
  });
};

/**
 * Close Numista results modal and clean up state
 */
const closeNumistaResultsModal = () => {
  const modal = document.getElementById('numistaResultsModal');
  if (modal) modal.style.display = 'none';
  selectedNumistaResult = null;
};

// Test function for Numista API
async function testNumistaAPI() {
  if (!catalogConfig.isNumistaEnabled()) {
    console.log('❌ Numista API not configured');
    return;
  }

  console.log('🧪 Testing Numista API...');
  
  try {
    // Test with a known coin ID (American Silver Eagle)
    const testId = '5685'; // This is a common test ID for American Silver Eagle 1986
    const result = await catalogAPI.lookupItem(testId, { action: 'test' });
    console.log('✅ Numista API test successful:', result);
    return result;
  } catch (error) {
    console.error('❌ Numista API test failed:', error);
    return null;
  }
}

/**
 * Renders Numista API usage progress bar into #numistaUsageBar
 * Reuses the same .api-usage / .usage-bar / .usage-text CSS as metals providers
 */
const renderNumistaUsageBar = () => {
  const container = document.getElementById('numistaUsageBar');
  if (!container) return;
  const usage = catalogConfig.getNumistaUsage();
  // Coerce to safe finite numbers and validate month format
  const used = Number.isFinite(usage.used) ? Math.max(0, usage.used) : 0;
  const quota = Number.isFinite(usage.quota) && usage.quota > 0 ? usage.quota : 2000;
  const month = /^\d{4}-\d{2}$/.test(usage.month) ? usage.month : '';
  const usedPercent = Math.min((used / quota) * 100, 100);
  const remainingPercent = 100 - usedPercent;
  const warning = used / quota >= 0.9;

  // Build DOM nodes safely (no innerHTML with localStorage-sourced values)
  container.textContent = '';
  const wrapper = document.createElement('div');
  wrapper.className = 'api-usage';
  const bar = document.createElement('div');
  bar.className = 'usage-bar';
  const usedDiv = document.createElement('div');
  usedDiv.className = 'used';
  usedDiv.style.width = `${usedPercent}%`;
  const remainDiv = document.createElement('div');
  remainDiv.className = 'remaining';
  remainDiv.style.width = `${remainingPercent}%`;
  bar.appendChild(usedDiv);
  bar.appendChild(remainDiv);
  const text = document.createElement('div');
  text.className = 'usage-text';
  text.textContent = `${used}/${quota} calls${month ? ` (${month})` : ''}${warning ? ' 🚩' : ''}`;
  wrapper.appendChild(bar);
  wrapper.appendChild(text);
  container.appendChild(wrapper);
};

/**
 * Renders PCGS API usage progress bar into #pcgsUsageBar
 * Clones the Numista pattern but uses daily granularity (1,000/day)
 */
const renderPcgsUsageBar = () => {
  const container = document.getElementById('pcgsUsageBar');
  if (!container) return;
  const usage = catalogConfig.getPcgsUsage();
  const used = Number.isFinite(usage.used) ? Math.max(0, usage.used) : 0;
  const quota = Number.isFinite(usage.limit) && usage.limit > 0 ? usage.limit : 1000;
  const day = /^\d{4}-\d{2}-\d{2}$/.test(usage.date) ? usage.date : '';
  const usedPercent = Math.min((used / quota) * 100, 100);
  const remainingPercent = 100 - usedPercent;
  const warning = used / quota >= 0.9;

  container.textContent = '';
  const wrapper = document.createElement('div');
  wrapper.className = 'api-usage';
  const bar = document.createElement('div');
  bar.className = 'usage-bar';
  const usedDiv = document.createElement('div');
  usedDiv.className = 'used';
  usedDiv.style.width = `${usedPercent}%`;
  const remainDiv = document.createElement('div');
  remainDiv.className = 'remaining';
  remainDiv.style.width = `${remainingPercent}%`;
  bar.appendChild(usedDiv);
  bar.appendChild(remainDiv);
  const text = document.createElement('div');
  text.className = 'usage-text';
  text.textContent = `${used}/${quota} calls${day ? ` (${day})` : ''}${warning ? ' \uD83D\uDEA9' : ''}`;
  wrapper.appendChild(bar);
  wrapper.appendChild(text);
  container.appendChild(wrapper);
};

// =============================================================================
// CATALOG HISTORY — SETTINGS LOG TABLE
// =============================================================================

/** @type {string} Sort column for settings catalog history table */
let settingsCatalogSortColumn = '';
/** @type {boolean} Sort ascending for settings catalog history table */
let settingsCatalogSortAsc = true;

/**
 * Renders the catalog history table in the Settings > Activity Log > Catalogs sub-tab.
 * Reads from global catalogHistory, sorts by timestamp descending by default.
 */
const renderCatalogHistoryForSettings = () => {
  const table = document.getElementById('settingsCatalogHistoryTable');
  if (!table) return;

  loadCatalogHistory();
  let data = [...catalogHistory];

  // Sort
  if (settingsCatalogSortColumn) {
    data.sort((a, b) => {
      const valA = a[settingsCatalogSortColumn];
      const valB = b[settingsCatalogSortColumn];
      if (valA < valB) return settingsCatalogSortAsc ? -1 : 1;
      if (valA > valB) return settingsCatalogSortAsc ? 1 : -1;
      return 0;
    });
  } else {
    data.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
  }

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

  if (data.length === 0) {
    // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml
    tbody.innerHTML = '<tr class="settings-log-empty"><td colspan="7">No catalog history recorded yet.</td></tr>';
    return;
  }

  const rows = data.map(e => {
    const resultClass = e.result === 'fail' ? ' style="color: var(--danger, #e74c3c);"' : '';
    const errorTitle = e.error ? ` title="${String(e.error).replace(/"/g, '&quot;')}"` : '';
    return `<tr><td>${e.timestamp || ''}</td><td>${e.action || ''}</td><td>${e.query || ''}</td><td${resultClass}${errorTitle}>${e.result || ''}</td><td>${e.itemCount || 0}</td><td>${e.provider || ''}</td><td>${e.duration || 0}ms</td></tr>`;
  });

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

  // Sortable headers
  table.querySelectorAll('th').forEach(th => {
    th.style.cursor = 'pointer';
    th.onclick = () => {
      const cols = ['timestamp', 'action', 'query', 'result', 'itemCount', 'provider', 'duration'];
      const idx = Array.from(th.parentNode.children).indexOf(th);
      const col = cols[idx];
      if (settingsCatalogSortColumn === col) {
        settingsCatalogSortAsc = !settingsCatalogSortAsc;
      } else {
        settingsCatalogSortColumn = col;
        settingsCatalogSortAsc = true;
      }
      renderCatalogHistoryForSettings();
    };
  });
};

/**
 * Clears all catalog API history after user confirmation.
 */
const clearCatalogHistory = () => {
  if (!confirm('Clear all catalog history? This cannot be undone.')) return;
  catalogHistory = [];
  saveCatalogHistory();
  const panel = document.getElementById('logPanel_catalogs');
  if (panel) delete panel.dataset.rendered;
  renderCatalogHistoryForSettings();
};

// Export for use in other modules
if (typeof window !== 'undefined') {
  window.catalogAPI = catalogAPI;
  window.catalogConfig = catalogConfig;
  window.testNumistaAPI = testNumistaAPI;
  window.CatalogAPI = CatalogAPI;
  window.NumistaProvider = NumistaProvider;
  window.LocalProvider = LocalProvider;
  window.showCatalogHistoryModal = showCatalogHistoryModal;
  window.hideCatalogHistoryModal = hideCatalogHistoryModal;
  window.recordCatalogHistory = recordCatalogHistory;
  window.loadCatalogHistory = loadCatalogHistory;
  window.saveCatalogHistory = saveCatalogHistory;
  window.showNumistaResults = showNumistaResults;
  window.fillFormFromNumistaResult = fillFormFromNumistaResult;
  window.closeNumistaResultsModal = closeNumistaResultsModal;
  window.renderNumistaUsageBar = renderNumistaUsageBar;
  window.renderPcgsUsageBar = renderPcgsUsageBar;
  window.renderCatalogHistoryForSettings = renderCatalogHistoryForSettings;
  window.clearCatalogHistory = clearCatalogHistory;
}

// Initialize UI event handlers when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
  // Numista API key input handler
  const numistaApiKeyInput = document.getElementById('numistaApiKey');
  const saveNumistaBtn = document.getElementById('saveNumistaBtn');
  const testNumistaBtn = document.getElementById('testNumistaBtn');
  const clearNumistaBtn = document.getElementById('clearNumistaBtn');

  if (numistaApiKeyInput) {
    // Load existing API key
    const existingConfig = catalogConfig.getNumistaConfig();
    if (existingConfig.apiKey) {
      numistaApiKeyInput.value = existingConfig.apiKey;
    }

    // Save API key when input changes
    numistaApiKeyInput.addEventListener('change', function() {
      const apiKey = this.value.trim();
      if (apiKey) {
        catalogConfig.setNumistaConfig(apiKey, 2000);
        catalogAPI.initializeProviders();
        console.log('✅ Numista API key saved');
      }
    });
  }

  // Save key button
  if (saveNumistaBtn) {
    saveNumistaBtn.addEventListener('click', function() {
      const apiKey = numistaApiKeyInput?.value.trim();
      if (!apiKey) {
        alert('Please enter your Numista API key first');
        return;
      }
      catalogConfig.setNumistaConfig(apiKey, 2000);
      catalogAPI.initializeProviders();
      renderNumistaUsageBar();
      alert('Numista API key saved.');
    });
  }

  // Test connection button
  if (testNumistaBtn) {
    testNumistaBtn.addEventListener('click', async function() {
      const apiKey = numistaApiKeyInput?.value.trim();
      if (!apiKey) {
        alert('Please enter your Numista API key first');
        return;
      }

      // Save the key first
      catalogConfig.setNumistaConfig(apiKey, 2000);
      catalogAPI.initializeProviders();

      // Test the connection
      this.textContent = 'Testing...';
      this.disabled = true;

      try {
        const result = await testNumistaAPI();
        if (result) {
          renderNumistaUsageBar();
          alert('✅ Numista API connection successful!');
        } else {
          alert('❌ Numista API connection failed. Please check your API key.');
        }
      } catch (error) {
        alert('❌ Connection failed: ' + error.message);
      } finally {
        this.textContent = 'Test Connection';
        this.disabled = false;
      }
    });
  }

  // Clear API key button
  if (clearNumistaBtn) {
    clearNumistaBtn.addEventListener('click', function() {
      if (confirm('Are you sure you want to clear your Numista API key?')) {
        catalogConfig.clearNumistaKey();
        if (numistaApiKeyInput) {
          numistaApiKeyInput.value = '';
        }
        catalogAPI.initializeProviders();
        console.log('🗑️ Numista API key cleared');
      }
    });
  }

  // =========================================================================
  // PCGS API — settings UI event wiring
  // =========================================================================

  const pcgsTokenInput = document.getElementById('pcgsBearerToken');
  const savePcgsBtn = document.getElementById('savePcgsBtn');
  const testPcgsBtn = document.getElementById('testPcgsBtn');
  const clearPcgsBtn = document.getElementById('clearPcgsBtn');
  const pcgsStatus = document.getElementById('pcgsStatus');

  if (pcgsTokenInput) {
    const existingPcgs = catalogConfig.getPcgsConfig();
    if (existingPcgs.bearerToken) {
      pcgsTokenInput.value = existingPcgs.bearerToken;
    }
  }

  if (savePcgsBtn) {
    savePcgsBtn.addEventListener('click', function() {
      const token = pcgsTokenInput?.value.trim();
      if (!token) {
        alert('Please enter your PCGS bearer token first');
        return;
      }
      catalogConfig.setPcgsConfig(token);
      if (pcgsStatus) pcgsStatus.textContent = 'Token saved.';
      // Update provider status indicator and header status row
      const statusEl = document.getElementById('pcgsProviderStatus');
      if (statusEl) {
        statusEl.querySelector('.status-dot')?.classList.add('connected');
        const txt = statusEl.querySelector('.status-text');
        if (txt) txt.textContent = 'Connected';
      }
      if (typeof renderApiStatusSummary === 'function') renderApiStatusSummary();
      renderPcgsUsageBar();
      alert('PCGS bearer token saved.');
    });
  }

  if (testPcgsBtn) {
    testPcgsBtn.addEventListener('click', async function() {
      const token = pcgsTokenInput?.value.trim();
      if (!token) {
        alert('Please enter your PCGS bearer token first');
        return;
      }

      // Save first
      catalogConfig.setPcgsConfig(token);

      this.textContent = 'Testing...';
      this.disabled = true;

      try {
        if (typeof verifyPcgsCert === 'function') {
          const result = await verifyPcgsCert('00000000');
          // Even a "not found" response means the API is reachable
          if (pcgsStatus) pcgsStatus.textContent = 'Connected — API reachable.';
          alert('PCGS API connection successful!');
        } else {
          if (pcgsStatus) pcgsStatus.textContent = 'pcgs-api.js not loaded.';
          alert('PCGS API module not loaded. Ensure pcgs-api.js is included.');
        }
      } catch (error) {
        const msg = error.message || 'Unknown error';
        if (pcgsStatus) pcgsStatus.textContent = 'Connection failed: ' + msg;
        alert('PCGS API connection failed: ' + msg);
      } finally {
        this.textContent = 'Test Connection';
        this.disabled = false;
      }
    });
  }

  if (clearPcgsBtn) {
    clearPcgsBtn.addEventListener('click', function() {
      if (confirm('Are you sure you want to clear your PCGS bearer token?')) {
        catalogConfig.clearPcgsToken();
        if (pcgsTokenInput) pcgsTokenInput.value = '';
        if (pcgsStatus) pcgsStatus.textContent = 'Token cleared.';
        // Update provider status indicator and header status row
        const statusEl = document.getElementById('pcgsProviderStatus');
        if (statusEl) {
          statusEl.querySelector('.status-dot')?.classList.remove('connected');
          const txt = statusEl.querySelector('.status-text');
          if (txt) txt.textContent = 'Disconnected';
        }
        if (typeof renderApiStatusSummary === 'function') renderApiStatusSummary();
      }
    });
  }

  // =========================================================================
  // NUMISTA RESULTS MODAL — event wiring
  // =========================================================================

  const numistaResultsModal = document.getElementById('numistaResultsModal');
  const numistaResultsCloseBtn = document.getElementById('numistaResultsCloseBtn');
  const numistaFillCancelBtn = document.getElementById('numistaFillCancelBtn');
  const numistaFillBtn = document.getElementById('numistaFillBtn');
  const numistaResultsList = document.getElementById('numistaResultsList');

  // Close button
  if (numistaResultsCloseBtn) {
    numistaResultsCloseBtn.addEventListener('click', closeNumistaResultsModal);
  }

  // Cancel button in field picker
  if (numistaFillCancelBtn) {
    numistaFillCancelBtn.addEventListener('click', closeNumistaResultsModal);
  }

  // Fill Fields button
  if (numistaFillBtn) {
    numistaFillBtn.addEventListener('click', function() {
      if (selectedNumistaResult) {
        fillFormFromNumistaResult();

        // Fire-and-forget: cache images + metadata in IndexedDB
        if (window.imageCache?.isAvailable() && selectedNumistaResult.catalogId &&
            window.featureFlags?.isEnabled('COIN_IMAGES')) {
          imageCache.cacheImages(
            selectedNumistaResult.catalogId,
            selectedNumistaResult.imageUrl || '',
            selectedNumistaResult.reverseImageUrl || ''
          ).catch(e => console.warn('Image cache failed:', e));
          imageCache.cacheMetadata(
            selectedNumistaResult.catalogId,
            selectedNumistaResult
          ).catch(e => console.warn('Metadata cache failed:', e));
        }
      }
      closeNumistaResultsModal();
    });
  }

  // Delegated click on result cards → select and show field picker
  if (numistaResultsList) {
    numistaResultsList.addEventListener('click', function(e) {
      const card = e.target.closest('.numista-result-card');
      if (!card) return;

      const index = parseInt(card.dataset.resultIndex, 10);
      const results = numistaResultsList._numistaResults;
      if (!results || !results[index]) return;

      // Highlight selected card
      numistaResultsList.querySelectorAll('.numista-result-card').forEach(c => c.classList.remove('selected'));
      card.classList.add('selected');

      // Transition to field picker
      selectedNumistaResult = results[index];
      const preview = document.getElementById('numistaSelectedItem');
      const picker = document.getElementById('numistaFieldPicker');
      const title = document.getElementById('numistaResultsTitle');
      // nosemgrep: javascript.browser.security.insecure-innerhtml.insecure-innerhtml, javascript.browser.security.insecure-document-method.insecure-document-method
      if (preview) preview.innerHTML = renderNumistaSelectedItem(selectedNumistaResult);
      renderNumistaFieldCheckboxes(selectedNumistaResult);
      if (numistaResultsList) numistaResultsList.style.display = 'none';
      if (picker) picker.style.display = 'block';
      if (title) title.textContent = 'Fill Form Fields';
    });
  }

  // Background click dismiss
  if (numistaResultsModal) {
    numistaResultsModal.addEventListener('click', function(e) {
      if (e.target === numistaResultsModal) {
        closeNumistaResultsModal();
      }
    });
  }

  // ESC key handler — results modal has higher z-index, check it first
  document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
      const resultsModal = document.getElementById('numistaResultsModal');
      if (resultsModal && resultsModal.style.display !== 'none') {
        e.stopImmediatePropagation();
        closeNumistaResultsModal();
      }
    }
  });
});