/**
 * RPC: listTechEvents
 *
 * Aggregates tech events from three sources:
 * - Techmeme ICS calendar
 * - dev.events RSS feed
 * - Curated major conferences
 *
 * Supports filtering by type, mappability, time range, and limit.
 * Includes geocoding via 500-city coordinate lookup.
 * Returns graceful error response on failure.
 */

import type {
  ServerContext,
  ListTechEventsRequest,
  ListTechEventsResponse,
  TechEvent,
  TechEventCoords,
} from '../../../../src/generated/server/worldmonitor/research/v1/service_server';
import { CITY_COORDS } from '../../../../api/data/city-coords';
import filterParamContracts from '../../../../shared/openapi-filter-param-contracts.json';
import { CHROME_UA } from '../../../_shared/constants';
import { resolveTechEventsPaging, type TechEventsPagingPresence } from './_tech-events-paging';
import { cachedFetchJson } from '../../../_shared/redis';
import { getRelayBaseUrl, getRelayHeaders } from '../../../_shared/relay';

const REDIS_CACHE_KEY = 'research:tech-events:v1';
const REDIS_CACHE_TTL = 21600; // 6 hr — weekly event data

// ---------- Constants ----------

const ICS_URL = 'https://www.techmeme.com/newsy_events.ics';
const DEV_EVENTS_RSS = 'https://dev.events/rss.xml';
const FETCH_TIMEOUT_MS = 8000;
const TECH_EVENT_TYPES = new Set(filterParamContracts.researchTechEventTypes);

function readTechEventsPagingPresence(ctx: ServerContext): TechEventsPagingPresence {
  const searchParams = new URL(ctx.request.url, 'http://localhost').searchParams;
  return {
    hasLimit: searchParams.has('limit'),
    hasDays: searchParams.has('days'),
  };
}

// ---------- Relay helpers (Railway proxy for blocked sources) ----------

const RSS_ACCEPT = 'application/rss+xml, application/xml, text/xml, text/calendar, */*';

async function fetchTextWithRelay(url: string): Promise<string | null> {
  // Try direct fetch first
  try {
    const resp = await fetch(url, {
      headers: { 'User-Agent': CHROME_UA },
      signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
    });
    if (resp.ok) {
      const text = await resp.text();
      if (text.length > 100) return text;
      console.warn(`[tech-events] Direct fetch ${url} returned short response (${text.length} chars)`);
    } else {
      console.warn(`[tech-events] Direct fetch ${url}: HTTP ${resp.status}`);
    }
  } catch (e) {
    console.warn(`[tech-events] Direct fetch ${url} failed: ${(e as Error).message}`);
  }

  // Fallback: route through Railway relay (different IP, avoids Vercel edge blocks)
  const relayBase = getRelayBaseUrl();
  if (relayBase) {
    try {
      const relayUrl = `${relayBase}/rss?url=${encodeURIComponent(url)}`;
      const resp = await fetch(relayUrl, {
        headers: getRelayHeaders({ Accept: RSS_ACCEPT }),
        signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
      });
      if (resp.ok) {
        const text = await resp.text();
        if (text.length > 100) {
          console.log(`[tech-events] Relay fetch ${url}: success (${text.length} chars)`);
          return text;
        }
      } else {
        console.warn(`[tech-events] Relay fetch ${url}: HTTP ${resp.status}`);
      }
    } catch (e) {
      console.warn(`[tech-events] Relay fetch ${url} failed: ${(e as Error).message}`);
    }
  }

  return null;
}

