import type maplibregl from 'maplibre-gl'
import { PIN_COLOR, PIN_GREEN, PIN_RED } from './colors.ts'
import {
  type Bookmark,
  bookmarkDisplayName,
  type BookmarkList,
  type SharedBookmark,
} from '../store/schema.ts'

/**
 * A pin as the GL source sees it. Only the three underscored properties are
 * ever read — by the layer paint expressions and the click handlers — so the
 * rest of a bookmark is deliberately left out rather than handed to the
 * renderer on every change.
 */
export type PinFeature = {
  type: 'Feature'
  geometry: { type: 'Point'; coordinates: [number, number] }
  properties: {
    _id: string
    _displayName: string
    _color: string | null
  }
}

export type BookmarkFeatureCollection = {
  type: 'FeatureCollection'
  features: PinFeature[]
}

function pin(
  id: string,
  coordinates: [number, number],
  displayName: string,
  color: string | null,
): PinFeature {
  return {
    type: 'Feature',
    geometry: { type: 'Point', coordinates },
    properties: { _id: id, _displayName: displayName, _color: color },
  }
}

/**
 * `activeListId` is the list the map is currently scoped to, if any. A
 * bookmark can be in several lists but a pin has one colour — inside a list
 * view it wears that list's colour, not whichever list happens to be first.
 */
export function buildBookmarksGeoJSON(
  bookmarks: Iterable<Bookmark>,
  lists: Iterable<BookmarkList>,
  activeListId?: string | null,
): BookmarkFeatureCollection {
  const listColors = new Map(
    [...lists].map((c) => [c.id, c.color ?? null]),
  )
  const colorFor = (b: Bookmark): string | null => {
    const listId = activeListId && b.categories.includes(activeListId)
      ? activeListId
      : b.categories[0]
    return listId ? (listColors.get(listId) ?? null) : null
  }
  return {
    type: 'FeatureCollection',
    features: [...bookmarks].map((b) =>
      pin(b.id, b.geometry.coordinates, bookmarkDisplayName(b), colorFor(b))
    ),
  }
}

/**
 * The same pins from the public projection, which carries no bookmark ids and
 * no list membership — the index stands in as a click key, and one colour
 * (the owning list's, when there is one) applies to every pin.
 */
export function buildSharedPinsGeoJSON(
  points: readonly SharedBookmark[],
  color: string | null,
): BookmarkFeatureCollection {
  return {
    type: 'FeatureCollection',
    features: points.map((b, i) =>
      pin(String(i), b.coordinates, b.name, color)
    ),
  }
}

export const BOOKMARK_LAYER_IDS = [
  'bookmarks-clusters',
  'bookmarks-cluster-count-label',
  'bookmarks',
  'bookmarks-hit',
] as const

export type BookmarkLayerHandlers = {
  onClusterClick: (
    clusterId: number,
    coordinates: [number, number],
    pointCount: number,
  ) => void
  onBookmarkClick: (
    coordinates: [number, number],
    id: string | undefined,
    displayName: string,
  ) => void
}

export function setupBookmarkLayers(
  map: maplibregl.Map,
  data: BookmarkFeatureCollection,
  handlers: BookmarkLayerHandlers,
): void {
  map.addSource('bookmarks', {
    type: 'geojson',
    data,
    cluster: true,
    clusterMaxZoom: 16,
    clusterRadius: 50,
    clusterMinPoints: 5,
  })

  map.addLayer({
    id: 'bookmarks-clusters',
    type: 'circle',
    source: 'bookmarks',
    filter: ['>=', ['get', 'point_count'], 5],
    minzoom: 0,
    maxzoom: 24,
    paint: {
      'circle-color': [
        'step',
        ['get', 'point_count'],
        PIN_COLOR,
        10,
        PIN_GREEN,
        30,
        PIN_RED,
      ],
      'circle-radius': ['step', ['get', 'point_count'], 18, 10, 22, 30, 24],
      'circle-stroke-width': 2,
      'circle-stroke-color': '#fff',
    },
  })
  map.addLayer({
    id: 'bookmarks-cluster-count-label',
    type: 'symbol',
    source: 'bookmarks',
    filter: ['>=', ['get', 'point_count'], 5],
    minzoom: 0,
    maxzoom: 24,
    layout: {
      'text-field': '{point_count_abbreviated}',
      'text-font': ['Noto Sans Regular'],
      'text-size': 12,
    },
    paint: {
      'text-color': '#000',
    },
  })
  map.addLayer({
    id: 'bookmarks',
    type: 'circle',
    source: 'bookmarks',
    filter: ['!', ['has', 'point_count']],
    minzoom: 0,
    maxzoom: 24,
    paint: {
      'circle-radius': 8,
      'circle-color': ['coalesce', ['get', '_color'], PIN_COLOR],
      'circle-stroke-width': 2,
      'circle-stroke-color': '#fff',
    },
  })
  map.addLayer({
    id: 'bookmarks-hit',
    type: 'circle',
    source: 'bookmarks',
    filter: ['!', ['has', 'point_count']],
    minzoom: 0,
    maxzoom: 24,
    paint: {
      'circle-radius': 20,
      'circle-color': 'transparent',
    },
  })

  map.on('click', 'bookmarks-clusters', (e) => {
    if (!e.features?.length) return
    const feature = e.features[0]
    const clusterId = feature.properties?.cluster_id
    if (clusterId === undefined) return
    const pointCount = feature.properties?.point_count ?? 0
    const coords = (feature.geometry as unknown as {
      coordinates: [number, number]
    }).coordinates
    handlers.onClusterClick(clusterId, coords, pointCount)
  })
  map.on('click', 'bookmarks-hit', (e) => {
    if (!e.features?.length) return
    const feature = e.features[0]
    const coords = (feature.geometry as unknown as {
      coordinates: [number, number]
    }).coordinates
    const id = feature.properties?._id as string | undefined
    const displayName = feature.properties?._displayName as string
    handlers.onBookmarkClick(coords, id, displayName)
  })

  for (const layerId of ['bookmarks-clusters', 'bookmarks-hit']) {
    map.on('mouseenter', layerId, () => {
      map.getCanvas().style.cursor = 'pointer'
    })
    map.on('mouseleave', layerId, () => {
      map.getCanvas().style.cursor = ''
    })
  }
}

export function ensureBookmarkLayersOnTop(map: maplibregl.Map): void {
  // Only the layers that actually exist — a sync can run before (or without)
  // setupBookmarkLayers, and moveLayer throws on a missing id.
  const ids = BOOKMARK_LAYER_IDS.filter((id) => map.getLayer(id))
  if (!ids.length) return
  const allLayers = map.getStyle().layers
  if (!allLayers?.length) return
  const topIds = allLayers.slice(-ids.length).map((l) => l.id)
  if (ids.every((id, i) => topIds[i] === id)) return
  ids.forEach((id) => map.moveLayer(id))
}
