<template>
  <div class="flex flex-col flex-1 min-h-0 w-full h-full">
    <ClientOnly>
      <MapboxMap
        :map-id="mapId"
        class="flex-1 min-h-0"
        style="width: 100%; height: 100%"
        :options="mapOptions"
        @load="onMapCreated"
      >
        <MapboxNavigationControl position="top-right" />
      </MapboxMap>
    </ClientOnly>
  </div>
</template>

<script setup lang="ts">
import type { Map as MapboxMapType } from 'mapbox-gl'
import { LngLatBounds } from 'mapbox-gl'
import type { ClusterPointType } from '~/composables/graphql/public/geo-generated'

interface MapItem {
  uuid?: string | null
  name?: string | null
  latitude?: number | null
  longitude?: number | null
  country?: string | null
}

export interface MapBounds {
  west: number
  south: number
  east: number
  north: number
  zoom: number
}

interface HoveredItem {
  latitude: number
  longitude: number
}

const props = withDefaults(defineProps<{
  mapId: string
  items?: MapItem[]
  clusteredPoints?: ClusterPointType[]
  useServerClustering?: boolean
  hoveredItemId?: string | null
  hoveredItem?: HoveredItem | null
  pointColor?: string
  entityType?: 'offer' | 'hub' | 'supplier'
  initialCenter?: [number, number]
  initialZoom?: number
  relatedPoints?: Array<{
    uuid: string
    name: string
    latitude: number
    longitude: number
    type: 'hub' | 'supplier' | 'offer'
  }>
}>(), {
  pointColor: '#f97316',
  entityType: 'offer',
  initialCenter: () => [37.64, 55.76],
  initialZoom: 2,
  useServerClustering: false,
  items: () => [],
  clusteredPoints: () => [],
  relatedPoints: () => []
})

const emit = defineEmits<{
  'select-item': [uuid: string, properties?: Record<string, any>]
  'bounds-change': [bounds: MapBounds]
}>()

const mapRef = useMapboxRef(props.mapId)
const { flyThroughSpace } = useMapboxFlyAnimation()
const didFitBounds = ref(false)
const mapInitialized = ref(false)

// Entity type icons - SVG data URLs with specific colors
const createEntityIcon = (type: 'offer' | 'hub' | 'supplier', color: string) => {
  const icons = {
    offer: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>`,
    hub: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 8.35V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8.35A2 2 0 0 1 3.26 6.5l8-3.2a2 2 0 0 1 1.48 0l8 3.2A2 2 0 0 1 22 8.35Z"/><path d="M6 18h12"/><path d="M6 14h12"/><rect width="12" height="12" x="6" y="10"/></svg>`,
    supplier: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7 5V8l-7 5V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M17 18h1"/><path d="M12 18h1"/><path d="M7 18h1"/></svg>`
  }
  return icons[type]
}

// Load icon into map as image
const loadEntityIcon = async (map: MapboxMapType, type: 'offer' | 'hub' | 'supplier', color: string) => {
  const iconName = `entity-icon-${type}`
  if (map.hasImage(iconName)) {
    map.removeImage(iconName)
  }

  const svg = createEntityIcon(type, color)
  const img = new Image(32, 32)

  return new Promise<void>((resolve) => {
    img.onload = () => {
      const canvas = document.createElement('canvas')
      canvas.width = 32
      canvas.height = 32
      const ctx = canvas.getContext('2d')
      if (ctx) {
        // Draw colored circle background
        ctx.beginPath()
        ctx.arc(16, 16, 15, 0, 2 * Math.PI)
        ctx.fillStyle = color
        ctx.fill()
        ctx.strokeStyle = 'white'
        ctx.lineWidth = 2
        ctx.stroke()
        // Draw icon on top
        ctx.drawImage(img, 4, 4, 24, 24)
      }

      const imageData = ctx?.getImageData(0, 0, 32, 32)
      if (imageData) {
        map.addImage(iconName, { width: 32, height: 32, data: imageData.data })
      }
      resolve()
    }
    img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
  })
}

// Standard colors for entity types
const ENTITY_COLORS = {
  hub: '#22c55e',      // green
  supplier: '#3b82f6', // blue
  offer: '#f97316'     // orange
} as const

