import { bboxFetchKey } from '~/utils/alprViewport.js'

const DEBOUNCE_MS = 300
const EMPTY_ENTITIES = Object.freeze({})

const expandBounds = (bounds, factor = 0.25) => {
  const latPad = (bounds.north - bounds.south) * factor
  const lngPad = (bounds.east - bounds.west) * factor
  return {
    south: Math.max(-90, bounds.south - latPad),
    north: Math.min(90, bounds.north + latPad),
    west: bounds.west - lngPad,
    east: bounds.east + lngPad,
  }
}

const entitiesFromList = list => Object.freeze(
  Object.fromEntries(
    (list ?? []).filter(entity => entity?.id).map(entity => [entity.id, entity]),
  ),
)

export function useCotStream(boundsRef, layerQueryRef) {
  const entities = ref(EMPTY_ENTITIES)
  const cotEntities = computed(() => Object.freeze(Object.values(entities.value)))
  const eventSource = ref(null)
  const debounceTimer = ref(null)
  const subscribedKey = ref('')
  const streamBounds = ref(null)

  const setEntities = (record) => {
    entities.value = Object.freeze(record)
  }

  const parseEvent = (e, fn) => {
    try {
      fn(JSON.parse(e.data))
    }
    catch { /* ignore */ }
  }

  const closeStream = () => {
    eventSource.value?.close()
    eventSource.value = null
  }

  const connect = () => {
    if (typeof window === 'undefined' || typeof EventSource === 'undefined') return
    const bounds = streamBounds.value ?? boundsRef.value
    if (!bounds) return

    closeStream()
    const q = new URLSearchParams({
      bbox: `${bounds.west},${bounds.south},${bounds.east},${bounds.north}`,
      layers: unref(layerQueryRef) || 'air,surface,ground',
    })
    const es = new EventSource(`/api/cot/stream?${q}`)
    eventSource.value = es

    es.addEventListener('snapshot', e => parseEvent(e, ({ entities: list }) => {
      setEntities(entitiesFromList(list))
    }))
    es.addEventListener('update', e => parseEvent(e, ({ entity }) => {
      if (!entity?.id) return
      setEntities({ ...entities.value, [entity.id]: entity })
    }))
    es.addEventListener('remove', e => parseEvent(e, ({ id }) => {
      if (!id) return
      setEntities(Object.fromEntries(
        Object.entries(entities.value).filter(([key]) => key !== String(id)),
      ))
    }))
    es.onerror = () => {
      closeStream()
      scheduleConnect()
    }
  }

  const scheduleConnect = () => {
    if (debounceTimer.value) clearTimeout(debounceTimer.value)
    debounceTimer.value = setTimeout(() => {
      debounceTimer.value = null
      if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return
      connect()
    }, DEBOUNCE_MS)
  }

  const maybeReconnect = (bounds) => {
    if (!bounds) return
    const key = bboxFetchKey(bounds)
    if (key === subscribedKey.value) return
    subscribedKey.value = key
    streamBounds.value = expandBounds(bounds)
    scheduleConnect()
  }

  watch(boundsRef, maybeReconnect, { deep: true })
  watch(layerQueryRef, () => {
    subscribedKey.value = ''
    scheduleConnect()
  })

  onMounted(() => {
    if (typeof document === 'undefined') return
    document.addEventListener('visibilitychange', () => {
      document.visibilityState === 'visible' ? scheduleConnect() : closeStream()
    })
    if (document.visibilityState === 'visible') maybeReconnect(boundsRef.value)
  })

  onBeforeUnmount(() => {
    if (debounceTimer.value) clearTimeout(debounceTimer.value)
    closeStream()
  })

  return Object.freeze({ cotEntities })
}
