import { ProductResponse } from '@/services/api/catalogService'; import { getCatalogPriceInfo, getImageUrl, parseMoney } from '@/lib/utils'; export type SpecValue = { text: string; chips?: string[]; tone?: 'default' | 'muted' | 'success' | 'danger' | 'best'; }; export type SpecRow = { label: string; values: SpecValue[]; }; export type ProductSpecItem = { label: string; value: string }; const COLOR_HINT = /black|white|grey|gray|red|blue|green|purple|gold|silver|starlight|midnight|titanium|sierra|storm|pink|orange|yellow|graphite|ultramarine|natural|desert|deep/i; function titleCase(value: string): string { return value .replace(/\s+/g, ' ') .trim() .replace(/\b([a-z])/g, (letter) => letter.toUpperCase()); } export function formatComparePrice(amount: number | string): string { const n = parseMoney(amount); if (!n) return '—'; return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0, }).format(n); } export function humanizeOption(raw: string, product?: ProductResponse): string { let s = String(raw || '').trim(); if (!s) return ''; const slug = String(product?.slug || '').toLowerCase(); let lower = s.toLowerCase(); if (slug && lower.includes(slug)) { lower = lower.split(slug).join(' '); } lower = lower .replace(/apple[-_]?mobile[-_]?(camera|case|caseless|cover|phone)?/g, ' ') .replace(/\b(apple|samsung|google|nothing|oneplus|iphone|pixel|product)\b/g, ' ') .replace(/[-_]+/g, ' ') .replace(/\s+/g, ' ') .trim(); const words = lower.split(' ').filter(Boolean); if (!words.length) return titleCase(s.replace(/[-_]+/g, ' ')); if (words.length > 3) { const colorish = words.filter((word) => COLOR_HINT.test(word)); return titleCase((colorish.length ? colorish : words).slice(-2).join(' ')); } return titleCase(words.join(' ')); } function uniqueLabels(values: string[]): string[] { const seen = new Set(); const out: string[] = []; values.forEach((value) => { const key = value.toLowerCase().trim(); if (!key || key === '—' || seen.has(key)) return; seen.add(key); out.push(value); }); return out; } export function productImage(product: ProductResponse): string { const extra = product as ProductResponse & { thumbnail_url?: string }; const candidates = [ product.images?.[0]?.image_url, product.images?.[1]?.image_url, extra.thumbnail_url, ...(product.variants || []).flatMap((variant) => (variant.images || []).map((image) => image.image_url) ), ]; for (const candidate of candidates) { const url = getImageUrl(candidate); if (url) return url; } return ''; } export function stockInfo(product: ProductResponse): SpecValue { const stocks = (product.variants || []) .map((variant) => variant.available_stock) .filter((n): n is number => typeof n === 'number'); if (!stocks.length) return { text: 'Check availability', tone: 'muted' }; const total = stocks.reduce((sum, n) => sum + n, 0); if (total <= 0) return { text: 'Out of stock', tone: 'danger' }; if (total <= 5) return { text: `Only ${total} left`, tone: 'danger' }; return { text: 'In stock', tone: 'success' }; } export function colorChips(product: ProductResponse): string[] { const raw: string[] = []; if (Array.isArray(product.colors)) raw.push(...product.colors); if (product.color) raw.push(product.color); (product.variants || []).forEach((variant) => { (variant.attributes || []).forEach((attr) => { const key = `${attr.attribute_code || ''} ${attr.attribute_name || ''}`.toLowerCase(); if (key.includes('color') || key.includes('colour')) { raw.push(attr.attribute_value); } }); }); return uniqueLabels( raw.map((value) => humanizeOption(value, product)).filter((value) => value.length > 1) ).slice(0, 3); } function specValue(specMap: Record, productId: string, matcher: RegExp): string { const match = (specMap[productId] || []).find((item) => matcher.test(item.label)); return match?.value?.trim() || ''; } function attributeValues(product: ProductResponse, matcher: RegExp): string[] { return uniqueLabels( (product.variants || []) .flatMap((variant) => variant.attributes || []) .filter((attr) => matcher.test(`${attr.attribute_code || ''} ${attr.attribute_name || ''}`)) .map((attr) => humanizeOption(attr.attribute_value, product)) .filter(Boolean) ).slice(0, 3); } function rowFromSources( products: ProductResponse[], specMap: Record, matcher: RegExp ): SpecRow['values'] { return products.map((product) => { const fromSpec = specValue(specMap, product.product_id, matcher); const fromAttrs = attributeValues(product, matcher); const chips = uniqueLabels( [...fromAttrs, ...(fromSpec ? fromSpec.split(/[,|/]+/).map((part) => humanizeOption(part, product)) : [])] .filter(Boolean) ).slice(0, 3); if (!chips.length) return { text: '—' }; return { text: chips.join(', '), chips }; }); } export function valuesDiffer(values: SpecValue[]): boolean { const unique = new Set(values.map((value) => value.text.trim().toLowerCase())); return unique.size > 1; } export type CompareKind = | 'audio' | 'speaker' | 'case' | 'camera' | 'charger' | 'phone' | 'laptop' | 'watch' | 'protector' | 'general'; function productHaystack(product: ProductResponse): string { const extra = product as ProductResponse & { category_name?: string }; return [product.name, product.slug, product.full_path, extra.category_name] .filter(Boolean) .join(' ') .toLowerCase(); } export function detectProductKind(product: ProductResponse): CompareKind { const hay = productHaystack(product); if (/headphone|earbud|earphone|airpod|headset/.test(hay)) return 'audio'; if (/speaker|soundbar/.test(hay)) return 'speaker'; if (/(^|[\s-])(case|cover|bumper)([\s-]|$)/.test(hay) || hay.includes('cases')) return 'case'; if (/camera|lens/.test(hay)) return 'camera'; if (/charger|charging|cable|adapter|power.?bank|usb/.test(hay)) return 'charger'; if (/glass|protector|tempered|screen guard/.test(hay)) return 'protector'; if (/watch|band|strap/.test(hay)) return 'watch'; if (/laptop|macbook|notebook/.test(hay)) return 'laptop'; if (/\b(iphone|pixel|galaxy|smartphone|mobile phone)\b/.test(hay) && !/camera|case|cover/.test(hay)) { return 'phone'; } return 'general'; } export function dominantCompareKind(products: ProductResponse[]): CompareKind { const counts = new Map(); products.forEach((product) => { const kind = detectProductKind(product); counts.set(kind, (counts.get(kind) || 0) + 1); }); return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || 'general'; } export function compareKindLabel(kind: CompareKind): string { const labels: Record = { audio: 'headphones', speaker: 'speakers', case: 'phone cases', camera: 'cameras', charger: 'chargers & cables', phone: 'smartphones', laptop: 'laptops', watch: 'smartwatches', protector: 'screen protectors', general: 'accessories', }; return labels[kind]; } export function sameTypeCompareList(list: ProductResponse[]): ProductResponse[] { if (list.length <= 1) return list; const kind = detectProductKind(list[0]); return list.filter((product) => detectProductKind(product) === kind); } export function isSameCompareKind( list: ProductResponse[], product: ProductResponse, ignoreId?: string ): boolean { const peers = ignoreId ? list.filter((item) => item.product_id !== ignoreId && item.slug !== ignoreId) : list; if (!peers.length) return true; return detectProductKind(peers[0]) === detectProductKind(product); } export function lockedCompareKind( list: ProductResponse[], replaceProductId?: string ): CompareKind | null { const peers = replaceProductId ? list.filter( (item) => item.product_id !== replaceProductId && item.slug !== replaceProductId ) : list; if (!peers.length) return null; return detectProductKind(peers[0]); } function compatibleDevice(product: ProductResponse, specMap: Record): string { const named = String(product.name || '').match(/\bfor\s+(.+)$/i); if (named?.[1]) return named[1].replace(/\s+/g, ' ').trim(); const fromSpec = specValue(specMap, product.product_id, /compatible|fits|model$/i); if (fromSpec && fromSpec.length > 3 && /[a-z]/i.test(fromSpec)) return fromSpec; return ''; } function connectivity(product: ProductResponse, specMap: Record): string { const fromSpec = specValue(specMap, product.product_id, /bluetooth|wireless|wired|connectivity/i); if (fromSpec) return fromSpec; const hay = productHaystack(product); if (/bluetooth|wireless|airpod/.test(hay)) return 'Wireless'; if (/wired|aux/.test(hay)) return 'Wired'; return ''; } function productTypeLabel(product: ProductResponse): string { const labels: Record = { audio: 'Headphones', speaker: 'Speaker', case: 'Phone case', camera: 'Camera', charger: 'Charger / cable', phone: 'Smartphone', laptop: 'Laptop', watch: 'Smartwatch', protector: 'Screen protector', general: 'Accessory', }; return labels[detectProductKind(product)]; } function cellFromMatch( products: ProductResponse[], specMap: Record, matcher: RegExp, infer?: (product: ProductResponse) => string ): SpecRow['values'] { return products.map((product) => { const inferred = infer?.(product)?.trim() || ''; const sourced = rowFromSources([product], specMap, matcher)[0]; if (sourced.text && sourced.text !== '—') return sourced; return { text: inferred || '—' }; }); } function brandCells(products: ProductResponse[], specMap: Record): SpecRow['values'] { return products.map((product) => ({ text: product.brand_name?.trim() || specValue(specMap, product.product_id, /brand|manufacturer/i) || '—', })); } function colourCells(products: ProductResponse[]): SpecRow['values'] { return products.map((product) => { const chips = colorChips(product); return chips.length ? { text: chips.join(', ') } : { text: '—' }; }); } const TYPE_FIELDS: Record< CompareKind, Array<{ label: string; match?: RegExp; infer?: (product: ProductResponse, specMap: Record) => string; colours?: boolean; brand?: boolean; }> > = { audio: [ { label: 'Brand', brand: true }, { label: 'Type', infer: (product) => productTypeLabel(product) }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Connectivity', infer: (product, specMap) => connectivity(product, specMap), match: /bluetooth|wireless|wired|connectivity/i }, { label: 'Colour', colours: true }, ], speaker: [ { label: 'Brand', brand: true }, { label: 'Type', infer: () => 'Speaker' }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Connectivity', infer: (product, specMap) => connectivity(product, specMap) }, { label: 'Colour', colours: true }, ], case: [ { label: 'Brand', brand: true }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Colour', colours: true }, { label: 'Material', match: /material|finish|case type/i }, ], camera: [ { label: 'Brand', brand: true }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Colour', colours: true }, { label: 'Type', infer: () => 'Mobile camera' }, ], charger: [ { label: 'Brand', brand: true }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Output', match: /watt|output|power|amp/i }, { label: 'Connector', match: /connector|port|usb|type.?c|lightning/i }, { label: 'Colour', colours: true }, ], phone: [ { label: 'Brand', brand: true }, { label: 'Storage', match: /storage|memory|rom|capacity/i }, { label: 'Display', match: /display|screen/i }, { label: 'Battery', match: /battery/i }, { label: 'Colour', colours: true }, ], laptop: [ { label: 'Brand', brand: true }, { label: 'Processor', match: /processor|cpu|chip/i }, { label: 'RAM', match: /\bram\b|memory/i }, { label: 'Storage', match: /storage|ssd|hdd/i }, { label: 'Display', match: /display|screen/i }, ], watch: [ { label: 'Brand', brand: true }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Size', match: /size|mm|case size/i }, { label: 'Colour', colours: true }, ], protector: [ { label: 'Brand', brand: true }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Material', match: /material|glass|tempered/i }, ], general: [ { label: 'Brand', brand: true }, { label: 'Type', infer: (product) => productTypeLabel(product) }, { label: 'Compatible with', infer: (product, specMap) => compatibleDevice(product, specMap) }, { label: 'Colour', colours: true }, ], }; export function buildTypeCompareRows( products: ProductResponse[], specMap: Record = {} ): SpecRow[] { if (!products.length) return []; const kind = dominantCompareKind(products); return TYPE_FIELDS[kind] .map((field) => { let values: SpecRow['values']; if (field.brand) values = brandCells(products, specMap); else if (field.colours) values = colourCells(products); else if (field.match) { values = cellFromMatch(products, specMap, field.match, (product) => field.infer ? field.infer(product, specMap) : '' ); } else { values = products.map((product) => ({ text: field.infer?.(product, specMap) || '—', })); } return { label: field.label, values }; }) .filter((row) => row.values.some((value) => value.text && value.text !== '—')); } /** @deprecated use buildTypeCompareRows */ export function buildCompareRows( products: ProductResponse[], specMap: Record = {} ): SpecRow[] { return buildTypeCompareRows(products, specMap); } export function cheapestProductId(products: ProductResponse[]): string | null { let bestId: string | null = null; let bestPrice = Number.POSITIVE_INFINITY; products.forEach((product) => { const price = getCatalogPriceInfo(product).price; if (price > 0 && price < bestPrice) { bestPrice = price; bestId = product.product_id; } }); const matches = products.filter((product) => getCatalogPriceInfo(product).price === bestPrice); return matches.length === 1 ? bestId : null; }