import { useMemo } from 'react'
import { useMapStore } from './providers/map/provider'
import type { MapComponentProps } from './types'
import {
  createViewportSpatialFilter,
  type SpatialFilter,
} from '@carto/api-client'
import { useLassoToolsState } from './editable/lasso-tools/provider/provider'
import { getMaskValues } from './editable/lasso-tools/util'
import { multiPolygon } from '@turf/turf'
import type { MultiPolygon, Polygon, Position } from 'geojson'

/**
 * Hook that provides access to the combined spatial filter state for a map, automatically
 * switching between lasso tools, static, and viewport spatial filters based on priority.
 *
 * @returns An object containing the active `spatialFilter` and individual filter sources:
 * `lassoToolsSpatialFilter`, `staticSpatialFilter`, and `viewportSpatialFilter`.
 *
 * @remarks
 * Priority order: lasso tools filter > static filter > viewport filter.
 * If you only need one type of filter, prefer {@link useViewportSpatialFilter} or
 * {@link useLassoToolsSpatialFilter} to avoid unnecessary re-renders.
 *
 * @example
 * ```tsx
 * const { spatialFilter, lassoToolsSpatialFilter, viewportSpatialFilter } = useSpatialFilter({ mapId: 'main-map' })
 * ```
 */
export function useSpatialFilter({
  mapId,
}: {
  mapId: MapComponentProps['id']
}) {
  const viewportSpatialFilter = useViewportSpatialFilter({ mapId })

  const lassoToolsSpatialFilter = useLassoToolsSpatialFilter({
    mapId,
  }) as unknown as SpatialFilter | undefined

  const staticSpatialFilter = useStaticSpatialFilter({ mapId })

  return {
    spatialFilter:
      lassoToolsSpatialFilter ?? staticSpatialFilter ?? viewportSpatialFilter,
    staticSpatialFilter,
    lassoToolsSpatialFilter,
    viewportSpatialFilter,
  }
}

/**
 * Hook that provides access to the current map viewport as a spatial filter polygon.
 * Updates automatically when the map viewport changes.
 *
 * @returns The current viewport as a `Polygon` spatial filter, or `undefined` if the viewport is not available.
 *
 * @remarks
 * Use this hook instead of {@link useSpatialFilter} to avoid unnecessary re-renders when
 * lasso tool or static spatial filter changes.
 *
 * @example
 * ```tsx
 * const viewportSpatialFilter = useViewportSpatialFilter({ mapId: 'main-map' })
 * ```
 */
export function useViewportSpatialFilter({
  mapId,
}: {
  mapId: MapComponentProps['id']
}) {
  const viewport = useMapStore((state) => state.viewports[mapId])

  return useMemo(() => {
    if (!viewport) return
    return createViewportSpatialFilter(viewport.getBounds())
  }, [viewport])
}

/**
 * Hook that provides access to the spatial filter polygon drawn by the lasso tool.
 *
 * @returns A `MultiPolygon` geometry from visible lasso tool values, or `undefined` if no lasso selection exists.
 *
 * @remarks
 * Use this hook instead of {@link useSpatialFilter} to avoid unnecessary re-renders when
 * the viewport or static spatial filter changes.
 *
 * @example
 * ```tsx
 * const lassoToolsSpatialFilter = useLassoToolsSpatialFilter({ mapId: 'main-map' })
 * ```
 */
export function useLassoToolsSpatialFilter({
  mapId,
}: {
  mapId: MapComponentProps['id']
}) {
  const _values = useLassoToolsState((state) => state.data[mapId]?.values)

  return useMemo(() => {
    const values = getMaskValues({ values: _values }).filter(
      (value) => value.properties.visible,
    )

    if (!values?.length) return

    const coordinates: Position[][][] = values.flatMap((value) => {
      const geom = value.geometry as Polygon | MultiPolygon
      return geom.type === 'Polygon' ? [geom.coordinates] : geom.coordinates
    })

    return multiPolygon(coordinates).geometry
  }, [_values])
}

/**
 * Hook that provides access to the static spatial filter set programmatically by the user.
 *
 * @returns A `MultiPolygon` geometry from static spatial filters, or `undefined` if none are set.
 *
 * @remarks
 * Use this hook to avoid unnecessary re-renders when viewport or lasso tool changes.
 */
export function useStaticSpatialFilter({
  mapId,
}: {
  mapId: MapComponentProps['id']
}): SpatialFilter | undefined {
  const spatialFilter = useMapStore((state) => state.spatialFilter[mapId])

  return useMemo(() => {
    if (!spatialFilter || !Object.keys(spatialFilter).length) return

    // Flatten the array to match the expected Position[][][] type
    const coordinates: Position[][][] = Object.values(spatialFilter)
      .filter((value) => value !== null && value !== undefined)
      .flatMap((geom) => {
        // If it's a Polygon, wrap its coordinates in an array to make it compatible with MultiPolygon format
        if (geom.type === 'Polygon') {
          return [geom.coordinates]
        }

        // Otherwise, validate or transform `geom` to ensure it matches the expected format
        return geom.coordinates
      })

    return multiPolygon(coordinates).geometry
  }, [spatialFilter])
}

