import { create } from 'zustand'
import type {
  QuerySource,
  RasterSource,
  TableSource,
  TilesetSource,
  Source,
  SourceStore,
  AllExternalSources,
  GeoJSONSource,
  AllSources,
} from './types'
import {
  DEFAULT_CARTO_API_URL,
  DEFAULT_CARTO_API_VERSION,
  DEFAULT_CLIENT_ID,
} from '@carto/ps-services'
import {
  TileFormat,
  WidgetQuerySource,
  WidgetTableSource,
  WidgetTilesetSource,
  WidgetRasterSource,
  addFilter,
  removeFilter,
  type Filters,
  FilterType,
  type SpatialDataType,
} from '@carto/api-client'
import { useMemo } from 'react'
import { WidgetGeoJsonSource } from './widgets/widget-geojson-source'
import { useShallow } from 'zustand/shallow'

/**
 * Zustand store providing global state for all data sources and their filters.
 *
 * @remarks
 * Use selective subscriptions to prevent unnecessary re-renders:
 * `const setSource = useSourceStore((state) => state.setSource)`.
 */
export const useSourceStore = create<SourceStore>()((set, get) => ({
  sources: {},
  filters: {},
  setSource: (id, source) =>
    set((state) => {
      const sources = { ...state.sources }

      const widgetClient = sources[id]?.widgets
      if (widgetClient) {
        widgetClient.destroy()
      }

      const previousSource = sources[id]
      const newSource = {
        apiBaseUrl: DEFAULT_CARTO_API_URL,
        clientId: DEFAULT_CLIENT_ID,
        apiVersion: DEFAULT_CARTO_API_VERSION,
        ...previousSource,
        ...source,
        id,
        widgets: undefined, // Clear widgets before reassigning
      } as Source

      newSource.widgets = getWidgetSource({
        source: newSource,
      })

      sources[id] = newSource

      return { sources }
    }),
  removeSource: (id) =>
    set((state) => {
      const sources = { ...state.sources }

      const widgetClient = sources[id]?.widgets
      if (widgetClient) {
        widgetClient.destroy()
      }

      delete sources[id]
      return { sources }
    }),
  getSource: (id: string) => {
    const source = get().sources[id]
    return source ?? null
  },
  setFilters: (id, newFilters) =>
    set((state) => {
      const currentFilters = state.filters[id] ?? {}

      const filters = newFilters.reduce((acc, filter) => {
        return addFilter(acc, filter)
      }, currentFilters)

      return {
        filters: {
          ...state.filters,
          [id]: { ...filters }, // important: destructuring here to not conservere the reference to the same object
        },
      }
    }),
  removeFilters: (id, column, owner) =>
    set((state) => {
      const currentFilters = state.filters[id] ?? {}
      if (!currentFilters) {
        return state
      }

      let filters = {}
      if (column) {
        filters = removeFilter(currentFilters, { column, owner })
      }

      return {
        filters: {
          ...state.filters,
          [id]: { ...filters }, // important: destructuring here to not conservere the reference to the same object
        },
      }
    }),
  getFilters: (id: string) => {
    const filters = get().filters[id]
    return filters ?? null
  },
  invalidateSourcesCache: (ids, { timeout = 500, maxAge = 10 } = {}) => {
    // Only invalidate cache for external sources (query, table, tileset, raster)
    // GeoJSON sources are local and don't require cache invalidation
    const externalSourceIds = ids.filter((id) => {
      const source = get().sources[id]
      return source && source.type !== 'geojson'
    })

    if (externalSourceIds.length === 0) {
      return
    }

    ids.forEach((id) => {
      get().setSource(id, {
        headers: {
          'Cache-Control': `no-cache, max-age=${maxAge}`,
        },
      } as Partial<Source<AllExternalSources>>)
    })

    setTimeout(() => {
      ids.forEach((id) => {
        const source = get().sources[id] as Source<AllExternalSources>
        if (source) {
          const { headers: _, ...sourceWithoutHeaders } = source
          void _
          get().setSource(id, sourceWithoutHeaders)
        }
      })
    }, timeout)
  },
}))

/**
 * Shortcut to grabbing the source from the sources store object
 * @param id - the id of the source you want to grab
 * @returns source object without filters
 */
export function useSource<T extends AllSources = AllExternalSources>(
  id: string,
) {
  return useSourceStore((state) => state.sources[id] as Source<T>)
}

