/**
 * RecordsMap — self-contained Leaflet map component (M2-T09).
 *
 * THIS IS THE ONLY MODULE IN THE APP THAT IMPORTS LEAFLET / REACT-LEAFLET.
 * All data fetching and state lives outside; this component receives typed props.
 */

import { useEffect, useRef, useCallback } from 'react'
import { MapContainer, TileLayer, useMap } from 'react-leaflet'
import * as L from 'leaflet'
import {
  Icon,
  DivIcon,
  marker as leafletMarker,
  heatLayer as leafletHeatLayer,
  type HeatLayer,
} from 'leaflet'

// Leaflet CSS — must be imported once; this component is the single place.
import 'leaflet/dist/leaflet.css'
import 'leaflet.markercluster/dist/MarkerCluster.css'
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'

// Side-effect imports (augment L with heatLayer and markerClusterGroup)
import 'leaflet.heat'
import 'leaflet.markercluster'

import type { HeatPoint, LocationMapPoint, PooMapPoint } from './mapUtils'
import type { LocationRecord, PooRecord } from '../records'

// Fix default Leaflet marker icon paths broken by Vite asset handling.
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'
import markerIcon from 'leaflet/dist/images/marker-icon.png'
import markerShadow from 'leaflet/dist/images/marker-shadow.png'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (Icon.Default.prototype as any)._getIconUrl
Icon.Default.mergeOptions({
  iconRetinaUrl: markerIcon2x,
  iconUrl: markerIcon,
  shadowUrl: markerShadow,
})

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------

export interface RecordsMapProps {
  locationHeatPoints: HeatPoint[]
  pooHeatPoints: HeatPoint[]
  locationScatterPoints: LocationMapPoint[]
  pooScatterPoints: PooMapPoint[]

  showLocationHeat: boolean
  showPooHeat: boolean
  showScatter: boolean

  onSelectLocation?: (record: LocationRecord) => void
  onSelectPoo?: (record: PooRecord) => void

  /** Map container height (CSS value). Default: '100%'. */
  height?: string
}

// ---------------------------------------------------------------------------
// Inner child: Heat layers (uses useMap hook — must be inside MapContainer)
// ---------------------------------------------------------------------------

interface HeatLayerChildProps {
  locationHeatPoints: HeatPoint[]
  pooHeatPoints: HeatPoint[]
  showLocationHeat: boolean
  showPooHeat: boolean
}

function HeatLayers({
  locationHeatPoints,
  pooHeatPoints,
  showLocationHeat,
  showPooHeat,
}: HeatLayerChildProps) {
  const map = useMap()
  const locationLayerRef = useRef<HeatLayer | null>(null)
  const pooLayerRef = useRef<HeatLayer | null>(null)

  // Location heat layer
  useEffect(() => {
    if (!locationLayerRef.current) {
      locationLayerRef.current = leafletHeatLayer([], {
        radius: 20,
        blur: 15,
        maxZoom: 17,
        gradient: { 0.4: 'blue', 0.65: 'lime', 1: 'red' },
      })
    }
    const layer = locationLayerRef.current
    layer.setLatLngs(locationHeatPoints)
    if (showLocationHeat) {
      if (!map.hasLayer(layer)) map.addLayer(layer)
    } else {
      if (map.hasLayer(layer)) map.removeLayer(layer)
    }
    return () => {
      if (map.hasLayer(layer)) map.removeLayer(layer)
    }
  }, [map, locationHeatPoints, showLocationHeat])

  // Poo heat layer
  useEffect(() => {
    if (!pooLayerRef.current) {
      pooLayerRef.current = leafletHeatLayer([], {
        radius: 25,
        blur: 18,
        maxZoom: 17,
        gradient: { 0.4: 'yellow', 0.65: 'orange', 1: '#8B4513' },
      })
    }
    const layer = pooLayerRef.current
    layer.setLatLngs(pooHeatPoints)
    if (showPooHeat) {
      if (!map.hasLayer(layer)) map.addLayer(layer)
    } else {
      if (map.hasLayer(layer)) map.removeLayer(layer)
    }
    return () => {
      if (map.hasLayer(layer)) map.removeLayer(layer)
    }
  }, [map, pooHeatPoints, showPooHeat])

  return null
}

