"use client";

import { useEffect, useRef, useState, type ReactNode } from "react";
import { importLibrary } from "@googlemaps/js-api-loader";
import { MapPin, Search, X } from "lucide-react";
import { HAS_GOOGLE_MAPS_KEY, MAP_CENTER } from "@/config/site";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { configureGoogleMaps } from "@/lib/google-maps";
import { useT } from "@/i18n";

type Point = { lat: number; lng: number };

/** The form keeps coordinates as text; anything that is not a finite number is "no pin". */
function toPoint(lat: string | number | undefined, lng: string | number | undefined): Point | null {
  if (String(lat ?? "").trim() === "" || String(lng ?? "").trim() === "") return null;

  const a = Number(lat);
  const b = Number(lng);

  return Number.isFinite(a) && Number.isFinite(b) ? { lat: a, lng: b } : null;
}

/** Six decimals is about ten centimetres, far finer than a doorway. */
const round = (value: number) => Math.round(value * 1e6) / 1e6;

/**
 * Pick a place on a Google map instead of typing coordinates.
 *
 * On the page it is one compact line — the chosen coordinates and a button — because an order form
 * already has a lot on it and a map per section would bury it. The map opens in a popup: click it,
 * drag the pin, or search for a place. The pin's position is handed back through `onChange` as it
 * moves, and `null, null` when cleared. The form still stores plain latitude/longitude.
 *
 * Without a Maps key, or if Google refuses to load (a bad key, a referrer restriction), the
 * `fallback` — the old coordinate inputs — is shown instead, so an order can always be created.
 */
export function LocationPicker({
  lat,
  lng,
  onChange,
  fallback,
  title,
  radiusM = null,
  clearable = true,
  disabled = false,
}: {
  lat: string | number | undefined;
  lng: string | number | undefined;
  onChange: (lat: number | null, lng: number | null) => void;
  fallback: ReactNode;
  /** What is being placed, for the popup's heading ("Pickup location"). */
  title?: string;
  /** Draws the area the point covers as a circle of this radius, in metres (a city or a district is a centre and a radius). */
  radiusM?: number | null;
  /** Whether the pin can be taken away again. Off where a point is required (an area's centre): changing it is allowed, having none is not. */
  clearable?: boolean;
  disabled?: boolean;
}) {
  const t = useT();
  const [open, setOpen] = useState(false);
  const [failed, setFailed] = useState(false);

  const point = toPoint(lat, lng);

  if (!HAS_GOOGLE_MAPS_KEY || failed) {
    return <div className="grid gap-4 sm:grid-cols-2">{fallback}</div>;
  }

  return (
    <div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border bg-muted/30 px-4 py-3">
      <span className="inline-flex items-center gap-2 text-sm" dir="ltr">
        <MapPin className={point ? "h-4 w-4 text-primary" : "h-4 w-4 text-muted-foreground"} aria-hidden />
        {point ? (
          <span className="font-medium text-foreground">
            {point.lat.toFixed(5)}, {point.lng.toFixed(5)}
          </span>
        ) : (
          <span className="text-muted-foreground" dir="auto">
            {t("orders.map.none")}
          </span>
        )}
      </span>

      <span className="flex items-center gap-1">
        {point && clearable ? (
          <Button type="button" variant="ghost" size="sm" onClick={() => onChange(null, null)} disabled={disabled}>
            <X className="h-3.5 w-3.5" aria-hidden />
            {t("orders.map.clear")}
          </Button>
        ) : null}
        <Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)} disabled={disabled}>
          <MapPin className="h-3.5 w-3.5" aria-hidden />
          {point ? t("orders.map.change") : t("orders.map.choose")}
        </Button>
      </span>

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="max-w-3xl">
          <DialogHeader>
            <DialogTitle>{title ?? t("orders.map.title")}</DialogTitle>
            <DialogDescription>{t("orders.map.hint")}</DialogDescription>
          </DialogHeader>

          {/* Mounted only while open, so the map is built when it is wanted and not once per section. */}
          {open ? <MapPane point={point} radiusM={radiusM} onChange={onChange} onFail={() => setFailed(true)} /> : null}

          <DialogFooter>
            <Button type="button" onClick={() => setOpen(false)}>
              {t("orders.map.done")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  );
}

