import {
  useEffect,
  useId,
  useRef,
  useState,
  type CSSProperties,
  type KeyboardEvent,
  type PointerEvent,
} from 'react';
import { cx } from '../utils';

type KnobAccent = 'lime' | 'ember';

type KnobProps = {
  /** Controlled value. */
  value?: number;
  /** Uncontrolled starting value. Defaults to `min`. */
  defaultValue?: number;
  /** Fires with the snapped value on every change. */
  onValueChange?: (value: number) => void;
  min?: number;
  max?: number;
  /** Snap increment. `0`/omitted = continuous (no detents, no ticks). */
  step?: number;
  /** Accent colour for the gauge fill + pointer. `'lime'` (default) or `'ember'`. */
  accent?: KnobAccent;
  /** Diameter in px. Drives every internal measurement via `--knob-size`. */
  size?: number;
  /**
   * Glide the gauge fill + pointer between detents. The dial always tracks the
   * pointer 1:1 while dragging; this only eases the *value* settle (keyboard,
   * wheel, drag-release) — like the stepped Slider. Defaults to `true` when
   * `step > 0`, `false` otherwise.
   */
  animated?: boolean;
  disabled?: boolean;
  className?: string;
  id?: string;
  'aria-label'?: string;
  'aria-labelledby'?: string;
};

// Gauge geometry: 270° sweep starting at the 7-o'clock position. R is the arc
// radius in the 100×100 SVG viewBox.
const START = -135;
const SWEEP = 270;
const R = 43;

const polar = (cx: number, cy: number, r: number, aDeg: number): [number, number] => {
  const t = (aDeg * Math.PI) / 180;
  return [cx + r * Math.sin(t), cy - r * Math.cos(t)];
};

const arcPath = (a0: number, a1: number) => {
  if (a1 - a0 < 0.01) a1 = a0 + 0.01;
  const [x0, y0] = polar(50, 50, R, a0);
  const [x1, y1] = polar(50, 50, R, a1);
  return `M${x0.toFixed(2)} ${y0.toFixed(2)} A${R} ${R} 0 ${a1 - a0 > 180 ? 1 : 0} 1 ${x1.toFixed(2)} ${y1.toFixed(2)}`;
};

const TRACK_PATH = arcPath(START, START + SWEEP);
// Arc length of the gauge. The fill is the full arc revealed via dashoffset, so
// the glide eases ALONG the circle (transitioning `d` interpolates endpoints in
// a straight chord and distorts the arc mid-animation).
const ARC_LEN = (R * SWEEP * Math.PI) / 180;