// Curated major tech events that may fall off limited RSS feeds
const CURATED_EVENTS: TechEvent[] = [
  {
    id: 'gitex-global-2026',
    title: 'GITEX Global 2026',
    type: 'conference',
    location: 'Dubai World Trade Centre, Dubai',
    coords: { lat: 25.2285, lng: 55.2867, country: 'UAE', original: 'Dubai World Trade Centre, Dubai', virtual: false },
    startDate: '2026-12-07',
    endDate: '2026-12-11',
    url: 'https://www.gitex.com',
    source: 'curated',
    description: 'World\'s largest tech & startup show',
  },
  {
    id: 'token2049-dubai-2026',
    title: 'TOKEN2049 Dubai 2026',
    type: 'conference',
    location: 'Dubai, UAE',
    coords: { lat: 25.2048, lng: 55.2708, country: 'UAE', original: 'Dubai, UAE', virtual: false },
    startDate: '2026-04-29',
    endDate: '2026-04-30',
    url: 'https://www.token2049.com',
    source: 'curated',
    description: 'Premier crypto event in Dubai',
  },
  {
    id: 'collision-2026',
    title: 'Collision 2026',
    type: 'conference',
    location: 'Toronto, Canada',
    coords: { lat: 43.6532, lng: -79.3832, country: 'Canada', original: 'Toronto, Canada', virtual: false },
    startDate: '2026-06-22',
    endDate: '2026-06-25',
    url: 'https://collisionconf.com',
    source: 'curated',
    description: 'North America\'s fastest growing tech conference',
  },
  {
    id: 'web-summit-2026',
    title: 'Web Summit 2026',
    type: 'conference',
    location: 'Lisbon, Portugal',
    coords: { lat: 38.7223, lng: -9.1393, country: 'Portugal', original: 'Lisbon, Portugal', virtual: false },
    startDate: '2026-11-02',
    endDate: '2026-11-05',
    url: 'https://websummit.com',
    source: 'curated',
    description: 'The world\'s premier tech conference',
  },
];

// ---------- Geocoding ----------

function normalizeLocation(location: string | null): (TechEventCoords) | null {
  if (!location) return null;

  // Clean up the location string
  let normalized = location.toLowerCase().trim();

  // Remove common suffixes/prefixes
  normalized = normalized.replace(/^hybrid:\s*/i, '');
  normalized = normalized.replace(/,\s*(usa|us|uk|canada)$/i, '');

  // Direct lookup
  if (CITY_COORDS[normalized]) {
    const c = CITY_COORDS[normalized];
    return { lat: c!.lat, lng: c!.lng, country: c!.country, original: location, virtual: c!.virtual ?? false };
  }

  // Try removing state/country suffix
  const parts = normalized.split(',');
  if (parts.length > 1) {
    const city = parts[0]!.trim();
    if (CITY_COORDS[city]) {
      const c = CITY_COORDS[city]!;
      return { lat: c.lat, lng: c.lng, country: c.country, original: location, virtual: c.virtual ?? false };
    }
  }

  // Try fuzzy match (contains)
  for (const [key, coords] of Object.entries(CITY_COORDS)) {
    if (normalized.includes(key) || key.includes(normalized)) {
      return { lat: coords.lat, lng: coords.lng, country: coords.country, original: location, virtual: coords.virtual ?? false };
    }
  }

  return null;
}

// ---------- ICS Parser ----------

function parseICS(icsText: string): TechEvent[] {
  const events: TechEvent[] = [];
  const eventBlocks = icsText.split('BEGIN:VEVENT').slice(1);

  for (const block of eventBlocks) {
    const summaryMatch = block.match(/SUMMARY:(.+)/);
    const locationMatch = block.match(/LOCATION:(.+)/);
    const dtstartMatch = block.match(/DTSTART;VALUE=DATE:(\d+)/);
    const dtendMatch = block.match(/DTEND;VALUE=DATE:(\d+)/);
    const urlMatch = block.match(/URL:(.+)/);
    const uidMatch = block.match(/UID:(.+)/);

    if (summaryMatch && dtstartMatch) {
      const summary = summaryMatch[1]!.trim();
      const location = locationMatch ? locationMatch[1]!.trim() : '';
      const startDate = dtstartMatch[1]!;
      const endDate = dtendMatch ? dtendMatch[1]! : startDate;
      const url = urlMatch ? urlMatch[1]!.trim() : '';
      const uid = uidMatch ? uidMatch[1]!.trim() : '';

      // Determine event type
      let type = 'other';
      if (summary.startsWith('Earnings:')) type = 'earnings';
      else if (summary.startsWith('IPO')) type = 'ipo';
      else if (location) type = 'conference';

      // Parse coordinates if location exists
      const coords = normalizeLocation(location || null);

      events.push({
        id: uid,
        title: summary,
        type,
        location: location,
        coords: coords ?? undefined,
        startDate: `${startDate.slice(0, 4)}-${startDate.slice(4, 6)}-${startDate.slice(6, 8)}`,
        endDate: `${endDate.slice(0, 4)}-${endDate.slice(4, 6)}-${endDate.slice(6, 8)}`,
        url: url,
        source: 'techmeme',
        description: '',
      });
    }
  }

  return events.sort((a, b) => a.startDate.localeCompare(b.startDate));
}