// ---------------------------------------------------------------------------
// Inner child: Scatter / cluster layer
// ---------------------------------------------------------------------------

interface ScatterLayerChildProps {
  locationScatterPoints: LocationMapPoint[]
  pooScatterPoints: PooMapPoint[]
  showScatter: boolean
  onSelectLocation?: (record: LocationRecord) => void
  onSelectPoo?: (record: PooRecord) => void
}

const locationIcon = new Icon({
  iconUrl: markerIcon,
  iconRetinaUrl: markerIcon2x,
  shadowUrl: markerShadow,
  iconSize: [25, 41],
  iconAnchor: [12, 41],
  popupAnchor: [1, -34],
  shadowSize: [41, 41],
})

const pooIcon = new DivIcon({
  html: '<div style="font-size:20px;line-height:1;">💩</div>',
  className: '',
  iconSize: [24, 24],
  iconAnchor: [12, 12],
})

export function ScatterLayer({
  locationScatterPoints,
  pooScatterPoints,
  showScatter,
  onSelectLocation,
  onSelectPoo,
}: ScatterLayerChildProps) {
  const map = useMap()
  const clusterGroupRef = useRef<L.MarkerClusterGroup | null>(null)

  const rebuild = useCallback(() => {
    if (clusterGroupRef.current) {
      map.removeLayer(clusterGroupRef.current)
      clusterGroupRef.current = null
    }
    if (!showScatter) return

    // markerClusterGroup is augmented onto the imported L namespace by the
    // leaflet.markercluster side-effect import above. Using the imported
    // namespace (not window.L) is what works in Vite ESM bundles.
    const group = L.markerClusterGroup({ maxClusterRadius: 50, showCoverageOnHover: false })

    for (const pt of locationScatterPoints) {
      const m = leafletMarker([pt.lat, pt.lng], { icon: locationIcon })
      m.bindTooltip(`${pt.record.person}<br/>${pt.record.datetime}`, { sticky: true })
      if (onSelectLocation) m.on('click', () => onSelectLocation(pt.record))
      group.addLayer(m)
    }

    for (const pt of pooScatterPoints) {
      const m = leafletMarker([pt.lat, pt.lng], { icon: pooIcon })
      m.bindTooltip(`${pt.record.timestamp}<br/>${pt.record.status}`, { sticky: true })
      if (onSelectPoo) m.on('click', () => onSelectPoo(pt.record))
      group.addLayer(m)
    }

    map.addLayer(group)
    clusterGroupRef.current = group
  }, [map, locationScatterPoints, pooScatterPoints, showScatter, onSelectLocation, onSelectPoo])

  useEffect(() => {
    rebuild()
    return () => {
      if (clusterGroupRef.current) {
        map.removeLayer(clusterGroupRef.current)
        clusterGroupRef.current = null
      }
    }
  }, [rebuild, map])

  return null
}

// ---------------------------------------------------------------------------
// Public component
// ---------------------------------------------------------------------------

/** Default map center: Beijing area. */
const DEFAULT_CENTER: [number, number] = [39.9, 116.4]
const DEFAULT_ZOOM = 11

export function RecordsMap({
  locationHeatPoints,
  pooHeatPoints,
  locationScatterPoints,
  pooScatterPoints,
  showLocationHeat,
  showPooHeat,
  showScatter,
  onSelectLocation,
  onSelectPoo,
  height = '100%',
}: RecordsMapProps) {
  return (
    <MapContainer
      center={DEFAULT_CENTER}
      zoom={DEFAULT_ZOOM}
      style={{ height, width: '100%' }}
      data-testid="records-map"
    >
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />

      <HeatLayers
        locationHeatPoints={locationHeatPoints}
        pooHeatPoints={pooHeatPoints}
        showLocationHeat={showLocationHeat}
        showPooHeat={showPooHeat}
      />

      <ScatterLayer
        locationScatterPoints={locationScatterPoints}
        pooScatterPoints={pooScatterPoints}
        showScatter={showScatter}
        onSelectLocation={onSelectLocation}
        onSelectPoo={onSelectPoo}
      />
    </MapContainer>
  )
}
