← back to Norma

app/api/globe/country/[code]/route.ts

180 lines

/**
 * GET /api/globe/country/[code]
 * Returns full detail for a single country: profile, organizations,
 * recent statements, and related topics.
 *
 * Response:
 *   CountryDetail (see CountryDetailPanel types)
 */

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { COUNTRY_CENTROIDS } from '@/components/globe/globe-utils';

// Basic country name lookup
const COUNTRY_NAMES: Record<string, string> = {
  US: 'United States', GB: 'United Kingdom', CA: 'Canada', AU: 'Australia',
  DE: 'Germany', FR: 'France', JP: 'Japan', BR: 'Brazil', IN: 'India',
  CN: 'China', MX: 'Mexico', KR: 'South Korea', ZA: 'South Africa',
  NG: 'Nigeria', SE: 'Sweden', NO: 'Norway', NL: 'Netherlands', IT: 'Italy',
  ES: 'Spain', NZ: 'New Zealand', AR: 'Argentina', RU: 'Russia', UA: 'Ukraine',
  PL: 'Poland', TR: 'Turkey', EG: 'Egypt', KE: 'Kenya', SA: 'Saudi Arabia',
  AE: 'UAE', IL: 'Israel', PK: 'Pakistan', BD: 'Bangladesh', PH: 'Philippines',
  ID: 'Indonesia', MY: 'Malaysia', SG: 'Singapore', TH: 'Thailand', VN: 'Vietnam',
  PT: 'Portugal', GR: 'Greece', BE: 'Belgium', CH: 'Switzerland', AT: 'Austria',
  CZ: 'Czech Republic', RO: 'Romania', HU: 'Hungary', DK: 'Denmark', FI: 'Finland',
  IE: 'Ireland', CL: 'Chile', CO: 'Colombia', PE: 'Peru', ET: 'Ethiopia',
  TZ: 'Tanzania', GH: 'Ghana', MA: 'Morocco', DZ: 'Algeria', TN: 'Tunisia',
};

