/**
 * Utility functions for Query Suggestions components
 */
import { createElement } from 'react';
// ============================================================================
// Field Extraction
// ============================================================================
/**
 * Get nested value from object using dot notation
 * @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
 */
export const getNestedValue = (obj, path) => {
    if (!obj || !path)
        return undefined;
    return path.split('.').reduce((current, key) => {
        if (current === null || current === undefined)
            return undefined;
        return current[key];
    }, obj);
};
/**
 * Extract suggestion fields from raw data
 */
export const extractSuggestion = (item, mapping = { query: 'query' }) => {
    return {
        query: getNestedValue(item, mapping.query) ?? '',
        count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
        id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
        categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
        highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
        _raw: item,
    };
};
/**
 * Extract product fields from raw data
 */
export const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
    return {
        id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
        title: getNestedValue(item, mapping.title) ?? '',
        image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
        price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
        comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
        url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
        brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
        category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
        rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
        reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
        discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
        inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
        currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
        images: mapping.images ? getNestedValue(item, mapping.images) : item?.images,
        originalPrice: mapping.originalPrice ? getNestedValue(item, mapping.originalPrice) : (item?.original_price ?? item?.compare_at_price),
        available: mapping.available ? getNestedValue(item, mapping.available) : item?.available,
        options: mapping.options ? getNestedValue(item, mapping.options) : item?.options,
        variants: mapping.variants ? getNestedValue(item, mapping.variants) : item?.variants,
        tags: mapping.tags ? getNestedValue(item, mapping.tags) : item?.tags,
        _raw: item,
    };
};
/**
 * Extract category fields from raw data
 */
export const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
    return {
        id: getNestedValue(item, mapping.id) ?? item?.id,
        label: getNestedValue(item, mapping.label) ?? '',
        count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
        icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
        image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
        _raw: item,
    };
};
/**
 * Extract brand fields from raw data
 */
export const extractBrand = (item, mapping = { name: 'name' }) => {
    return {
        id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
        name: getNestedValue(item, mapping.name) ?? '',
        logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
        count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
        _raw: item,
    };
};
// ============================================================================
// Formatting
// ============================================================================
/**
 * Format price with currency
 */
export const formatPrice = (value, config = {}) => {
    if (value === undefined || value === null)
        return '';
    const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
    const num = typeof value === 'string' ? parseFloat(value) : value;
    if (isNaN(num))
        return String(value);
    const formatted = num.toLocaleString(undefined, {
        minimumFractionDigits: priceDecimals,
        maximumFractionDigits: priceDecimals,
    });
    return currencyPosition === 'before'
        ? `${currency}${formatted}`
        : `${formatted}${currency}`;
};
/**
 * Format count with abbreviation (1.2K, 3.4M, etc.)
 */
export const formatCount = (count) => {
    if (count === undefined || count === null)
        return '';
    if (count < 1000)
        return count.toString();
    if (count < 1000000)
        return `${(count / 1000).toFixed(1)}K`;
    return `${(count / 1000000).toFixed(1)}M`;
};
/**
 * Calculate discount percentage
 */
export const calculateDiscount = (price, comparePrice) => {
    if (!price || !comparePrice || comparePrice <= price)
        return undefined;
    return Math.round(((comparePrice - price) / comparePrice) * 100);
};
// ============================================================================
// Text Processing
// ============================================================================
/**
 * Escape HTML special characters
 */
export const escapeHtml = (text) => {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
    };
    return text.replace(/[&<>"']/g, (char) => map[char]);
};
/**
 * Highlight matching text in a string
 */
export const highlightText = (text, query, options = {}) => {
    if (!query || !text)
        return escapeHtml(text);
    const { tag = 'mark', className = '', style } = options;
    const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`(${escapedQuery})`, 'gi');
    const styleAttr = style
        ? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
        : '';
    const classAttr = className ? ` class="${className}"` : '';
    return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
};
/**
 * Safe React-based highlight rendering (no dangerouslySetInnerHTML).
 * Returns React nodes with matched text wrapped in the specified tag element.
 */
