import { useLayoutEffect, useState, type ReactNode } from 'react'
import {
  WidgetContext,
  createWidgetStore,
  hasWidgetStore,
  getWidgetStore,
  registerWidgetStore,
  setWidgetStoreEntry,
  unregisterWidgetStore,
} from '../stores'
import type { WidgetStoreApi } from '../stores'

export interface ProviderProps {
  id: string
  data: unknown
  isLoading?: boolean
  isFetching?: boolean
  error?: unknown
  formatter?: (value: number) => string
  labelFormatter?: (value: string | number) => string | number
  /**
   * When `true`, the per-widget store survives unmount so transformStates,
   * search text, fullscreen state, etc. are preserved across remounts.
   * Consumers using keepAlive must call `deleteWidgetStore(id)` themselves
   * when the widget is permanently removed — there is no automatic GC.
   */
  keepAlive?: boolean
  children: ReactNode
}

/**
 * Renderer-agnostic widget shell. Creates a per-widget Zustand store
 * keyed by `id`, syncs the consumer props (`data`, `isLoading`, `error`,
 * `formatter`, …) onto it, and exposes the `id` via `WidgetContext` so
 * descendants can resolve their store with `useWidgetId()`.
 *
 * The Provider deliberately doesn't know about ECharts — there's no
 * `optionFactory` prop here. ECharts-backed widgets pass their factory
 * to `<Widget.Echart optionFactory={…}>`, which owns the structural
 * seed + the data-fusion render step entirely. Non-Echart widgets
 * (Formula, Spread, Range, Table, Category, Markdown) just don't pass
 * anything ECharts-shaped.
 */
export function Provider({
  id,
  data,
  isLoading,
  isFetching,
  error,
  formatter,
  labelFormatter,
  keepAlive = false,
  children,
}: ProviderProps) {
  // Lazy init — reuse an existing keepAlive store, otherwise create one and
  // synchronously place it in the registry so descendants can call
  // useWidget(id, ...) on first render. Refcounting + dup-id detection happen
  // in the mount layout-effect below.
  const [store] = useState<WidgetStoreApi>(() => {
    if (hasWidgetStore(id)) return getWidgetStore(id)
    const created = createWidgetStore(id, {
      data,
      isLoading,
      isFetching,
      error,
      formatter,
      labelFormatter,
    })
    setWidgetStoreEntry(id, created)
    return created
  })

  // Registration: refcount the mount for dup-id detection; unregister on
  // unmount or when the id/store/keepAlive triple changes.
  useLayoutEffect(() => {
    registerWidgetStore(id, store)
    return () => {
      unregisterWidgetStore(id, { keepAlive })
    }
  }, [id, store, keepAlive])

  // Per-prop sync. `useLayoutEffect` so kept-alive remounts apply incoming
  // props before paint (no flash of stale data when reusing a store).
  // The pipeline middleware bypasses unchanged fields, so unchanged props
  // are a no-op even though setState is called every effect run.
  useLayoutEffect(() => {
    store.setState({ rawData: data })
  }, [store, data])

  useLayoutEffect(() => {
    store.setState({
      isLoading: isLoading ?? false,
      isFetching: isFetching ?? false,
      error,
    })
  }, [store, isLoading, isFetching, error])

  // Sync the consumer's formatter prop to BOTH `rawFormatter` (kept canonical
  // for actions that need to restore the absolute view) and `formatter` (the
  // active value). Actions like RelativeData re-apply their override after
  // `rawFormatter` changes.
  useLayoutEffect(() => {
    store.setState({
      rawFormatter: formatter,
      formatter,
      labelFormatter,
    })
  }, [store, formatter, labelFormatter])

  return <WidgetContext.Provider value={id}>{children}</WidgetContext.Provider>
}
