// @design-system: domain/MapView/MapViewInner
// This file is the lazy chunk — only imported when view=map is active.
// Top-level Leaflet imports stay HERE so they never land in the /deals bundle.

'use client';

import { useEffect, useRef, useState } from 'react';
import type { Map as LeafletMap, Layer, Circle } from 'leaflet';
import type { FeedMarker } from '@/server/db/queries/feed.js';
import { Skeleton } from '@/components/ui/feedback/Skeleton/Skeleton';
import { cn } from '@/lib/cn';
import type { MapViewProps } from './MapView';
import { useLocale, useT } from '@/lib/i18n/react';
import { formatAgorotPricePrefixed } from '@/lib/money';
import { FALLBACK_BRAND_500 } from '@/lib/theme-fallbacks';
import {
  buildClusterPopup,
  buildVendorPopup,
  groupMarkersByVendor,
  type VendorPin,
} from '@/lib/map-popups';

function fmtPrice(shekels: number): string {
  return formatAgorotPricePrefixed(Math.round(shekels * 100));
}

function getViewportRadiusKm(map: LeafletMap): number {
  const center = map.getCenter();
  const ne = map.getBounds().getNorthEast();
  const R = 6371;
  const dLat = ((ne.lat - center.lat) * Math.PI) / 180;
  const dLng = ((ne.lng - center.lng) * Math.PI) / 180;
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos((center.lat * Math.PI) / 180) *
      Math.cos((ne.lat * Math.PI) / 180) *
      Math.sin(dLng / 2) ** 2;
  const km = 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return Math.min(50, Math.max(1, Math.ceil(km)));
}

function q4(n: number): number {
  return Math.round(n * 1e4) / 1e4;
}

