"use client";

import { useEffect, useRef, useState } from "react";
import { importLibrary, setOptions } from "@googlemaps/js-api-loader";
import { GOOGLE_MAPS_KEY, MAP_CENTER } from "@/config/site";
import { Skeleton } from "@/components/ui/skeleton";
import { GPS_MARKER_COLOUR } from "@/services/tracking";
import type { LiveCaptain } from "@/types/tracking";

/**
 * The loader's options are global to the page, not to this component, and the library warns
 * — and ignores the call — if they are set more than once. An effect cannot be the place for
 * them: React runs effects twice in development's StrictMode, and this component also
 * remounts whenever the tracking page swaps it for the skeleton. Setting them once, here at
 * module scope, is what the package's functional API expects.
 *
 * This runs in the browser only: the page imports this module through `next/dynamic` with
 * `ssr: false`, and only once a key exists.
 */
let optionsSet = false;

function configureLoader() {
  if (optionsSet) return;

  setOptions({ key: GOOGLE_MAPS_KEY, v: "weekly" });
  optionsSet = true;
}

/**
 * The fleet on a Google map.
 *
 * Driven imperatively rather than through a React wrapper, for a reason that matters at this
 * refresh rate: the screen re-polls every ten seconds, and tearing down and rebuilding a marker
 * layer on every response makes the map flicker and throws away the user's pan and zoom. Markers
 * are therefore keyed by captain and **moved** when a position changes.
 *
 * Imported through `next/dynamic` with `ssr: false` by the page — the Maps SDK touches `window`
 * and cannot render on the server.
 *
 * **On the key.** `NEXT_PUBLIC_GOOGLE_MAPS_KEY` is compiled into the JavaScript the browser
 * downloads, so it is public by construction and must be restricted **by HTTP referrer** in the
 * Google Cloud console. It is deliberately *not* the same key as the server's
 * `DISPATCH_GOOGLE_MAPS_KEY`, which drives the Routes API: that one should be secret and
 * IP-restricted, and a referrer-restricted key cannot be used from a server anyway. Sharing one
 * key between them means either exposing the server's key or breaking the server's calls.
 */
export function FleetMap({
  captains,
  selected,
  onSelect,
}: {
  captains: LiveCaptain[];
  selected: string | null;
  onSelect: (uuid: string) => void;
}) {
  const container = useRef<HTMLDivElement | null>(null);
  const map = useRef<google.maps.Map | null>(null);
  const markers = useRef<Map<string, google.maps.Marker>>(new Map());
  const hasFitted = useRef(false);
  const [failed, setFailed] = useState<string | null>(null);
  // Google's SDK is a few hundred kilobytes over the network. Between the chunk arriving and the
  // map drawing there is a real pause, and an undressed `<div>` in it reads as a broken page.
  const [ready, setReady] = useState(false);

  // Create the map once.
  useEffect(() => {
    let cancelled = false;

    async function boot() {
      if (map.current || !container.current) return;

      try {
        // The functional API, not the `Loader` class — that is deprecated in this package.
        // Options must be set before any library is imported, and exactly once per page.
        configureLoader();

        const { Map: GoogleMap } = await importLibrary("maps");

        if (cancelled || !container.current) return;

        map.current = new GoogleMap(container.current, {
          // The starting view, replaced by `fitBounds` the moment any captain reports — so
          // what this really decides is the map somebody sees when nobody is on duty.
          center: MAP_CENTER,
          zoom: 11,
          mapTypeControl: false,
          streetViewControl: false,
          fullscreenControl: true,
          // A tracking map is read at a glance; the default interest points compete with the
          // thing somebody is actually looking for.
          styles: [
            { featureType: "poi", stylers: [{ visibility: "off" }] },
            { featureType: "transit", stylers: [{ visibility: "off" }] },
          ],
        });

        setReady(true);
      } catch (error) {
        // A rejected key, a referrer restriction, or no billing account all land here. Saying so
        // is far better than the grey rectangle Google otherwise leaves behind.
        setFailed(error instanceof Error ? error.message : String(error));
      }
    }

    void boot();

    return () => {
      cancelled = true;
      markers.current.forEach((marker) => marker.setMap(null));
      markers.current.clear();
      map.current = null;
      hasFitted.current = false;
      setReady(false);
    };
  }, []);

  // Move the markers that exist, add the ones that do not, remove the ones that left.
  useEffect(() => {
    const instance = map.current;
    if (!instance || typeof google === "undefined") return;

    const placed = captains.filter((captain) => captain.lat !== null && captain.lng !== null);
    const seen = new Set<string>();

    for (const captain of placed) {
      seen.add(captain.uuid);

      const position = { lat: captain.lat as number, lng: captain.lng as number };
      const colour = GPS_MARKER_COLOUR[captain.gps_state];

      /**
       * A circle symbol rather than `AdvancedMarkerElement`.
       *
       * Advanced markers need a Map ID created in the Cloud console, which is one more piece of
       * setup before anything appears on screen. `Marker` is deprecated — Google has committed
       * to at least twelve months' notice before withdrawing it — and for a coloured dot on an
       * operations screen the trade is worth it. Moving to advanced markers later is this block
       * and nothing else.
       */
      const icon: google.maps.Symbol = {
        path: google.maps.SymbolPath.CIRCLE,
        scale: 7,
        fillColor: colour,
        fillOpacity: 0.75,
        strokeColor: colour,
        strokeWeight: 2,
      };

      const existing = markers.current.get(captain.uuid);

      if (existing) {
        existing.setPosition(position);
        existing.setIcon(icon);
        continue;
      }

      const marker = new google.maps.Marker({
        map: instance,
        position,
        icon,
        title: captain.name,
      });

      marker.addListener("click", () => onSelect(captain.uuid));
      markers.current.set(captain.uuid, marker);
    }

    // A captain who went off duty should not linger on the map.
    for (const [uuid, marker] of markers.current) {
      if (!seen.has(uuid)) {
        marker.setMap(null);
        markers.current.delete(uuid);
      }
    }

    // Fit once, on the first response that has anything to fit. Doing it on every poll would
    // yank the view back while somebody was reading a corner of it.
    if (!hasFitted.current && placed.length > 0) {
      const bounds = new google.maps.LatLngBounds();

      for (const captain of placed) {
        bounds.extend({ lat: captain.lat as number, lng: captain.lng as number });
      }

      instance.fitBounds(bounds, 48);
      hasFitted.current = true;
    }
  }, [captains, onSelect]);

  // Centre on whoever the table selected, without disturbing the zoom.
  useEffect(() => {
    if (!selected || !map.current) return;

    const captain = captains.find((c) => c.uuid === selected);
    if (!captain || captain.lat === null || captain.lng === null) return;

    map.current.panTo({ lat: captain.lat, lng: captain.lng });
  }, [selected, captains]);

  if (failed !== null) {
    return (
      <div className="flex h-[28rem] w-full items-center justify-center rounded-lg border border-dashed p-6 text-center">
        <p className="max-w-md text-sm text-muted-foreground">{failed}</p>
      </div>
    );
  }

  return (
    <div className="relative h-[28rem] w-full">
      <div ref={container} className="h-full w-full rounded-lg" />

      {/*
        Sits over the container rather than replacing it: the Maps SDK needs the element to exist
        and to have a size before it will draw into it, so this cannot be an `if (!ready)` early
        return.
      */}
      {!ready ? (
        <div className="absolute inset-0 flex items-center justify-center rounded-lg bg-muted/40">
          <Skeleton className="h-full w-full rounded-lg" />
        </div>
      ) : null}
    </div>
  );
}
