import { NextResponse } from "next/server" import { fetchQuery } from "convex/nextjs" import { api } from "@/convex/_generated/api" import { hasConvexUrl } from "@/lib/convex-config" import { filterTrustedEbayListings, rankListingsForPart, type CachedEbayListing, type EbayCacheState, type ManualPartInput, } from "@/lib/ebay-parts-match" type MatchPart = ManualPartInput & { key?: string ebayListings?: CachedEbayListing[] } type ManualPartsMatchResponse = { manualFilename: string parts: Array< MatchPart & { ebayListings: CachedEbayListing[] } > cache: EbayCacheState cacheSource: "convex" | "fallback" error?: string } type ManualPartsRequest = { manualFilename?: string parts?: unknown[] limit?: number } function getDisabledCacheState(message: string): EbayCacheState { return { key: "manual-parts", status: "disabled", lastSuccessfulAt: null, lastAttemptAt: null, nextEligibleAt: null, lastError: message, consecutiveFailures: 0, queryCount: 0, itemCount: 0, sourceQueries: [], freshnessMs: null, isStale: true, listingCount: 0, activeListingCount: 0, message, } } function getErrorCacheState(message: string): EbayCacheState { const now = Date.now() return { key: "manual-parts", status: "error", lastSuccessfulAt: null, lastAttemptAt: now, nextEligibleAt: null, lastError: message, consecutiveFailures: 1, queryCount: 0, itemCount: 0, sourceQueries: [], freshnessMs: null, isStale: true, listingCount: 0, activeListingCount: 0, message, } } function createEmptyListingsParts(parts: MatchPart[]) { return parts.map((part) => ({ ...part, ebayListings: [], })) } function normalizePartInput(value: unknown): MatchPart | null { if (!value || typeof value !== "object") { return null } const part = value as Record const partNumber = typeof part.partNumber === "string" ? part.partNumber.trim() : "" const description = typeof part.description === "string" ? part.description.trim() : "" if (!partNumber && !description) { return null } return { key: typeof part.key === "string" ? part.key : undefined, partNumber, description, manufacturer: typeof part.manufacturer === "string" ? part.manufacturer.trim() : undefined, category: typeof part.category === "string" ? part.category.trim() : undefined, manualFilename: typeof part.manualFilename === "string" ? part.manualFilename.trim() : undefined, ebayListings: Array.isArray(part.ebayListings) ? (part.ebayListings as CachedEbayListing[]) : undefined, } } export async function POST(request: Request) { let payload: ManualPartsRequest | null = null try { payload = (await request.json()) as ManualPartsRequest } catch { payload = null } const manualFilename = payload?.manualFilename?.trim() || "" const limit = Math.min( Math.max(Number.parseInt(String(payload?.limit ?? 5), 10) || 5, 1), 10 ) const parts: MatchPart[] = (payload?.parts || []) .map(normalizePartInput) .filter((part): part is MatchPart => Boolean(part)) if (!manualFilename) { return NextResponse.json( { error: "manualFilename is required" }, { status: 400 } ) } if (!parts.length) { const message = "No manual parts were provided." return NextResponse.json({ manualFilename, parts: [], cache: getDisabledCacheState(message), cacheSource: "fallback", error: message, } satisfies ManualPartsMatchResponse) } if (!hasConvexUrl()) { const message = "Cached eBay backend is disabled because NEXT_PUBLIC_CONVEX_URL is not configured." return NextResponse.json({ manualFilename, parts: createEmptyListingsParts(parts), cache: getDisabledCacheState(message), cacheSource: "fallback", error: message, } satisfies ManualPartsMatchResponse) } try { const [overview, listings] = await Promise.all([ fetchQuery(api.ebay.getCacheOverview, {}), fetchQuery(api.ebay.listCachedListings, { limit: 200 }), ]) const trustedListings = filterTrustedEbayListings( listings as CachedEbayListing[] ) const rankedParts = parts .map((part) => ({ ...part, ebayListings: rankListingsForPart(part, trustedListings, limit), })) .sort((a, b) => { const aCount = a.ebayListings.length const bCount = b.ebayListings.length if (aCount !== bCount) { return bCount - aCount } const aFreshness = a.ebayListings[0]?.lastSeenAt ?? a.ebayListings[0]?.fetchedAt ?? 0 const bFreshness = b.ebayListings[0]?.lastSeenAt ?? b.ebayListings[0]?.fetchedAt ?? 0 return bFreshness - aFreshness }) .slice(0, limit) return NextResponse.json({ manualFilename, parts: rankedParts, cache: overview, cacheSource: "convex", } satisfies ManualPartsMatchResponse) } catch (error) { console.error("Failed to load cached eBay matches:", error) const message = error instanceof Error ? `Cached eBay listings are unavailable: ${error.message}` : "Cached eBay listings are unavailable." return NextResponse.json( { manualFilename, parts: createEmptyListingsParts(parts), cache: getErrorCacheState(message), cacheSource: "fallback", error: message, } satisfies ManualPartsMatchResponse, { status: 200 } ) } }