function MapPane({
  point,
  radiusM,
  onChange,
  onFail,
}: {
  point: Point | null;
  radiusM: number | null;
  onChange: (lat: number | null, lng: number | null) => void;
  onFail: () => void;
}) {
  const t = useT();
  const container = useRef<HTMLDivElement | null>(null);
  const map = useRef<google.maps.Map | null>(null);
  const marker = useRef<google.maps.Marker | null>(null);
  const circle = useRef<google.maps.Circle | null>(null);
  // The view is fitted to the area once, when the popup opens: moving the pin must not zoom the map under the admin's hand.
  const fitted = useRef(false);
  const geocoder = useRef<google.maps.Geocoder | null>(null);
  // The latest callbacks and value, so the map's listeners (set up once) never hold a stale form.
  const emit = useRef(onChange);
  const fail = useRef(onFail);
  const pointRef = useRef<Point | null>(point);

  const [ready, setReady] = useState(false);
  const [query, setQuery] = useState("");
  const [searchMessage, setSearchMessage] = useState<string | null>(null);

  useEffect(() => {
    emit.current = onChange;
    fail.current = onFail;
    pointRef.current = point;
  });

  useEffect(() => {
    let cancelled = false;

    async function boot() {
      // The dialog's content mounts a frame after this component; wait for the element to exist.
      for (let attempt = 0; attempt < 20 && !container.current; attempt++) {
        await new Promise((resolve) => requestAnimationFrame(resolve));
      }

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

      try {
        configureGoogleMaps();

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

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

        const start = pointRef.current;

        const instance = new GoogleMap(container.current, {
          center: start ?? MAP_CENTER,
          zoom: start ? 16 : 12,
          mapTypeControl: false,
          streetViewControl: false,
          fullscreenControl: false,
        });

        instance.addListener("click", (event: google.maps.MapMouseEvent) => {
          if (event.latLng) emit.current(round(event.latLng.lat()), round(event.latLng.lng()));
        });

        map.current = instance;
        setReady(true);
      } catch {
        // A rejected key, a referrer restriction or no billing: hand the form back its inputs.
        if (!cancelled) fail.current();
      }
    }

    void boot();

    return () => {
      cancelled = true;
      marker.current?.setMap(null);
      marker.current = null;
      circle.current?.setMap(null);
      circle.current = null;
      fitted.current = false;
      map.current = null;
    };
  }, []);

  // Keep the pin in step with the value, whoever changed it (a click, a drag, a search).
  const latKey = point?.lat;
  const lngKey = point?.lng;

  useEffect(() => {
    const instance = map.current;
    if (!ready || !instance) return;

    if (latKey === undefined || lngKey === undefined) {
      marker.current?.setMap(null);
      marker.current = null;
      circle.current?.setMap(null);
      circle.current = null;

      return;
    }

    const position = { lat: latKey, lng: lngKey };

    // The area around the pin, when the caller has one: it follows the pin as it moves.
    if (radiusM !== null && radiusM > 0) {
      if (!circle.current) {
        circle.current = new google.maps.Circle({
          map: instance,
          center: position,
          radius: radiusM,
          strokeColor: "#2563eb",
          strokeOpacity: 0.9,
          strokeWeight: 2,
          fillColor: "#2563eb",
          fillOpacity: 0.12,
          clickable: false,
        });
      } else {
        circle.current.setCenter(position);
        circle.current.setRadius(radiusM);
      }

      if (!fitted.current) {
        const bounds = circle.current.getBounds();
        if (bounds) instance.fitBounds(bounds);
        fitted.current = true;
      }
    } else {
      circle.current?.setMap(null);
      circle.current = null;
    }

    if (!marker.current) {
      marker.current = new google.maps.Marker({ map: instance, position, draggable: true });
      marker.current.addListener("dragend", () => {
        const moved = marker.current?.getPosition();
        if (moved) emit.current(round(moved.lat()), round(moved.lng()));
      });
    } else {
      marker.current.setPosition(position);
    }

    if (!instance.getBounds()?.contains(position)) {
      instance.panTo(position);
    }
  }, [latKey, lngKey, radiusM, ready]);

  async function search() {
    const text = query.trim();
    if (text === "" || !map.current) return;

    setSearchMessage(null);

    try {
      await importLibrary("geocoding");
      geocoder.current ??= new google.maps.Geocoder();

      const { results } = await geocoder.current.geocode({ address: text });
      const found = results[0]?.geometry.location;

      if (!found) {
        setSearchMessage(t("orders.map.notFound"));

        return;
      }

      emit.current(round(found.lat()), round(found.lng()));
      map.current.setZoom(16);
    } catch {
      setSearchMessage(t("orders.map.notFound"));
    }
  }

  return (
    <div className="grid gap-2">
      <div className="flex gap-2">
        <Input
          value={query}
          onChange={(event) => setQuery(event.target.value)}
          onKeyDown={(event) => {
            // Enter searches; it must not submit the order form behind the popup.
            if (event.key === "Enter") {
              event.preventDefault();
              void search();
            }
          }}
          placeholder={t("orders.map.search")}
          aria-label={t("orders.map.search")}
          autoFocus
        />
        <Button type="button" variant="outline" onClick={() => void search()} aria-label={t("orders.map.searchButton")}>
          <Search className="h-4 w-4" aria-hidden />
        </Button>
      </div>
      {searchMessage ? <p className="text-xs text-destructive">{searchMessage}</p> : null}

      <div ref={container} className="h-[55vh] min-h-72 w-full overflow-hidden rounded-lg border bg-muted" />

      <p className="text-xs text-muted-foreground" dir="ltr">
        {point ? `${point.lat.toFixed(5)}, ${point.lng.toFixed(5)}` : ""}
        {point && radiusM !== null && radiusM > 0 ? ` · ${t("orders.map.radius", { meters: String(Math.round(radiusM)) })}` : ""}
      </p>
    </div>
  );
}
