import { NextRequest, NextResponse } from "next/server"; import { sql } from "@/lib/db"; import { cacheGet, cacheSet, hashParams } from "@/lib/cache"; import type { CityStats } from "@transportationer/shared"; export const runtime = "nodejs"; export async function GET(req: NextRequest) { const city = req.nextUrl.searchParams.get("city") ?? "berlin"; const mode = req.nextUrl.searchParams.get("mode") ?? "walking"; const threshold = parseInt( req.nextUrl.searchParams.get("threshold") ?? "15", 10, ); const profile = req.nextUrl.searchParams.get("profile") ?? "universal"; const cacheKey = `api:stats:${hashParams({ city, mode, threshold, profile })}`; const cached = await cacheGet(cacheKey); if (cached) { return NextResponse.json(cached, { headers: { "X-Cache": "HIT" } }); } const [poiCounts, gridCount, percentiles] = await Promise.all([ Promise.resolve(sql<{ category: string; count: number }[]>` SELECT category, COUNT(*)::int AS count FROM raw_pois WHERE city_slug = ${city} GROUP BY category `), Promise.resolve(sql<{ count: number }[]>` SELECT COUNT(*)::int AS count FROM grid_points WHERE city_slug = ${city} `), Promise.resolve(sql<{ category: string; p10: number; p25: number; p50: number; p75: number; p90: number; coverage_pct: number; }[]>` SELECT gs.category, PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY gs.score)::float AS p10, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY gs.score)::float AS p25, PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY gs.score)::float AS p50, PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY gs.score)::float AS p75, PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY gs.score)::float AS p90, (COUNT(*) FILTER (WHERE gs.score >= 0.5) * 100.0 / COUNT(*))::float AS coverage_pct FROM grid_scores gs JOIN grid_points gp ON gp.id = gs.grid_point_id WHERE gp.city_slug = ${city} AND gs.travel_mode = ${mode} AND gs.threshold_min = ${threshold} AND gs.profile = ${profile} GROUP BY gs.category `), ]); const poisByCategory = Object.fromEntries( poiCounts.map((r) => [r.category, r.count]), ); const stats: CityStats = { citySlug: city, totalPois: Object.values(poisByCategory).reduce((a, b) => a + b, 0), gridPointCount: gridCount[0]?.count ?? 0, categoryStats: percentiles.map((r) => ({ category: r.category as any, poiCount: poisByCategory[r.category] ?? 0, p10: r.p10, p25: r.p25, p50: r.p50, p75: r.p75, p90: r.p90, coveragePct: r.coverage_pct, })), }; await cacheSet(cacheKey, stats, "API_STATS"); return NextResponse.json(stats); }