export const highlightTextReact = (text, query, options = {}) => {
    if (!query || !text)
        return text;
    const { tag = 'mark', className, style } = options;
    const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`(${escapedQuery})`, 'gi');
    const parts = text.split(regex);
    if (parts.length <= 1)
        return text;
    const defaultHighlightStyle = {
        backgroundColor: 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.4))',
        fontWeight: 500,
        borderRadius: '2px',
        padding: '0 2px',
        ...style,
    };
    return parts.map((part, i) => {
        if (regex.test(part)) {
            // Reset lastIndex after test
            regex.lastIndex = 0;
            return createElement(tag, {
                key: i,
                className,
                style: defaultHighlightStyle,
            }, part);
        }
        // Reset lastIndex after test
        regex.lastIndex = 0;
        return part;
    });
};
/**
 * Truncate text to specified length
 */
export const truncateText = (text, maxLength, ellipsis = '...') => {
    if (!text || text.length <= maxLength)
        return text;
    return text.slice(0, maxLength - ellipsis.length) + ellipsis;
};
// ============================================================================
// Variant Utilities
// ============================================================================
/**
 * Extract badges from product tags (Shopify convention: "badge:new", "badge:limited")
 * and auto-generate sale/soldOut badges from product data.
 */
export const extractBadges = (tags, product) => {
    const badges = [];
    // Extract from tags (Shopify convention)
    if (tags) {
        for (const tag of tags) {
            const lower = tag.toLowerCase().trim();
            if (lower.startsWith('badge:') || lower.startsWith('badge: ')) {
                const text = tag.slice(tag.indexOf(':') + 1).trim();
                if (text) {
                    badges.push({ text, type: 'custom' });
                }
            }
            else if (lower === 'new' || lower === 'new arrival') {
                badges.push({ text: 'New', type: 'new' });
            }
            else if (lower === 'limited' || lower === 'limited edition') {
                badges.push({ text: 'Limited', type: 'limited' });
            }
        }
    }
    // Auto-generate sale badge
    if (product) {
        const comparePrice = product.original_price ?? product.compare_at_price;
        if (comparePrice && product.price && comparePrice > product.price) {
            const discount = Math.round(((comparePrice - product.price) / comparePrice) * 100);
            badges.push({ text: `${discount}% Off`, type: 'sale' });
        }
    }
    // Auto-generate sold out badge
    if (product && product.available === false) {
        badges.push({ text: 'Sold Out', type: 'soldOut' });
    }
    return badges;
};
/**
 * Compute min/max price from variants. Returns null if all variants have the same price.
 */
export const getPriceRange = (variants) => {
    if (!variants || variants.length === 0)
        return null;
    const prices = variants
        .map((v) => v.price)
        .filter((p) => p != null && !isNaN(p));
    if (prices.length === 0)
        return null;
    const min = Math.min(...prices);
    const max = Math.max(...prices);
    if (min === max)
        return null;
    return { min, max };
};
/**
 * Format a price range like "$54.00 - $72.00"
 */
export const formatPriceRange = (range, config = {}) => {
    return `${formatPrice(range.min, config)} - ${formatPrice(range.max, config)}`;
};
/**
 * Given current selections, return which values for an option are still available
 * based on variant availability.
 */
