import { useEffect, useRef } from 'react';
import type { ComponentType, KeyboardEvent } from 'react';
import type { DemoStep } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { DASH, formatInt, formatMs, humaniseToken, usePrefersReducedMotion } from './format';

export interface StepTimelineProps<T> {
  steps: DemoStep<T>[];
  current: number;
  onSelect: (index: number) => void;
  /** The demo's own board, drawn small. Omit and the chips are text only. */
  Surface?: ComponentType<{ state: T; compact?: boolean }>;
  /** Bound to Space, per the transport convention on the rest of the page. */
  onTogglePlay?: () => void;
  className?: string;
}

/**
 * The scrubber: one chip per recorded step, each carrying its own board
 * preview and its own real numbers.
 *
 * Why numbers on a chip at all — the chips are the only place the cost of the
 * run is visible without opening a panel, and "this took 6 calls and 4,200
 * tokens" is the sentence a buyer repeats to their CFO.
 *
 * Keyboard: this is a radiogroup, so arrows move AND select, which is the
 * standard pattern. Space is bound to play/pause rather than to select — a
 * deliberate break from the radio pattern, because the timeline sits under a
 * transport bar where Space means play everywhere else, and being internally
 * consistent beats being technically canonical. It is advertised on the group
 * with `aria-keyshortcuts`.
 */
export function StepTimeline<T>({
  steps,
  current,
  onSelect,
  Surface,
  onTogglePlay,
  className,
}: StepTimelineProps<T>) {
  const reducedMotion = usePrefersReducedMotion();
  const chipRefs = useRef<(HTMLDivElement | null)[]>([]);
  const scrollerRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const chip = chipRefs.current[current];
    const scroller = scrollerRef.current;
    if (!chip || !scroller) return;
    // `scrollIntoView` on the element would also scroll the PAGE to the
    // timeline on every step, which is intolerable while the run plays. Scroll
    // the strip only.
    const chipBox = chip.getBoundingClientRect();
    const viewBox = scroller.getBoundingClientRect();
    const delta = chipBox.left - viewBox.left - (viewBox.width - chipBox.width) / 2;
    scroller.scrollBy({ left: delta, behavior: reducedMotion ? 'auto' : 'smooth' });
  }, [current, reducedMotion]);

  const move = (next: number, event: KeyboardEvent) => {
    event.preventDefault();
    const clamped = Math.min(Math.max(next, 0), steps.length - 1);
    onSelect(clamped);
    chipRefs.current[clamped]?.focus();
  };

  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
    switch (event.key) {
      case 'ArrowRight':
      case 'ArrowDown':
        move(current + 1, event);
        break;
      case 'ArrowLeft':
      case 'ArrowUp':
        move(current - 1, event);
        break;
      case 'Home':
        move(0, event);
        break;
      case 'End':
        move(steps.length - 1, event);
        break;
      case ' ':
      case 'Spacebar':
        if (onTogglePlay) {
          event.preventDefault();
          onTogglePlay();
        }
        break;
      default:
        break;
    }
  };

  return (
    <div
      ref={scrollerRef}
      role="radiogroup"
      aria-label="Steps in the recorded run"
      aria-keyshortcuts="ArrowLeft ArrowRight Home End Space"
      onKeyDown={handleKeyDown}
      className={cn('flex snap-x gap-2 overflow-x-auto pb-2', className)}
    >
      {steps.map((step, index) => {
        const selected = index === current;
        const call = step.call;
        return (
          // A div rather than a button: the chip embeds the demo's own Surface,
          // and a Surface is a grid of divs. Nesting flow content inside a
          // <button> is invalid HTML and browsers reflow it unpredictably. The
          // radio role plus the group's key handling gives the same semantics.
          <div
            key={step.index}
            role="radio"
            aria-checked={selected}
            // Roving tabindex: one stop for the whole strip, arrows inside it.
            tabIndex={selected ? 0 : -1}
            ref={(node) => {
              chipRefs.current[index] = node;
            }}
            onClick={() => onSelect(index)}
            className={cn(
              'tap w-[9.5rem] shrink-0 snap-center rounded-lg border p-2 text-left transition-colors duration-2 ease-enter',
              selected
                ? 'border-brand bg-accent-subtle/60'
                : 'border-border bg-surface hover:bg-surface-2',
            )}
          >
            <span className="flex items-baseline justify-between gap-1">
              <span className="nums text-xs font-semibold">Step {index + 1}</span>
              {call?.finishReason ? (
                <span
                  className="max-w-[4.5rem] truncate text-[10px] uppercase tracking-wide text-muted"
                  title={call.finishReason}
                >
                  {humaniseToken(call.finishReason)}
                </span>
              ) : null}
            </span>

            {Surface ? (
              <span className="mt-1.5 block overflow-hidden rounded-md bg-surface-2 p-1.5">
                <Surface state={step.state} compact />
              </span>
            ) : null}

            {step.caption ? (
              <span className="mt-1.5 block truncate text-xs text-muted" title={step.caption}>
                {step.caption}
              </span>
            ) : null}

            <span className="nums mt-1 flex flex-wrap gap-x-2 text-[10px] text-muted">
              <span title="Completion tokens">
                {call?.completionTokens === null || call?.completionTokens === undefined
                  ? DASH
                  : `${formatInt(call.completionTokens)} tok`}
              </span>
              <span title="Recorded latency">{formatMs(call?.durationMs ?? null)}</span>
            </span>
          </div>
        );
      })}
    </div>
  );
}
