import { getRpcBaseUrl } from '@/services/rpc-client'; import type { GetDisplacementSummaryResponse as ProtoResponse, CountryDisplacement as ProtoCountry, DisplacementFlow as ProtoFlow } from '@/generated/client/worldmonitor/displacement/v1/service_client'; import { createCircuitBreaker, getCSSColor } from '@/utils'; import { DisplacementServiceClient } from '@/services/generated-rpc-clients'; import { publicRpcFetch } from '@/services/public-rpc-fetch'; // ─── Consumer-friendly types (matching legacy shape exactly) ─── export interface DisplacementFlow { originCode: string; originName: string; asylumCode: string; asylumName: string; refugees: number; // number, NOT string originLat?: number; // flat, NOT GeoCoordinates originLon?: number; asylumLat?: number; asylumLon?: number; } export interface CountryDisplacement { code: string; name: string; refugees: number; asylumSeekers: number; idps: number; stateless: number; totalDisplaced: number; hostRefugees: number; hostAsylumSeekers: number; hostTotal: number; lat?: number; lon?: number; } export interface UnhcrSummary { year: number; globalTotals: { refugees: number; asylumSeekers: number; idps: number; stateless: number; total: number; }; countries: CountryDisplacement[]; topFlows: DisplacementFlow[]; } export interface UnhcrFetchResult { ok: boolean; data: UnhcrSummary; cachedAt?: string; } // ─── Internal: proto -> legacy mapping ─── const emptyResult: UnhcrSummary = { year: new Date().getFullYear(), globalTotals: { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 }, countries: [], topFlows: [], }; function toDisplaySummary(proto: ProtoResponse): UnhcrSummary { const s = proto.summary; if (!s) return { ...emptyResult, globalTotals: { ...emptyResult.globalTotals } }; const gt = s.globalTotals || { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 }; return { year: s.year || new Date().getFullYear(), globalTotals: { refugees: Number(gt.refugees || 0), asylumSeekers: Number(gt.asylumSeekers || 0), idps: Number(gt.idps || 0), stateless: Number(gt.stateless || 0), total: Number(gt.total || 0), }, countries: (s.countries || []).map(toDisplayCountry), topFlows: (s.topFlows || []).map(toDisplayFlow), }; } function toDisplayCountry(proto: ProtoCountry): CountryDisplacement { return { code: proto.code || '', name: proto.name || '', refugees: Number(proto.refugees || 0), asylumSeekers: Number(proto.asylumSeekers || 0), idps: Number(proto.idps || 0), stateless: Number(proto.stateless || 0), totalDisplaced: Number(proto.totalDisplaced || 0), hostRefugees: Number(proto.hostRefugees || 0), hostAsylumSeekers: Number(proto.hostAsylumSeekers || 0), hostTotal: Number(proto.hostTotal || 0), lat: proto.location?.latitude, lon: proto.location?.longitude, }; } function toDisplayFlow(proto: ProtoFlow): DisplacementFlow { return { originCode: proto.originCode || '', originName: proto.originName || '', asylumCode: proto.asylumCode || '', asylumName: proto.asylumName || '', refugees: Number(proto.refugees || 0), originLat: proto.originLocation?.latitude, originLon: proto.originLocation?.longitude, asylumLat: proto.asylumLocation?.latitude, asylumLon: proto.asylumLocation?.longitude, }; } // ─── Client + circuit breaker ─── async function fetchPublicDisplacementSummary(): Promise { return new DisplacementServiceClient(getRpcBaseUrl(), { fetch: publicRpcFetch }) .getDisplacementSummary({ year: 0, // 0 = handler uses year fallback countryLimit: 0, // 0 = all countries flowLimit: 50, // top 50 flows (matching legacy) }); } const breaker = createCircuitBreaker({ name: 'UNHCR Displacement', cacheTtlMs: 10 * 60 * 1000, persistCache: true, }); // ─── Main fetch (public API) ─── export async function fetchUnhcrPopulation(): Promise { const data = await breaker.execute(async () => { const response = await fetchPublicDisplacementSummary(); return toDisplaySummary(response); }, emptyResult, { shouldCache: (r) => r.countries.length > 0 }); return { ok: data !== emptyResult && data.countries.length > 0, data, }; } // ─── Presentation helpers (copied verbatim from legacy src/services/unhcr.ts) ─── export function getDisplacementColor(totalDisplaced: number): [number, number, number, number] { if (totalDisplaced >= 1_000_000) return [255, 50, 50, 200]; if (totalDisplaced >= 500_000) return [255, 150, 0, 200]; if (totalDisplaced >= 100_000) return [255, 220, 0, 180]; return [100, 200, 100, 150]; } export function getDisplacementBadge(totalDisplaced: number): { label: string; color: string } { if (totalDisplaced >= 1_000_000) return { label: 'CRISIS', color: getCSSColor('--semantic-critical') }; if (totalDisplaced >= 500_000) return { label: 'HIGH', color: getCSSColor('--semantic-high') }; if (totalDisplaced >= 100_000) return { label: 'ELEVATED', color: getCSSColor('--semantic-elevated') }; return { label: '', color: '' }; } export function formatPopulation(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`; return String(n); } export function getOriginCountries(data: UnhcrSummary): CountryDisplacement[] { return [...data.countries] .filter(c => c.refugees + c.asylumSeekers > 0) .sort((a, b) => (b.refugees + b.asylumSeekers) - (a.refugees + a.asylumSeekers)); } export function getHostCountries(data: UnhcrSummary): CountryDisplacement[] { return [...data.countries] .filter(c => (c.hostTotal || 0) > 0) .sort((a, b) => (b.hostTotal || 0) - (a.hostTotal || 0)); }