import { useEffect, useRef, useState } from 'react';
import { loadModules, setDefaultOptions } from 'esri-loader';
import type EsriConfigModule from '@arcgis/core/config.js';
import type CircleConstructor from '@arcgis/core/geometry/Circle.js';
import type GraphicConstructor from '@arcgis/core/Graphic.js';
import type GraphicsLayerConstructor from '@arcgis/core/layers/GraphicsLayer.js';
import type EsriMapConstructor from '@arcgis/core/Map.js';
import type MapViewConstructor from '@arcgis/core/views/MapView.js';
import { JobMapProps, LiveStatus, liveStatusToMapPoint, MapPoint, pointDetailRows, UTILITY_COLORS } from '../types';

const API_KEY = process.env.NEXT_PUBLIC_ARCGIS_API_KEY || '';
const LIVE_COLOR = '#1a73e8';

const DEFAULT_CENTER: [number, number] = [-98.35, 39.5]; // continental US, [lng, lat]
const DEFAULT_ZOOM = 4;

// Keep in sync with @arcgis/core's version in package.json (major.minor —
// js.arcgis.com is keyed by major.minor only, the full patch version 404s).
const CDN_VERSION = '4.34';

// Loaded via Esri's CDN (esri-loader), not bundled through webpack: the SDK
// is large enough that having Next's dev-mode compiler process it — full
// inline source maps for every vendored module — pegged CPU/memory badly
// enough to OOM the whole host on a first compile. The browser parses/runs
// the CDN script directly, so the dev server never touches it. @arcgis/core
// stays a devDependency purely for its .d.ts types (import type below is
// fully erased, never bundled or evaluated at runtime).
setDefaultOptions({ version: CDN_VERSION, css: true });

interface EsriModules {
  EsriMap: typeof EsriMapConstructor;
  MapView: typeof MapViewConstructor;
  Graphic: typeof GraphicConstructor;
  GraphicsLayer: typeof GraphicsLayerConstructor;
  Circle: typeof CircleConstructor;
  esriConfig: typeof EsriConfigModule;
}

let modulesPromise: Promise<EsriModules> | null = null;

// Cached at module scope (not per-mount) so navigating between pages that
// each render a map doesn't re-trigger the CDN load — esri-loader caches
// the underlying script tag globally too, but this also skips re-running
// the loadModules() round trip on every mount.
function loadEsriModules(): Promise<EsriModules> {
  if (!modulesPromise) {
    modulesPromise = loadModules([
      'esri/Map',
      'esri/views/MapView',
      'esri/Graphic',
      'esri/layers/GraphicsLayer',
      'esri/geometry/Circle',
      'esri/config',
    ]).then(([EsriMap, MapView, Graphic, GraphicsLayer, Circle, esriConfig]: any[]) => {
      esriConfig.apiKey = API_KEY;
      return { EsriMap, MapView, Graphic, GraphicsLayer, Circle, esriConfig } as EsriModules;
    });
  }
  return modulesPromise;
}

function colorFor(utilityType: string): string {
  return UTILITY_COLORS[utilityType] ?? UTILITY_COLORS.UNKNOWN;
}

function orderKey(p: MapPoint): number {
  return p.sequence ?? new Date(p.recordedAt).getTime();
}

// Identifies whether the set of points (including each one's own position)
// has actually changed, so a moving point re-fits bounds even when the
// count doesn't change (e.g. a device's single "current location" point).
function pointsSignature(points: MapPoint[]): string {
  return points.map((p) => `${p.id}:${p.lat.toFixed(7)}:${p.lng.toFixed(7)}`).join('|');
}

function popupHtml(point: MapPoint): string {
  const rows = pointDetailRows(point)
    .map(
      ([label, value]) =>
        `<tr><td style="color:#666;padding-right:0.75rem;white-space:nowrap;">${label}</td><td style="font-weight:600;">${value}</td></tr>`,
    )
    .join('');
  return `<table style="border-collapse:collapse;font-family:sans-serif;">${rows}</table>`;
}

// Esri opens/closes/dismisses-on-outside-click popups natively via each
// graphic's popupTemplate — no hand-rolled "selected point" state needed.
function pointGraphic(mods: EsriModules, point: MapPoint, color: string) {
  return new mods.Graphic({
    geometry: { type: 'point', longitude: point.lng, latitude: point.lat },
    symbol: {
      type: 'simple-marker',
      style: 'circle',
      color,
      size: 10,
      outline: { color: '#ffffff', width: 1.5 },
    },
    popupTemplate: {
      title: `${point.utilityType} · ${point.fixType}`,
      content: popupHtml(point),
    },
  } as ConstructorParameters<typeof GraphicConstructor>[0]);
}

function lineGraphic(mods: EsriModules, points: MapPoint[], color: string) {
  return new mods.Graphic({
    geometry: {
      type: 'polyline',
      paths: [points.map((p) => [p.lng, p.lat])],
    },
    symbol: {
      type: 'simple-line',
      color,
      width: 3,
      opacity: 0.8,
    },
  } as ConstructorParameters<typeof GraphicConstructor>[0]);
}