export default function MapViewInner({ deals, center, radius, onViewportChange }: MapViewProps) {
  const tMap = useT('domain_map');
  const { locale } = useLocale();
  const containerRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<LeafletMap | null>(null);
  const clusterRef = useRef<Layer | null>(null);
  const circleRef = useRef<Circle | null>(null);
  const [loaded, setLoaded] = useState(false);

  // Tel Aviv default center (used as fallback when no geo)
  const centerLat = center.lat;
  const centerLng = center.lng;

  // ── Map init ──────────────────────────────────────────────────────────────
  useEffect(() => {
    if (!containerRef.current || mapRef.current) return;
    const container = containerRef.current;

    void (async () => {
      await import('leaflet/dist/leaflet.css');
      await import('leaflet.markercluster/dist/MarkerCluster.css');
      await import('leaflet.markercluster/dist/MarkerCluster.Default.css');
      const L = (await import('leaflet')).default;
      await import('leaflet.markercluster');

      // Fix Leaflet's default marker icon path (broken with Vite asset hashing)
      const iconUrl = 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png';
      const iconRetinaUrl = 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png';
      const shadowUrl = 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png';
      Reflect.deleteProperty(L.Icon.Default.prototype, '_getIconUrl');
      L.Icon.Default.mergeOptions({ iconUrl, iconRetinaUrl, shadowUrl });

      const map = L.map(container).setView([centerLat, centerLng], 13);
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
        maxZoom: 19,
      }).addTo(map);

      mapRef.current = map;
      setLoaded(true);

      if (onViewportChange) {
        const fireViewport = () => {
          const c = map.getCenter();
          onViewportChange({ lat: q4(c.lat), lng: q4(c.lng), km: getViewportRadiusKm(map) });
        };
        map.on('moveend', fireViewport);
        map.on('zoomend', fireViewport);
      }
    })();

    return () => {
      mapRef.current?.remove();
      mapRef.current = null;
      clusterRef.current = null;
      circleRef.current = null;
    };
    // Map init runs once — center changes handled by markers effect below
  }, [centerLat, centerLng, onViewportChange]);

  // ── Markers + cluster + radius circle ─────────────────────────────────────
  // Rebuild when marker content, locale, or the translated popup label changes.
  const dealsSignature = JSON.stringify(deals);
  const dealsCountLabel = tMap('deals_count');
  const shouldFitBounds = !onViewportChange;

  useEffect(() => {
    if (!mapRef.current) return;
    const map = mapRef.current;
    let cancelled = false;

    void (async () => {
      const L = (await import('leaflet')).default;
      await import('leaflet.markercluster');
      if (cancelled) return;

      // Remove previous cluster + circle only (tile layer stays)
      if (clusterRef.current) {
        map.removeLayer(clusterRef.current);
        clusterRef.current = null;
      }
      if (circleRef.current) {
        map.removeLayer(circleRef.current);
        circleRef.current = null;
      }

      // Resolve brand token for radius circle stroke
      const rootStyle = getComputedStyle(document.documentElement);
      const brandBlue = rootStyle.getPropertyValue('--color-brand-primary-500').trim();

      // Cluster group — zoomToBoundsOnClick:false so clusterclick shows popup instead of zooming
      const cluster = L.markerClusterGroup({
        maxClusterRadius: 50,
        zoomToBoundsOnClick: false,
        iconCreateFunction: (clusterLayer) =>
          L.divIcon({
            html: `<span>${clusterLayer.getChildCount()}</span>`,
            className: 'md-map-cluster-spring',
            iconSize: [40, 40],
          }),
      });

      const dealsWithCoords = deals.filter((d): d is FeedMarker => !isNaN(d.lat) && !isNaN(d.lng));

      // One pin per vendor location; deals at same (vendorId, lat, lng) share a pin
      const vendorPins = groupMarkersByVendor(dealsWithCoords);

      for (const pin of vendorPins) {
        const marker = L.marker([pin.lat, pin.lng], {
          vendorPin: pin,
        } as L.MarkerOptions & { vendorPin: VendorPin });
        marker.bindPopup(buildVendorPopup(pin, locale, fmtPrice));
        cluster.addLayer(marker);
      }

      map.addLayer(cluster);
      clusterRef.current = cluster;

      // Cluster click: show popup listing all vendors and their deals grouped
      cluster.on(
        'clusterclick',
        (e: L.LeafletEvent & { layer: InstanceType<typeof L.MarkerCluster> }) => {
          const childMarkers = e.layer.getAllChildMarkers() as Array<
            L.Marker & { options: { vendorPin?: VendorPin } }
          >;
          const pins = childMarkers
            .map((m) => m.options.vendorPin)
            .filter((p): p is VendorPin => p !== undefined);
          if (pins.length === 0) return;
          e.layer
            .bindPopup(buildClusterPopup(pins, dealsCountLabel, locale, fmtPrice), {
              maxHeight: 300,
              maxWidth: 300,
            })
            .openPopup();
        },
      );

      // Auto-fit bounds if we have vendor pins
      if (vendorPins.length > 0) {
        const group = L.featureGroup(vendorPins.map((p) => L.marker([p.lat, p.lng])));
        if (shouldFitBounds) {
          map.fitBounds(group.getBounds().pad(0.1), { maxZoom: 15 });
        }
      } else {
        // No coords — centre on provided center
        map.setView([centerLat, centerLng], 13);
      }

      // Radius circle (km → m)
      if (radius) {
        const circle = L.circle([centerLat, centerLng], {
          radius: radius * 1000,
          color: brandBlue || FALLBACK_BRAND_500,
          weight: 2,
          fillOpacity: 0.08,
        });
        circle.addTo(map);
        circleRef.current = circle;
      }
    })();

    return () => {
      cancelled = true;
    };
    // Leaflet consumes the serialized marker inputs inside the async builder.
  }, [
    deals,
    dealsSignature,
    dealsCountLabel,
    locale,
    radius,
    centerLat,
    centerLng,
    loaded,
    shouldFitBounds,
  ]);

  // Sr-only summary for screen readers
  const uniqueCities = [...new Set(deals.map((d) => d.city).filter(Boolean))];

  // After Leaflet initializes and the skeleton is hidden, invalidate the map size.
  // Leaflet can initialize on a visibility:hidden container and miss its true dimensions;
  // invalidateSize() corrects this once the container is fully visible.
  useEffect(() => {
    if (loaded && mapRef.current) {
      mapRef.current.invalidateSize({ animate: false });
    }
  }, [loaded]);

  return (
    <div className={cn('relative w-full overflow-hidden rounded-xl', 'h-96')}>
      <style>{`
        .md-map-cluster-spring {
          display: flex;
          align-items: center;
          justify-content: center;
          width: var(--map-cluster-size);
          height: var(--map-cluster-size);
          border-radius: var(--map-cluster-radius);
          background: var(--map-cluster-background);
          color: var(--map-cluster-foreground);
          font-weight: var(--map-cluster-font-weight);
          box-shadow: var(--map-cluster-shadow);
        }
        @media (prefers-reduced-motion: no-preference) {
          .md-map-cluster-spring {
            animation: md-map-cluster-spring var(--map-cluster-duration)
              var(--map-cluster-easing);
          }
        }
        @keyframes md-map-cluster-spring {
          0% { transform: scale(var(--map-cluster-scale-start)); }
          65% { transform: scale(var(--map-cluster-scale-peak)); }
          100% { transform: scale(var(--map-cluster-scale-end)); }
        }
      `}</style>
      {/* Skeleton while Leaflet loads */}
      {!loaded && <Skeleton className="absolute inset-0" />}

      {/* Sr-only summary */}
      <p className="sr-only">
        {tMap('cities_count')
          .replace('{deals}', String(deals.length))
          .replace('{cities}', String(uniqueCities.length))}
      </p>

      {/*
       * Wrapper owns the visibility toggle so React never mutates className on
       * the Leaflet container div. Leaflet adds leaflet-container (and touch/zoom
       * classes) to the inner div; if React re-renders that div with a new
       * className it strips those classes, breaking the CSS rule:
       *   .leaflet-container img.leaflet-tile { max-width: none !important }
       * Without that rule Tailwind's img { max-width: 100% } constrains tiles to
       * 0px inside the 0-width tile-container → blank white map.
       */}
      <div className={cn('h-full w-full', !loaded && 'invisible')}>
        <div
          ref={containerRef}
          role="region"
          aria-label={tMap('map_aria_label')}
          className="h-full w-full"
        />
      </div>
    </div>
  );
}