export const getAvailableValuesForOption = (optionName, options, variants, selections) => {
    const option = options.find((o) => o.name === optionName);
    if (!option)
        return [];
    const optionIndex = options.indexOf(option);
    const optionKey = `option${optionIndex + 1}`;
    return option.values.map((value) => {
        // Check if any variant with this value and current other selections is available
        const available = variants.some((variant) => {
            if (variant[optionKey] !== value)
                return false;
            if (variant.available === false)
                return false;
            // Check other selections match
            for (const [selName, selValue] of Object.entries(selections)) {
                if (selName === optionName)
                    continue;
                const selOption = options.find((o) => o.name === selName);
                if (!selOption)
                    continue;
                const selIdx = options.indexOf(selOption);
                const selKey = `option${selIdx + 1}`;
                if (variant[selKey] !== selValue)
                    return false;
            }
            return true;
        });
        return { value, available };
    });
};
/**
 * Find the exact variant matching all selected options.
 */
export const findVariantBySelections = (options, variants, selections) => {
    return variants.find((variant) => {
        return options.every((option, idx) => {
            const key = `option${idx + 1}`;
            const selected = selections[option.name];
            if (!selected)
                return true; // not yet selected
            return variant[key] === selected;
        });
    }) ?? null;
};
// ============================================================================
// Theme & Styling
// ============================================================================
/**
 * Generate CSS variables from theme config
 */
export const generateCSSVariables = (theme, prefix = 'seekora') => {
    const vars = {};
    if (theme.primaryColor)
        vars[`--${prefix}-primary`] = theme.primaryColor;
    if (theme.backgroundColor)
        vars[`--${prefix}-bg-surface`] = theme.backgroundColor;
    if (theme.surfaceColor)
        vars[`--${prefix}-bg-secondary`] = theme.surfaceColor;
    if (theme.textColor)
        vars[`--${prefix}-text-primary`] = theme.textColor;
    if (theme.textSecondaryColor)
        vars[`--${prefix}-text-secondary`] = theme.textSecondaryColor;
    if (theme.borderColor)
        vars[`--${prefix}-border-color`] = theme.borderColor;
    if (theme.hoverColor)
        vars[`--${prefix}-bg-hover`] = theme.hoverColor;
    if (theme.highlightColor)
        vars[`--${prefix}-highlight-bg`] = theme.highlightColor;
    if (theme.successColor)
        vars[`--${prefix}-success`] = theme.successColor;
    if (theme.errorColor)
        vars[`--${prefix}-error`] = theme.errorColor;
    if (theme.fontFamily)
        vars[`--${prefix}-font-family`] = theme.fontFamily;
    if (theme.fontSize)
        vars[`--${prefix}-font-size`] = typeof theme.fontSize === 'number' ? `${theme.fontSize}px` : theme.fontSize;
    if (theme.borderRadius)
        vars[`--${prefix}-border-radius`] = typeof theme.borderRadius === 'number' ? `${theme.borderRadius}px` : theme.borderRadius;
    if (theme.boxShadow)
        vars[`--${prefix}-box-shadow`] = theme.boxShadow;
    // Merge custom CSS variables
    if (theme.cssVariables) {
        Object.entries(theme.cssVariables).forEach(([key, value]) => {
            vars[key.startsWith('--') ? key : `--${key}`] = value;
        });
    }
    return vars;
};
/**
 * Merge class names, filtering out falsy values
 */
export const cx = (...classes) => {
    return classes.filter(Boolean).join(' ');
};
/**
 * Merge styles, filtering out undefined values
 */
export const mergeStyles = (...styles) => {
    return Object.assign({}, ...styles.filter(Boolean));
};
export const animations = {
    fade: {
        initial: { opacity: 0 },
        animate: { opacity: 1 },
        exit: { opacity: 0 },
    },
    slide: {
        initial: { opacity: 0, transform: 'translateY(-10px)' },
        animate: { opacity: 1, transform: 'translateY(0)' },
        exit: { opacity: 0, transform: 'translateY(-10px)' },
    },
    scale: {
        initial: { opacity: 0, transform: 'scale(0.95)' },
        animate: { opacity: 1, transform: 'scale(1)' },
        exit: { opacity: 0, transform: 'scale(0.95)' },
    },
    spring: {
        initial: { opacity: 0, transform: 'translateY(-20px) scale(0.9)' },
        animate: { opacity: 1, transform: 'translateY(0) scale(1)' },
        exit: { opacity: 0, transform: 'translateY(-10px) scale(0.95)' },
    },
};
/**
 * Get animation keyframes CSS
 */
