state/config/config.state.tstypescript
/**
 * Config Store - Zustand store for SDK configuration management
 */

import { create } from 'zustand';
import { createLogger } from '@/core/logger';
import type {
  ConfigState,
  PageType,
  PaymentConfig,
  GoogleMapsConfig,
  AddressConfig,
  DiscountDefinition,
} from '@/types/global';

const logger = createLogger('ConfigStore');

/**
 * The canonical form of `tag` if it is a usable BCP 47 locale, otherwise `null`.
 *
 * Guards the one config value that would otherwise throw on *every price on the page*:
 * `new Intl.NumberFormat('de_DE')` raises a `RangeError`, and an underscore instead of a
 * hyphen is the obvious way to mistype a locale. Rejecting it here means a bad tag costs a
 * warning and the browser's own locale, not a blank storefront.
 */
function canonicalLocale(tag: unknown): string | null {
  if (typeof tag !== 'string' || tag.trim() === '') return null;
  try {
    return Intl.getCanonicalLocales(tag.trim())[0] ?? null;
  } catch {
    return null;
  }
}

interface ConfigActions {
  loadFromMeta: () => void;
  loadFromWindow: () => void;
  updateConfig: (config: Partial<ConfigState>) => void;
  setSpreedlyEnvironmentKey: (key: string) => void;
  reset: () => void;
  getCurrency: () => string;
}

const initialState: ConfigState = {
  apiKey: '',
  campaignId: '',
  debug: false,
  debugger: false,
  pageType: 'product',
  // spreedlyEnvironmentKey: undefined, - omitted to avoid exactOptionalPropertyTypes issue
  paymentConfig: {},
  googleMapsConfig: {},
  addressConfig: {},

  // Additional configuration with enterprise defaults
  autoInit: true,
  rateLimit: 4,
  cacheTtl: 300,
  retryAttempts: 3,
  timeout: 10000,
  testMode: false,

  // API and performance settings
  maxRetries: 3,
  requestTimeout: 30000,
  enableAnalytics: true,
  enableDebugMode: false,

  // Environment and deployment settings
  environment: 'production',
  // version: undefined, - omitted
  // buildTimestamp: undefined, - omitted

  // Discount system
  discounts: {},

  // Attribution
  // utmTransfer: undefined, - omitted

  // Tracking configuration
  tracking: 'auto', // 'auto', 'manual', 'disabled'

  // Location and currency detection
  detectedCountry: '',
  detectedCurrency: '',
  detectedIp: '', // User's IP address from location detection
  selectedCurrency: '',
  locationData: null as any, // Cache the entire location response
  currencyBehavior: 'auto' as 'auto' | 'manual', // Default to auto-switch currency on country change
  currencyFallbackOccurred: false, // Track if currency fallback happened
  // locale: undefined, - omitted deliberately: unset means "follow the browser", which is
  // not the same as pinning 'en-US'. See CurrencyFormatter.getUserLocale().

  clearCartOnInit: false,

  // Error monitoring removed - add externally via HTML/scripts
};

