import { env } from '../../../config/env';
import { redis } from '../../../config/redis';
import { prisma } from '../../../config/database';
import { logger } from '../../../utils/logger';
import { parseGeoJsonPolygon, calculateCentroid } from '../../../utils/spatial';
import { geocodingService } from '../geocoding/geocoding.service';
import { siteSettingsService } from '../../settings/settings.service';

// --- Types ---

export interface MapEvent {
  id: number;
  title: string;
  description: string;
  placeName: string;
  placeAddress: string;
  latitude: number;
  longitude: number;
  startDatetime: string;
  endDatetime: string | null;
  tags: string[];
  shiftId: string | null;
}

// --- Constants ---

const MAP_EVENTS_CACHE_KEY = 'MAP_EVENTS_CACHE';
const CACHE_TTL_SECONDS = 5 * 60; // 5 minutes
const MAX_EVENTS = 50;
const MAX_DAYS_AHEAD = 30;

// --- Service ---

export async function getMapEvents(): Promise<MapEvent[]> {
  // Check feature flag
  const settings = await siteSettingsService.getPublic();
  if (!settings.enableEvents) return [];

  // Check Redis cache
  try {
    const cached = await redis.get(MAP_EVENTS_CACHE_KEY);
    if (cached) return JSON.parse(cached) as MapEvent[];
  } catch {
    // Cache miss or error, proceed with fresh fetch
  }

  // Fetch events from Gancio public API (no auth needed)
  let rawEvents: Array<{
    id: number;
    title: string;
    description: string;
    place_name: string;
    place_address: string;
    start_datetime: number;
    end_datetime?: number;
    tags: string[];
  }>;

  try {
    const url = `${env.GANCIO_URL}/api/events`;
    const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
    if (!res.ok) {
      logger.debug(`Gancio events API returned ${res.status}`);
      return [];
    }
    rawEvents = await res.json() as typeof rawEvents;
  } catch (err) {
    logger.debug('Failed to fetch events from Gancio for map layer:', err);
    return [];
  }

  // Filter to upcoming events (now → 30 days ahead), limit to 50
  const nowUnix = Math.floor(Date.now() / 1000);
  const maxUnix = nowUnix + MAX_DAYS_AHEAD * 86400;

  const upcoming = rawEvents
    .filter(e => e.start_datetime >= nowUnix && e.start_datetime <= maxUnix)
    .sort((a, b) => a.start_datetime - b.start_datetime)
    .slice(0, MAX_EVENTS);

  if (upcoming.length === 0) {
    await cacheResult([]);
    return [];
  }

  // Batch-load matching Shifts by gancioEventId (single query)
  const gancioIds = upcoming.map(e => e.id);
  const shifts = await prisma.shift.findMany({
    where: { gancioEventId: { in: gancioIds } },
    select: {
      id: true,
      gancioEventId: true,
      cut: { select: { geojson: true } },
    },
  });

  // Build lookup: gancioEventId → { shiftId, geojson }
  const shiftLookup = new Map<number, { shiftId: string; geojson: string | null }>();
  for (const shift of shifts) {
    if (shift.gancioEventId !== null) {
      shiftLookup.set(shift.gancioEventId, {
        shiftId: shift.id,
        geojson: shift.cut?.geojson ?? null,
      });
    }
  }

  // Resolve locations for each event
  const results: MapEvent[] = [];

  for (const event of upcoming) {
    const shiftData = shiftLookup.get(event.id) ?? null;
    const coords = await resolveEventLocation(event.place_address, shiftData?.geojson ?? null);
    if (!coords) continue;

    results.push({
      id: event.id,
      title: event.title,
      description: (event.description || '').slice(0, 300),
      placeName: event.place_name || '',
      placeAddress: event.place_address || '',
      latitude: coords.lat,
      longitude: coords.lng,
      startDatetime: new Date(event.start_datetime * 1000).toISOString(),
      endDatetime: event.end_datetime ? new Date(event.end_datetime * 1000).toISOString() : null,
      tags: Array.isArray(event.tags) ? event.tags : [],
      shiftId: shiftData?.shiftId ?? null,
    });
  }

  await cacheResult(results);
  return results;
}

/**
 * Resolve lat/lng for a Gancio event.
 * Priority: 1) Shift's Cut centroid, 2) Geocode place_address, 3) null (skip)
 */
async function resolveEventLocation(
  placeAddress: string,
  geojson: string | null,
): Promise<{ lat: number; lng: number } | null> {
  // Try centroid from Shift's Cut polygon
  if (geojson) {
    try {
      const rings = parseGeoJsonPolygon(geojson);
      if (rings.length > 0 && rings[0]!.length > 0) {
        return calculateCentroid(rings[0]!);
      }
    } catch {
      // Bad GeoJSON, fall through to geocoding
    }
  }

  // Fallback: geocode the place_address
  if (placeAddress && placeAddress !== 'TBD') {
    try {
      const result = await geocodingService.geocode(placeAddress);
      if (result) {
        return { lat: result.latitude, lng: result.longitude };
      }
    } catch {
      // Geocoding failed, skip this event
    }
  }

  return null;
}

async function cacheResult(events: MapEvent[]): Promise<void> {
  try {
    await redis.setex(MAP_EVENTS_CACHE_KEY, CACHE_TTL_SECONDS, JSON.stringify(events));
  } catch {
    // Non-critical, log silently
  }
}
