/**
 * Slider — 滑动输入条
 * @prop {number} [min=0] — 最小值
 * @prop {number} [max=100] — 最大值
 * @prop {number} [step=1] — 步长
 * @prop {Array} value — 当前值（受控）
 * @prop {Array} [defaultValue=[20,80]] — 默认值
 * @prop {boolean} [disabled=false] — 是否禁用
 * @prop {boolean} [showTooltip=true] — 是否在拖动时显示提示
 * @prop {*} [marks=null] — 刻度标记
 * @prop {function} [onChange=null] — 变化回调
 * @prop {function} [onAfterChange=null] — 松手回调
 */

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Tooltip from './Tooltip';

/* ── 容器与轨道 ── */
const WRAPPER_BASE = 'relative w-full min-w-0 max-w-full select-none';
const TRACK_AREA = 'relative h-[32px]';
const TRACK = 'absolute left-0 right-0 top-1/2 h-[4px] -translate-y-1/2 rounded-full bg-fill';
const TRACK_ACTIVE = 'absolute top-1/2 h-[4px] -translate-y-1/2 rounded-full bg-brand-500';
const TRACK_ACTIVE_DISABLED = 'absolute top-1/2 h-[4px] -translate-y-1/2 rounded-full bg-blueGrey-300';

/* ── 滑块 ── */
const HANDLE_BASE = [
  'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
  'inline-flex size-[20px] items-center justify-center rounded-full',
  'border-2 shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
  'transition-[box-shadow,border-color,background-color] tfds-motion-ui-instant',
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-200 focus-visible:ring-offset-1',
].join(' ');

const HANDLE_ENABLED = [
  'bg-white border-border-brand',
  'hover:border-border-brand',
  'active:border-brand-700',
  'cursor-grab',
].join(' ');

const HANDLE_DRAGGING = 'cursor-grabbing shadow-[0_0_0_4px_rgba(86,211,188,0.15)]';

const HANDLE_DISABLED = 'bg-blueGrey-300 border-blueGrey-300 cursor-not-allowed';

/* ── 提示与刻度 ── */
const TOOLTIP_TRIGGER = 'absolute inset-0';
const MARK_DOT_BASE = 'block size-[4px] rounded-full';
const MARK_LABEL = 'absolute left-1/2 top-full mt-[12px] -translate-x-1/2 text-[12px] leading-[18px] text-blueGrey-600 whitespace-nowrap';

function clamp(v, min, max) {
  return Math.min(max, Math.max(min, v));
}

function getPrecision(step) {
  const text = String(step);
  const dot = text.indexOf('.');
  return dot === -1 ? 0 : text.length - dot - 1;
}

function snap(raw, min, max, step) {
  const safeStep = Number.isFinite(step) && step > 0 ? step : 1;
  const precision = getPrecision(safeStep);
  const base = Math.round(((raw - min) / safeStep)) * safeStep + min;
  const rounded = Number(base.toFixed(precision));
  return clamp(rounded, min, max);
}

function isSameValue(a, b) {
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
  return a.every((v, i) => v === b[i]);
}

function normalizeRaw(raw, isRange, min, max, step) {
  if (isRange) {
    const tuple = Array.isArray(raw) ? raw : [min, max];
    let start = snap(Number(tuple[0]), min, max, step);
    let end = snap(Number(tuple[1]), min, max, step);
    if (!Number.isFinite(start)) start = min;
    if (!Number.isFinite(end)) end = max;
    if (start > end) [start, end] = [end, start];
    return [start, end];
  }
  const single = Array.isArray(raw) ? raw[0] : raw;
  const safe = Number.isFinite(Number(single)) ? Number(single) : min;
  return [snap(safe, min, max, step)];
}

function valueToPercent(v, min, max) {
  if (max <= min) return 0;
  return ((v - min) / (max - min)) * 100;
}