export const Knob = ({
  value: valueProp,
  defaultValue,
  onValueChange,
  min = 0,
  max = 100,
  step = 0,
  accent = 'lime',
  size,
  animated,
  disabled = false,
  className,
  id,
  'aria-label': ariaLabel,
  'aria-labelledby': ariaLabelledby,
}: KnobProps) => {
  const span = max - min || 1;
  const stepped = step > 0;
  const isAnimated = animated !== undefined ? animated : stepped;

  const clamp = (v: number) => Math.min(max, Math.max(min, v));
  const snap = (v: number) =>
    stepped ? clamp(Math.round((v - min) / step) * step + min) : clamp(v);

  const isControlled = valueProp !== undefined;
  const [internal, setInternal] = useState(() => snap(defaultValue ?? min));
  const committed = isControlled ? snap(valueProp) : internal;

  // Continuous, pointer-bound position for the dial; only meaningful mid-drag.
  const [visual, setVisual] = useState(committed);
  const [dragging, setDragging] = useState(false);

  const elRef = useRef<HTMLDivElement>(null);
  // Drag tracking: previous pointer angle + a continuous value accumulator.
  // Accumulating per-move deltas (each < 180°) instead of measuring from the
  // grab angle lets the dial wrap past ±180° without the value snapping.
  const lastAngle = useRef(0);
  const acc = useRef(0);

  const reactId = useId();
  const knobId = id ?? reactId;

  // `continuous` keeps the dial on the raw pointer angle; otherwise the dial
  // settles (and glides) to the snapped detent.
  const commit = (raw: number, continuous: boolean) => {
    const c = clamp(raw);
    const snapped = snap(c);
    setVisual(continuous ? c : snapped);
    if (!isControlled) setInternal(snapped);
    if (snapped !== committed) onValueChange?.(snapped);
  };

  const angleAt = (clientX: number, clientY: number) => {
    const el = elRef.current;
    if (!el) return 0;
    const r = el.getBoundingClientRect();
    return (
      (Math.atan2(clientX - (r.left + r.width / 2), -(clientY - (r.top + r.height / 2))) * 180) /
      Math.PI
    );
  };

  const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
    if (disabled) return;
    e.preventDefault();
    elRef.current?.focus();
    elRef.current?.setPointerCapture(e.pointerId);
    setDragging(true);
    lastAngle.current = angleAt(e.clientX, e.clientY);
    acc.current = clamp(committed);
  };

  const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {
    if (!dragging) return;
    const a = angleAt(e.clientX, e.clientY);
    let d = a - lastAngle.current;
    if (d > 180) d -= 360;
    if (d < -180) d += 360;
    lastAngle.current = a;
    // Clamp the accumulator (not just the committed value) so overshoot past
    // an end doesn't build a dead zone you must unwind before reversing.
    acc.current = clamp(acc.current + (d / SWEEP) * span);
    commit(acc.current, true);
  };

  const endDrag = (e: PointerEvent<HTMLDivElement>) => {
    if (!dragging) return;
    elRef.current?.releasePointerCapture(e.pointerId);
    setDragging(false);
  };

  // React's synthetic onWheel is passive, so preventDefault can't block page
  // scroll — attach a native non-passive listener. A ref keeps it current
  // without re-binding every render.
  const wheelRef = useRef<(e: WheelEvent) => void>(() => {});
  wheelRef.current = (e) => {
    if (disabled) return;
    e.preventDefault();
    commit(committed - (step || span / 100) * Math.sign(e.deltaY), false);
  };
  useEffect(() => {
    const el = elRef.current;
    if (!el) return;
    const handler = (e: WheelEvent) => wheelRef.current(e);
    el.addEventListener('wheel', handler, { passive: false });
    return () => el.removeEventListener('wheel', handler);
  }, []);

  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
    if (disabled) return;
    const big = step || span / 10;
    const sm = step || span / 100;
    const k = e.key;
    if (k === 'ArrowUp' || k === 'ArrowRight') commit(committed + sm, false);
    else if (k === 'ArrowDown' || k === 'ArrowLeft') commit(committed - sm, false);
    else if (k === 'PageUp') commit(committed + big, false);
    else if (k === 'PageDown') commit(committed - big, false);
    else if (k === 'Home') commit(min, false);
    else if (k === 'End') commit(max, false);
    else return;
    e.preventDefault();
  };

  const dialValue = dragging ? visual : committed;
  const dialAngle = START + ((clamp(dialValue) - min) / span) * SWEEP;
  const fillOffset = ARC_LEN * (1 - (committed - min) / span);
  const valueNow = stepped ? committed : Math.round(committed);

  const ticks = stepped
    ? Array.from({ length: Math.round(span / step) + 1 }, (_, i) => {
        const a = START + (i / Math.round(span / step)) * SWEEP;
        const [x1, y1] = polar(50, 50, R + 6, a);
        const [x2, y2] = polar(50, 50, R + 10.5, a);
        const val = min + (i / Math.round(span / step)) * span;
        return { x1, y1, x2, y2, on: val <= committed + 1e-6, key: i };
      })
    : [];

  return (
    <div
      ref={elRef}
      id={knobId}
      role="slider"
      tabIndex={disabled ? -1 : 0}
      aria-label={ariaLabel}
      aria-labelledby={ariaLabelledby}
      aria-valuemin={min}
      aria-valuemax={max}
      aria-valuenow={valueNow}
      aria-disabled={disabled || undefined}
      className={cx(
        'modern-sk-knob',
        `modern-sk-knob--${accent}`,
        isAnimated && 'modern-sk-knob--animated',
        dragging && 'is-dragging',
        disabled && 'modern-sk-knob--disabled',
        className,
      )}
      style={size != null ? ({ '--knob-size': `${size}px` } as CSSProperties) : undefined}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={endDrag}
      onPointerCancel={endDrag}
      onKeyDown={onKeyDown}
    >
      <svg className="modern-sk-knob__gauge" viewBox="0 0 100 100" aria-hidden>
        <path className="modern-sk-knob__track" d={TRACK_PATH} />
        <path
          className="modern-sk-knob__fill"
          d={TRACK_PATH}
          strokeDasharray={ARC_LEN}
          strokeDashoffset={fillOffset}
        />
        <g className="modern-sk-knob__ticks">
          {ticks.map((t) => (
            <line
              key={t.key}
              className={cx('modern-sk-knob__tick', t.on && 'is-on')}
              x1={t.x1.toFixed(2)}
              y1={t.y1.toFixed(2)}
              x2={t.x2.toFixed(2)}
              y2={t.y2.toFixed(2)}
            />
          ))}
        </g>
      </svg>
      <div className="modern-sk-knob__cap">
        <div className="modern-sk-knob__dial" style={{ transform: `rotate(${dialAngle}deg)` }}>
          <span className="modern-sk-knob__pointer" />
        </div>
        <div className="modern-sk-knob__hub" />
      </div>
    </div>
  );
};