// Load all icons for related points (each type with its standard color)
const loadRelatedPointIcons = async (map: MapboxMapType) => {
  const types: Array<'hub' | 'supplier' | 'offer'> = ['hub', 'supplier', 'offer']
  for (const type of types) {
    const iconName = `related-icon-${type}`
    if (map.hasImage(iconName)) {
      map.removeImage(iconName)
    }

    const svg = createEntityIcon(type, ENTITY_COLORS[type])
    const img = new Image(32, 32)

    await new Promise<void>((resolve) => {
      img.onload = () => {
        const canvas = document.createElement('canvas')
        canvas.width = 32
        canvas.height = 32
        const ctx = canvas.getContext('2d')
        if (ctx) {
          ctx.beginPath()
          ctx.arc(16, 16, 15, 0, 2 * Math.PI)
          ctx.fillStyle = ENTITY_COLORS[type]
          ctx.fill()
          ctx.strokeStyle = 'white'
          ctx.lineWidth = 2
          ctx.stroke()
          ctx.drawImage(img, 4, 4, 24, 24)
        }

        const imageData = ctx?.getImageData(0, 0, 32, 32)
        if (imageData) {
          map.addImage(iconName, { width: 32, height: 32, data: imageData.data })
        }
        resolve()
      }
      img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
    })
  }
}

const mapOptions = computed(() => ({
  style: 'mapbox://styles/mapbox/satellite-streets-v12',
  center: props.initialCenter,
  zoom: props.initialZoom,
  projection: 'globe',
  pitch: 20
}))

// Client-side clustering GeoJSON (when not using server clustering)
const geoJsonData = computed(() => ({
  type: 'FeatureCollection' as const,
  features: props.items
    .filter(item => item.latitude != null && item.longitude != null)
    .map(item => ({
      type: 'Feature' as const,
      properties: { uuid: item.uuid, name: item.name, country: item.country },
      geometry: { type: 'Point' as const, coordinates: [item.longitude!, item.latitude!] }
    }))
}))

// Server-side clustering GeoJSON
const serverClusteredGeoJson = computed(() => ({
  type: 'FeatureCollection' as const,
  features: (props.clusteredPoints || []).filter(Boolean).map(point => ({
    type: 'Feature' as const,
    properties: {
      id: point!.id,
      name: point!.name,
      count: point!.count ?? 1,
      expansionZoom: point!.expansionZoom,
      isCluster: (point!.count ?? 1) > 1
    },
    geometry: {
      type: 'Point' as const,
      coordinates: [point!.longitude ?? 0, point!.latitude ?? 0]
    }
  }))
}))

// Hovered point GeoJSON (separate layer on top)
const hoveredPointGeoJson = computed(() => ({
  type: 'FeatureCollection' as const,
  features: props.hoveredItem ? [{
    type: 'Feature' as const,
    properties: {},
    geometry: {
      type: 'Point' as const,
      coordinates: [props.hoveredItem.longitude, props.hoveredItem.latitude]
    }
  }] : []
}))

// Related points GeoJSON (for Info mode)
const relatedPointsGeoJson = computed(() => {
  if (!props.relatedPoints || props.relatedPoints.length === 0) {
    return { type: 'FeatureCollection' as const, features: [] }
  }

  return {
    type: 'FeatureCollection' as const,
    features: props.relatedPoints.map(point => ({
      type: 'Feature' as const,
      properties: {
        uuid: point.uuid,
        name: point.name,
        type: point.type
      },
      geometry: {
        type: 'Point' as const,
        coordinates: [point.longitude, point.latitude]
      }
    }))
  }
})

const sourceId = computed(() => `${props.mapId}-points`)
const hoveredSourceId = computed(() => `${props.mapId}-hovered`)
const relatedSourceId = computed(() => `${props.mapId}-related`)

const emitBoundsChange = (map: MapboxMapType) => {
  const bounds = map.getBounds()
  if (!bounds) return
  emit('bounds-change', {
    west: bounds.getWest(),
    south: bounds.getSouth(),
    east: bounds.getEast(),
    north: bounds.getNorth(),
    zoom: map.getZoom()
  })
}

const onMapCreated = (map: MapboxMapType) => {
  const initMap = async () => {
    map.setFog({
      color: 'rgb(186, 210, 235)',
      'high-color': 'rgb(36, 92, 223)',
      'horizon-blend': 0.02,
      'space-color': 'rgb(11, 11, 25)',
      'star-intensity': 0.6
    })

    if (props.useServerClustering) {
      await initServerClusteringLayers(map)
    } else {
      await initClientClusteringLayers(map)
    }

    // Emit initial bounds
    emitBoundsChange(map)

    // Emit bounds on move/zoom end
    map.on('moveend', () => emitBoundsChange(map))

    mapInitialized.value = true
  }

  if (map.loaded()) {
    initMap()
  } else {
    map.on('load', initMap)
  }
}