/**
 * Type for grouped spatial filter result
 */
export type GroupedSpatialFilterResult = SpatialFilter[]

/**
 * Hook that provides grouped spatial filter results, returning arrays of spatial filters
 * instead of single merged filters. Follows the same priority as {@link useSpatialFilter}:
 * lasso tools > static > viewport.
 *
 * @returns An object containing grouped `spatialFilter`, `staticSpatialFilter`,
 * `lassoToolsSpatialFilter`, and `viewportSpatialFilter` arrays.
 */
export function useGroupedSpatialFilter({
  mapId,
}: {
  mapId: MapComponentProps['id']
}) {
  const _viewportSpatialFilter = useViewportSpatialFilter({ mapId })
  const viewportSpatialFilter = useMemo(
    (): GroupedSpatialFilterResult =>
      _viewportSpatialFilter ? [_viewportSpatialFilter] : [],
    [_viewportSpatialFilter],
  )

  const lassoToolsSpatialFilter = useGroupedLassoToolsSpatialFilters({
    mapId,
  })

  const staticSpatialFilter = useGroupedStaticSpatialFilters({ mapId })

  return {
    spatialFilter: lassoToolsSpatialFilter.length
      ? lassoToolsSpatialFilter
      : staticSpatialFilter.length
        ? staticSpatialFilter
        : viewportSpatialFilter,
    staticSpatialFilter,
    lassoToolsSpatialFilter,
    viewportSpatialFilter,
  }
}

// Per-mapId cache for group-specific multipolygon geometries
// Using per-mapId structure prevents state pollution between different map instances
const groupMultiPolygonCache = new Map<string, Map<string, SpatialFilter>>()

/**
 * Hook to create multipolygon geometries for each group of lasso tool values.
 * Groups are identified by the groupId property in LassoToolsData.
 * Uses per-group memoization to only recompute multipolygons when a specific group changes.
 * This optimizes performance by avoiding expensive multiPolygon() calls for unchanged groups.
 *
 * @param mapId - The map component ID
 * @param groupIds - Optional array of group IDs to filter results. If empty or undefined, returns all groups
 * @returns Array of objects with groupId and spatialFilter, ordered by groupId
 */
export function useGroupedLassoToolsSpatialFilters({
  mapId,
  groupIds,
}: {
  mapId: MapComponentProps['id']
  groupIds?: number[]
}): GroupedSpatialFilterResult {
  const _values = useLassoToolsState((state) => state.data[mapId]?.values)

  // First memo: group the values by groupId
  const groups = useMemo(() => {
    const values = getMaskValues({ values: _values }).filter(
      (value) => value.properties.visible,
    )

    if (!values?.length) return {}

    return values.reduce(
      (acc, value) => {
        const groupId = value.groupId ?? 0
        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
        if (!acc[groupId]) {
          acc[groupId] = []
        }
        acc[groupId].push(value)
        return acc
      },
      {} as Record<number, typeof values>,
    )
  }, [_values])

  // Create individual memoized multipolygons for each group
  // Each group is memoized separately based on a stable hash of its values
  return useMemo(() => {
    const result: GroupedSpatialFilterResult = []

    // Get or create the per-mapId cache
    let mapCache = groupMultiPolygonCache.get(mapId)
    if (!mapCache) {
      mapCache = new Map<string, SpatialFilter>()
      groupMultiPolygonCache.set(mapId, mapCache)
    }

    // Get all group IDs from the actual groups (not filtered by groupIds parameter)
    const allGroupIds = Object.keys(groups).map(Number)

    // Sort group IDs numerically to ensure consistent ordering
    let sortedGroupIds = [...allGroupIds].sort((a, b) => a - b)

    // Filter by groupIds if specified (for result building only)
    if (groupIds && groupIds.length > 0) {
      sortedGroupIds = sortedGroupIds.filter((id) => groupIds.includes(id))
    }

    for (const groupId of sortedGroupIds) {
      const groupValues = groups[groupId]
      if (groupValues && groupValues.length > 0) {
        // Create a stable key based on the group's value IDs and visibility
        const groupKey = groupValues
          .map((v) => `${v.id}:${v.properties.visible ? '1' : '0'}`)
          .join('|')

        // Store in a cache map to enable per-group memoization
        const cacheKey = `${groupId}:${groupKey}`

        // Use the cache or compute
        if (!mapCache.has(cacheKey)) {
          const coordinates: Position[][][] = groupValues.flatMap((value) => {
            const geom = value.geometry as Polygon | MultiPolygon
            return geom.type === 'Polygon'
              ? [geom.coordinates]
              : geom.coordinates
          })

          mapCache.set(cacheKey, multiPolygon(coordinates).geometry)
        }

        result.push(mapCache.get(cacheKey)!)
      }
    }

    // Clean up old cache entries that are no longer in use
    // Use allGroupIds (not sortedGroupIds) to ensure all cache entries are considered for cleanup
    const activeKeys = new Set(
      allGroupIds
        .filter((groupId) => {
          const values = groups[groupId]
          return values && values.length > 0
        })
        .map((groupId) => {
          const values = groups[groupId]!
          const groupKey = values
            .map((v) => `${v.id}:${v.properties.visible ? '1' : '0'}`)
            .join('|')
          return `${groupId}:${groupKey}`
        }),
    )

    for (const key of mapCache.keys()) {
      if (!activeKeys.has(key)) {
        mapCache.delete(key)
      }
    }

    return result
  }, [mapId, groups, groupIds])
}

