import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' import { verifyJWT } from '@/lib/auth' const FREE_DAILY_LIMIT = 20 const CACHE_TTL_MS = 24 * 60 * 60 * 1000 interface OverpassElement { id: number lat?: number lon?: number center?: { lat: number; lon: number } tags?: Record } interface Place { id: string lat: number lon: number name: string type: 'mosque' | 'restaurant' address?: string } const OVERPASS_ENDPOINTS = [ 'https://overpass-api.de/api/interpreter', 'https://maps.mail.ru/osm/tools/overpass/api/interpreter', 'https://overpass.kumi.systems/api/interpreter', ] async function queryOverpass(query: string): Promise { let lastError: Error | null = null for (const endpoint of OVERPASS_ENDPOINTS) { try { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', }, body: `data=${encodeURIComponent(query)}`, signal: AbortSignal.timeout(15000), }) if (!res.ok) { lastError = new Error(`Overpass error ${res.status} from ${endpoint}`); continue } const data = await res.json() return data.elements || [] } catch (e) { lastError = e as Error } } throw lastError ?? new Error('All Overpass endpoints failed') } function buildMosqueQuery(lat: number, lon: number, radius: number): string { return `[out:json][timeout:10];(node["amenity"="place_of_worship"]["religion"="muslim"](around:${radius},${lat},${lon});way["amenity"="place_of_worship"]["religion"="muslim"](around:${radius},${lat},${lon}););out center;` } function buildRestaurantQuery(lat: number, lon: number, radius: number): string { return `[out:json][timeout:10];(node["diet:halal"="yes"](around:${radius},${lat},${lon});node["cuisine"~"halal"](around:${radius},${lat},${lon});way["diet:halal"="yes"](around:${radius},${lat},${lon}););out center;` } function elementsToPlaces(elements: OverpassElement[], type: 'mosque' | 'restaurant'): Place[] { return elements .filter(e => { const lat = e.lat ?? e.center?.lat const lon = e.lon ?? e.center?.lon return lat !== undefined && lon !== undefined }) .map(e => ({ id: String(e.id), lat: (e.lat ?? e.center?.lat)!, lon: (e.lon ?? e.center?.lon)!, name: e.tags?.name || e.tags?.['name:en'] || (type === 'mosque' ? 'Mosque' : 'Halal Restaurant'), type, address: [e.tags?.['addr:street'], e.tags?.['addr:city']].filter(Boolean).join(', ') || undefined, })) } async function getUserFromRequest(req: NextRequest) { const auth = req.headers.get('authorization') if (!auth?.startsWith('Bearer ')) return null const payload = await verifyJWT(auth.slice(7)) if (!payload) return null return prisma.user.findUnique({ where: { id: payload.id } }) } export async function GET(req: NextRequest) { try { const { searchParams } = new URL(req.url) const lat = parseFloat(searchParams.get('lat') || '') const lon = parseFloat(searchParams.get('lng') || '') const radius = Math.min(parseInt(searchParams.get('radius') || '5000'), 10000) if (isNaN(lat) || isNaN(lon)) { return NextResponse.json({ error: 'lat and lng required' }, { status: 400 }) } const user = await getUserFromRequest(req) const isPremium = user?.isPremium || user?.isPro || false // Rate limit free users if (user && !isPremium) { const today = new Date() today.setHours(0, 0, 0, 0) let usage = await prisma.halalUsage.findUnique({ where: { userId: user.id } }) if (!usage || !usage.periodEnd || usage.periodEnd < today) { usage = await prisma.halalUsage.upsert({ where: { userId: user.id }, create: { userId: user.id, queriesUsed: 0, queriesLimit: FREE_DAILY_LIMIT, periodEnd: new Date(today.getTime() + 86400000) }, update: { queriesUsed: 0, queriesLimit: FREE_DAILY_LIMIT, periodEnd: new Date(today.getTime() + 86400000) }, }) } if (usage.queriesUsed >= FREE_DAILY_LIMIT) { return NextResponse.json({ error: 'daily_limit_reached', limit: FREE_DAILY_LIMIT }, { status: 429 }) } } // Check cache const cacheKey = `${isPremium ? 'premium' : 'free'}:${lat.toFixed(2)}:${lon.toFixed(2)}:${radius}` const cached = await prisma.halalPlaceCache.findUnique({ where: { cacheKey } }) if (cached && cached.expiresAt > new Date()) { return NextResponse.json(JSON.parse(cached.data)) } // Fetch from Overpass const mosqueElements = await queryOverpass(buildMosqueQuery(lat, lon, radius)) const mosques = elementsToPlaces(mosqueElements, 'mosque') let restaurants: Place[] = [] if (isPremium) { const restaurantElements = await queryOverpass(buildRestaurantQuery(lat, lon, radius)) restaurants = elementsToPlaces(restaurantElements, 'restaurant') } const result = { mosques, restaurants } // Cache result await prisma.halalPlaceCache.upsert({ where: { cacheKey }, create: { cacheKey, data: JSON.stringify(result), expiresAt: new Date(Date.now() + CACHE_TTL_MS) }, update: { data: JSON.stringify(result), expiresAt: new Date(Date.now() + CACHE_TTL_MS) }, }) // Increment usage for free users if (user && !isPremium) { await prisma.halalUsage.update({ where: { userId: user.id }, data: { queriesUsed: { increment: 1 } }, }) } return NextResponse.json(result) } catch (e) { console.error('Halal places error:', e) return NextResponse.json({ error: 'Failed to fetch places' }, { status: 500 }) } }