import { useCallback, useMemo } from 'react'
import type * as echarts from 'echarts'
import {
  applyTransforms,
  setEchartInstance,
  useWidgetId,
  useWidgetShallow,
  type Transform,
} from '../stores'
import { EchartUI, type EchartsEventHandler } from './echart-ui'

/**
 * Default ECharts `replaceMerge` keys for every widget. `dataZoom` and
 * `brush` are omitted so ZoomToggle / BrushToggle's user-driven runtime
 * state (slider range, brushed areas in `multiple` mode) survives unrelated
 * re-renders. `series` and `dataset` are also omitted: EchartUI fingerprints
 * them per-render and adds them to `replaceMerge` only when their shape
 * actually changes, keeping ECharts' per-series runtime state alive when
 * only callback-style fields differ.
 *
 * Lives in this module (not the generic store) because it's an ECharts
 * concept. Module-private — callers should not need it.
 */
const DEFAULT_REPLACE_MERGE: readonly string[] = ['toolbox']

/**
 * Reactive context passed to every {@link OptionFactory} call so the
 * factory can rebuild render-time pieces (axis label / tooltip formatters)
 * from the live store. Drives RelativeData / consumer-formatter changes
 * through to the chart without rebuilding the structural option.
 */
export interface OptionFactoryContext {
  formatter?: (value: number) => string
  labelFormatter?: (value: string | number) => string | number
}

/**
 * The per-widget option factory — a single callable that owns BOTH phases
 * of option construction:
 *
 * - **Structural phase** — when `option == null`, return the theme-aware
 *   structural option (tooltip / legend / color palette / series
 *   template, optionally merged with a consumer-supplied `optionsOverride`).
 *   No data is read. `<Widget.Echart>` calls the factory with
 *   `(undefined, undefined)` synchronously during render to derive the
 *   structural base; `configTransforms` (Stack/Zoom/Brush) then mutate it
 *   in the same render pass.
 * - **Merge phase** — when `option` is defined, fuse `data` into the
 *   post-configTransforms option at fusion time. `<Widget.Echart>` calls
 *   the factory with `(transformed, data, ctx)` on every render.
 *
 * The two phases share a closure (the factory creator captures `theme`,
 * `formatter`, `labelFormatter`, `seriesNames`, `selection`, `optionsOverride`,
 * …), so structural and merge agree on the same widget configuration.
 *
 * The third arg `ctx` carries the **live** store-side formatters at the
 * call site — distinct from the closure-time formatters because actions
 * like RelativeData can install a percent formatter on the store after
 * the factory was constructed. The merge phase reads from `ctx`; the
 * structural phase typically uses the closure-time values.
 */
export type OptionFactory = (
  option: echarts.EChartsOption | undefined,
  data: unknown,
  ctx?: OptionFactoryContext,
) => echarts.EChartsOption

export interface EchartProps {
  /**
   * The per-widget {@link OptionFactory}. Required — `<Widget.Echart>`
   * derives the structural option from it (so configTransforms have a base
   * to mutate) and fuses `state.data` into the post-pipeline option at
   * render time. Wrap the factory creator in `useMemo` so its identity is
   * stable across renders.
   */
  optionFactory: OptionFactory
  onEvents?: Record<string, EchartsEventHandler>
  init?: echarts.EChartsInitOpts
  className?: string
}

interface EchartSlice {
  data: unknown
  configTransforms: readonly Transform[]
  formatter?: (value: number) => string
  labelFormatter?: (value: string | number) => string | number
}

const echartSelector = (s: {
  data: unknown
  configTransforms: readonly Transform[]
  formatter?: (value: number) => string
  labelFormatter?: (value: string | number) => string | number
}): EchartSlice => ({
  data: s.data,
  configTransforms: s.configTransforms,
  formatter: s.formatter,
  labelFormatter: s.labelFormatter,
})

/**
 * Stateful Echart bridge — owns the entire ECharts coupling. The whole
 * option pipeline lives here, not in the store:
 *
 * 1. **Structural** — `optionFactory(undefined, undefined)` produces the
 *    theme-aware base. Memoized on the factory identity, so the consumer's
 *    `useMemo` ID gates the rebuild.
 * 2. **Transformed** — `applyTransforms(structural, configTransforms)`
 *    applies any registered configTransforms (Stack/Zoom/Brush) over the
 *    structural base. Memoized on `[structural, configTransforms]`.
 * 3. **`replaceMerge`** — derived from the enabled configTransforms'
 *    `replaceMergeKeys`, deduped and sorted, seeded with
 *    {@link DEFAULT_REPLACE_MERGE}. Memoized on `[configTransforms]` so
 *    ECharts sees a stable array reference across non-transform changes.
 * 4. **Merge** — `optionFactory(transformed, data, ctx)` fuses post-
 *    pipeline data into the option. Reactive `ctx` carries the live store
 *    formatters so RelativeData's percent formatter flows through without
 *    a structural rebuild.
 *
 * The `<Widget.Provider>` doesn't know about the factory at all: it stays
 * a renderer-agnostic shell. The `ProviderProps` surface has no ECharts
 * coupling, so non-Echart widgets don't transitively import the type.
 */
export function Echart({
  optionFactory,
  onEvents,
  init,
  className,
}: EchartProps) {
  const id = useWidgetId()
  const slice = useWidgetShallow(id, echartSelector)

  // Publish the live ECharts instance to the per-id registry so actions
  // (e.g. `ZoomToggle`'s disable handler) can reach it imperatively. The
  // callback identity is stable across renders (only `id` is in deps), so
  // EchartUI's init effect doesn't see a fresh `onInstance` and re-init.
  const onInstance = useCallback(
    (chart: echarts.ECharts | null) => setEchartInstance(id, chart),
    [id],
  )

  // Destructure so React Compiler sees specific deps instead of inferring
  // the whole `slice` object. `useWidgetShallow` already shallow-compares
  // the slice, so per-field deps drive each memo exactly when its inputs
  // change.
  const {
    data: sliceData,
    configTransforms,
    formatter: sliceFormatter,
    labelFormatter: sliceLabelFormatter,
  } = slice

  const structural = useMemo(
    () => optionFactory(undefined, undefined),
    [optionFactory],
  )

  const transformed = useMemo(
    () =>
      applyTransforms(structural, configTransforms) as echarts.EChartsOption,
    [structural, configTransforms],
  )

  const replaceMerge = useMemo(() => {
    const keys = new Set<string>(DEFAULT_REPLACE_MERGE)
    for (const xf of configTransforms) {
      if (xf.enabled && xf.replaceMergeKeys?.length) {
        for (const k of xf.replaceMergeKeys) keys.add(k)
      }
    }
    return Array.from(keys).sort()
  }, [configTransforms])

  const option = useMemo(
    () =>
      optionFactory(transformed, sliceData, {
        formatter: sliceFormatter,
        labelFormatter: sliceLabelFormatter,
      }),
    [
      optionFactory,
      transformed,
      sliceData,
      sliceFormatter,
      sliceLabelFormatter,
    ],
  )

  return (
    <EchartUI
      option={option}
      replaceMerge={replaceMerge}
      onEvents={onEvents}
      init={init}
      onInstance={onInstance}
      className={className}
    />
  )
}
