'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { Layer, Marker, Source, useMap, type MarkerDragEvent } from 'react-map-gl/mapbox';

import {
  formatCorners,
  imageCorners,
  overlayDataUrl,
  type ImageCorners,
  type OverlayMeta,
} from '@/lib/overlays';

interface AlignToolProps {
  /** Slug of the raster overlay being georeferenced. */
  slug: string;
  /** Its catalogue entry, when it exists -- used to resume an earlier alignment. */
  overlay: OverlayMeta | null;
  /** Layer the scan is drawn beneath, so the PI boundary stays on top as a reference. */
  anchorLayerId: string;
}

/** Labels for the four handles, in Mapbox image-source corner order. */
const CORNER_LABELS = ['Top-left', 'Top-right', 'Bottom-right', 'Bottom-left'] as const;

/**
 * Screen space, in CSS px, to keep the scan out of so no handle ever starts
 * underneath a panel: the drawing toolbar on the left, the overlay chooser on
 * the right, the page badge along the top. The bottom inset is measured from
 * this tool's own panel at runtime, since its height varies with its content.
 */
const FIXED_INSETS = { top: 56, left: 170, right: 280 };
const PANEL_GAP = 24;

/**
 * Georeferencing tool for scanned paper maps -- the working half of the
 * "analog source" requirement in SRS 3.2.5.
 *
 * Opened with `?align=<slug>`. Load a scan from disk, then drag its four
 * corners until its coastline and borders sit on the satellite imagery and the
 * white PI boundary; the centre handle moves the whole sheet at once. The
 * resulting corner coordinates feed `php artisan overlay:attach`, which is the
 * only way data gets written -- this tool itself stores nothing and talks to no
 * write endpoint, so it adds no attack surface to a site SRS 5.3 flags as a
 * likely target.
 *
 * Four corners support a scan that is rotated or skewed, which is the normal
 * case: paper rarely sits perfectly square on a scanner bed, and older maps are
 * not always drawn north-up. What four corners cannot absorb is a difference in
 * map PROJECTION across a large sheet; for a map of the whole region that can
 * leave some drift mid-sheet, and the precise route is QGIS's georeferencer
 * with many control points. For the educational precision SRS 2.1.10 calls for,
 * four corners are the proportionate method.
 */
