import {
  useCallback,
  useState,
  type FocusEvent,
  type KeyboardEvent,
} from 'react'
import { Box, Slider, TextField, Typography } from '@mui/material'
import type { RangeDataItem, RangeItemValue } from './types'
import { styles } from './style'

export interface RangeUIProps {
  items: readonly RangeDataItem[]
  /**
   * Fires on every pointer tick while a thumb is being dragged (mirrors
   * MUI `<Slider>`'s `onChange`). Use this to update local UI state —
   * not to persist expensive operations like remote queries.
   */
  onChange?: (index: number, value: RangeItemValue) => void
  /**
   * Fires once when the user *releases* a slider thumb after dragging,
   * after an arrow-key adjustment commits, or when the text inputs
   * blur / Enter. Mirrors MUI `<Slider>`'s `onChangeCommitted`. Use
   * this for side-effects you want to throttle to "drag end" — e.g.
   * writing the value to a source filter that triggers refetches.
   */
  onChangeCommitted?: (index: number, value: RangeItemValue) => void
  /** Number formatter for the slider tooltip and the text input display. */
  formatter?: (value: number) => string
}

type Bound = 'min' | 'max'

/**
 * Pure presentational component for the Range widget. Renders one MUI Slider
 * per item with editable min/max text inputs below — matching the Range v1
 * UX. Each item is always a two-thumb range; supply `value` to seed the
 * starting selection, otherwise it defaults to `[min, max]`.
 */
export function RangeUI({
  items,
  onChange,
  onChangeCommitted,
  formatter,
}: RangeUIProps) {
  const fmt = formatter ?? ((n: number) => String(n))
  return (
    <Box sx={styles.root}>
      {items.map((item, i) => (
        // Composite of the row's track bounds — stable across reorders for
        // any realistic widget configuration. Falls back to a literal +
        // index when bounds collide (degenerate same-min-same-max rows).
        <RangeRow
          key={`range-${item.min}-${item.max}-${i}`}
          index={i}
          item={item}
          fmt={fmt}
          onChange={onChange}
          onChangeCommitted={onChangeCommitted}
        />
      ))}
    </Box>
  )
}

interface RangeRowProps {
  index: number
  item: RangeDataItem
  fmt: (n: number) => string
  onChange?: (index: number, value: RangeItemValue) => void
  onChangeCommitted?: (index: number, value: RangeItemValue) => void
}

