<template>
  <div
    ref="mapRef"
    data-testid="kestrel-map"
    class="kestrel-map-container relative h-full w-full min-h-[300px]"
  >
    <div
      v-if="contextMenu.type"
      ref="contextMenuRef"
      class="pointer-events-auto absolute z-[1000] min-w-[120px] rounded border border-kestrel-border bg-kestrel-surface py-1 shadow-glow shadow-glow-context"
      :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
    >
      <template v-if="contextMenu.type === 'map'">
        <button
          type="button"
          class="kestrel-context-menu-item"
          @click="openAddPoiModal(contextMenu.latlng)"
        >
          Add POI here
        </button>
      </template>
      <template v-else-if="contextMenu.type === 'poi'">
        <button
          type="button"
          class="kestrel-context-menu-item"
          @click="openEditPoiModal(contextMenu.poi)"
        >
          Edit
        </button>
        <button
          type="button"
          class="kestrel-context-menu-item-danger"
          @click="openDeletePoiModal(contextMenu.poi)"
        >
          Delete
        </button>
      </template>
    </div>

    <PoiModal
      :show="showPoiModal"
      :mode="poiModalMode"
      :form="poiForm"
      :edit-poi="editPoi"
      :delete-poi="deletePoi"
      @close="closePoiModal"
      @submit="onPoiSubmit"
      @confirm-delete="confirmDeletePoi"
    />

    <div
      v-if="mapContext"
      class="pointer-events-auto absolute right-3 top-3 z-[1000] flex gap-0.5 rounded border border-kestrel-border bg-kestrel-surface/95 p-0.5 text-xs shadow-glow"
      data-testid="cot-layer-toggles"
    >
      <button
        v-for="layer in COT_LAYERS"
        :key="layer.key"
        type="button"
        class="kestrel-cot-layer-btn"
        :class="{ 'kestrel-cot-layer-btn-active': cotLayers[layer.key] }"
        @click="emit('toggleCotLayer', layer.key)"
      >
        {{ layer.label }}
      </button>
    </div>
  </div>
</template>

<script setup>
import 'leaflet/dist/leaflet.css'
import {
  createAlprControl,
  createAlprLayer,
  setAlprControlPressed,
  syncAlprLayer,
} from '~/utils/alprMapLayer.js'
import {
  createCotLayer,
  getCotClusters,
  loadCotCluster,
  syncCotLayer,
} from '~/utils/cotMapLayer.js'
import { clearFeatureMarkers } from '~/utils/mapMarkerSync.js'

const props = defineProps({
  devices: {
    type: Array,
    default: () => [],
  },
  pois: {
    type: Array,
    default: () => [],
  },
  liveSessions: {
    type: Array,
    default: () => [],
  },
  cotEntities: {
    type: Array,
    default: () => [],
  },
  cotLayers: {
    type: Object,
    default: () => ({ air: true, surface: true, ground: true }),
  },
  alprMarkers: {
    type: Array,
    default: () => [],
  },
  showAlpr: {
    type: Boolean,
    default: false,
  },
  canEditPois: {
    type: Boolean,
    default: false,
  },
})

const emit = defineEmits(['select', 'selectLive', 'refreshPois', 'boundsChange', 'toggleAlpr', 'toggleCotLayer'])
const CONTEXT_MENU_EMPTY = Object.freeze({ type: null, poi: null, latlng: null, x: 0, y: 0 })
const mapRef = ref(null)
const contextMenuRef = ref(null)
const leafletRef = shallowRef(null)
const mapContext = ref(null)
const markersRef = ref([])
const poiMarkersRef = ref({})
const liveMarkersRef = ref({})
const cotLayerRef = ref(null)
const cotMapView = ref(null)
const alprLayerRef = ref(null)
const contextMenu = ref({ ...CONTEXT_MENU_EMPTY })

const showPoiModal = ref(false)
const poiModalMode = ref('add') // 'add' | 'edit' | 'delete'
const addPoiLatlng = ref(null)
const editPoi = ref(null)
const deletePoi = ref(null)
const poiForm = ref({ label: '', iconType: 'pin' })
const resizeObserver = ref(null)