export const configStore = create<ConfigState & ConfigActions>((set, get) => ({
  ...(initialState as ConfigState),

  loadFromMeta: () => {
    if (typeof document === 'undefined') return;

    const updates: Partial<ConfigState> = {};

    // Load API key
    const apiKeyMeta = document.querySelector('meta[name="next-api-key"]');
    if (apiKeyMeta) {
      updates.apiKey = apiKeyMeta.getAttribute('content') ?? '';
    }

    // Load campaign ID
    const campaignIdMeta = document.querySelector(
      'meta[name="next-campaign-id"]'
    );
    if (campaignIdMeta) {
      updates.campaignId = campaignIdMeta.getAttribute('content') ?? '';
    }

    // Load debug flag
    const debugMeta = document.querySelector('meta[name="next-debug"]');
    if (debugMeta) {
      updates.debug = debugMeta.getAttribute('content') === 'true';
    }

    // Load clear cart on init flag
    const clearCartMeta = document.querySelector(
      'meta[name="next-clear-cart"]'
    );
    if (clearCartMeta) {
      updates.clearCartOnInit =
        clearCartMeta.getAttribute('content') === 'true';
    }

    // Load page type
    const pageTypeMeta = document.querySelector('meta[name="next-page-type"]');
    if (pageTypeMeta) {
      updates.pageType = pageTypeMeta.getAttribute('content') as PageType;
    }

    // Load Spreedly environment key (fallback - campaign data takes precedence)
    const spreedlyKeyMeta =
      document.querySelector('meta[name="next-spreedly-key"]') ||
      document.querySelector('meta[name="next-payment-env-key"]');
    if (spreedlyKeyMeta) {
      const spreedlyKey = spreedlyKeyMeta.getAttribute('content');
      if (spreedlyKey) {
        updates.spreedlyEnvironmentKey = spreedlyKey;
      }
    }

    if (Object.keys(updates).length > 0) {
      set(updates);
    }
  },

  loadFromWindow: () => {
    if (typeof window === 'undefined') return;

    const windowConfig = (window as any).nextConfig;
    if (!windowConfig || typeof windowConfig !== 'object') return;

    const updates: Partial<ConfigState> = {};

    if (typeof windowConfig.apiKey === 'string') {
      updates.apiKey = windowConfig.apiKey;
    }

    if (typeof windowConfig.campaignId === 'string') {
      updates.campaignId = windowConfig.campaignId;
    }

    if (typeof windowConfig.debug === 'boolean') {
      updates.debug = windowConfig.debug;
    }

    if (typeof windowConfig.debugger === 'boolean') {
      updates.debugger = windowConfig.debugger;
    }

    if (typeof windowConfig.storeName === 'string') {
      updates.storeName = windowConfig.storeName;
    }

    if (typeof windowConfig.pageType === 'string') {
      updates.pageType = windowConfig.pageType as PageType;
    }

    if (typeof windowConfig.spreedlyEnvironmentKey === 'string') {
      updates.spreedlyEnvironmentKey = windowConfig.spreedlyEnvironmentKey;
    }

    if (windowConfig.payment && typeof windowConfig.payment === 'object') {
      updates.paymentConfig = windowConfig.payment as PaymentConfig;
    }

    // Support both payment and paymentConfig for backwards compatibility
    if (
      windowConfig.paymentConfig &&
      typeof windowConfig.paymentConfig === 'object'
    ) {
      updates.paymentConfig = windowConfig.paymentConfig as PaymentConfig;
    }

    // Load card input configuration from window.nextConfig
    // Supports multiple naming conventions for flexibility and backward compatibility:
    // - cardInputConfig (preferred, generic naming)
    // - spreedly (legacy naming, still supported)
    // - spreedlyConfig (legacy naming, still supported)
    // Priority: cardInputConfig > spreedly > spreedlyConfig
    if (
      windowConfig.cardInputConfig &&
      typeof windowConfig.cardInputConfig === 'object'
    ) {
      if (!updates.paymentConfig) {
        updates.paymentConfig = {};
      }
      updates.paymentConfig.cardInputConfig = windowConfig.cardInputConfig;
    } else if (
      windowConfig.spreedly &&
      typeof windowConfig.spreedly === 'object'
    ) {
      if (!updates.paymentConfig) {
        updates.paymentConfig = {};
      }
      updates.paymentConfig.cardInputConfig = windowConfig.spreedly;
    } else if (
      windowConfig.spreedlyConfig &&
      typeof windowConfig.spreedlyConfig === 'object'
    ) {
      if (!updates.paymentConfig) {
        updates.paymentConfig = {};
      }
      updates.paymentConfig.cardInputConfig = windowConfig.spreedlyConfig;
    }

    if (
      windowConfig.googleMaps &&
      typeof windowConfig.googleMaps === 'object'
    ) {
      updates.googleMapsConfig = windowConfig.googleMaps as GoogleMapsConfig;
    }

    // Load address config from window config
    if (
      windowConfig.addressConfig &&
      typeof windowConfig.addressConfig === 'object'
    ) {
      updates.addressConfig = windowConfig.addressConfig as AddressConfig;
    }

    // Load currency behavior from window config
    if (
      windowConfig.currencyBehavior &&
      (windowConfig.currencyBehavior === 'auto' ||
        windowConfig.currencyBehavior === 'manual')
    ) {
      updates.currencyBehavior = windowConfig.currencyBehavior;
    }

    // Load the price-formatting locale. Left unset on a bad tag so the browser locale
    // still applies — see canonicalLocale().
    if (windowConfig.locale !== undefined) {
      const locale = canonicalLocale(windowConfig.locale);
      if (locale) {
        updates.locale = locale;
      } else {
        logger.warn(
          '[Config] Ignoring invalid locale, using the browser locale instead:',
          windowConfig.locale
        );
      }
    }

    // Load discount definitions from window config
    if (windowConfig.discounts && typeof windowConfig.discounts === 'object') {
      updates.discounts = windowConfig.discounts as Record<
        string,
        DiscountDefinition
      >;
    }

    // Load tracking mode
    if (typeof windowConfig.tracking === 'string') {
      updates.tracking = windowConfig.tracking as
        | 'auto'
        | 'manual'
        | 'disabled';
    }

    // Load analytics configuration
    if (windowConfig.analytics && typeof windowConfig.analytics === 'object') {
      updates.analytics = windowConfig.analytics;
    }

    // Monitoring configuration removed - add error tracking externally via HTML/scripts

    // Load UTM transfer configuration
    if (
      windowConfig.utmTransfer &&
      typeof windowConfig.utmTransfer === 'object'
    ) {
      updates.utmTransfer = windowConfig.utmTransfer;
    }

    if (Object.keys(updates).length > 0) {
      set(updates);
    }
  },

  updateConfig: (config: Partial<ConfigState>) => {
    set(state => ({ ...state, ...config }));
  },

  setSpreedlyEnvironmentKey: (key: string) => {
    set({ spreedlyEnvironmentKey: key });
  },

  reset: () => {
    set(initialState);
  },

  getCurrency: () => {
    const state = get();
    return state.selectedCurrency || state.detectedCurrency || 'USD';
  },
}));

/**
 * The config store — the SDK's resolved {@link ConfigState} (API credentials,
 * page type, payment/address setup, detected location and currency). Mostly
 * read-only at runtime; set from the loader script at init.
 *
 * @example
 * ```ts
 * const currency = useConfigStore.getState().getCurrency();
 * ```
 *
 * @category Core
 */
export const useConfigStore = configStore;