export default function Slider({
  min = 0,
  max = 100,
  step = 1,
  value,
  defaultValue = [20, 80],
  disabled = false,
  showTooltip = true,
  marks,
  onChange,
  onAfterChange,
  ariaLabel,
  ariaLabelStart,
  ariaLabelEnd,
  ariaLabelledBy,
  ariaDescribedBy,
  className = '',
  style,
  ...rest
}) {
  const isControlled = value !== undefined;
  const isRange = Array.isArray(isControlled ? value : defaultValue);

  const normalize = useCallback(
    (raw) => normalizeRaw(raw, isRange, min, max, step),
    [isRange, min, max, step]
  );

  const [innerValue, setInnerValue] = useState(() => normalize(defaultValue));
  const normalizedControlledValue = useMemo(() => (isControlled ? normalize(value) : null), [isControlled, normalize, value]);
  const current = isControlled ? normalizedControlledValue : innerValue;

  useEffect(() => {
    if (!isControlled) setInnerValue(normalize(defaultValue));
  }, [defaultValue, isControlled, normalize]);

  const currentRef = useRef(current);
  useEffect(() => {
    currentRef.current = current;
  }, [current]);

  const trackRef = useRef(null);
  const cleanupRef = useRef(null);
  const [draggingIndex, setDraggingIndex] = useState(null);

  const updateValue = useCallback((nextRaw, fireAfter = false) => {
    const next = normalize(nextRaw);
    if (isSameValue(next, currentRef.current)) {
      if (fireAfter) {
        const payload = isRange ? next : next[0];
        onAfterChange?.(payload);
      }
      return next;
    }

    if (!isControlled) setInnerValue(next);
    const payload = isRange ? next : next[0];
    onChange?.(payload);
    if (fireAfter) onAfterChange?.(payload);
    return next;
  }, [isControlled, isRange, normalize, onAfterChange, onChange]);

  const valueFromClientX = useCallback((clientX) => {
    const rect = trackRef.current?.getBoundingClientRect();
    if (!rect || rect.width <= 0) return null;
    const ratio = clamp((clientX - rect.left) / rect.width, 0, 1);
    const raw = min + ratio * (max - min);
    return snap(raw, min, max, step);
  }, [max, min, step]);

  const applyAtIndex = useCallback((base, index, nextPoint, fireAfter = false) => {
    if (nextPoint == null) return;

    if (!isRange) {
      updateValue([nextPoint], fireAfter);
      return;
    }

    const next = [...base];
    if (index === 0) next[0] = Math.min(nextPoint, next[1]);
    else next[1] = Math.max(nextPoint, next[0]);
    updateValue(next, fireAfter);
  }, [isRange, updateValue]);

  const clearListeners = useCallback(() => {
    if (cleanupRef.current) cleanupRef.current();
    cleanupRef.current = null;
  }, []);

  useEffect(() => () => clearListeners(), [clearListeners]);

  const startDrag = useCallback((index, startEvent) => {
    if (disabled) return;
    startEvent.preventDefault();
    startEvent.stopPropagation();
    setDraggingIndex(index);

    const onMove = (ev) => {
      const nextPoint = valueFromClientX(ev.clientX);
      applyAtIndex(currentRef.current, index, nextPoint, false);
    };

    const onUp = (ev) => {
      const nextPoint = valueFromClientX(ev.clientX);
      applyAtIndex(currentRef.current, index, nextPoint, true);
      setDraggingIndex(null);
      clearListeners();
    };

    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    cleanupRef.current = () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
    };
  }, [applyAtIndex, clearListeners, disabled, valueFromClientX]);

  const onTrackPointerDown = useCallback((e) => {
    if (disabled) return;
    const point = valueFromClientX(e.clientX);
    if (point == null) return;

    if (!isRange) {
      updateValue([point], true);
      return;
    }

    const [start, end] = currentRef.current;
    const pick = Math.abs(point - start) <= Math.abs(point - end) ? 0 : 1;
    applyAtIndex(currentRef.current, pick, point, true);
  }, [applyAtIndex, disabled, isRange, updateValue, valueFromClientX]);

  const onHandleKeyDown = useCallback((index, val) => (e) => {
    if (disabled) return;

    let nextPoint = val;
    const largeStep = step * 10;

    if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') nextPoint = val - step;
    else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') nextPoint = val + step;
    else if (e.key === 'PageDown') nextPoint = val - largeStep;
    else if (e.key === 'PageUp') nextPoint = val + largeStep;
    else if (e.key === 'Home') nextPoint = min;
    else if (e.key === 'End') nextPoint = max;
    else return;

    e.preventDefault();
    applyAtIndex(currentRef.current, index, snap(nextPoint, min, max, step), true);
  }, [applyAtIndex, disabled, max, min, step]);

  const [startVal, endVal] = isRange ? current : [min, current[0]];
  const startPercent = valueToPercent(startVal, min, max);
  const endPercent = valueToPercent(endVal, min, max);
  const activeLeft = isRange ? startPercent : 0;
  const activeWidth = isRange ? Math.max(0, endPercent - startPercent) : Math.max(0, endPercent);

  const markItems = useMemo(() => {
    if (!marks) return [];
    return Object.entries(marks)
      .map(([raw, label]) => {
        const num = Number(raw);
        if (!Number.isFinite(num)) return null;
        const val = clamp(num, min, max);
        const percent = valueToPercent(val, min, max);
        const active = isRange ? (val >= startVal && val <= endVal) : (val <= current[0]);
        return { key: raw, label, val, percent, active };
      })
      .filter(Boolean);
  }, [current, endVal, isRange, marks, max, min, startVal]);

  const wrapperClass = [
    WRAPPER_BASE,
    markItems.length > 0 ? 'pb-[30px]' : '',
    disabled ? 'opacity-60' : '',
    className,
  ].filter(Boolean).join(' ');

  return (
    <div className={[`tfds-slider`, wrapperClass].filter(Boolean).join(' ')} style={style} {...rest} data-tfds-component="Slider">
      <div className={TRACK_AREA}>
        <div ref={trackRef} className={TRACK} onPointerDown={onTrackPointerDown} role="presentation">
          <div
            className={disabled ? TRACK_ACTIVE_DISABLED : TRACK_ACTIVE}
            style={{ left: `${activeLeft}%`, width: `${activeWidth}%` }}
          />

          {markItems.map((mark) => (
            <div key={mark.key} style={{ left: `${mark.percent}%` }} className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2">
              <span
                className={[
                  MARK_DOT_BASE,
                  disabled
                    ? 'bg-blueGrey-500'
                    : mark.active
                      ? 'bg-brand-500'
                      : 'bg-blueGrey-400',
                ].join(' ')}
              />
              <span className={MARK_LABEL}>{mark.label}</span>
            </div>
          ))}

          {current.map((val, index) => {
            const left = valueToPercent(val, min, max);
            const handleClass = [
              HANDLE_BASE,
              disabled ? HANDLE_DISABLED : HANDLE_ENABLED,
              draggingIndex === index ? HANDLE_DRAGGING : '',
            ].filter(Boolean).join(' ');
            const shouldShowValueTooltip = showTooltip && draggingIndex === index;
            const fallbackAriaLabel = isRange
              ? (index === 0 ? (ariaLabelStart || '起始值') : (ariaLabelEnd || '结束值'))
              : (ariaLabel || '滑块值');

            return (
              <button
                key={`${index}-${val}`}
                type="button"
                className={handleClass}
                style={{ left: `${left}%` }}
                onPointerDown={(e) => startDrag(index, e)}
                onKeyDown={onHandleKeyDown(index, val)}
                disabled={disabled}
                role="slider"
                aria-label={ariaLabelledBy ? undefined : fallbackAriaLabel}
                aria-labelledby={ariaLabelledBy}
                aria-describedby={ariaDescribedBy}
                aria-valuemin={min}
                aria-valuemax={max}
                aria-valuenow={val}
                aria-disabled={disabled}
              >
                {shouldShowValueTooltip ? (
                  <Tooltip
                    content={String(val)}
                    placement="top"
                    defaultOpen
                    triggerClassName={TOOLTIP_TRIGGER}
                  >
                    <span aria-hidden="true" />
                  </Tooltip>
                ) : null}
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

Slider.displayName = 'Slider';

export { Slider };
