import { useEffect, useRef, useState } from 'react';
// The Radix primitive rather than `@/components/ui/scroll-area`: the stream
// has to pin the viewport to the bottom as characters land, and that needs a
// ref on the viewport element, which the wrapper does not expose.
import * as ScrollArea from '@radix-ui/react-scroll-area';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils';
import { usePrefersReducedMotion } from './format';
import type { PlaybackSpeed } from '@/lib/demo-kit/player';

/**
 * Characters per second, clamped. A 40-character reasoning trace recorded over
 * 30 seconds would otherwise crawl at 1.3 chars/s and read as a hung page,
 * and a 6,000-character one recorded in 800 ms would flash past unread.
 */
const MIN_CPS = 24;
const MAX_CPS = 900;
const FALLBACK_CPS = 90;

export interface ReasoningPanelProps {
  reasoning: string | null;
  /** The recorded latency of the call this reasoning came from. */
  durationMs: number | null;
  playing: boolean;
  speed: PlaybackSpeed;
  /** Changing this restarts the stream. Pass the step index. */
  stepIndex: number;
  /**
   * Lines of height held open whether or not there is text. Reserving the box
   * is not a nicety: this panel sits beside the board, and letting it grow as
   * the text arrives shoves the board down the page mid-run.
   */
  reservedLines?: number;
  title?: string;
  className?: string;
}

function charsPerSecond(length: number, durationMs: number | null): number {
  if (!durationMs || durationMs <= 0 || length === 0) return FALLBACK_CPS;
  return Math.min(Math.max((length / durationMs) * 1000, MIN_CPS), MAX_CPS);
}

export function ReasoningPanel({
  reasoning,
  durationMs,
  playing,
  speed,
  stepIndex,
  reservedLines = 10,
  title = 'Reasoning',
  className,
}: ReasoningPanelProps) {
  const reducedMotion = usePrefersReducedMotion();
  const full = reasoning ?? '';
  const [visible, setVisible] = useState(full.length);
  const viewportRef = useRef<HTMLDivElement | null>(null);

  // Whether this step's text should stream at all. Scrubbing to a step while
  // paused shows it whole — someone reading at their own pace is not asking to
  // be typed at.
  const shouldStream = playing && speed !== 'instant' && !reducedMotion && full.length > 0;

  useEffect(() => {
    if (!shouldStream) {
      setVisible(full.length);
      return;
    }
    setVisible(0);
    const cps = charsPerSecond(full.length, durationMs) * (typeof speed === 'number' ? speed : 1);
    const started = performance.now();
    let frame = 0;
    let last = -1;
    const tick = (now: number) => {
      const next = Math.min(Math.floor(((now - started) / 1000) * cps), full.length);
      // Only re-render when a character actually lands; at 120 Hz the naive
      // version re-renders the whole panel twice per revealed character.
      if (next !== last) {
        last = next;
        setVisible(next);
        const viewport = viewportRef.current;
        if (viewport) viewport.scrollTop = viewport.scrollHeight;
      }
      if (next < full.length) frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [shouldStream, full, durationMs, speed, stepIndex]);

  const streaming = visible < full.length;

  return (
    <section
      aria-label={title}
      className={cn('card flex flex-col overflow-hidden', className)}
    >
      <header className="flex items-center gap-2 border-b border-border px-3 py-2">
        <Brain className="h-4 w-4 text-muted" aria-hidden="true" />
        <h3 className="text-sm font-semibold">{title}</h3>
        {streaming ? (
          <span className="nums ml-auto text-xs text-muted">
            {visible}/{full.length}
          </span>
        ) : null}
      </header>

      <ScrollArea.Root
        type="auto"
        className="min-h-0 flex-1"
        // Height, not min-height: the panel is the same size on every step,
        // including the ones with no reasoning at all.
        style={{ height: `${reservedLines * 1.45}rem` }}
      >
        <ScrollArea.Viewport
          ref={viewportRef}
          className="h-full w-full px-3 py-2.5"
          // The shell owns the page's single polite live region. A streaming
          // region here would read every partial word over the top of it.
          aria-live="off"
        >
          {full.length === 0 ? (
            <p className="text-sm leading-relaxed text-muted">
              This step has no recorded reasoning. The run was captured with thinking disabled, so
              there is nothing to show here — which is different from the model having thought
              nothing.
            </p>
          ) : (
            <p className="whitespace-pre-wrap font-mono text-[13px] leading-relaxed text-fg">
              {full.slice(0, visible)}
              {streaming ? (
                <span
                  aria-hidden="true"
                  className="ml-px inline-block h-[1em] w-[0.5ch] translate-y-[0.15em] animate-pulse bg-brand align-baseline"
                />
              ) : null}
            </p>
          )}
        </ScrollArea.Viewport>
        <ScrollArea.Scrollbar
          orientation="vertical"
          className="flex w-2 touch-none select-none p-0.5"
        >
          <ScrollArea.Thumb className="flex-1 rounded-full bg-border" />
        </ScrollArea.Scrollbar>
      </ScrollArea.Root>
    </section>
  );
}