// ---------- RSS Parser ----------

function parseDevEventsRSS(rssText: string): TechEvent[] {
  const events: TechEvent[] = [];

  // Simple regex-based RSS parsing for edge runtime
  const itemMatches = rssText.matchAll(/<item>([\s\S]*?)<\/item>/g);

  for (const match of itemMatches) {
    const item = match[1]!;

    const titleMatch = item.match(/<title><!\[CDATA\[(.*?)\]\]><\/title>|<title>(.*?)<\/title>/);
    const linkMatch = item.match(/<link>(.*?)<\/link>/);
    const descMatch = item.match(/<description><!\[CDATA\[(.*?)\]\]><\/description>|<description>(.*?)<\/description>/s);
    const guidMatch = item.match(/<guid[^>]*>(.*?)<\/guid>/);

    const title = titleMatch ? (titleMatch[1] ?? titleMatch[2]) : null;
    const link = linkMatch ? linkMatch[1] ?? '' : '';
    const description = descMatch ? (descMatch[1] ?? descMatch[2] ?? '') : '';
    const guid = guidMatch ? guidMatch[1] ?? '' : '';

    if (!title) continue;

    // Parse date from description: "EventName is happening on Month Day, Year"
    const dateMatch = description.match(/on\s+(\w+\s+\d{1,2},?\s+\d{4})/i);
    let startDate: string | null = null;
    if (dateMatch) {
      const parsed = new Date(dateMatch[1]!);
      if (!Number.isNaN(parsed.getTime())) {
        startDate = parsed.toISOString().split('T')[0]!;
      }
    }

    // Parse location from description: various formats
    let location: string | null = null;
    const locationMatch = description.match(/(?:in|at)\s+([A-Za-z\s]+,\s*[A-Za-z\s]+)(?:\.|$)/i) ||
                          description.match(/Location:\s*([^<\n]+)/i);
    if (locationMatch) {
      location = locationMatch[1]!.trim();
    }
    // Check for "Online" events
    if (description.toLowerCase().includes('online')) {
      location = 'Online';
    }

    // Skip events without valid dates or in the past
    if (!startDate) continue;
    const eventDate = new Date(startDate);
    const now = new Date();
    now.setHours(0, 0, 0, 0);
    if (eventDate < now) continue;

    const coords = location && location !== 'Online' ? normalizeLocation(location) : null;

    events.push({
      id: guid || `dev-events-${title.slice(0, 20)}`,
      title: title,
      type: 'conference',
      location: location || '',
      coords: coords ?? (location === 'Online' ? { lat: 0, lng: 0, country: 'Virtual', original: 'Online', virtual: true } : undefined),
      startDate: startDate,
      endDate: startDate, // RSS doesn't have end date
      url: link,
      source: 'dev.events',
      description: '',
    });
  }

  return events;
}

// ---------- Fetch ----------