const TILE_URL = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'
const TILE_SUBDOMAINS = 'abcd'
const ATTRIBUTION = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
const DEFAULT_VIEW = [37.7749, -122.4194]
const DEFAULT_ZOOM = 17
const MARKER_ICON_PATH = '/'
const POI_TOOLTIP_CLASS = 'kestrel-poi-tooltip'
const POI_ICON_COLORS = { pin: '#22c9c9', flag: '#e53e3e', waypoint: '#a78bfa' }
const COT_LAYERS = Object.freeze([
  { key: 'air', label: 'Air' },
  { key: 'surface', label: 'Surface' },
  { key: 'ground', label: 'Team' },
])

const ICON_SIZE = 28

/** Embedded SVGs so each POI type has a distinct marker (no network, always correct). */
function getPoiIconSvg(type) {
  const c = POI_ICON_COLORS[type] || POI_ICON_COLORS.pin
  const shapes = {
    pin: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8c0 4.5-6 9-6 9s-6-4.5-6-9a6 6 0 0 1 12 0z"/><circle cx="12" cy="8" r="2" fill="${c}"/></svg>`,
    flag: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4v16"/><path d="M6 6h10l4 4-4 4H6"/></svg>`,
    waypoint: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="2" fill="${c}"/></svg>`,
  }
  return shapes[type] || shapes.pin
}

const VALID_POI_TYPES = ['pin', 'flag', 'waypoint']
function getPoiIcon(L, poi) {
  const type = VALID_POI_TYPES.includes(poi.icon_type) ? poi.icon_type : 'pin'
  const html = getPoiIconSvg(type)
  return L.divIcon({
    className: 'poi-div-icon',
    html: `<span class="poi-icon-svg poi-icon-${type}">${html}</span>`,
    iconSize: [ICON_SIZE, ICON_SIZE],
    iconAnchor: [ICON_SIZE / 2, ICON_SIZE],
  })
}

const LIVE_ICON_COLOR = '#22c9c9' /* kestrel-accent - JS string for Leaflet SVG */
function getLiveSessionIcon(L) {
  const html = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${LIVE_ICON_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="2" fill="${LIVE_ICON_COLOR}"/></svg>`
  return L.divIcon({
    className: 'poi-div-icon live-session-icon',
    html: `<span class="poi-icon-svg">${html}</span>`,
    iconSize: [ICON_SIZE, ICON_SIZE],
    iconAnchor: [ICON_SIZE / 2, ICON_SIZE],
  })
}

function refreshCotMapView(map) {
  const bounds = map.getBounds()
  cotMapView.value = {
    south: bounds.getSouth(),
    west: bounds.getWest(),
    north: bounds.getNorth(),
    east: bounds.getEast(),
    zoom: map.getZoom(),
  }
}

function renderCotLayer() {
  const ctx = mapContext.value
  const { L } = leafletRef.value || {}
  const layer = cotLayerRef.value
  if (!ctx?.map || !L || !layer) return
  const view = cotMapView.value
  const features = view ? getCotClusters(view) : []
  syncCotLayer(L, ctx.map, layer, features)
}

function reloadCotCluster() {
  loadCotCluster(props.cotEntities || [])
  renderCotLayer()
}

function emitBounds(map) {
  refreshCotMapView(map)
  const bounds = map.getBounds()
  emit('boundsChange', {
    south: bounds.getSouth(),
    west: bounds.getWest(),
    north: bounds.getNorth(),
    east: bounds.getEast(),
    zoom: map.getZoom(),
  })
}

function createMap(initialCenter) {
  const { L, offlineApi } = leafletRef.value || {}
  if (typeof document === 'undefined' || !mapRef.value || !L?.map) return

  const center = Array.isArray(initialCenter) && initialCenter.length === 2
    ? initialCenter
    : DEFAULT_VIEW

  const map = L.map(mapRef.value, { zoomControl: false, attributionControl: false, minZoom: 1, maxZoom: 19 }).setView(center, DEFAULT_ZOOM)
  L.control.zoom({ position: 'topleft' }).addTo(map)

  const locateControl = L.control({ position: 'topleft' })
  locateControl.onAdd = function () {
    const el = document.createElement('button')
    el.type = 'button'
    el.className = 'leaflet-bar leaflet-control-locate'
    el.title = 'Center on my location'
    el.setAttribute('aria-label', 'Center on my location')
    el.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" aria-hidden="true"><circle cx="12" cy="12" r="8"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>`
    el.addEventListener('click', () => {
      if (!navigator?.geolocation) return
      navigator.geolocation.getCurrentPosition(
        (pos) => {
          const { latitude, longitude } = pos.coords
          map.setView([latitude, longitude], DEFAULT_ZOOM, { animate: true })
        },
        () => {},
        { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 },
      )
    })
    return el
  }
  locateControl.addTo(map)

  const alprControl = createAlprControl(L, {
    showAlpr: props.showAlpr,
    onToggle: () => emit('toggleAlpr'),
  })
  alprControl.addTo(map)
  const alprLayer = createAlprLayer(L, map)
  const cotLayer = createCotLayer(L, map)

  const baseLayer = L.tileLayer(TILE_URL, {
    attribution: ATTRIBUTION,
    subdomains: TILE_SUBDOMAINS,
    minZoom: 1,
    maxZoom: 19,
  })
  baseLayer.addTo(map)

  const control = (() => {
    if (!offlineApi?.tileLayerOffline || !offlineApi.savetiles) return null
    const layer = offlineApi.tileLayerOffline(TILE_URL, { attribution: ATTRIBUTION, subdomains: TILE_SUBDOMAINS, minZoom: 1, maxZoom: 19 })
    const c = offlineApi.savetiles(layer, {
      zoomlevels: [10, 11, 12, 13, 14, 15],
      position: 'topleft',
      saveText: 'Save tiles',
      rmText: 'Clear tiles',
    })
    c.addTo(map)
    return c
  })()

  map.on('contextmenu', (e) => {
    if (!props.canEditPois) return
    e.originalEvent?.preventDefault()
    const pt = map.latLngToContainerPoint(e.latlng)
    contextMenu.value = { type: 'map', latlng: e.latlng, x: pt.x, y: pt.y }
  })

  map.on('moveend', () => {
    emitBounds(map)
    renderCotLayer()
  })
  map.on('zoomend', () => {
    emitBounds(map)
    renderCotLayer()
  })

  mapContext.value = { map, layer: baseLayer, control, locateControl, alprControl }
  alprLayerRef.value = alprLayer
  cotLayerRef.value = cotLayer
  refreshCotMapView(map)
  updateMarkers()
  updatePoiMarkers()
  updateLiveMarkers()
  reloadCotCluster()
  updateAlprLayer()
  nextTick(() => {
    map.invalidateSize()
    emitBounds(map)
  })
}

function updateMarkers() {
  const ctx = mapContext.value
  const { L } = leafletRef.value || {}
  if (!ctx?.map || !L) return

  markersRef.value.forEach((m) => {
    if (m) m.remove()
  })

  const validSources = (props.devices || []).filter(f => typeof f?.lat === 'number' && typeof f?.lng === 'number')
  markersRef.value = validSources.map(item =>
    L.marker([item.lat, item.lng]).addTo(ctx.map).on('click', () => emit('select', item)),
  )
}

function updatePoiMarkers() {
  const ctx = mapContext.value
  const { L } = leafletRef.value || {}
  if (!ctx?.map || !L) return

  const pois = (props.pois || []).filter(p => typeof p?.lat === 'number' && typeof p?.lng === 'number' && p?.id)
  const byId = Object.fromEntries(pois.map(p => [p.id, p]))
  const prev = poiMarkersRef.value

  Object.keys(prev).forEach((id) => {
    if (!byId[id]) prev[id]?.remove()
  })

  const next = pois.reduce((acc, poi) => {
    const existing = prev[poi.id]
    const icon = getPoiIcon(L, poi)
    if (existing) {
      existing.setLatLng([poi.lat, poi.lng])
      if (icon) existing.setIcon(icon)
      existing.getTooltip()?.setContent(poi.label || '')
      if (!existing.getTooltip()?.isOpen() && (poi.label || props.canEditPois)) existing.bindTooltip(poi.label || poi.id, { permanent: false, className: POI_TOOLTIP_CLASS })
      return { ...acc, [poi.id]: existing }
    }
    const marker = L.marker([poi.lat, poi.lng], icon ? { icon } : undefined).addTo(ctx.map)
    if (poi.label || props.canEditPois) marker.bindTooltip(poi.label || poi.id, { permanent: false, className: POI_TOOLTIP_CLASS })
    if (props.canEditPois) {
      marker.on('contextmenu', (e) => {
        e.originalEvent?.preventDefault()
        e.originalEvent?.stopPropagation()
        const pt = ctx.map.latLngToContainerPoint(e.latlng)
        contextMenu.value = { type: 'poi', poi, x: pt.x, y: pt.y }
      })
    }
    return { ...acc, [poi.id]: marker }
  }, {})
  poiMarkersRef.value = next
}

function updateLiveMarkers() {
  const ctx = mapContext.value
  const { L } = leafletRef.value || {}
  if (!ctx?.map || !L) return

  const sessions = (props.liveSessions || []).filter(
    s => typeof s?.lat === 'number' && typeof s?.lng === 'number' && s?.id,
  )
  const byId = Object.fromEntries(sessions.map(s => [s.id, s]))
  const prev = liveMarkersRef.value
  const icon = getLiveSessionIcon(L)

  Object.keys(prev).forEach((id) => {
    if (!byId[id]) prev[id]?.remove()
  })

  const next = sessions.reduce((acc, session) => {
    const content = `<div class="kestrel-live-popup"><strong>${escapeHtml(session.label)}</strong>${session.hasStream ? ' <span class="text-kestrel-accent">● Live</span>' : ''}</div>`
    const existing = prev[session.id]
    if (existing) {
      existing.setLatLng([session.lat, session.lng])
      existing.setIcon(icon)
      existing.getPopup()?.setContent(content)
      return { ...acc, [session.id]: existing }
    }
    const marker = L.marker([session.lat, session.lng], { icon })
      .addTo(ctx.map)
      .bindPopup(content, { className: 'kestrel-live-popup-wrap', maxWidth: 360 })
      .on('click', () => emit('selectLive', session))
    return { ...acc, [session.id]: marker }
  }, {})
  liveMarkersRef.value = next
}

function updateAlprLayer() {
  const ctx = mapContext.value
  const { L } = leafletRef.value || {}
  const layer = alprLayerRef.value
  if (!ctx?.map || !L || !layer) return
  if (!props.showAlpr) {
    clearFeatureMarkers(layer)
    return
  }
  syncAlprLayer(L, ctx.map, layer, props.alprMarkers || [])
}

function escapeHtml(text) {
  const div = document.createElement('div')
  div.textContent = text
  return div.innerHTML
}

function closeContextMenu() {
  contextMenu.value = { ...CONTEXT_MENU_EMPTY }
}

function openAddPoiModal(latlng) {
  closeContextMenu()
  poiModalMode.value = 'add'
  addPoiLatlng.value = { lat: latlng.lat, lng: latlng.lng }
  editPoi.value = null
  deletePoi.value = null
  poiForm.value = { label: '', iconType: 'pin' }
  showPoiModal.value = true
}

function openEditPoiModal(poi) {
  closeContextMenu()
  poiModalMode.value = 'edit'
  editPoi.value = poi
  addPoiLatlng.value = null
  deletePoi.value = null
  poiForm.value = { label: (poi.label ?? '').trim(), iconType: poi.icon_type || 'pin' }
  showPoiModal.value = true
}

function openDeletePoiModal(poi) {
  closeContextMenu()
  poiModalMode.value = 'delete'
  deletePoi.value = poi
  addPoiLatlng.value = null
  editPoi.value = null
  showPoiModal.value = true
}

function closePoiModal() {
  showPoiModal.value = false
  poiModalMode.value = 'add'
  addPoiLatlng.value = null
  editPoi.value = null
  deletePoi.value = null
}

async function doPoiFetch(fn) {
  try {
    await fn()
    emit('refreshPois')
    closePoiModal()
  }
  catch { /* ignore */ }
}

async function onPoiSubmit(payload) {
  const { label, iconType } = payload
  const body = { label: (label ?? '').trim(), iconType: iconType || 'pin' }
  if (poiModalMode.value === 'add') {
    const latlng = addPoiLatlng.value
    if (!latlng) return
    await doPoiFetch(() => $fetch('/api/pois', { method: 'POST', body: { ...body, lat: latlng.lat, lng: latlng.lng } }))
    return
  }
  if (poiModalMode.value === 'edit' && editPoi.value) {
    await doPoiFetch(() => $fetch(`/api/pois/${editPoi.value.id}`, { method: 'PATCH', body }))
  }
}

async function confirmDeletePoi() {
  const poi = deletePoi.value
  if (!poi?.id) return
  await doPoiFetch(() => $fetch(`/api/pois/${poi.id}`, { method: 'DELETE' }))
}

function destroyMap() {
  markersRef.value.forEach((m) => {
    if (m) m.remove()
  })
  markersRef.value = []
  Object.values(poiMarkersRef.value).forEach(m => m?.remove())
  poiMarkersRef.value = {}
  Object.values(liveMarkersRef.value).forEach(m => m?.remove())
  liveMarkersRef.value = {}
  clearFeatureMarkers(cotLayerRef.value)
  clearFeatureMarkers(alprLayerRef.value)

  const ctx = mapContext.value
  if (ctx) {
    if (ctx.control && ctx.map) ctx.map.removeControl(ctx.control)
    if (ctx.locateControl && ctx.map) ctx.map.removeControl(ctx.locateControl)
    if (ctx.alprControl && ctx.map) ctx.map.removeControl(ctx.alprControl)
    if (cotLayerRef.value && ctx.map) ctx.map.removeLayer(cotLayerRef.value)
    if (alprLayerRef.value && ctx.map) ctx.map.removeLayer(alprLayerRef.value)
    if (ctx.layer && ctx.map) ctx.map.removeLayer(ctx.layer)
    if (ctx.map) ctx.map.remove()
    mapContext.value = null
  }
  cotLayerRef.value = null
  alprLayerRef.value = null
}

function initMapWithLocation() {
  if (!navigator?.geolocation) {
    createMap(DEFAULT_VIEW)
    return
  }
  navigator.geolocation.getCurrentPosition(
    (pos) => {
      const { latitude, longitude } = pos.coords
      createMap([latitude, longitude])
    },
    () => {
      createMap(DEFAULT_VIEW)
    },
    { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 },
  )
}

onMounted(async () => {
  if (!import.meta.client || typeof document === 'undefined') return
  const [leaflet, offline] = await Promise.all([
    import('leaflet'),
    import('leaflet.offline'),
  ])
  const L = leaflet.default

  if (L.Icon?.Default?.mergeOptions) {
    L.Icon.Default.mergeOptions({
      iconUrl: `${MARKER_ICON_PATH}marker-icon.png`,
      iconRetinaUrl: `${MARKER_ICON_PATH}marker-icon-2x.png`,
      shadowUrl: `${MARKER_ICON_PATH}marker-shadow.png`,
    })
  }

  leafletRef.value = { L, offlineApi: offline }
  initMapWithLocation()
  document.addEventListener('click', onDocumentClick)

  nextTick(() => {
    if (mapRef.value) {
      resizeObserver.value = new ResizeObserver(() => {
        mapContext.value?.map?.invalidateSize()
      })
      resizeObserver.value.observe(mapRef.value)
    }
  })
})

function onDocumentClick(e) {
  if (contextMenu.value.type && contextMenuRef.value && !contextMenuRef.value.contains(e.target)) closeContextMenu()
}

onBeforeUnmount(() => {
  document.removeEventListener('click', onDocumentClick)
  if (resizeObserver.value && mapRef.value) {
    resizeObserver.value.disconnect()
    resizeObserver.value = null
  }
  destroyMap()
})

watch(() => props.devices, () => updateMarkers(), { deep: true })
watch([() => props.pois, () => props.canEditPois], () => updatePoiMarkers(), { deep: true })
watch(() => props.liveSessions, () => updateLiveMarkers(), { deep: true })
watch(() => props.cotEntities, () => reloadCotCluster())
watch(() => props.alprMarkers, () => updateAlprLayer())
watch(() => props.showAlpr, (enabled) => {
  setAlprControlPressed(mapContext.value?.alprControl, enabled)
  if (enabled && mapContext.value?.map) emitBounds(mapContext.value.map)
  else updateAlprLayer()
})
</script>