/**
 * Shortcut to grabbing the source from the sources store object
 * @param ids - the ids of the sources you want to grab
 * @returns source object without filters
 */
export function useSources<T extends AllSources = AllExternalSources>(
  ids: string[],
) {
  return useSourceStore(
    useShallow((state) =>
      ids
        .map((id) => state.sources[id] as Source<T>)
        .filter((source) => !!source),
    ),
  )
}

/**
 * Shortcut to grabbing the filters from the filters store object
 * @param ids - the ids of the sources the filters are applied to
 * @returns filters object as used in the `@carto/api-client` methods calls
 */
export function useFilters(ids: string): Filters | undefined
export function useFilters(ids: string[]): (Filters | undefined)[]
export function useFilters(
  ids: string | string[],
): Filters | undefined | (Filters | undefined)[] {
  return useSourceStore(
    useShallow((state) => {
      if (Array.isArray(ids)) {
        return ids.map((id) => state.filters[id])
      }
      return state.filters[ids]
    }),
  )
}

/**
 * Returns a source together with its filters, pulling from the separate `sources` and `filters` state objects
 * @param id - the id of the source
 * @returns source object as used in `@carto/api-client`
 */
export function useSourceWithFilters<T extends AllSources = AllExternalSources>(
  id: string,
) {
  const source = useSource<T>(id)
  const filters = useFilters(id)

  return useMemo(() => {
    if (!source) {
      return undefined
    }
    return {
      ...source,
      filters: filters ?? {},
    }
  }, [source, filters])
}

/**
 * Extracts filter values for a specific column and filter type from a filters object.
 *
 * @param filters - The filters object to extract values from.
 * @param options - Object with `column` name and `type` (FilterType) to look up.
 * @returns The array of filter values, or an empty array if not found.
 */
export function getFilterValues(
  filters: Filters | undefined,
  { column, type }: { column: string; type: FilterType },
) {
  return filters?.[column]?.[type]?.values ?? []
}

function getSpatialDataFallback(source: Source<AllExternalSources>) {
  const segments = source.type.split('+')
  const lastSegment = segments.at(-1)
  const baseTypes = ['query', 'table', 'tileset', 'raster', 'geojson', 'geo']
  const fallback = baseTypes.includes(lastSegment!) ? undefined : lastSegment
  return {
    spatialDataType: (source.spatialDataType ??
      fallback ??
      'geo') as SpatialDataType,
    spatialDataColumn: source.spatialDataColumn ?? fallback ?? 'geom',
  }
}

function getWidgetSource({ source }: { source: Source }) {
  const { spatialDataType, spatialDataColumn } = getSpatialDataFallback(
    source as Source<AllExternalSources>,
  )

  const options = {
    query: () => {
      const newSource = source as Source & QuerySource
      return new WidgetQuerySource({
        ...newSource,
        accessToken: '',
        sqlQuery: newSource.data as string,
        spatialDataType,
        spatialDataColumn,
      })
    },
    table: () => {
      const newSource = source as Source & TableSource
      return new WidgetTableSource({
        ...newSource,
        accessToken: '',
        tableName: newSource.data as string,
        spatialDataType,
        spatialDataColumn,
      })
    },
    tileset: () => {
      const newSource = source as Source & TilesetSource

      if (!newSource.spatialDataBounds) {
        return null
      }

      return new WidgetTilesetSource({
        ...newSource,
        accessToken: '',
        tableName: newSource.data as string,
        tileFormat: newSource.tileFormat ?? TileFormat.BINARY,
        spatialDataType,
        spatialDataColumn,
        spatialDataBounds: newSource.spatialDataBounds,
      })
    },
    raster: () => {
      const newSource = source as Source & RasterSource

      if (!newSource.rasterMetadata || !newSource.spatialDataBounds) {
        return null
      }

      return new WidgetRasterSource({
        ...newSource,
        accessToken: '',
        tableName: newSource.data as string,
        spatialDataType: 'quadbin',
        spatialDataColumn: 'quadbin',
        rasterMetadata: newSource.rasterMetadata,
        spatialDataBounds: newSource.spatialDataBounds,
      })
    },
    geojson: () => {
      const newSource = source as Source<GeoJSONSource>

      return new WidgetGeoJsonSource({
        ...newSource,
        connectionName: '',
        accessToken: '',
        data: newSource.data,
      })
    },
  }

  const key = source.type.split('+')[0] as keyof typeof options
  const option = options[key]

  return option?.() as Source['widgets']
}
