import { readFile } from "node:fs/promises" import path from "node:path" import { rankListingsForQuery, sortListingsByFreshness, type CachedEbayListing, } from "@/lib/ebay-parts-match" export type ManualPartRow = { partNumber: string description: string ebayListings?: CachedEbayListing[] } type ManualPartsLookup = Record type ManualPagesPartsLookup = Record> let manualPartsCache: ManualPartsLookup | null = null let manualPagesPartsCache: ManualPagesPartsLookup | null = null let staticEbayListingsCache: CachedEbayListing[] | null = null async function readJsonFile(filename: string): Promise { const filePath = path.join(process.cwd(), "public", filename) const contents = await readFile(filePath, "utf8") return JSON.parse(contents) as T } export async function loadManualPartsLookup(): Promise { if (!manualPartsCache) { manualPartsCache = await readJsonFile( "manual_parts_lookup.json" ) } return manualPartsCache } export async function loadManualPagesPartsLookup(): Promise { if (!manualPagesPartsCache) { manualPagesPartsCache = await readJsonFile( "manual_pages_parts.json" ) } return manualPagesPartsCache } export async function findManualParts( manualFilename: string ): Promise { const manualParts = await loadManualPartsLookup() if (manualFilename in manualParts) { return manualParts[manualFilename] } const lowerFilename = manualFilename.toLowerCase() for (const [filename, parts] of Object.entries(manualParts)) { if (filename.toLowerCase() === lowerFilename) { return parts } } const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, "") const lowerWithoutExt = filenameWithoutExt.toLowerCase() for (const [filename, parts] of Object.entries(manualParts)) { const otherWithoutExt = filename.replace(/\.pdf$/i, "").toLowerCase() if ( otherWithoutExt === lowerWithoutExt || otherWithoutExt.includes(lowerWithoutExt) || lowerWithoutExt.includes(otherWithoutExt) ) { return parts } } return [] } export async function findManualPageParts( manualFilename: string, pageNumber: number ): Promise { const manualPagesParts = await loadManualPagesPartsLookup() if ( manualPagesParts[manualFilename] && manualPagesParts[manualFilename][pageNumber.toString()] ) { return manualPagesParts[manualFilename][pageNumber.toString()] } const lowerFilename = manualFilename.toLowerCase() for (const [filename, pages] of Object.entries(manualPagesParts)) { if ( filename.toLowerCase() === lowerFilename && pages[pageNumber.toString()] ) { return pages[pageNumber.toString()] } } const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, "") const lowerWithoutExt = filenameWithoutExt.toLowerCase() for (const [filename, pages] of Object.entries(manualPagesParts)) { const otherWithoutExt = filename.replace(/\.pdf$/i, "").toLowerCase() if ( otherWithoutExt === lowerWithoutExt || otherWithoutExt.includes(lowerWithoutExt) || lowerWithoutExt.includes(otherWithoutExt) ) { if (pages[pageNumber.toString()]) { return pages[pageNumber.toString()] } } } return [] } export async function listManualsWithParts(): Promise> { const manualParts = await loadManualPartsLookup() const manualsWithParts = new Set() for (const [filename, parts] of Object.entries(manualParts)) { if (parts.length > 0) { manualsWithParts.add(filename) manualsWithParts.add(filename.toLowerCase()) manualsWithParts.add(filename.replace(/\.pdf$/i, "")) manualsWithParts.add(filename.replace(/\.pdf$/i, "").toLowerCase()) } } return manualsWithParts } function dedupeListings(listings: CachedEbayListing[]): CachedEbayListing[] { const byItemId = new Map() for (const listing of listings) { const itemId = listing.itemId?.trim() if (!itemId) { continue } const existing = byItemId.get(itemId) if (!existing) { byItemId.set(itemId, listing) continue } const existingFreshness = existing.lastSeenAt ?? existing.fetchedAt ?? 0 const nextFreshness = listing.lastSeenAt ?? listing.fetchedAt ?? 0 if (nextFreshness >= existingFreshness) { byItemId.set(itemId, { ...existing, ...listing, sourceQueries: Array.from( new Set([...(existing.sourceQueries || []), ...(listing.sourceQueries || [])]) ), }) } } return sortListingsByFreshness(Array.from(byItemId.values())) } export async function loadStaticEbayListings(): Promise { if (staticEbayListingsCache) { return staticEbayListingsCache } const [manualParts, manualPagesParts] = await Promise.all([ loadManualPartsLookup(), loadManualPagesPartsLookup(), ]) const listings: CachedEbayListing[] = [] for (const parts of Object.values(manualParts)) { for (const part of parts) { if (Array.isArray(part.ebayListings)) { listings.push(...part.ebayListings) } } } for (const pages of Object.values(manualPagesParts)) { for (const parts of Object.values(pages)) { for (const part of parts) { if (Array.isArray(part.ebayListings)) { listings.push(...part.ebayListings) } } } } staticEbayListingsCache = dedupeListings(listings) return staticEbayListingsCache } export async function searchStaticEbayListings( query: string, limit = 6 ): Promise { const listings = await loadStaticEbayListings() return rankListingsForQuery(query, listings, limit) }