import { useLayerStore } from './providers/layers/store'

/**
 * Minimal structural view of the react-ui legend store that the bridge needs.
 *
 * `StoreApi<LegendState>` from `@carto/ps-react-ui/legend/stores` structurally
 * satisfies this interface, so react-maps takes on **no** dependency on
 * `@carto/ps-react-ui` (not even a type-only import) — the consumer, who
 * already has react-ui, resolves the store with `getLegendStore(id)` and hands
 * it in.
 *
 * @experimental This API is new and may change in a future release.
 */
export interface LegendSyncStore {
  getState: () => {
    layers: Record<
      string,
      {
        visible: boolean
        opacity: number
        sections?: { id: string; active: string }[]
      }
    >
    setVisibility: (id: string, visible: boolean) => void
    setOpacity: (id: string, opacity: number) => void
  }
  subscribe: (listener: () => void) => () => void
}

/** @experimental This API is new and may change in a future release. */
export interface SyncLegendLayersOptions {
  /**
   * Fired (legend→map) when a config-select section's active value changes, so
   * the consumer can drive a layer property — e.g.
   * `({ layerId, value }) => setLayer(layerId, { props: { … } })`. There is no
   * map→legend direction for sections (the map has no section concept).
   */
  onSectionChange?: (change: {
    layerId: string
    sectionId: string
    value: string
  }) => void
}

interface FieldSnapshot {
  visible: boolean
  opacity: number
}

type Direction = 'map' | 'legend' | 'none'

/**
 * Decide which store wins for a single field given the last reconciled value.
 *
 * - First encounter (`!hasSnapshot`): the **map is authoritative** — adopt its
 *   value into the legend (seeds visibility/opacity from the live map, incl. a
 *   `keepAlive` panel remount where the ref is gone).
 * - Live: whichever side changed since the last reconcile is the writer.
 * - True conflict (both changed): the **legend wins** (deliberate panel action).
 */
function resolve<T>(
  legendVal: T,
  mapVal: T,
  snapVal: T | undefined,
  hasSnapshot: boolean,
): { chosen: T; writeTo: Direction } {
  if (!hasSnapshot) return { chosen: mapVal, writeTo: 'legend' }
  const legendChanged = legendVal !== snapVal
  const mapChanged = mapVal !== snapVal
  if (legendChanged) return { chosen: legendVal, writeTo: 'map' }
  if (mapChanged) return { chosen: mapVal, writeTo: 'legend' }
  return { chosen: legendVal, writeTo: 'none' }
}

/**
 * Bidirectionally syncs layer `visible`/`opacity` between the react-maps layer
 * store and an externally-supplied react-ui legend store, and surfaces
 * config-select section changes (legend→map) through `onSectionChange`.
 *
 * Framework-agnostic: the legend store is passed in (see {@link LegendSyncStore})
 * and the layer store is the react-maps singleton. It subscribes both stores,
 * seeds once immediately, and returns an **unsubscribe teardown**. `options` is
 * captured at call time — to change `onSectionChange`, tear down and
 * call again (the standard subscription contract). In React, call it from an
 * effect and return its result:
 *
 * @example
 * ```tsx
 * function LegendSync() {
 *   const legendStore = getLegendStore(useLegendId()) // react-ui
 *   const setLayer = useLayerStore((s) => s.setLayer)
 *   useEffect(
 *     () =>
 *       syncLegendLayers(legendStore, {
 *         onSectionChange: ({ layerId, value }) =>
 *           setLayer(layerId, { props: {  } }),
 *       }),
 *     [legendStore, setLayer],
 *   )
 *   return null
 * }
 * ```
 *
 * @param legendStore - The panel's react-ui legend store (`getLegendStore(id)`).
 * @param options - Optional id filter and `onSectionChange` handler.
 * @returns An unsubscribe function that detaches both store subscriptions.
 *
 * @experimental This API is new and may change in a future release.
 */
export function syncLegendLayers(
  legendStore: LegendSyncStore,
  options: SyncLegendLayersOptions = {},
): () => void {
  const lastFields = new Map<string, FieldSnapshot>()
  const lastSections = new Map<string, Map<string, string>>()
  // Re-entrancy guard: our own writes notify the stores synchronously, which
  // would recurse into reconcile before snapshots are recorded.
  let running = false

  const reconcile = (): void => {
    if (running) return
    running = true

    const legendLayers = legendStore.getState().layers
    const mapLayers = useLayerStore.getState().layers
    const ids = Object.keys(legendLayers)
    const pendingSections: {
      layerId: string
      sectionId: string
      value: string
    }[] = []

    for (const id of ids) {
      const legend = legendLayers[id]
      const map = mapLayers[id]
      if (!legend || !map) continue

      const mapVisible = map.props.visible ?? true
      const mapOpacity = map.props.opacity ?? 1
      const snap = lastFields.get(id)
      const hasSnap = snap !== undefined

      const vis = resolve(legend.visible, mapVisible, snap?.visible, hasSnap)
      if (vis.writeTo === 'map') {
        if (map.props.visible !== vis.chosen)
          useLayerStore
            .getState()
            .setLayer(id, { props: { visible: vis.chosen } })
      } else if (vis.writeTo === 'legend') {
        // setVisibility is idempotent in the legend store.
        legendStore.getState().setVisibility(id, vis.chosen)
      }

      const op = resolve(legend.opacity, mapOpacity, snap?.opacity, hasSnap)
      if (op.writeTo === 'map') {
        if (map.props.opacity !== op.chosen)
          useLayerStore
            .getState()
            .setLayer(id, { props: { opacity: op.chosen } })
      } else if (op.writeTo === 'legend') {
        // setOpacity always writes — guard to avoid a redundant update.
        if (legend.opacity !== op.chosen)
          legendStore.getState().setOpacity(id, op.chosen)
      }

      lastFields.set(id, { visible: vis.chosen, opacity: op.chosen })

      // Sections: legend→map only. Seed silently on first encounter; collect
      // changes now and fire them after the guard is released (below).
      const prevSections = lastSections.get(id)
      const nextSections = new Map<string, string>()
      for (const section of legend.sections ?? []) {
        nextSections.set(section.id, section.active)
        const prev = prevSections?.get(section.id)
        if (prev !== undefined && prev !== section.active) {
          pendingSections.push({
            layerId: id,
            sectionId: section.id,
            value: section.active,
          })
        }
      }
      lastSections.set(id, nextSections)
    }

    running = false

    // Fire section callbacks outside the re-entrancy guard: the consumer
    // typically calls setLayer here (re-entering reconcile, now allowed and a
    // no-op), and a throwing handler must not strand `running`.
    for (const change of pendingSections) {
      options.onSectionChange?.(change)
    }
  }

  // Seed first-encounter ids, then keep both stores reconciled.
  reconcile()
  const unsubscribeLegend = legendStore.subscribe(reconcile)
  const unsubscribeLayers = useLayerStore.subscribe(reconcile)
  return () => {
    unsubscribeLegend()
    unsubscribeLayers()
  }
}