const COUNTRY_REGIONS: Record<string, string> = {
  US: 'North America', CA: 'North America', MX: 'North America',
  GB: 'Europe', DE: 'Europe', FR: 'Europe', IT: 'Europe', ES: 'Europe',
  NL: 'Europe', BE: 'Europe', CH: 'Europe', AT: 'Europe', SE: 'Europe',
  NO: 'Europe', DK: 'Europe', FI: 'Europe', PL: 'Europe', CZ: 'Europe',
  RO: 'Europe', HU: 'Europe', GR: 'Europe', PT: 'Europe', IE: 'Europe',
  UA: 'Europe', RU: 'Europe/Asia',
  CN: 'Asia', JP: 'Asia', KR: 'Asia', IN: 'Asia', ID: 'Asia',
  PH: 'Asia', VN: 'Asia', TH: 'Asia', MY: 'Asia', SG: 'Asia',
  BD: 'Asia', PK: 'Asia', TR: 'Asia',
  BR: 'South America', AR: 'South America', CL: 'South America',
  CO: 'South America', PE: 'South America',
  AU: 'Oceania', NZ: 'Oceania',
  NG: 'Africa', ZA: 'Africa', KE: 'Africa', EG: 'Africa',
  GH: 'Africa', ET: 'Africa', TZ: 'Africa', MA: 'Africa',
  DZ: 'Africa', TN: 'Africa',
  SA: 'Middle East', AE: 'Middle East', IL: 'Middle East',
};

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ code: string }> },
) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { code } = await params;
  const countryCode = code.toUpperCase().trim();

  if (!/^[A-Z]{2,3}$/.test(countryCode)) {
    return NextResponse.json({ error: 'Invalid country code' }, { status: 400 });
  }

  try {
    const centroid = COUNTRY_CENTROIDS[countryCode] || [0, 0];

    // Base profile
    let profile = {
      code: countryCode,
      name: COUNTRY_NAMES[countryCode] || countryCode,
      region: COUNTRY_REGIONS[countryCode] || null,
      lat: centroid[0],
      lng: centroid[1],
      article_count: 0,
      org_count: 0,
      sentiment_avg: null as number | null,
      todays_headlines: 0,
    };

    // Try country_profiles table
    await query(
      `SELECT country_name, region, latitude, longitude, article_count, org_count, sentiment_avg
       FROM country_profiles
       WHERE country_code = $1
       LIMIT 1`,
      [countryCode],
    ).then(res => {
      if (res.rows.length > 0) {
        const r = res.rows[0];
        profile = {
          ...profile,
          name: r.country_name || profile.name,
          region: r.region || profile.region,
          lat: typeof r.latitude === 'number' ? r.latitude : profile.lat,
          lng: typeof r.longitude === 'number' ? r.longitude : profile.lng,
          article_count: Number(r.article_count) || 0,
          org_count: Number(r.org_count) || 0,
          sentiment_avg: r.sentiment_avg != null ? Number(r.sentiment_avg) : null,
        };
      }
    }).catch(() => { /* table may not exist */ });

    // Fetch today's headlines count
    await query(
      `SELECT COUNT(*)::int AS cnt FROM newspaper_frontpages
       WHERE country = $1 AND scraped_date = CURRENT_DATE`,
      [countryCode],
    ).then(res => {
      profile.todays_headlines = Number(res.rows[0]?.cnt) || 0;
    }).catch(() => { /* table may not exist */ });

    // Fetch matching organizations
    const orgsResult = await query(
      `SELECT id, name, type, state, country, website
       FROM organizations
       WHERE UPPER(TRIM(country)) = $1
          OR UPPER(TRIM(state)) = $1
       ORDER BY name ASC
       LIMIT 20`,
      [countryCode],
    ).catch(() => ({ rows: [] }));

    // Fetch recent statements from this country
    const stmtsResult = await query(
      `SELECT
         ns.id,
         o.name AS organization_name,
         ns.title,
         ns.topic,
         ns.published_at
       FROM nonprofit_statements ns
       JOIN organizations o ON o.id = ns.organization_id
       WHERE UPPER(TRIM(ns.country)) = $1
          OR UPPER(TRIM(o.country)) = $1
       ORDER BY ns.published_at DESC NULLS LAST
       LIMIT 8`,
      [countryCode],
    ).catch(() => ({ rows: [] }));

    // Fetch related topics from Pulse API (via DB if available)
    let topics: Array<{ title: string; urgency: string; article_count: number }> = [];
    await query(
      `SELECT title, urgency, article_count
       FROM pulse_topics
       WHERE geo_states @> ARRAY[$1]::text[]
          OR geo_states @> ARRAY[$2]::text[]
       ORDER BY urgency ASC, article_count DESC
       LIMIT 6`,
      [countryCode, COUNTRY_NAMES[countryCode] || countryCode],
    ).then(res => {
      topics = res.rows.map(r => ({
        title: String(r.title),
        urgency: String(r.urgency),
        article_count: Number(r.article_count) || 0,
      }));
    }).catch(() => { /* pulse_topics table may not exist */ });

    return NextResponse.json({
      ...profile,
      organizations: orgsResult.rows.map(r => ({
        id: String(r.id),
        name: String(r.name),
        type: r.type ? String(r.type) : null,
        state: r.state ? String(r.state) : null,
      })),
      statements: stmtsResult.rows.map(r => ({
        id: String(r.id),
        organization_name: String(r.organization_name),
        title: r.title ? String(r.title) : null,
        topic: r.topic ? String(r.topic) : null,
        published_at: r.published_at ? String(r.published_at) : null,
      })),
      topics,
    });
  } catch (err) {
    console.error('[api/globe/country/[code]] error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch country detail' }, { status: 500 });
  }
}