import { useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { RotateCcw } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { LabelledContrastToggle } from '@/components/site/ContrastToggle';
import type { DemoModule } from '@/lib/demo-kit';
import { cn } from '@/lib/utils';

import { Label } from './chrome';

/**
 * The visitor's own attempt, on the same seed as the run beside it.
 *
 * Generic over the demo: it knows only `interactive.init`, `interactive.Controls`
 * and `Surface` from the contract, so a demo that ships an interactive mode gets
 * this for free and one that does not renders nothing rather than a broken pane.
 *
 * The seed is the shared coordinate. Both boards derive from it, which is what
 * makes "you and the model got the same word" a fact about the page rather than
 * a sentence on it.
 */
export function PlayYourself<TState>({
  demo,
  seed,
  afterMove,
  className,
}: {
  demo: DemoModule<TState>;
  seed: number;
  /**
   * Rendered under the board once the visitor has changed the state at all.
   * The shell has no contract-level way to know a board is FINISHED — the
   * state is the demo's own type and the shell never inspects it — so "has
   * acted" is the honest gate for the bridge line, and the copy is written for
   * that moment rather than for a terminal one.
   */
  afterMove?: ReactNode;
  className?: string;
}) {
  const interactive = demo.interactive;
  const Surface = demo.Surface;

  // `init` is pure and seeded, so remounting on a seed change is the whole of
  // "start a new game" — there is no other state to reset.
  const initial = useMemo(
    () => (interactive ? interactive.init(seed) : null),
    [interactive, seed],
  );
  const [state, setState] = useState<TState | null>(initial);
  const [generation, setGeneration] = useState(0);

  useEffect(() => setState(initial), [initial]);

  if (!interactive || state === null) return null;
  const Controls = interactive.Controls;
  const moved = state !== initial;

  return (
    <div className={cn('space-y-3', className)}>
      <div className="flex flex-wrap items-center justify-between gap-2">
        <Label as="h2">Your attempt</Label>
        <div className="flex items-center gap-1">
          {/* The contrast toggle sits here as well as in the header because
              this is where a colour-blind visitor is looking when they need it. */}
          <LabelledContrastToggle />
          <Button
            type="button"
            variant="ghost"
            size="touch"
            className="text-muted hover:text-fg lg:min-h-9"
            onClick={() => {
              setState(interactive.init(seed));
              setGeneration((g) => g + 1);
            }}
          >
            <RotateCcw aria-hidden="true" />
            Start over
          </Button>
        </div>
      </div>

      {/* The shell decides the CARD, the Surface decides its own width (§8):
          a word grid is a narrow column with its keyboard beside it, an
          analyst screen wants most of the page. `flex-wrap` lets either stack
          under its controls the moment the two do not fit side by side. */}
      <div className="flex flex-wrap items-start gap-4">
        <div className="w-fit max-w-full rounded-xl border border-border bg-surface p-3 sm:p-4">
          <Surface state={state} />
        </div>

        {/* Remounting on `generation` clears any state the controls hold of
            their own — a half-typed word survives a reset otherwise, and the
            board and the keyboard then disagree about the current row. */}
        <div className="min-w-[18rem] max-w-md flex-1 pb-[max(1rem,var(--safe-bottom))] sm:pb-0 [@media(pointer:coarse)]:max-w-lg">
          <Controls key={generation} state={state} onChange={setState} seed={seed} />
        </div>
      </div>

      {moved && afterMove ? <div>{afterMove}</div> : null}
    </div>
  );
}

export default PlayYourself;
