import { useEffect, useRef, useState } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { gridToLatLon, gridSquareBounds, greatCirclePoints, pathBetween, destinationPoint } from '@/lib/maidenhead';
import { writeUiPref } from '@/lib/uiPref';

// Persisted free-pan view of the world map (when auto-zoom is off).
function loadMapView(): { lat: number; lon: number; zoom: number } | null {
  try { const v = JSON.parse(localStorage.getItem('opslog.mapView') || 'null'); return v && typeof v.zoom === 'number' ? v : null; }
  catch { return null; }
}
function saveMapView(m: L.Map) {
  const c = m.getCenter();
  writeUiPref('opslog.mapView', JSON.stringify({ lat: c.lat, lon: c.lng, zoom: m.getZoom() }));
}

// The Main tab is built from two independent map panes that the operator can
// place on either side (Settings → Main view):
//  • WorldMap   ("map1"): a world map with the great-circle path from the
//    operator to the contacted station, distance, short/long-path azimuth and
//    the antenna beam lobe.
//  • LocatorMap ("map2"): a street map zoomed onto the contacted station's grid.
// Built on Leaflet + free OSM/Carto tiles (no API key). Endpoints use
// circleMarkers / divIcons so we don't depend on Leaflet's image assets.

// unwrapLon makes a lat/lon ring continuous in longitude (each point within
// 180° of the previous) so a polygon crossing the antimeridian doesn't snap
// across the whole map. Coords may exceed ±180; Leaflet (worldCopyJump) is fine.
function unwrapLon(ring: [number, number][]): [number, number][] {
  const out: [number, number][] = [];
  let prev = NaN;
  for (const [la, lo] of ring) {
    let lon = lo;
    if (!Number.isNaN(prev)) {
      while (lon - prev > 180) lon -= 360;
      while (lon - prev < -180) lon += 360;
    }
    prev = lon;
    out.push([la, lon]);
  }
  return out;
}

const CARTO_LIGHT = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
const OSM = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
const CARTO_ATTR = '&copy; OpenStreetMap &copy; CARTO';
const OSM_ATTR = '&copy; OpenStreetMap contributors';

function dot(color: string): L.DivIcon {
  return L.divIcon({
    className: '',
    html: `<span style="display:block;width:12px;height:12px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.4)"></span>`,
    iconSize: [12, 12],
    iconAnchor: [6, 6],
  });
}

interface WorldProps {
  fromGrid: string;   // operator grid (active profile)
  toGrid: string;     // contacted-station grid
  fromLabel?: string; // operator callsign
  toLabel?: string;   // DX callsign
  beamAzimuths?: number[];     // antenna heading(s) (deg) → draw a beam lobe each
  beamWidth?: number;          // beamwidth (deg), default 30
}