const initClientClusteringLayers = async (map: MapboxMapType) => {
  // Load entity icon first
  await loadEntityIcon(map, props.entityType, props.pointColor)

  map.addSource(sourceId.value, {
    type: 'geojson',
    data: geoJsonData.value,
    cluster: true,
    clusterMaxZoom: 14,
    clusterRadius: 50
  })

  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: sourceId.value,
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': props.pointColor,
      'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 50, 40],
      'circle-opacity': 0.8,
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff'
    }
  })

  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: sourceId.value,
    filter: ['has', 'point_count'],
    layout: {
      'text-field': ['get', 'point_count_abbreviated'],
      'text-size': 14
    },
    paint: { 'text-color': '#ffffff' }
  })

  map.addLayer({
    id: 'unclustered-point',
    type: 'symbol',
    source: sourceId.value,
    filter: ['!', ['has', 'point_count']],
    layout: {
      'icon-image': `entity-icon-${props.entityType}`,
      'icon-size': 1,
      'icon-allow-overlap': true
    }
  })

  map.addLayer({
    id: 'point-labels',
    type: 'symbol',
    source: sourceId.value,
    filter: ['!', ['has', 'point_count']],
    layout: {
      'text-field': ['get', 'name'],
      'text-offset': [0, 1.8],
      'text-size': 12,
      'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold']
    },
    paint: {
      'text-color': '#ffffff',
      'text-halo-color': '#000000',
      'text-halo-width': 1.5
    }
  })

  map.on('click', 'clusters', (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] })
    const feature = features[0]
    if (!feature) return
    const clusterId = feature.properties?.cluster_id
    const source = map.getSource(sourceId.value) as mapboxgl.GeoJSONSource
    source.getClusterExpansionZoom(clusterId, (err, zoom) => {
      if (err) return
      const geometry = feature.geometry as GeoJSON.Point
      map.easeTo({ center: geometry.coordinates as [number, number], zoom: zoom || 4 })
    })
  })

  map.on('click', 'unclustered-point', (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: ['unclustered-point'] })
    const feature = features[0]
    if (!feature) return
    emit('select-item', feature.properties?.uuid)
  })

  map.on('mouseenter', 'clusters', () => { map.getCanvas().style.cursor = 'pointer' })
  map.on('mouseleave', 'clusters', () => { map.getCanvas().style.cursor = '' })
  map.on('mouseenter', 'unclustered-point', () => { map.getCanvas().style.cursor = 'pointer' })
  map.on('mouseleave', 'unclustered-point', () => { map.getCanvas().style.cursor = '' })

  // Hovered point layer (on top of everything) - "target" effect with border
  map.addSource(hoveredSourceId.value, {
    type: 'geojson',
    data: hoveredPointGeoJson.value
  })
  // Outer ring (white)
  map.addLayer({
    id: 'hovered-point-ring',
    type: 'circle',
    source: hoveredSourceId.value,
    paint: {
      'circle-radius': 20,
      'circle-color': 'transparent',
      'circle-stroke-width': 3,
      'circle-stroke-color': '#ffffff'
    }
  })
  // Inner point (same as entity color)
  map.addLayer({
    id: 'hovered-point-layer',
    type: 'circle',
    source: hoveredSourceId.value,
    paint: {
      'circle-radius': 14,
      'circle-color': props.pointColor,
      'circle-stroke-width': 3,
      'circle-stroke-color': '#ffffff'
    }
  })

  // Related points layer (for Info mode - icons by type)
  await loadRelatedPointIcons(map)

  map.addSource(relatedSourceId.value, {
    type: 'geojson',
    data: relatedPointsGeoJson.value
  })
  map.addLayer({
    id: `${props.mapId}-related-points`,
    type: 'symbol',
    source: relatedSourceId.value,
    layout: {
      'icon-image': [
        'match',
        ['get', 'type'],
        'hub', 'related-icon-hub',
        'supplier', 'related-icon-supplier',
        'offer', 'related-icon-offer',
        'related-icon-offer'  // default
      ],
      'icon-size': 1,
      'icon-allow-overlap': true
    }
  })
  map.addLayer({
    id: `${props.mapId}-related-labels`,
    type: 'symbol',
    source: relatedSourceId.value,
    layout: {
      'text-field': ['get', 'name'],
      'text-size': 11,
      'text-anchor': 'top',
      'text-offset': [0, 1.5]
    },
    paint: {
      'text-color': '#ffffff',
      'text-halo-color': '#000000',
      'text-halo-width': 1
    }
  })

  // Click handlers for related points
  map.on('click', `${props.mapId}-related-points`, (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-points`] })
    const feature = features[0]
    if (!feature) return
    const props_data = feature.properties as Record<string, any> | undefined
    emit('select-item', props_data?.uuid, props_data)
  })

  map.on('mouseenter', `${props.mapId}-related-points`, () => {
    map.getCanvas().style.cursor = 'pointer'
  })

  map.on('mouseleave', `${props.mapId}-related-points`, () => {
    map.getCanvas().style.cursor = ''
  })

  // Auto-fit bounds to all items
  if (!didFitBounds.value && props.items.length > 0) {
    const bounds = new LngLatBounds()
    props.items.forEach(item => {
      if (item.longitude != null && item.latitude != null) {
        bounds.extend([item.longitude, item.latitude])
      }
    })
    map.fitBounds(bounds, { padding: 50, maxZoom: 10 })
    didFitBounds.value = true
  }
}

const initServerClusteringLayers = async (map: MapboxMapType) => {
  // Load entity icon first
  await loadEntityIcon(map, props.entityType, props.pointColor)

  map.addSource(sourceId.value, {
    type: 'geojson',
    data: serverClusteredGeoJson.value
  })

  // Clusters (count > 1) - circle with count
  map.addLayer({
    id: 'server-clusters',
    type: 'circle',
    source: sourceId.value,
    filter: ['>', ['get', 'count'], 1],
    paint: {
      'circle-color': props.pointColor,
      'circle-radius': ['step', ['get', 'count'], 20, 10, 30, 50, 40],
      'circle-opacity': 0.8,
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff'
    }
  })

  map.addLayer({
    id: 'server-cluster-count',
    type: 'symbol',
    source: sourceId.value,
    filter: ['>', ['get', 'count'], 1],
    layout: {
      'text-field': ['get', 'count'],
      'text-size': 14
    },
    paint: { 'text-color': '#ffffff' }
  })

  // Individual points (count == 1) - icon with entity type
  map.addLayer({
    id: 'server-points',
    type: 'symbol',
    source: sourceId.value,
    filter: ['==', ['get', 'count'], 1],
    layout: {
      'icon-image': `entity-icon-${props.entityType}`,
      'icon-size': 1,
      'icon-allow-overlap': true
    }
  })

  map.addLayer({
    id: 'server-point-labels',
    type: 'symbol',
    source: sourceId.value,
    filter: ['==', ['get', 'count'], 1],
    layout: {
      'text-field': ['get', 'name'],
      'text-offset': [0, 1.8],
      'text-size': 12,
      'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold']
    },
    paint: {
      'text-color': '#ffffff',
      'text-halo-color': '#000000',
      'text-halo-width': 1.5
    }
  })

  // Click on cluster to zoom in
  map.on('click', 'server-clusters', (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: ['server-clusters'] })
    const feature = features[0]
    if (!feature) return
    const expansionZoom = feature.properties?.expansionZoom
    const geometry = feature.geometry as GeoJSON.Point
    map.easeTo({
      center: geometry.coordinates as [number, number],
      zoom: expansionZoom || map.getZoom() + 2
    })
  })

  // Click on individual point - emit full properties
  map.on('click', 'server-points', (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: ['server-points'] })
    const feature = features[0]
    if (!feature) return
    const props = feature.properties || {}
    emit('select-item', props.id, props)
  })

  map.on('mouseenter', 'server-clusters', () => { map.getCanvas().style.cursor = 'pointer' })
  map.on('mouseleave', 'server-clusters', () => { map.getCanvas().style.cursor = '' })
  map.on('mouseenter', 'server-points', () => { map.getCanvas().style.cursor = 'pointer' })
  map.on('mouseleave', 'server-points', () => { map.getCanvas().style.cursor = '' })

  // Hovered point layer (on top of everything) - "target" effect with border
  map.addSource(hoveredSourceId.value, {
    type: 'geojson',
    data: hoveredPointGeoJson.value
  })
  // Outer ring (white)
  map.addLayer({
    id: 'hovered-point-ring',
    type: 'circle',
    source: hoveredSourceId.value,
    paint: {
      'circle-radius': 20,
      'circle-color': 'transparent',
      'circle-stroke-width': 3,
      'circle-stroke-color': '#ffffff'
    }
  })
  // Inner point (same as entity color)
  map.addLayer({
    id: 'hovered-point-layer',
    type: 'circle',
    source: hoveredSourceId.value,
    paint: {
      'circle-radius': 14,
      'circle-color': props.pointColor,
      'circle-stroke-width': 3,
      'circle-stroke-color': '#ffffff'
    }
  })

  // Related points layer (for Info mode - icons by type)
  await loadRelatedPointIcons(map)

  map.addSource(relatedSourceId.value, {
    type: 'geojson',
    data: relatedPointsGeoJson.value
  })
  map.addLayer({
    id: `${props.mapId}-related-points`,
    type: 'symbol',
    source: relatedSourceId.value,
    layout: {
      'icon-image': [
        'match',
        ['get', 'type'],
        'hub', 'related-icon-hub',
        'supplier', 'related-icon-supplier',
        'offer', 'related-icon-offer',
        'related-icon-offer'  // default
      ],
      'icon-size': 1,
      'icon-allow-overlap': true
    }
  })
  map.addLayer({
    id: `${props.mapId}-related-labels`,
    type: 'symbol',
    source: relatedSourceId.value,
    layout: {
      'text-field': ['get', 'name'],
      'text-size': 11,
      'text-anchor': 'top',
      'text-offset': [0, 1.5]
    },
    paint: {
      'text-color': '#ffffff',
      'text-halo-color': '#000000',
      'text-halo-width': 1
    }
  })

  // Click handlers for related points
  map.on('click', `${props.mapId}-related-points`, (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-points`] })
    const feature = features[0]
    if (!feature) return
    const props_data = feature.properties as Record<string, any> | undefined
    emit('select-item', props_data?.uuid, props_data)
  })

  map.on('mouseenter', `${props.mapId}-related-points`, () => {
    map.getCanvas().style.cursor = 'pointer'
  })

  map.on('mouseleave', `${props.mapId}-related-points`, () => {
    map.getCanvas().style.cursor = ''
  })
}