function RangeRow({
  index,
  item,
  fmt,
  onChange,
  onChangeCommitted,
}: RangeRowProps) {
  const current: readonly [number, number] = item.value ?? [item.min, item.max]
  const [editing, setEditing] = useState<'' | Bound>('')

  // Clamp inside [min, max] and keep `low <= high`. Pulled out so both the
  // live `onChange` path and the commit path share the same normalization.
  const normalize = useCallback(
    (next: readonly [number, number]): readonly [number, number] => {
      const [lowRaw, highRaw] = next
      const low = Math.min(Math.max(lowRaw, item.min), item.max)
      const high = Math.min(Math.max(highRaw, item.min), item.max)
      return low <= high ? [low, high] : [high, low]
    },
    [item.min, item.max],
  )

  const commit = useCallback(
    (next: readonly [number, number]) => {
      onChange?.(index, normalize(next))
    },
    [index, normalize, onChange],
  )

  const commitFinal = useCallback(
    (next: readonly [number, number]) => {
      onChangeCommitted?.(index, normalize(next))
    },
    [index, normalize, onChangeCommitted],
  )

  // A text-input commit (blur / Enter) is both a value change AND a final
  // commit, so it fires `onChange` *and* `onChangeCommitted`. Firing both
  // keeps the widget responsive for consumers that only wired `onChange`
  // (the pre-`onChangeCommitted` API) — without it, typing a value and
  // pressing Enter would silently no-op for them.
  const commitText = useCallback(
    (next: readonly [number, number]) => {
      const value = normalize(next)
      onChange?.(index, value)
      onChangeCommitted?.(index, value)
    },
    [index, normalize, onChange, onChangeCommitted],
  )

  const handleSlider = (_: Event, raw: number | number[]) => {
    if (!Array.isArray(raw)) return
    // Hoist defaults out of the destructure: react-compiler can't safely
    // reorder MemberExpression defaults inside an array pattern.
    const low = raw[0] ?? item.min
    const high = raw[1] ?? item.max
    commit([low, high])
  }

  const handleSliderCommitted = (
    _: Event | React.SyntheticEvent,
    raw: number | number[],
  ) => {
    if (!Array.isArray(raw)) return
    const low = raw[0] ?? item.min
    const high = raw[1] ?? item.max
    commitFinal([low, high])
  }

  return (
    <Box sx={styles.item}>
      <Box sx={styles.sliderContainer}>
        <Slider
          sx={{
            ...styles.slider,
            ...(item.color ? { color: item.color } : null),
          }}
          value={[...current]}
          min={item.min}
          max={item.max}
          step={item.step ?? 1}
          marks={resolveMarks(item.marks)}
          disabled={item.disabled}
          valueLabelDisplay='auto'
          valueLabelFormat={fmt}
          onChange={handleSlider}
          onChangeCommitted={handleSliderCommitted}
        />
      </Box>
      <Box sx={styles.inputsRow}>
        {/* Text-input commits (blur / Enter) are final, but also notify
            `onChange` — see `commitText` — so consumers that only wired
            `onChange` still update on a typed value. */}
        <BoundInput
          // Bumping the key on a fresh external value resets the local
          // editing state — matches v1's RangeItem behaviour.
          key={`min-${current[0]}`}
          name='min'
          value={current[0]}
          item={item}
          fmt={fmt}
          editing={editing}
          setEditing={setEditing}
          commit={commitText}
          current={current}
          ariaLabel='Minimum value'
        />
        <BoundInput
          key={`max-${current[1]}`}
          name='max'
          value={current[1]}
          item={item}
          fmt={fmt}
          editing={editing}
          setEditing={setEditing}
          commit={commitText}
          current={current}
          ariaLabel='Maximum value'
        />
      </Box>
      {item.note ? (
        <Typography variant='caption' sx={styles.note}>
          {item.note}
        </Typography>
      ) : null}
    </Box>
  )
}

interface BoundInputProps {
  name: Bound
  value: number
  item: RangeDataItem
  fmt: (n: number) => string
  editing: '' | Bound
  setEditing: (next: '' | Bound) => void
  /**
   * Called when the user commits a new value (blur / Enter). Text input
   * edits never produce intermediate "live" values, so the consumer
   * only needs one callback — the row wires this to `commitFinal`.
   */
  commit: (next: readonly [number, number]) => void
  current: readonly [number, number]
  ariaLabel: string
}

function BoundInput({
  name,
  value,
  item,
  fmt,
  editing,
  setEditing,
  commit,
  current,
  ariaLabel,
}: BoundInputProps) {
  const [raw, setRaw] = useState<string>(String(value))

  const beginEditing = () => {
    setEditing(name)
  }
  const finishEditingAndCommit = (e: FocusEvent<HTMLInputElement>) => {
    setEditing('')
    commitFromText(e.target.value)
  }
  const commitOnEnter = (e: KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      commitFromText((e.target as HTMLInputElement).value)
      ;(e.target as HTMLInputElement).blur()
    }
  }

  function commitFromText(input: string) {
    const parsed = parseFloat(input)
    const safe = Number.isFinite(parsed) ? parsed : value
    const next: readonly [number, number] =
      name === 'min' ? [safe, current[1]] : [current[0], safe]
    commit(next)
  }

  const display = editing === name ? raw : fmt(Number(raw))

  return (
    <TextField
      name={name}
      value={display}
      onChange={(e) => setRaw(e.target.value)}
      onFocus={beginEditing}
      onBlur={finishEditingAndCommit}
      onKeyDown={commitOnEnter}
      disabled={item.disabled}
      size='small'
      sx={styles.input}
      inputProps={{ 'aria-label': ariaLabel }}
    />
  )
}

function resolveMarks(
  marks: RangeDataItem['marks'],
): boolean | { value: number; label?: string }[] | undefined {
  if (marks == null) return undefined
  if (typeof marks === 'boolean') return marks
  return [...marks]
}