function liveMarkerGraphics(mods: EsriModules, status: LiveStatus) {
  const position = { type: 'point' as const, longitude: status.lng, latitude: status.lat };
  const marker = new mods.Graphic({
    geometry: position,
    symbol: {
      type: 'simple-marker',
      style: 'circle',
      color: LIVE_COLOR,
      size: 16,
      outline: { color: '#ffffff', width: 2 },
    },
    popupTemplate: {
      title: `Live · ${status.serial}`,
      content: popupHtml(liveStatusToMapPoint(status)),
    },
  } as ConstructorParameters<typeof GraphicConstructor>[0]);

  const halo = new mods.Graphic({
    geometry: new mods.Circle({
      center: [status.lng, status.lat],
      radius: status.hAccuracy ?? 5,
      radiusUnit: 'meters',
      geodesic: true,
    }),
    symbol: {
      type: 'simple-fill',
      color: [...hexToRgb(LIVE_COLOR), 0.15],
      outline: { color: [...hexToRgb(LIVE_COLOR), 0.3], width: 1 },
    },
  } as ConstructorParameters<typeof GraphicConstructor>[0]);

  return [halo, marker];
}

function hexToRgb(hex: string): [number, number, number] {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}

export default function EsriJobMap({ points, liveStatus = null, fitBounds = true, heightPx = 480 }: JobMapProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewRef = useRef<InstanceType<typeof MapViewConstructor> | null>(null);
  const pointsLayerRef = useRef<InstanceType<typeof GraphicsLayerConstructor> | null>(null);
  const liveLayerRef = useRef<InstanceType<typeof GraphicsLayerConstructor> | null>(null);
  const modsRef = useRef<EsriModules | null>(null);
  const fittedSignatureRef = useRef('');
  const [ready, setReady] = useState(false);

  useEffect(() => {
    if (!containerRef.current || !API_KEY) {
      return;
    }
    let cancelled = false;
    let view: InstanceType<typeof MapViewConstructor> | undefined;

    loadEsriModules().then((mods) => {
      if (cancelled || !containerRef.current) {
        return;
      }
      const pointsLayer = new mods.GraphicsLayer();
      const liveLayer = new mods.GraphicsLayer();
      const map = new mods.EsriMap({ basemap: 'hybrid', layers: [pointsLayer, liveLayer] });
      view = new mods.MapView({
        container: containerRef.current,
        map,
        center: DEFAULT_CENTER,
        zoom: DEFAULT_ZOOM,
      });

      modsRef.current = mods;
      pointsLayerRef.current = pointsLayer;
      liveLayerRef.current = liveLayer;
      viewRef.current = view;
      setReady(true);
    });

    return () => {
      cancelled = true;
      view?.destroy();
      viewRef.current = null;
      pointsLayerRef.current = null;
      liveLayerRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    const view = viewRef.current;
    const layer = pointsLayerRef.current;
    const mods = modsRef.current;
    if (!view || !layer || !mods) {
      return;
    }
    layer.removeAll();

    const byUtility = new Map<string, MapPoint[]>();
    for (const p of points) {
      const group = byUtility.get(p.utilityType) ?? [];
      group.push(p);
      byUtility.set(p.utilityType, group);
    }

    for (const [utility, group] of byUtility) {
      const sorted = [...group].sort((a, b) => orderKey(a) - orderKey(b));
      const color = colorFor(utility);
      layer.add(lineGraphic(mods, sorted, color));
      for (const p of sorted) {
        layer.add(pointGraphic(mods, p, color));
      }
    }

    const signature = pointsSignature(points);
    if (fitBounds && signature !== fittedSignatureRef.current && points.length > 0) {
      fittedSignatureRef.current = signature;
      view.when(() => {
        const target =
          points.length === 1 ? { center: [points[0].lng, points[0].lat] } : layer.graphics.toArray();
        view.goTo(target).catch((err: Error) => {
          if (err.name !== 'view:goTo-aborted') {
            console.error('EsriJobMap: goTo failed', err);
          }
        });
      });
    }
  }, [points, fitBounds, ready]);

  useEffect(() => {
    const view = viewRef.current;
    const layer = liveLayerRef.current;
    const mods = modsRef.current;
    if (!view || !layer || !mods) {
      return;
    }
    layer.removeAll();
    if (!liveStatus) {
      return;
    }
    liveMarkerGraphics(mods, liveStatus).forEach((g) => layer.add(g));
    view.when(() => {
      view.goTo({ center: [liveStatus.lng, liveStatus.lat] }).catch((err: Error) => {
        if (err.name !== 'view:goTo-aborted') {
          console.error('EsriJobMap: goTo failed', err);
        }
      });
    });
  }, [liveStatus, ready]);

  if (!API_KEY) {
    return (
      <div
        style={{
          height: heightPx,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          border: '1px dashed #999',
          borderRadius: 8,
          color: '#666',
        }}
      >
        Set NEXT_PUBLIC_ARCGIS_API_KEY in .env to enable the map.
      </div>
    );
  }

  return (
    <div style={{ height: heightPx, borderRadius: 8, overflow: 'hidden' }}>
      <div ref={containerRef} style={{ height: '100%' }} />
    </div>
  );
}
