import type { SupabaseClient } from "@supabase/supabase-js"; import { countryCodeFromIp } from "@/lib/ip-geolocation"; import type { DbUser } from "@/types/database"; export type GeolocationCountryStat = { countryCode: string; count: number; avgPing: number | null; }; export type GeolocationAnalyticsResult = { userAccounts: GeolocationCountryStat[]; matches: GeolocationCountryStat[]; error: string | null; }; const EMPTY_RESULT: GeolocationAnalyticsResult = { userAccounts: [], matches: [], error: null, }; async function resolveUniqueIps( ips: Iterable, ): Promise> { const unique = new Set(); for (const ip of ips) { const trimmed = (ip ?? "").trim(); if (trimmed) unique.add(trimmed); } const entries = await Promise.all( [...unique].map(async (ip) => [ip, await countryCodeFromIp(ip)] as const), ); return new Map(entries); } function isValidCountryCode(code: string | null | undefined): code is string { const cc = (code ?? "").trim().toUpperCase(); return /^[A-Z]{2}$/.test(cc); } export async function loadUserGeolocationStats( users: DbUser[], playerIds: number[], ): Promise { let filtered = users; if (playerIds.length > 0) { const allowed = new Set(playerIds); filtered = users.filter((u) => allowed.has(u.id)); } const ips = filtered.map((u) => u.ip_address); const countryByIp = await resolveUniqueIps(ips); const byCountry = new Map(); for (const user of filtered) { const ip = user.ip_address?.trim(); if (!ip) continue; const country = countryByIp.get(ip); if (!isValidCountryCode(country)) continue; byCountry.set(country, (byCountry.get(country) ?? 0) + 1); } return [...byCountry.entries()] .map(([countryCode, count]) => ({ countryCode, count, avgPing: null })) .sort((a, b) => b.count - a.count || a.countryCode.localeCompare(b.countryCode)); } type PingReportRow = { match_id: number; ip_address: string | null; ping: number; }; export async function loadMatchGeolocationStats( supabase: SupabaseClient, from: string, to: string, playerIds: number[], ): Promise<{ stats: GeolocationCountryStat[]; error: string | null }> { const rangeStartIso = `${from}T00:00:00.000Z`; const rangeEndIso = `${to}T23:59:59.999Z`; let query = supabase .from("ping_reports") .select("match_id, ip_address, ping") .gte("created_at", rangeStartIso) .lte("created_at", rangeEndIso) .limit(10000); if (playerIds.length > 0) { query = query.in("user_id", playerIds); } const { data, error } = await query; if (error) { return { stats: [], error: error.message }; } const rows = (data ?? []) as PingReportRow[]; const countryByIp = await resolveUniqueIps(rows.map((r) => r.ip_address)); const byCountry = new Map< string, { matchIds: Set; pings: number[] } >(); for (const row of rows) { const ip = row.ip_address?.trim(); if (!ip) continue; const country = countryByIp.get(ip); if (!isValidCountryCode(country)) continue; const matchId = Number(row.match_id); const ping = Number(row.ping); if (!Number.isFinite(matchId) || !Number.isFinite(ping)) continue; const state = byCountry.get(country) ?? { matchIds: new Set(), pings: [], }; state.matchIds.add(matchId); state.pings.push(ping); byCountry.set(country, state); } const stats = [...byCountry.entries()] .map(([countryCode, state]) => { const pings = state.pings; const avgPing = pings.length > 0 ? pings.reduce((acc, value) => acc + value, 0) / pings.length : null; return { countryCode, count: state.matchIds.size, avgPing, }; }) .sort((a, b) => b.count - a.count || a.countryCode.localeCompare(b.countryCode)); return { stats, error: null }; } export async function loadGeolocationAnalytics( supabase: SupabaseClient, users: DbUser[], from: string, to: string, playerIds: number[], ): Promise { try { const [userAccounts, matchResult] = await Promise.all([ loadUserGeolocationStats(users, playerIds), loadMatchGeolocationStats(supabase, from, to, playerIds), ]); if (matchResult.error) { return { userAccounts, matches: [], error: matchResult.error, }; } return { userAccounts, matches: matchResult.stats, error: null, }; } catch (err) { return { ...EMPTY_RESULT, error: err instanceof Error ? err.message : "Failed to load geolocation data", }; } }