<script setup lang="ts">
import { computed, onMounted, onUnmounted, shallowRef, watch } from "vue"
import type { LeafletMouseEvent, Map as LeafletMap, Marker } from "leaflet"
import { FieldFrame } from "@/components/ui/field-frame"
import "leaflet/dist/leaflet.css"
import markerIconUrl from "leaflet/dist/images/marker-icon.png"
import markerIcon2xUrl from "leaflet/dist/images/marker-icon-2x.png"
import markerShadowUrl from "leaflet/dist/images/marker-shadow.png"

/**
 * Props-driven map picker. Renders a Leaflet map inside a FieldFrame.
 * Clicking the map drops a pin and emits the {lat, lng} pair via
 * `update:modelValue`.
 *
 * Leaflet is imported dynamically inside `onMounted` so it never runs
 * its initialisation code during SSR or module evaluation — avoiding
 * the `L.Icon.Default` crash that plagues wrapper packages in Vite.
 */

export interface LatLng {
  lat: number
  lng: number
}

export interface MapPickerProps {
  modelValue?: LatLng | null
  id?: string
  name?: string
  label?: string
  hideLabel?: boolean
  required?: boolean
  readonly?: boolean
  disabled?: boolean
  helpText?: string
  errors?: string[]
  /** Centre used when `modelValue` is empty. Defaults to London. */
  defaultCenter?: LatLng
  /** Initial zoom level. */
  defaultZoom?: number
  /** CSS height applied to the map container. */
  height?: string
  /** Tile server URL template. Defaults to OpenStreetMap. */
  tileUrl?: string
  /** Tile attribution string rendered by Leaflet. */
  tileAttribution?: string
}

const props = withDefaults(defineProps<MapPickerProps>(), {
  errors: () => [],
  defaultCenter: () => ({ lat: 51.505, lng: -0.09 }),
  defaultZoom: 13,
  height: "24rem",
  tileUrl: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
  tileAttribution:
    '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
})

const model = defineModel<LatLng | null>("modelValue")

const mapEl = shallowRef<HTMLDivElement>()
const leafletMap = shallowRef<LeafletMap | null>(null)
const leafletMarker = shallowRef<Marker | null>(null)

// Non-reactive Leaflet library reference populated after mount.
let L: typeof import("leaflet") | null = null

const isInteractive = computed(() => !props.disabled && !props.readonly)

const formattedLatLng = computed<string>(() => {
  if (!model.value) return ""
  const { lat, lng } = model.value
  return `${lat.toFixed(4)}, ${lng.toFixed(4)}`
})

const containerStyle = computed<Record<string, string>>(() => ({
  height: props.height,
  width: "100%",
}))

// Sync external modelValue changes into the live map marker.
watch(
  () => model.value,
  (val) => {
    if (!L || !leafletMap.value) return
    if (!val) {
      leafletMarker.value?.remove()
      leafletMarker.value = null
      return
    }
    const pos: [number, number] = [val.lat, val.lng]
    if (leafletMarker.value) {
      leafletMarker.value.setLatLng(pos)
    } else {
      leafletMarker.value = L.marker(pos).addTo(leafletMap.value)
    }
  },
)

onMounted(async () => {
  if (!mapEl.value) return
  try {
    const mod = await import("leaflet")
    L = (mod.default ?? mod) as typeof import("leaflet")

    // Vite hashes asset filenames; patch the default icon so the marker
    // renders correctly in both dev and production builds.
    delete (L.Icon.Default.prototype as unknown as Record<string, unknown>)._getIconUrl
    L.Icon.Default.mergeOptions({
      iconUrl: markerIconUrl,
      iconRetinaUrl: markerIcon2xUrl,
      shadowUrl: markerShadowUrl,
    })

    const center = model.value ?? props.defaultCenter
    const map = L.map(mapEl.value, {
      center: [center.lat, center.lng],
      zoom: props.defaultZoom,
    })

    L.tileLayer(props.tileUrl, {
      attribution: props.tileAttribution,
    }).addTo(map)

    if (model.value) {
      leafletMarker.value = L.marker([model.value.lat, model.value.lng]).addTo(map)
    }

    map.on("click", (e: LeafletMouseEvent) => {
      if (!isInteractive.value) return
      const { lat, lng } = e.latlng
      model.value = { lat, lng }
      const pos: [number, number] = [lat, lng]
      if (leafletMarker.value) {
        leafletMarker.value.setLatLng(pos)
      } else {
        leafletMarker.value = L!.marker(pos).addTo(map)
      }
    })

    leafletMap.value = map
  } catch {
    // Leaflet unavailable in this environment.
  }
})

onUnmounted(() => {
  leafletMap.value?.remove()
  leafletMap.value = null
  leafletMarker.value = null
  L = null
})
</script>

<template>
  <FieldFrame
    :label="label"
    :required="required"
    :hide-label="hideLabel"
    :for="id"
    :help-text="helpText"
    :errors="errors"
  >
    <div
      :id="id"
      :data-element-name="name ?? undefined"
      :data-readonly="readonly || undefined"
      :data-disabled="disabled || undefined"
      :class="[
        'overflow-hidden rounded-md border',
        errors.length ? 'border-destructive' : 'border-input',
        !isInteractive ? 'pointer-events-none opacity-70' : '',
      ]"
      data-testid="map-picker-container"
    >
      <div ref="mapEl" :style="containerStyle" data-testid="map-picker-map" />
    </div>
    <p
      v-if="formattedLatLng"
      class="text-xs text-muted-foreground"
      data-testid="map-picker-latlng"
    >
      {{ formattedLatLng }}
    </p>
  </FieldFrame>
</template>