async function fetchTechEvents(
  req: ListTechEventsRequest,
  pagingPresence: TechEventsPagingPresence,
): Promise<ListTechEventsResponse> {
  const { type, mappable } = req;
  const { limit, days } = resolveTechEventsPaging(req, pagingPresence);

  // Fetch both sources in parallel (direct → relay fallback)
  const [icsText, rssText] = await Promise.all([
    fetchTextWithRelay(ICS_URL),
    fetchTextWithRelay(DEV_EVENTS_RSS),
  ]);

  let events: TechEvent[] = [];
  let externalSourcesFailed = 0;

  // Parse Techmeme ICS
  if (icsText) {
    const parsed = parseICS(icsText);
    events.push(...parsed);
    console.log(`[tech-events] Techmeme ICS: ${parsed.length} events parsed`);
  } else {
    externalSourcesFailed++;
    console.warn(`[tech-events] Techmeme ICS: no data (direct + relay both failed)`);
  }

  // Parse dev.events RSS
  if (rssText) {
    const devEvents = parseDevEventsRSS(rssText);
    events.push(...devEvents);
    console.log(`[tech-events] dev.events RSS: ${devEvents.length} events parsed`);
  } else {
    externalSourcesFailed++;
    console.warn(`[tech-events] dev.events RSS: no data (direct + relay both failed)`);
  }

  // Add curated events (major conferences that may fall off limited RSS feeds)
  const now = new Date();
  now.setHours(0, 0, 0, 0);
  for (const curated of CURATED_EVENTS) {
    const eventDate = new Date(curated.startDate);
    if (eventDate >= now) {
      events.push(curated);
    }
  }

  // Deduplicate by title similarity (rough match)
  const seen = new Set<string>();
  events = events.filter(e => {
    const year = e.startDate.slice(0, 4);
    const key = e.title.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 30) + year;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });

  // Sort by date
  events.sort((a, b) => a.startDate.localeCompare(b.startDate));

  // Filter by type if specified
  if (type && type !== 'all') {
    events = TECH_EVENT_TYPES.has(type) ? events.filter(e => e.type === type) : [];
  }

  // Filter to only mappable events if requested
  if (mappable) {
    events = events.filter(e => e.coords && !e.coords.virtual);
  }

  // Filter by time range if specified
  if (days > 0) {
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() + days);
    events = events.filter(e => new Date(e.startDate) <= cutoff);
  }

  // Apply limit if specified
  if (limit > 0) {
    events = events.slice(0, limit);
  }

  // Add metadata
  const conferences = events.filter(e => e.type === 'conference');
  const mappableCount = conferences.filter(e => e.coords && !e.coords.virtual).length;

  if (externalSourcesFailed > 0) {
    console.warn(`[tech-events] ${externalSourcesFailed}/2 external sources failed, returning ${events.length} events (curated fallback)`);
  }

  return {
    success: true,
    count: events.length,
    conferenceCount: conferences.length,
    mappableCount,
    lastUpdated: new Date().toISOString(),
    events,
    error: '',
  };
}

// ---------- Geocode + filter ----------

function geocodeEvents(events: TechEvent[]): TechEvent[] {
  return events.map(e => {
    if (e.coords) return e;
    const coords = normalizeLocation(e.location || null);
    return coords ? { ...e, coords } : e;
  });
}

function filterEvents(
  events: TechEvent[],
  req: ListTechEventsRequest,
  pagingPresence: TechEventsPagingPresence,
): ListTechEventsResponse {
  const { type, mappable } = req;
  const { limit, days } = resolveTechEventsPaging(req, pagingPresence);

  let filtered = [...events];

  if (type && type !== 'all') {
    filtered = TECH_EVENT_TYPES.has(type) ? filtered.filter(e => e.type === type) : [];
  }
  if (mappable) {
    filtered = filtered.filter(e => e.coords && !e.coords.virtual);
  }
  if (days > 0) {
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() + days);
    filtered = filtered.filter(e => new Date(e.startDate) <= cutoff);
  }
  if (limit > 0) {
    filtered = filtered.slice(0, limit);
  }

  const conferences = filtered.filter(e => e.type === 'conference');
  const mappableCount = conferences.filter(e => e.coords && !e.coords.virtual).length;

  return {
    success: true,
    count: filtered.length,
    conferenceCount: conferences.length,
    mappableCount,
    lastUpdated: new Date().toISOString(),
    events: filtered,
    error: '',
  };
}

// ---------- Handler ----------

export async function listTechEvents(
  ctx: ServerContext,
  req: ListTechEventsRequest,
): Promise<ListTechEventsResponse> {
  try {
    const pagingPresence = readTechEventsPagingPresence(ctx);

    // Primary: read from seed-populated Redis key (Railway relay seeds this every 6h)
    const result = await cachedFetchJson<ListTechEventsResponse>(REDIS_CACHE_KEY, REDIS_CACHE_TTL, async () => {
      // Fallback fetcher: only runs on cold start when seed hasn't populated yet
      const fetched = await fetchTechEvents(req, pagingPresence);
      return fetched.events.length > 0 ? fetched : null;
    });

    if (!result || result.events.length === 0) {
      return { success: true, count: 0, conferenceCount: 0, mappableCount: 0, lastUpdated: new Date().toISOString(), events: [], error: '' };
    }

    // Apply geocoding (seed stores events without coords) and filter by request params
    const geocoded = geocodeEvents(result.events);
    return filterEvents(geocoded, req, pagingPresence);
  } catch (error) {
    return {
      success: false,
      count: 0,
      conferenceCount: 0,
      mappableCount: 0,
      lastUpdated: new Date().toISOString(),
      events: [],
      error: error instanceof Error ? error.message : String(error),
    };
  }
}