export default function AlignTool({ slug, overlay, anchorLayerId }: AlignToolProps) {
  const { current: mapRef } = useMap();

  // Resume from the overlay's existing registration when it has one, so a scan
  // can be re-aligned rather than placed again from scratch.
  const [imageUrl, setImageUrl] = useState<string | null>(() =>
    overlay?.hasData ? overlayDataUrl(slug) : null,
  );
  const [corners, setCorners] = useState<ImageCorners | null>(() =>
    overlay ? imageCorners(overlay) : null,
  );
  const [sourceLabel, setSourceLabel] = useState<string | null>(() =>
    overlay?.hasData ? 'current server copy' : null,
  );
  const [opacity, setOpacity] = useState(0.6);
  const [copied, setCopied] = useState(false);

  // Object URLs pin the file in memory until revoked.
  const objectUrlRef = useRef<string | null>(null);
  useEffect(() => {
    return () => {
      if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
    };
  }, []);

  // The panel sits over the bottom of the map; where its top edge is tells us
  // how much of the screen is actually free for the scan and its handles.
  // Measured from the edge rather than from the panel's height, so the gap
  // above it holds whatever the panel's own distance from the bottom.
  const panelRef = useRef<HTMLDivElement | null>(null);
  const clearInsets = useCallback(() => {
    const panel = panelRef.current?.getBoundingClientRect();
    const canvas = mapRef?.getMap().getCanvas().getBoundingClientRect();
    const bottom = panel && canvas ? canvas.bottom - panel.top + PANEL_GAP : 220;
    return { ...FIXED_INSETS, bottom };
  }, [mapRef]);

  // When resuming an existing registration, bring the whole scan into the free
  // part of the screen so all four handles can be reached without panning.
  // Runs once, on the corners the tool opened with.
  const initialCornersRef = useRef(corners);
  useEffect(() => {
    const map = mapRef?.getMap();
    const start = initialCornersRef.current;
    if (!map || !start) return;
    const lngs = start.map((c) => c[0]);
    const lats = start.map((c) => c[1]);
    map.fitBounds(
      [
        [Math.min(...lngs), Math.min(...lats)],
        [Math.max(...lngs), Math.max(...lats)],
      ],
      { padding: clearInsets(), duration: 0 },
    );
  }, [mapRef, clearInsets]);

  const onFile = useCallback(
    (file: File) => {
      const map = mapRef?.getMap();
      if (!map) return;
      const url = URL.createObjectURL(file);
      const img = new Image();
      img.onload = () => {
        if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
        objectUrlRef.current = url;
        // Show the scan's controls BEFORE measuring the panel: they make it
        // about three times taller. Measured beforehand, a first load placed
        // the sheet against the short, file-picker-only panel and its bottom
        // two handles started underneath the controls.
        flushSync(() => {
          setImageUrl(url);
          setSourceLabel(file.name);
        });

        // Initial placement: centred in the FREE part of the view (clear of
        // every panel), 90% of it, at the scan's own aspect ratio. Worked out
        // in screen pixels and unprojected, so the sheet starts undistorted
        // wherever the map is -- and with all four handles reachable.
        const canvas = map.getCanvas();
        const inset = clearInsets();
        const freeW = Math.max(120, canvas.clientWidth - inset.left - inset.right);
        const freeH = Math.max(120, canvas.clientHeight - inset.top - inset.bottom);
        const aspect = img.naturalWidth / img.naturalHeight;
        let boxH = freeH * 0.9;
        let boxW = boxH * aspect;
        if (boxW > freeW * 0.9) {
          boxW = freeW * 0.9;
          boxH = boxW / aspect;
        }
        const cx = inset.left + freeW / 2;
        const cy = inset.top + freeH / 2;
        const at = (x: number, y: number): [number, number] => {
          const p = map.unproject([x, y]);
          return [p.lng, p.lat];
        };
        setCorners([
          at(cx - boxW / 2, cy - boxH / 2),
          at(cx + boxW / 2, cy - boxH / 2),
          at(cx + boxW / 2, cy + boxH / 2),
          at(cx - boxW / 2, cy + boxH / 2),
        ]);
      };
      img.onerror = () => URL.revokeObjectURL(url);
      img.src = url;
    },
    [mapRef, clearInsets],
  );

  const moveCorner = useCallback((index: number, e: MarkerDragEvent) => {
    setCorners((prev) => {
      if (!prev) return prev;
      const next = prev.map((c) => [c[0], c[1]]) as ImageCorners;
      next[index] = [e.lngLat.lng, e.lngLat.lat];
      return next;
    });
    setCopied(false);
  }, []);

  const centre = useMemo<[number, number] | null>(() => {
    if (!corners) return null;
    const lng = corners.reduce((s, c) => s + c[0], 0) / 4;
    const lat = corners.reduce((s, c) => s + c[1], 0) / 4;
    return [lng, lat];
  }, [corners]);

  // The centre handle moves all four corners together -- rough placement
  // first, fine-tuning at the corners after.
  const moveAll = useCallback((e: MarkerDragEvent) => {
    setCorners((prev) => {
      if (!prev) return prev;
      const cLng = prev.reduce((s, c) => s + c[0], 0) / 4;
      const cLat = prev.reduce((s, c) => s + c[1], 0) / 4;
      const dLng = e.lngLat.lng - cLng;
      const dLat = e.lngLat.lat - cLat;
      return prev.map(([lng, lat]) => [lng + dLng, lat + dLat]) as ImageCorners;
    });
    setCopied(false);
  }, []);

  const command = corners
    ? `php artisan overlay:attach ${slug} "<path-to-scan>" --corners="${formatCorners(corners)}"`
    : null;

  const copyCommand = useCallback(async () => {
    if (!command) return;
    try {
      await navigator.clipboard.writeText(command);
      setCopied(true);
    } catch {
      setCopied(false);
    }
  }, [command]);

  return (
    <>
      {imageUrl && corners && (
        <Source id="align-image" type="image" url={imageUrl} coordinates={corners}>
          <Layer
            id="align-image-layer"
            type="raster"
            beforeId={anchorLayerId}
            paint={{ 'raster-opacity': opacity, 'raster-fade-duration': 0 }}
          />
        </Source>
      )}

      {corners &&
        corners.map((c, i) => (
          <Marker
            key={CORNER_LABELS[i]}
            longitude={c[0]}
            latitude={c[1]}
            draggable
            onDrag={(e) => moveCorner(i, e)}
          >
            <div
              title={`${CORNER_LABELS[i]} corner -- drag to align`}
              aria-label={`${CORNER_LABELS[i]} corner`}
              className="h-4 w-4 cursor-grab rounded-full border-2 border-white bg-fuchsia-500 shadow-md"
            />
          </Marker>
        ))}

      {centre && (
        <Marker longitude={centre[0]} latitude={centre[1]} draggable onDrag={moveAll}>
          <div
            title="Drag to move the whole scan"
            aria-label="Move whole scan"
            className="flex h-6 w-6 cursor-move items-center justify-center rounded-full border-2 border-white bg-zinc-900/80 text-[11px] text-white shadow-md"
          >
            ✥
          </div>
        </Marker>
      )}

      <div
        ref={panelRef}
        className="absolute bottom-10 left-1/2 z-20 w-[min(34rem,calc(100%-1.5rem))] -translate-x-1/2 rounded-lg bg-zinc-900/90 p-3 text-xs text-zinc-100 shadow-lg backdrop-blur-sm">
        <div className="mb-2 flex items-baseline justify-between gap-2">
          <span className="font-semibold">
            Align scan · <span className="font-mono text-fuchsia-300">{slug}</span>
          </span>
          {sourceLabel && <span className="truncate text-zinc-400">{sourceLabel}</span>}
        </div>

        {!overlay && (
          <p className="mb-2 text-amber-300">
            No overlay called “{slug}” is in the catalogue — the command below will be refused
            until it is seeded.
          </p>
        )}
        {overlay && overlay.kind !== 'raster' && (
          <p className="mb-2 text-amber-300">
            “{slug}” is a vector overlay; alignment is only for scanned maps.
          </p>
        )}

        <label className="mb-2 flex items-center gap-2">
          <span className="shrink-0 text-zinc-400">Scan file</span>
          <input
            type="file"
            accept="image/png,image/jpeg,image/webp"
            onChange={(e) => {
              const f = e.target.files?.[0];
              if (f) onFile(f);
            }}
            className="min-w-0 flex-1 text-zinc-300 file:mr-2 file:rounded file:border-0 file:bg-zinc-700 file:px-2 file:py-1 file:text-zinc-100"
          />
        </label>

        {imageUrl && (
          <>
            <label className="mb-2 flex items-center gap-2">
              <span className="shrink-0 text-zinc-400">See-through</span>
              <input
                type="range"
                min={0.1}
                max={1}
                step={0.05}
                value={opacity}
                onChange={(e) => setOpacity(Number(e.target.value))}
                className="flex-1"
              />
            </label>
            <p className="mb-1 text-zinc-400">
              Drag the pink corners until the scan’s coastline and borders sit on the map; ✥
              moves the whole sheet.
            </p>
            <div className="flex items-center gap-2">
              <code className="min-w-0 flex-1 truncate rounded bg-black/40 px-2 py-1 font-mono text-[10px] text-zinc-300">
                {command}
              </code>
              <button
                type="button"
                onClick={copyCommand}
                className="shrink-0 rounded bg-zinc-100 px-2 py-1 font-medium text-zinc-900 hover:bg-white"
              >
                {copied ? 'Copied' : 'Copy command'}
              </button>
            </div>
          </>
        )}
      </div>
    </>
  );
}
