import type { SearchResponse, SearchResultItem, SearchSort } from "@/lib/search/types" import type { Player, PlayerService, Shop } from "@/lib/types" export interface SearchCatalogParams { q?: string selectedGames?: string[] min?: string max?: string onlyOnline?: boolean minRating?: string sort?: SearchSort limit?: number offset?: number } export interface SearchCatalogData { players: Player[] shops: Shop[] services: PlayerService[] } type InternalResultItem = SearchResultItem & { __index: number } function minPriceFromServices(services: PlayerService[]) { return services.length === 0 ? 0 : Math.min(...services.map((s) => s.price)) } function unitFromCheapestService(services: PlayerService[]): PlayerService["unit"] { if (services.length === 0) return "局" return services.reduce((prev, curr) => (prev.price < curr.price ? prev : curr)).unit } export function searchCatalog( params: SearchCatalogParams, data: SearchCatalogData, ): SearchResponse { const q = params.q ?? "" const query = q ? q.toLowerCase() : "" const selectedGames = params.selectedGames ?? [] const min = params.min ?? "" const max = params.max ?? "" const minP = min ? Number(min) : 0 const maxP = max ? Number(max) : Infinity const onlyOnline = params.onlyOnline ?? false const minRating = Number(params.minRating ?? "0") const sort = params.sort ?? "composite" const offset = Math.max(0, params.offset ?? 0) const limit = Math.max(0, params.limit ?? 12) const shopDerivedById = new Map< string, { minPrice: number unit: PlayerService["unit"] games: string[] hasAvailable: boolean } >() for (const shop of data.shops) { const shopPlayers = data.players.filter((player) => player.shopId === shop.id) const playerIds = new Set(shopPlayers.map((player) => player.id)) const shopServices = data.services.filter((service) => playerIds.has(service.playerId)) const shopMinPrice = shopServices.length > 0 ? Math.min(...shopServices.map((s) => s.price)) : 0 const shopUnit = shopServices.length > 0 ? unitFromCheapestService(shopServices) : "局" const shopGames = [...new Set(shopServices.map((service) => service.gameName))] const hasAvailable = shopPlayers.some((player) => player.status === "available") shopDerivedById.set(shop.id, { minPrice: shopMinPrice, unit: shopUnit, games: shopGames, hasAvailable, }) } let nextIndex = 0 const results: InternalResultItem[] = [] for (const player of data.players) { results.push({ __index: nextIndex++, type: "player", player, rating: player.rating, orders: player.totalOrders, minPrice: minPriceFromServices(player.services), unit: unitFromCheapestService(player.services), }) } for (const shop of data.shops) { const derived = shopDerivedById.get(shop.id) if (!derived) continue results.push({ __index: nextIndex++, type: "shop", shop, rating: Number(shop.rating), orders: shop.totalOrders, minPrice: derived.minPrice, unit: derived.unit, games: derived.games, hasAvailable: derived.hasAvailable, }) } const filtered = results.filter((item) => { if (query) { if (item.type === "player") { const matchName = item.player.user.nickname.toLowerCase().includes(query) const matchTags = item.player.tags.some((tag) => tag.toLowerCase().includes(query)) const matchGames = item.player.games.some((game) => game.toLowerCase().includes(query)) if (!matchName && !matchTags && !matchGames) return false } else { const matchName = item.shop.name.toLowerCase().includes(query) const matchDescription = item.shop.description.toLowerCase().includes(query) const matchGames = item.games.some((game) => game.toLowerCase().includes(query)) if (!matchName && !matchDescription && !matchGames) return false } } if (selectedGames.length > 0) { if (item.type === "player") { const hasGame = item.player.games.some((game) => selectedGames.includes(game)) if (!hasGame) return false } else { const hasGame = item.games.some((game) => selectedGames.includes(game)) if (!hasGame) return false } } if (item.minPrice < minP) return false if (max && item.minPrice > maxP) return false if (onlyOnline) { if (item.type === "player") { if (item.player.status !== "available") return false } else { if (!item.hasAvailable) return false } } if (item.type === "player") { if (item.player.rating < minRating) return false } else { if (Number(item.shop.rating) < minRating) return false } return true }) filtered.sort((a, b) => { let diff = 0 switch (sort) { case "rating": diff = b.rating - a.rating break case "price_asc": diff = a.minPrice - b.minPrice break case "price_desc": diff = b.minPrice - a.minPrice break case "orders": diff = b.orders - a.orders break case "composite": { const scoreA = a.rating * Math.log10(a.orders + 1) const scoreB = b.rating * Math.log10(b.orders + 1) diff = scoreB - scoreA break } default: diff = 0 } if (diff !== 0) return diff return a.__index - b.__index }) const total = filtered.length const items = filtered.slice(offset, offset + limit).map(({ __index, ...item }) => item) return { items, meta: { total, offset, limit } } }