export const getAnimationCSS = (type, duration = 200) => {
    if (type === 'none')
        return '';
    const anim = animations[type];
    return `
    @keyframes seekoraDropdownEnter {
      from {
        opacity: ${anim.initial.opacity};
        transform: ${anim.initial.transform || 'none'};
      }
      to {
        opacity: ${anim.animate.opacity};
        transform: ${anim.animate.transform || 'none'};
      }
    }
    @keyframes seekoraDropdownExit {
      from {
        opacity: ${anim.animate.opacity};
        transform: ${anim.animate.transform || 'none'};
      }
      to {
        opacity: ${anim.exit.opacity};
        transform: ${anim.exit.transform || 'none'};
      }
    }
  `;
};
// ============================================================================
// Keyboard Navigation
// ============================================================================
/**
 * Create keyboard handler for dropdown navigation
 */
export const createKeyboardHandler = (handlers) => {
    return (event) => {
        switch (event.key) {
            case 'ArrowUp':
                event.preventDefault();
                handlers.onUp?.();
                break;
            case 'ArrowDown':
                event.preventDefault();
                handlers.onDown?.();
                break;
            case 'ArrowLeft':
                handlers.onLeft?.();
                break;
            case 'ArrowRight':
                handlers.onRight?.();
                break;
            case 'Enter':
                event.preventDefault();
                handlers.onEnter?.();
                break;
            case 'Escape':
                event.preventDefault();
                handlers.onEscape?.();
                break;
            case 'Tab':
                handlers.onTab?.(event.shiftKey);
                break;
        }
    };
};
// ============================================================================
// Local Storage (Recent Searches)
// ============================================================================
const RECENT_SEARCHES_KEY = 'seekora_recent_searches';
const MAX_RECENT_SEARCHES = 10;
export const getRecentSearches = (storeId) => {
    try {
        const key = storeId ? `${RECENT_SEARCHES_KEY}_${storeId}` : RECENT_SEARCHES_KEY;
        const stored = localStorage.getItem(key);
        return stored ? JSON.parse(stored) : [];
    }
    catch {
        return [];
    }
};
export const addRecentSearch = (query, storeId) => {
    try {
        const key = storeId ? `${RECENT_SEARCHES_KEY}_${storeId}` : RECENT_SEARCHES_KEY;
        let searches = getRecentSearches(storeId);
        searches = searches.filter(s => s !== query);
        searches.unshift(query);
        searches = searches.slice(0, MAX_RECENT_SEARCHES);
        localStorage.setItem(key, JSON.stringify(searches));
        return searches;
    }
    catch {
        return [];
    }
};
export const removeRecentSearch = (query, storeId) => {
    try {
        const key = storeId ? `${RECENT_SEARCHES_KEY}_${storeId}` : RECENT_SEARCHES_KEY;
        let searches = getRecentSearches(storeId);
        searches = searches.filter(s => s !== query);
        localStorage.setItem(key, JSON.stringify(searches));
        return searches;
    }
    catch {
        return [];
    }
};
export const clearRecentSearches = (storeId) => {
    try {
        const key = storeId ? `${RECENT_SEARCHES_KEY}_${storeId}` : RECENT_SEARCHES_KEY;
        localStorage.removeItem(key);
    }
    catch {
        // Silent fail
    }
};
// ============================================================================
// Misc Utilities
// ============================================================================
/**
 * Debounce function
 */
export const debounce = (fn, delay) => {
    let timeoutId = null;
    const debounced = (...args) => {
        if (timeoutId)
            clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fn(...args), delay);
    };
    debounced.cancel = () => {
        if (timeoutId)
            clearTimeout(timeoutId);
    };
    return debounced;
};
/**
 * Generate unique ID
 */