// Per-mapId cache for static spatial filter multipolygon geometries
// Using per-mapId structure prevents state pollution between different map instances
const staticMultiPolygonCache = new Map<string, Map<string, SpatialFilter>>()

/**
 * Hook to create multipolygon geometries for each group of static spatial filters.
 * Groups are identified by the groupId property in SpatialFilterWithGroup.
 * Uses per-group memoization to only recompute multipolygons when a specific group changes.
 * This optimizes performance by avoiding expensive multiPolygon() calls for unchanged groups.
 *
 * @param mapId - The map component ID
 * @param groupIds - Optional array of group IDs to filter results. If empty or undefined, returns all groups
 * @returns Array of objects with groupId and spatialFilter, ordered by groupId
 */
export function useGroupedStaticSpatialFilters({
  mapId,
  groupIds,
}: {
  mapId: MapComponentProps['id']
  groupIds?: number[]
}): GroupedSpatialFilterResult {
  const spatialFilter = useMapStore((state) => state.spatialFilter[mapId])

  // First memo: group the spatial filters by groupId
  const groups = useMemo(() => {
    if (!spatialFilter || !Object.keys(spatialFilter).length) return {}

    return Object.values(spatialFilter)
      .filter((value) => value !== null && value !== undefined)
      .reduce(
        (acc, geom) => {
          const groupId = geom.groupId ?? 0
          // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
          if (!acc[groupId]) {
            acc[groupId] = []
          }
          acc[groupId].push(geom)
          return acc
        },
        {} as Record<number, NonNullable<(typeof spatialFilter)[string]>[]>,
      )
  }, [spatialFilter])

  // Create individual memoized multipolygons for each group
  return useMemo(() => {
    const result: GroupedSpatialFilterResult = []

    // Get or create the per-mapId cache
    let mapCache = staticMultiPolygonCache.get(mapId)
    if (!mapCache) {
      mapCache = new Map<string, SpatialFilter>()
      staticMultiPolygonCache.set(mapId, mapCache)
    }

    // Get all group IDs from the actual groups (not filtered by groupIds parameter)
    const allGroupIds = Object.keys(groups).map(Number)

    // Sort group IDs numerically to ensure consistent ordering
    let sortedGroupIds = [...allGroupIds].sort((a, b) => a - b)

    // Filter by groupIds if specified (for result building only)
    if (groupIds && groupIds.length > 0) {
      sortedGroupIds = sortedGroupIds.filter((id) => groupIds.includes(id))
    }

    for (const groupId of sortedGroupIds) {
      const groupGeometries = groups[groupId]
      if (groupGeometries && groupGeometries.length > 0) {
        // Create a stable key based on the group's geometry types and coordinate lengths
        const groupKey = groupGeometries
          .map(
            (geom) => `${geom.type}:${JSON.stringify(geom.coordinates).length}`,
          )
          .join('|')

        const cacheKey = `static:${groupId}:${groupKey}`

        // Use the cache or compute
        if (!mapCache.has(cacheKey)) {
          const coordinates: Position[][][] = groupGeometries.flatMap(
            (geom) => {
              // If it's a Polygon, wrap its coordinates in an array to make it compatible with MultiPolygon format
              if (geom.type === 'Polygon') {
                return [geom.coordinates]
              }
              // Otherwise, use the coordinates as-is for MultiPolygon
              return geom.coordinates
            },
          )

          mapCache.set(cacheKey, multiPolygon(coordinates).geometry)
        }

        result.push(mapCache.get(cacheKey)!)
      }
    }

    // Clean up old cache entries that are no longer in use
    // Use allGroupIds (not sortedGroupIds) to ensure all cache entries are considered for cleanup
    const activeKeys = new Set(
      allGroupIds
        .filter((groupId) => {
          const geometries = groups[groupId]
          return geometries && geometries.length > 0
        })
        .map((groupId) => {
          const geometries = groups[groupId]!
          const groupKey = geometries
            .map(
              (geom) =>
                `${geom.type}:${JSON.stringify(geom.coordinates).length}`,
            )
            .join('|')
          return `static:${groupId}:${groupKey}`
        }),
    )

    for (const key of mapCache.keys()) {
      if (!activeKeys.has(key)) {
        mapCache.delete(key)
      }
    }

    return result
  }, [mapId, groups, groupIds])
}
