'use client';

/**
 * Map block — thin chat wrapper around `LazyMapContainer`. Renders point
 * markers plus optional routes (polylines) and polygons (filled zones) by
 * building GeoJSON from the block's lat/lng payloads and feeding the map's
 * existing `MapSource` + layer factories — no bespoke layer logic here.
 */

import { LazyMapContainer, MapMarker, MapPopup, MarkerCard, MapSource, MapLayer, useMarkerCard } from '../../../../dev/Map/lazy';
import type { MarkerData } from '../../../../dev/Map/lazy';
import type { MapContainerProps } from '../../../../dev/Map/components';
import {
  createRouteLayers,
  createPolygonLayer,
  createPolygonOutlineLayer,
} from '../../../../dev/Map/layers';
import {
  calculateBounds,
  createLineString,
  createPolygon,
  createFeatureCollection,
} from '../../../../dev/Map/utils';
import type { MapBlock } from '../../../types/block';
import type { BlockRendererProps } from './types';

/** `[lng, lat]` tuple — GeoJSON / maplibre coordinate order. */
type LngLat = [number, number];

/**
 * Derive an `initialViewport` that frames every coordinate. The map exposes
 * an imperative `fitBounds` only through `useMapControl` (inside its own
 * provider), which this frameless wrapper sits outside of — so instead we
 * seed the initial center/zoom from the bounding box. Mirrors maplibre's own
 * fit math: zoom for whichever axis (lng/lat) needs the most room.
 */
function viewportForBounds(points: LngLat[]): { longitude: number; latitude: number; zoom: number } {
  const [[minLng, minLat], [maxLng, maxLat]] = calculateBounds(points) as [LngLat, LngLat];
  const longitude = (minLng + maxLng) / 2;
  const latitude = (minLat + maxLat) / 2;

  const lngSpan = Math.max(maxLng - minLng, 1e-6);
  const latSpan = Math.max(maxLat - minLat, 1e-6);
  // World is 360° at zoom 0; each zoom level halves the visible span. Add a
  // little padding (0.6) so geometry never hugs the edges, and clamp.
  const zoomForSpan = (span: number, fullSpan: number) =>
    Math.log2((fullSpan / span) * 0.6);
  const zoom = Math.min(16, Math.max(1, Math.min(zoomForSpan(lngSpan, 360), zoomForSpan(latSpan, 180))));

  return { longitude, latitude, zoom };
}

type BlockMarker = NonNullable<MapBlock['markers']>[number];

/**
 * Marker layer for the chat map block. Renders each marker (preserving the
 * `icon` thumbnail / `color` pin) and, when a marker carries a serializable
 * `card`, opens a `MarkerCard` popup ABOVE the pin on click. Selection is
 * single-card via `useMarkerCard`. Lives inside `LazyMapContainer` so it has
 * the MapProvider context.
 */
function MapBlockMarkers({ markers }: { markers: BlockMarker[] }) {
  const { openId, toggle, close } = useMarkerCard();
  const open = openId ? markers.find((m) => m.id === openId) : undefined;

  return (
    <>
      {markers.map((m) => {
        const marker: MarkerData = {
          id: m.id,
          longitude: m.lng,
          latitude: m.lat,
          data: m.label ? { label: m.label } : undefined,
        };
        return (
          <MapMarker
            key={m.id}
            marker={marker}
            color={m.color}
            ariaLabel={m.card?.title ?? m.label}
            onClick={m.card ? () => toggle(m.id) : undefined}
          >
            {m.icon ? (
              <img
                src={m.icon}
                alt={m.label ?? ''}
                className="h-7 w-7 rounded-full object-cover ring-2 ring-background shadow-md"
              />
            ) : undefined}
          </MapMarker>
        );
      })}

      {open?.card && (
        <MapPopup
          longitude={open.lng}
          latitude={open.lat}
          anchor="bottom"
          onClose={close}
          maxWidth="none"
        >
          <MarkerCard {...open.card} />
        </MapPopup>
      )}
    </>
  );
}

