import { createClusterIndex } from '~/utils/mapCluster.js'
import {
  MAX_BBOX_DEGREES,
  bboxFetchKey,
  bboxToTileKey,
  tileKey,
  tilesNearCenter,
} from '~/utils/alprViewport.js'

const STORAGE_KEY = 'kestrelos:showAlprLayer'
const MIN_FETCH_ZOOM = 10
const MAX_TILE_FETCHES = 16
const MAX_CACHED_TILES = 64
const TILE_RETENTION = 3
const EMPTY_TILES = Object.freeze({})

const mergeFeatures = lists => Object.values(
  lists.flat().reduce((acc, feature) => {
    const id = feature.id ?? feature.properties?.osmId
    return id == null ? acc : { ...acc, [id]: feature }
  }, {}),
)

const offsets = radius => Array.from({ length: 2 * radius + 1 }, (_, i) => i - radius)

const retentionKeys = bounds => new Set(
  tilesNearCenter(bounds, MAX_TILE_FETCHES).flatMap((tile) => {
    const r0 = Math.floor(tile.south / MAX_BBOX_DEGREES)
    const c0 = Math.floor(tile.west / MAX_BBOX_DEGREES)
    return offsets(TILE_RETENTION).flatMap(dr =>
      offsets(TILE_RETENTION).map(dc => tileKey(r0 + dr, c0 + dc)),
    )
  }),
)

const pruneTileCache = (cache, bounds) => {
  const keep = retentionKeys(bounds)
  const retained = Object.fromEntries(
    Object.entries(cache).filter(([key]) => keep.has(key)),
  )
  const overflow = Object.keys(retained).length - MAX_CACHED_TILES
  if (overflow <= 0) return Object.freeze(retained)

  const cr = Math.floor((bounds.south + bounds.north) / 2 / MAX_BBOX_DEGREES)
  const cc = Math.floor((bounds.west + bounds.east) / 2 / MAX_BBOX_DEGREES)
  const drop = new Set(
    Object.keys(retained)
      .map((key) => {
        const [r, c] = key.split(',').map(Number)
        return { key, dist: Math.hypot(r - cr, c - cc) }
      })
      .sort((a, b) => b.dist - a.dist)
      .slice(0, overflow)
      .map(({ key }) => key),
  )
  return Object.freeze(
    Object.fromEntries(Object.entries(retained).filter(([key]) => !drop.has(key))),
  )
}

const cacheChanged = (before, after) => Object.keys(before).length !== Object.keys(after).length

export function useAlprCameras() {
  const showAlpr = useState('showAlprLayer', () => {
    if (!import.meta.client) return true
    return localStorage.getItem(STORAGE_KEY) !== '0'
  })
  const view = ref(null)
  const tiles = ref(EMPTY_TILES)
  const cluster = createClusterIndex({ radius: 50, maxZoom: 17 })
  const debounceTimer = ref(null)
  const requestId = ref(0)
  const lastFetchKey = ref('')

  const applyTiles = (next) => {
    tiles.value = next
    cluster.load(mergeFeatures(Object.values(next)))
  }

  watch(showAlpr, (enabled) => {
    if (import.meta.client) localStorage.setItem(STORAGE_KEY, enabled ? '1' : '0')
    if (!enabled) {
      applyTiles(EMPTY_TILES)
      view.value = null
      lastFetchKey.value = ''
    }
  })

  const alprMarkers = computed(() => view.value ? cluster.query(view.value) : [])

  const toggleAlpr = () => {
    showAlpr.value = !showAlpr.value
  }

  const onBoundsChange = (bounds) => {
    if (!showAlpr.value || !bounds) return
    view.value = bounds

    const pruned = pruneTileCache(tiles.value, bounds)
    if (cacheChanged(tiles.value, pruned)) applyTiles(pruned)

    if ((bounds.zoom ?? 14) < MIN_FETCH_ZOOM) return

    const key = bboxFetchKey(bounds)
    if (key === lastFetchKey.value) return
    if (debounceTimer.value) clearTimeout(debounceTimer.value)
    debounceTimer.value = setTimeout(() => {
      lastFetchKey.value = key
      fetchViewport(bounds)
    }, 400)
  }

  const fetchViewport = async (bounds) => {
    const id = requestId.value + 1
    requestId.value = id
    const missing = tilesNearCenter(bounds, MAX_TILE_FETCHES)
      .filter(tile => !(bboxToTileKey(tile) in tiles.value))

    try {
      const fetched = missing.length
        ? await Promise.all(missing.map(async (tile) => {
            const params = new URLSearchParams({
              south: String(tile.south),
              west: String(tile.west),
              north: String(tile.north),
              east: String(tile.east),
            })
            const data = await $fetch(`/api/alpr?${params}`).catch(() => null)
            return [bboxToTileKey(tile), data?.features ?? []]
          }))
        : []

      if (id !== requestId.value) return

      const merged = Object.freeze({
        ...tiles.value,
        ...Object.fromEntries(fetched),
      })
      const pruned = pruneTileCache(merged, bounds)
      applyTiles(pruned)
    }
    catch { /* keep last good index */ }
  }

  return Object.freeze({ showAlpr, toggleAlpr, alprMarkers, onBoundsChange })
}