// Update map data when items or clusteredPoints change
watch(() => props.useServerClustering ? serverClusteredGeoJson.value : geoJsonData.value, (newData) => {
  if (!mapRef.value || !mapInitialized.value) return
  const source = mapRef.value.getSource(sourceId.value) as mapboxgl.GeoJSONSource | undefined
  if (source) {
    source.setData(newData)
  }
}, { deep: true })

// Update hovered point layer when hoveredItem changes
watch(() => props.hoveredItem, () => {
  if (!mapRef.value || !mapInitialized.value) return
  const source = mapRef.value.getSource(hoveredSourceId.value) as mapboxgl.GeoJSONSource | undefined
  if (source) {
    source.setData(hoveredPointGeoJson.value)
  }
}, { deep: true })

// Update related points layer when relatedPoints changes
watch(() => props.relatedPoints, () => {
  if (!mapRef.value || !mapInitialized.value) return
  const source = mapRef.value.getSource(relatedSourceId.value) as mapboxgl.GeoJSONSource | undefined
  if (source) {
    source.setData(relatedPointsGeoJson.value)
  }
}, { deep: true })

// Watch for pointColor or entityType changes - update colors and icons
watch([() => props.pointColor, () => props.entityType], async ([newColor, newType]) => {
  if (!mapRef.value || !mapInitialized.value) return
  const map = mapRef.value

  // Reload icon with new color and type
  await loadEntityIcon(map, newType, newColor)

  // Update cluster circle colors
  if (props.useServerClustering) {
    if (map.getLayer('server-clusters')) {
      map.setPaintProperty('server-clusters', 'circle-color', newColor)
    }
    // Update icon reference for points
    if (map.getLayer('server-points')) {
      map.setLayoutProperty('server-points', 'icon-image', `entity-icon-${newType}`)
    }
  } else {
    if (map.getLayer('clusters')) {
      map.setPaintProperty('clusters', 'circle-color', newColor)
    }
    if (map.getLayer('unclustered-point')) {
      map.setLayoutProperty('unclustered-point', 'icon-image', `entity-icon-${newType}`)
    }
  }

  // Update hovered point color
  if (map.getLayer('hovered-point-layer')) {
    map.setPaintProperty('hovered-point-layer', 'circle-color', newColor)
  }
})

// fitBounds for server clustering when first data arrives
watch(() => props.clusteredPoints, (points) => {
  if (!mapRef.value || !mapInitialized.value) return
  if (!didFitBounds.value && points && points.length > 0) {
    const bounds = new LngLatBounds()
    points.forEach(p => {
      if (p?.longitude && p?.latitude) {
        bounds.extend([p.longitude, p.latitude])
      }
    })
    if (!bounds.isEmpty()) {
      mapRef.value.fitBounds(bounds, { padding: 50, maxZoom: 6 })
      didFitBounds.value = true
    }
  }
}, { immediate: true })

// Expose flyTo method for external use (with space fly animation)
const flyTo = async (lat: number, lng: number, zoom = 8) => {
  if (!mapRef.value) return
  await flyThroughSpace(mapRef.value, {
    targetCenter: [lng, lat],
    targetZoom: zoom,
    totalDuration: 5000,
    minZoom: 3
  })
}

defineExpose({ flyTo })
</script>