// WorldMap — great-circle path + beam lobe(s), the "map1" pane.
export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, beamWidth }: WorldProps) {
  const worldRef = useRef<HTMLDivElement>(null);
  const worldMap = useRef<L.Map | null>(null);
  const worldOverlay = useRef<L.LayerGroup | null>(null);

  // Auto-zoom the world map to fit QTH→DX on each QSO. When off, the operator
  // pans/zooms freely (e.g. a whole-world view) and the view is remembered
  // across restarts. Default on.
  const [autoZoom, setAutoZoom] = useState(() => localStorage.getItem('opslog.mapAutoZoomDX') !== '0');
  const autoZoomRef = useRef(autoZoom);
  useEffect(() => { autoZoomRef.current = autoZoom; }, [autoZoom]);

  // One-time map creation.
  useEffect(() => {
    if (worldRef.current && !worldMap.current) {
      const m = L.map(worldRef.current, { zoomControl: true, attributionControl: true, worldCopyJump: true })
        .setView([20, 0], 1);
      L.tileLayer(CARTO_LIGHT, { attribution: CARTO_ATTR, subdomains: 'abcd', maxZoom: 19 }).addTo(m);
      worldOverlay.current = L.layerGroup().addTo(m);
      worldMap.current = m;
      const sv = loadMapView();
      if (!autoZoomRef.current && sv) m.setView([sv.lat, sv.lon], sv.zoom);
      m.on('moveend', () => { if (!autoZoomRef.current) saveMapView(m); });
    }
    const t = window.setTimeout(() => { worldMap.current?.invalidateSize(); }, 80);
    return () => window.clearTimeout(t);
  }, []);

  // Redraw overlays whenever the operator/DX grids (or beam) change.
  useEffect(() => {
    const wm = worldMap.current, wo = worldOverlay.current;
    if (!wm || !wo) return;
    wo.clearLayers();

    const from = gridToLatLon(fromGrid);
    const to = gridToLatLon(toGrid);

    if (from && to) {
      L.marker([from.lat, from.lon], { icon: dot('#2563eb'), title: fromLabel || 'QTH' })
        .bindTooltip(`${fromLabel ? fromLabel + ' · ' : ''}${fromGrid.toUpperCase()}`, { permanent: false, direction: 'top' })
        .addTo(wo);
      L.marker([to.lat, to.lon], { icon: dot('#dc2626'), title: toLabel || 'DX' })
        .bindTooltip(`${toLabel ? toLabel + ' · ' : ''}${toGrid.toUpperCase()}`, { permanent: false, direction: 'top' })
        .addTo(wo);
      const pts = greatCirclePoints(from.lat, from.lon, to.lat, to.lon, 96);
      L.polyline(unwrapLon(pts) as L.LatLngExpression[],
        { color: '#2563eb', weight: 2, opacity: 0.8 }).addTo(wo);

      // ── Antenna beam lobe(s) (drawn first, under the arc/markers) ──
      if (beamAzimuths && beamAzimuths.length) {
        const half = (beamWidth ?? 30) / 2;
        const D = 5500; // lobe length (km)
        // Great-circle radial out to distance D, stopping just short of the pole
        // so a poleward line doesn't snap across the top of the Mercator map.
        const radial = (b: number): [number, number][] => {
          const out: [number, number][] = [];
          const N = 64;
          for (let i = 1; i <= N; i++) {
            const d = destinationPoint(from.lat, from.lon, b, (D * i) / N);
            out.push([d.lat, d.lon]);
            if (Math.abs(d.lat) > 86) break; // near a pole — stop before the snap
          }
          return out;
        };
        for (const az of beamAzimuths) {
          // Draw the lobe as a FAN of translucent great-circle radials, not a
          // filled polygon: a polygon breaks badly near the poles on Mercator
          // (its edges run off toward ±90° and the fill smears across the map),
          // while each radial LINE stays clean. The overlapping lines read as a
          // lobe — solid near the antenna, fanning out toward the front. Works
          // for any azimuth, north/south included.
          for (let b = az - half; b <= az + half + 0.001; b += 1.5) {
            const line = unwrapLon([[from.lat, from.lon], ...radial(b)]);
            L.polyline(line as L.LatLngExpression[], { color: '#dc2626', weight: 6, opacity: 0.07 }).addTo(wo);
          }
          const cl = unwrapLon([[from.lat, from.lon], ...radial(az)]);
          L.polyline(cl as L.LatLngExpression[], { color: '#dc2626', weight: 1.5, opacity: 0.7, dashArray: '5 4' })
            .bindTooltip(`Beam ${Math.round(az)}°`, { permanent: false, direction: 'top' }).addTo(wo);
        }
      }

      if (autoZoom) {
        const bounds = L.latLngBounds([[from.lat, from.lon], [to.lat, to.lon]]);
        pts.forEach((p) => bounds.extend(p as L.LatLngExpression));
        wm.fitBounds(bounds, { padding: [30, 30], maxZoom: 6 });
      }
    } else if (autoZoom && to) {
      wm.setView([to.lat, to.lon], 3);
    } else if (autoZoom && from) {
      wm.setView([from.lat, from.lon], 3);
    }
    setTimeout(() => { wm.invalidateSize(); }, 0);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [fromGrid, toGrid, fromLabel, toLabel, (beamAzimuths ?? []).map((a) => Math.round(a)).join(','), beamWidth, autoZoom]);

  const path = pathBetween(fromGrid, toGrid);

  return (
    <div className="relative isolate h-full w-full rounded-lg overflow-hidden border border-border">
      <div ref={worldRef} className="absolute inset-0" />
      {/* Auto-zoom toggle: on = frame QTH→DX each QSO; off = free pan/zoom
          (remembered across restarts), so the beam heading stays visible. */}
      <button
        type="button"
        onClick={() => {
          const v = !autoZoom;
          setAutoZoom(v);
          writeUiPref('opslog.mapAutoZoomDX', v ? '1' : '0');
          const m = worldMap.current;
          if (!v && m) saveMapView(m);
        }}
        title={autoZoom ? 'Auto-zoom to DX is ON — click for free pan/zoom (remembered)' : 'Free pan/zoom — click to auto-zoom to the DX'}
        className={`absolute top-1 right-1 z-[500] rounded-md px-2 py-1 text-[11px] font-medium shadow border backdrop-blur transition-colors ${
          autoZoom ? 'bg-primary text-primary-foreground border-primary' : 'bg-card/90 text-muted-foreground border-border hover:bg-card'
        }`}
      >
        Zoom DX
      </button>
      {path && (
        <div className="absolute bottom-1 left-1 z-[500] rounded-md bg-card/90 backdrop-blur px-2 py-1 text-[11px] font-mono shadow border border-border pointer-events-none">
          <div><span className="text-muted-foreground">Dist</span> {Math.round(path.distanceShort).toLocaleString()} km
            <span className="text-muted-foreground"> · LP</span> {Math.round(path.distanceLong).toLocaleString()} km</div>
          <div><span className="text-muted-foreground">Az SP</span> {Math.round(path.bearingShort)}°
            <span className="text-muted-foreground"> · LP</span> {Math.round(path.bearingLong)}°</div>
        </div>
      )}
    </div>
  );
}

interface LocatorProps {
  toGrid: string;     // contacted-station grid
  toLabel?: string;   // DX callsign
}

// LocatorMap — street map zoomed onto the DX grid, the "map2" pane.
export function LocatorMap({ toGrid, toLabel }: LocatorProps) {
  const locatorRef = useRef<HTMLDivElement>(null);
  const locatorMap = useRef<L.Map | null>(null);
  const locatorOverlay = useRef<L.LayerGroup | null>(null);

  useEffect(() => {
    if (locatorRef.current && !locatorMap.current) {
      const m = L.map(locatorRef.current, { zoomControl: true, attributionControl: true })
        .setView([20, 0], 2);
      L.tileLayer(OSM, { attribution: OSM_ATTR, maxZoom: 19 }).addTo(m);
      locatorOverlay.current = L.layerGroup().addTo(m);
      locatorMap.current = m;
    }
    const t = window.setTimeout(() => { locatorMap.current?.invalidateSize(); }, 80);
    return () => window.clearTimeout(t);
  }, []);

  useEffect(() => {
    const lm = locatorMap.current, lo = locatorOverlay.current;
    if (!lm || !lo) return;
    lo.clearLayers();
    const to = gridToLatLon(toGrid);
    if (to) {
      L.marker([to.lat, to.lon], { icon: dot('#dc2626'), title: toLabel || 'DX' })
        .bindTooltip(`${toLabel ? toLabel + ' · ' : ''}${toGrid.toUpperCase()}`, { permanent: false, direction: 'top' })
        .addTo(lo);
      const b = gridSquareBounds(toGrid);
      if (b) {
        L.rectangle([[b.south, b.west], [b.north, b.east]], { color: '#dc2626', weight: 1, fillOpacity: 0.06 }).addTo(lo);
      }
      lm.setView([to.lat, to.lon], toGrid.trim().length >= 6 ? 9 : 7);
    }
    setTimeout(() => { lm.invalidateSize(); }, 0);
  }, [toGrid, toLabel]);

  return (
    <div className="relative isolate h-full w-full rounded-lg overflow-hidden border border-border">
      <div ref={locatorRef} className="absolute inset-0" />
      {!gridToLatLon(toGrid) && (
        <div className="absolute inset-0 z-[500] flex items-center justify-center text-xs text-muted-foreground bg-card/60 pointer-events-none">
          Enter a grid or look up the callsign to center the map.
        </div>
      )}
    </div>
  );
}
