import type { SearchItem, SearchResult, UpgradeSearchResult } from './types'; const IMAGE_EXTENSION_RE = /\.(png|jpe?g|gif|webp)(?:[?#].*)?$/i; const IMAGE_FALLBACK_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp'] as const; interface CatalogResponse { updatedAt: string; items: SearchItem[]; upgrades: UpgradeSearchResult[]; } interface PopularResponse { picks: SearchResult[]; } let catalogPromise: Promise | null = null; function normalizeKnownLocations(value: unknown): string[] | undefined { if (!Array.isArray(value)) { return undefined; } const normalized = value .map((location) => (typeof location === 'string' ? location.trim() : '')) .filter((location) => Boolean(location)); return normalized.length > 0 ? normalized : undefined; } function normalizeCatalog(payload: CatalogResponse): CatalogResponse { const normalizedItems = payload.items.map((item) => ({ ...item, knownLocations: normalizeKnownLocations(item.knownLocations), })); const normalizedUpgrades = payload.upgrades.map((upgrade) => { if (Array.isArray(upgrade.levels)) { return { ...upgrade, levels: upgrade.levels.map((level) => ({ ...level, salvage: Array.isArray(level.salvage) ? level.salvage.map((salvageItem) => ({ ...salvageItem, knownLocations: normalizeKnownLocations(salvageItem.knownLocations), })) : [], })), }; } const legacyUpgrade = upgrade as UpgradeSearchResult & { salvage?: UpgradeSearchResult['levels'][number]['salvage'] }; const salvage = Array.isArray(legacyUpgrade.salvage) ? legacyUpgrade.salvage : []; return { ...upgrade, levels: [ { level: 1, salvage: salvage.map((salvageItem) => ({ ...salvageItem, knownLocations: normalizeKnownLocations(salvageItem.knownLocations), })), }, ], }; }); return { ...payload, items: normalizedItems, upgrades: normalizedUpgrades, }; } function scoreResult(name: string, query: string): number { const lowerName = name.toLowerCase(); if (lowerName === query) { return 0; } if (lowerName.startsWith(query)) { return 1; } const index = lowerName.indexOf(query); if (index >= 0) { return 2 + index; } return Number.POSITIVE_INFINITY; } async function fetchCatalog(): Promise { const response = await fetch('/api/catalog'); if (!response.ok) { throw new Error(`Failed to fetch catalog: ${response.status}`); } const payload = (await response.json()) as CatalogResponse; if (!Array.isArray(payload.items) || !Array.isArray(payload.upgrades)) { throw new Error('Invalid catalog payload'); } return normalizeCatalog(payload); } async function getCatalog(): Promise { if (!catalogPromise) { catalogPromise = fetchCatalog().catch((error: unknown) => { catalogPromise = null; throw error; }); } return catalogPromise; } export function buildImageSearchCandidates(iconUrl: string): string[] { if (!iconUrl) { return []; } if (IMAGE_EXTENSION_RE.test(iconUrl)) { return [iconUrl]; } const candidates = [iconUrl, ...IMAGE_FALLBACK_EXTENSIONS.map((ext) => `${iconUrl}.${ext}`)]; return Array.from(new Set(candidates)); } export async function getAllItems(): Promise { const catalog = await getCatalog(); return catalog.items; } export async function searchResultsByName(query: string, limit = 5): Promise { const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) { return []; } const catalog = await getCatalog(); const candidates: SearchResult[] = [ ...catalog.items.filter((item) => item.name.toLowerCase().includes(normalizedQuery)), ...catalog.upgrades.filter((upgrade) => upgrade.name.toLowerCase().includes(normalizedQuery)), ]; return candidates .sort((a, b) => { const scoreDiff = scoreResult(a.name, normalizedQuery) - scoreResult(b.name, normalizedQuery); if (scoreDiff !== 0) { return scoreDiff; } return a.name.localeCompare(b.name); }) .slice(0, limit); } export async function getPopularResults(limit = 5): Promise { const response = await fetch(`/api/popularity?limit=${Math.max(1, Math.floor(limit))}`); if (!response.ok) { throw new Error(`Failed to fetch popular picks: ${response.status}`); } const payload = (await response.json()) as PopularResponse; if (!Array.isArray(payload.picks)) { throw new Error('Invalid popular picks payload'); } return payload.picks; } export async function trackResultAdded(result: SearchResult): Promise { const payload = { type: 'isUpgrade' in result ? 'upgrade' : 'item', slug: result.slug, }; try { await fetch('/api/popularity/track', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); } catch { // Ignore analytics failures so add-to-list UX is never blocked. } }