import { getConfiguredAssetBaseUrl } from './backendUrl'; const WIKIMEDIA_FILEPATH_SEGMENT = 'Special:FilePath/'; const WIKIMEDIA_REDIRECT_BASE = 'https://commons.wikimedia.org/wiki/Special:FilePath/'; export const DEFAULT_PLANT_IMAGE_URI = 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Monstera_deliciosa2.jpg/330px-Monstera_deliciosa2.jpg'; // Verified working fallback images per category (from main database URLs) const CATEGORY_FALLBACK_IMAGES: Record = { succulent: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Aloe_Vera_houseplant.jpg/330px-Aloe_Vera_houseplant.jpg', flowering: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/AnthuriumAndraenum.jpg/330px-AnthuriumAndraenum.jpg', medicinal: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Lavandula_angustifolia_-_K%C3%B6hler%E2%80%93s_Medizinal-Pflanzen-087.jpg/330px-Lavandula_angustifolia_-_K%C3%B6hler%E2%80%93s_Medizinal-Pflanzen-087.jpg', tree: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/25/Ficus_benjamina.jpg/330px-Ficus_benjamina.jpg', hanging: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Money_Plant_%28Epipremnum_aureum%29_4.jpg/330px-Money_Plant_%28Epipremnum_aureum%29_4.jpg', patterned: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Starr_080716-9470_Calathea_crotalifera.jpg/330px-Starr_080716-9470_Calathea_crotalifera.jpg', pet_friendly: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Chlorophytum_comosum_01.jpg/330px-Chlorophytum_comosum_01.jpg', high_humidity: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Spathiphyllum_cochlearispathum_RTBG.jpg/330px-Spathiphyllum_cochlearispathum_RTBG.jpg', air_purifier: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg/330px-Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg', sun: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Echeveria_elegans_-_1.jpg/330px-Echeveria_elegans_-_1.jpg', low_light: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Zamioculcas_zamiifolia_1.jpg/330px-Zamioculcas_zamiifolia_1.jpg', easy: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Pilea_peperomioides_Chinese_money_plant.jpg/330px-Pilea_peperomioides_Chinese_money_plant.jpg', large: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Strelitzia_nicolai_3.jpg/330px-Strelitzia_nicolai_3.jpg', }; const CATEGORY_PRIORITY = [ 'succulent', 'flowering', 'medicinal', 'tree', 'hanging', 'patterned', 'pet_friendly', 'high_humidity', 'air_purifier', 'sun', 'low_light', 'easy', 'large', ]; export const getCategoryFallbackImage = (categories: string[]): string => { for (const cat of CATEGORY_PRIORITY) { if (categories.includes(cat) && CATEGORY_FALLBACK_IMAGES[cat]) { return CATEGORY_FALLBACK_IMAGES[cat]; } } return DEFAULT_PLANT_IMAGE_URI; }; const tryDecode = (value: string): string => { try { return decodeURIComponent(value); } catch { return value; } }; const decodeRepeatedly = (value: string, rounds = 3): string => { let current = value; for (let index = 0; index < rounds; index += 1) { const decoded = tryDecode(current); if (decoded === current) break; current = decoded; } return current; }; type PlantManifestItem = { localImageUri?: string; sourceUri?: string; }; let manifestSourceByLocalUriCache: Map | null = null; let wikimediaSearchCache: Record | null = null; const normalizePlantAssetPath = (value?: string | null): string | null => { const trimmed = String(value || '').trim(); if (!trimmed) return null; const withoutQuery = trimmed.split(/[?#]/)[0].replace(/\\/g, '/'); const normalizedPath = withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`; if (!normalizedPath.startsWith('/plants/')) return null; if (normalizedPath.includes('..')) return null; return normalizedPath; }; const loadManifestSourceByLocalUri = (): Map => { if (manifestSourceByLocalUriCache) return manifestSourceByLocalUriCache; const nextCache = new Map(); try { const manifest = require('../server/public/plants/manifest.json') as { items?: PlantManifestItem[] }; const items = Array.isArray(manifest?.items) ? manifest.items : []; for (const item of items) { const localImageUri = normalizePlantAssetPath(item?.localImageUri); const sourceUri = String(item?.sourceUri || '').trim(); if (!localImageUri || !sourceUri) continue; nextCache.set(localImageUri, sourceUri); } } catch { // Keep empty cache when manifest is unavailable. } manifestSourceByLocalUriCache = nextCache; return nextCache; }; const loadWikimediaSearchCache = (): Record => { if (wikimediaSearchCache) return wikimediaSearchCache; try { wikimediaSearchCache = require('../server/public/plants/wikimedia-search-cache.json') as Record; } catch { wikimediaSearchCache = {}; } return wikimediaSearchCache; }; const resolveServerAssetPath = (rawPath: string): string | null => { const normalizedPath = normalizePlantAssetPath(rawPath); if (!normalizedPath) return null; return `${getConfiguredAssetBaseUrl()}${normalizedPath}`; }; const unwrapMarkdownLink = (value: string): string => { const markdownLink = value.match(/^\[[^\]]+]\((https?:\/\/[^)]+)\)(.*)$/i); if (!markdownLink) return value; const [, url, suffix] = markdownLink; return `${url}${suffix || ''}`; }; const convertWikimediaFilePathUrl = (value: string): string | null => { const segmentIndex = value.indexOf(WIKIMEDIA_FILEPATH_SEGMENT); if (segmentIndex < 0) return null; const fileNameStart = segmentIndex + WIKIMEDIA_FILEPATH_SEGMENT.length; const rawFileName = value.slice(fileNameStart).split(/[?#]/)[0].trim(); if (!rawFileName) return null; const decodedFileName = tryDecode(rawFileName).replace(/\s+/g, ' ').trim(); const encodedFileName = encodeURIComponent(decodedFileName).replace(/%2F/g, '/'); return `${WIKIMEDIA_REDIRECT_BASE}${encodedFileName}`; }; export const getWikimediaFilePathFromThumbnailUrl = (url: string): string | null => { if (!url.includes('upload.wikimedia.org/wikipedia/commons/thumb/')) return null; // Example: https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Monstera_deliciosa2.jpg/330px-Monstera_deliciosa2.jpg // The filename is the segment between the second-to-last and last slash (in many cases) // or use a more robust regex const parts = url.split('/'); // Filter out empty parts from trailing slashes const filtered = parts.filter(p => !!p); if (filtered.length < 5) return null; // For thumb URLs, the filename is typically the part before the last part (which is the px-filename) // but let's be careful. const lastName = filtered[filtered.length - 1]; const secondLastName = filtered[filtered.length - 2]; // In thumb URLs, the last part is usually "330px-Filename.jpg" // The second to last is "Filename.jpg" if (lastName.includes('px-') && lastName.endsWith(secondLastName)) { try { return decodeURIComponent(secondLastName); } catch { return secondLastName; } } return null; }; export const getPlantImageSourceFallbackUri = (rawUri?: string | null): string | null => { const localImageUri = normalizePlantAssetPath(rawUri); if (!localImageUri) return null; const sourceUri = loadManifestSourceByLocalUri().get(localImageUri); if (!sourceUri) return null; if (/^https?:\/\//i.test(sourceUri)) { return tryResolveImageUri(sourceUri); } if (!sourceUri.startsWith('wikimedia-search:')) { return null; } const rawQuery = sourceUri.slice('wikimedia-search:'.length).trim(); const decodedQuery = decodeRepeatedly(rawQuery); if (!decodedQuery) return null; const searchCache = loadWikimediaSearchCache(); const cachedUrl = searchCache[decodedQuery] || searchCache[rawQuery] || searchCache[encodeURIComponent(decodedQuery)] || null; return cachedUrl ? tryResolveImageUri(cachedUrl) : null; }; export const tryResolveImageUri = (rawUri?: string | null): string | null => { if (!rawUri) return null; const trimmed = rawUri.trim(); if (!trimmed) return null; const localAssetUri = resolveServerAssetPath(trimmed); if (localAssetUri) return localAssetUri; const normalized = unwrapMarkdownLink(trimmed); const converted = convertWikimediaFilePathUrl(normalized); const candidate = (converted || normalized).replace(/^http:\/\//i, 'https://'); if (!/^(https?:\/\/|file:\/\/|content:\/\/|data:image\/|blob:)/i.test(candidate)) { return null; } return candidate; }; export const resolveImageUri = (rawUri?: string | null): string => { return tryResolveImageUri(rawUri) || DEFAULT_PLANT_IMAGE_URI; };