export default function MapBlockRenderer({ block, ctx }: BlockRendererProps<MapBlock>) {
  const height = ctx.appearance === 'compact' ? 240 : 380;

  // The serializable map shape (center / markers / routes / polygons) is
  // validated centrally at the registry dispatch point (MessageBlocks) before
  // this renderer runs, so we can trust the payload here.
  const routes = block.routes ?? [];
  const polygons = block.polygons ?? [];
  const markers = block.markers ?? [];

  // Collect every coordinate so the viewport can frame the whole scene when
  // the block doesn't pin an explicit center/zoom.
  const allPoints: LngLat[] = [
    ...markers.map((m): LngLat => [m.lng, m.lat]),
    ...routes.flatMap((r) => r.points.map((p): LngLat => [p.lng, p.lat])),
    ...polygons.flatMap((p) => p.points.map((pt): LngLat => [pt.lng, pt.lat])),
  ];

  // Respect an explicit zoom (author intent); otherwise auto-fit to geometry.
  // Center always defaults to the block's `center` unless we have geometry to
  // frame and no explicit zoom was given.
  const fitted = block.zoom === undefined && allPoints.length > 1 ? viewportForBounds(allPoints) : null;
  const initialViewport = {
    longitude: fitted?.longitude ?? block.center.lng,
    latitude: fitted?.latitude ?? block.center.lat,
    zoom: block.zoom ?? fitted?.zoom ?? 11,
  };

  // Build the optional props into ONE typed object before spreading, rather
  // than several inline conditional `{...(cond ? {…} : {})}` spreads. The
  // inline form unions each branch to `{} | {prop}` and widens the call to a
  // shape no single `MapContainer` overload accepts (TS2769) under stricter
  // consumer resolution; a single `Partial<MapContainerProps>` literal is
  // checked against the prop type as a unit.
  const mapOptions: Partial<MapContainerProps> = {
    ...(block.basemap ? { mapStyle: block.basemap } : {}),
    ...(block.terrain ? { terrain: true } : {}),
    ...(block.userLocation ? { geolocate: true } : {}),
  };

  // Frame (radius / border / shadow / clip) comes from the shared
  // `BLOCK_SURFACE` in MessageBlocks — here we only set the height.
  return (
    <div className="w-full" style={{ height }}>
      <LazyMapContainer
        initialViewport={initialViewport}
        fullscreenTitle={block.caption ?? 'Map'}
        // A chat transcript column is < 768px, which would otherwise trip the
        // map's width-based "mobile" collapse (gestures off, scroll-lock
        // overlay invisible, just a static tile) even on a desktop with a fine
        // pointer. `0` opts out of width-based collapse — the map keeps its
        // desktop scroll-lock + controls in the narrow column, and a REAL
        // touchscreen still degrades to the mobile preview (coarse pointer).
        mobileBreakpoint={0}
        {...mapOptions}
      >
        {/* Filled zones first so routes/markers stack above them. */}
        {polygons.map((poly) => {
          const sourceId = `poly-${poly.id}`;
          const data = createFeatureCollection([
            createPolygon(poly.points.map((p): LngLat => [p.lng, p.lat]), poly.label ? { label: poly.label } : {}),
          ]);
          const fill = createPolygonLayer(`${sourceId}-fill`, sourceId, {
            fillColor: poly.fillColor,
            strokeColor: poly.strokeColor,
          });
          const outline = createPolygonOutlineLayer(`${sourceId}-outline`, sourceId, {
            color: poly.strokeColor,
          });
          return (
            <MapSource key={sourceId} id={sourceId} data={data}>
              <MapLayer {...fill} />
              <MapLayer {...outline} />
            </MapSource>
          );
        })}

        {/* Routes (polylines) — white-outlined line via the route factory. */}
        {routes.map((route) => {
          const sourceId = `route-${route.id}`;
          const data = createFeatureCollection([
            createLineString(route.points.map((p): LngLat => [p.lng, p.lat]), route.label ? { label: route.label } : {}),
          ]);
          const { outline, route: line } = createRouteLayers({ sourceId, color: route.color });
          return (
            <MapSource key={sourceId} id={sourceId} data={data}>
              <MapLayer {...outline} />
              <MapLayer {...line} />
            </MapSource>
          );
        })}

        <MapBlockMarkers markers={markers} />
      </LazyMapContainer>
    </div>
  );
}