export const generateId = (prefix = 'seekora') => {
    return `${prefix}-${Math.random().toString(36).substring(2, 9)}`;
};
/**
 * Check if element is in viewport
 */
export const isInViewport = (element) => {
    const rect = element.getBoundingClientRect();
    return (rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
        rect.right <= (window.innerWidth || document.documentElement.clientWidth));
};
/**
 * Scroll element into view if needed
 */
export const scrollIntoViewIfNeeded = (element, container) => {
    const elementRect = element.getBoundingClientRect();
    const containerRect = container.getBoundingClientRect();
    if (elementRect.top < containerRect.top) {
        element.scrollIntoView({ block: 'start', behavior: 'smooth' });
    }
    else if (elementRect.bottom > containerRect.bottom) {
        element.scrollIntoView({ block: 'end', behavior: 'smooth' });
    }
};
/**
 * Simple in-memory cache with TTL support for suggestions
 * Reduces API calls by caching results for quick repeated queries
 */
class SuggestionsCache {
    constructor(options = {}) {
        this.cache = new Map();
        this.maxSize = options.maxSize ?? 100;
        this.defaultTtl = options.defaultTtlMs ?? 30000; // 30 seconds default
    }
    /**
     * Generate a cache key from query and options
     */
    generateKey(query, options) {
        const normalizedQuery = query.toLowerCase().trim();
        if (!options)
            return normalizedQuery;
        // Create a stable key from options
        const optionsKey = Object.keys(options)
            .sort()
            .filter(k => options[k] !== undefined && options[k] !== null)
            .map(k => `${k}:${JSON.stringify(options[k])}`)
            .join('|');
        return optionsKey ? `${normalizedQuery}::${optionsKey}` : normalizedQuery;
    }
    /**
     * Get cached data if valid (not expired)
     */
    get(key) {
        const entry = this.cache.get(key);
        if (!entry)
            return null;
        const now = Date.now();
        if (now - entry.timestamp > entry.ttl) {
            // Expired - remove and return null
            this.cache.delete(key);
            return null;
        }
        return entry.data;
    }
    /**
     * Store data in cache
     */
    set(key, data, ttlMs) {
        // Evict oldest entries if at max size
        if (this.cache.size >= this.maxSize) {
            const oldestKey = this.cache.keys().next().value;
            if (oldestKey)
                this.cache.delete(oldestKey);
        }
        this.cache.set(key, {
            data,
            timestamp: Date.now(),
            ttl: ttlMs ?? this.defaultTtl,
        });
    }
    /**
     * Check if key exists and is valid
     */
    has(key) {
        return this.get(key) !== null;
    }
    /**
     * Clear all cached entries
     */
    clear() {
        this.cache.clear();
    }
    /**
     * Clear expired entries
     */
    cleanup() {
        const now = Date.now();
        for (const [key, entry] of this.cache.entries()) {
            if (now - entry.timestamp > entry.ttl) {
                this.cache.delete(key);
            }
        }
    }
    /**
     * Get cache statistics
     */
    getStats() {
        return {
            size: this.cache.size,
            maxSize: this.maxSize,
        };
    }
}
// Global cache instance for suggestions (shared across components)
let globalSuggestionsCache = null;
/**
 * Get the global suggestions cache instance
 */
export const getSuggestionsCache = (options) => {
    if (!globalSuggestionsCache) {
        globalSuggestionsCache = new SuggestionsCache(options);
    }
    return globalSuggestionsCache;
};
/**
 * Create a new cache instance (for isolated caching per component)
 */
export const createSuggestionsCache = (options) => {
    return new SuggestionsCache(options);
};
/**
 * Clear the global cache
 */
export const clearSuggestionsCache = () => {
    globalSuggestionsCache?.clear();
};
export { SuggestionsCache };
