/**
 * A hand-written demo, used to build and exercise the shell without waiting for
 * real fixtures.
 *
 * It is NOT a fixture and it is not shipped as a demo: nothing in
 * `src/demos/` imports it, and the shell mounts it only for the reserved slug
 * `__mock` in a dev build. It exists so that every surface in this directory
 * has something complete to render — including the awkward cases a real trace
 * eventually produces: a step with no reasoning, a null model call, a
 * not-scored reward component, and a truncated run.
 *
 * It ships an `interactive` mode for the same reason. Play is the tab the page
 * opens on, so a fixture without one would leave the shell's default tab the
 * only surface here with nothing to render against.
 */
import { useCallback, useId, useState } from 'react';

import type {
  DemoEpisode,
  DemoModule,
  DemoStep,
  RewardValues,
  RunRef,
} from '@/lib/demo-kit/types';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

export type MarkKind = 'exact' | 'present' | 'absent';

export interface MockGuess {
  word: string;
  marks: MarkKind[];
}

export interface MockState {
  answer: string;
  guesses: MockGuess[];
  solved: boolean;
}

const ANSWER = 'CRANE';
const MAX_GUESSES = 6;

/** Standard Wordle marking, duplicates and all. */
function mark(guess: string, answer: string): MarkKind[] {
  const marks: MarkKind[] = Array.from({ length: guess.length }, () => 'absent');
  const pool = new Map<string, number>();
  for (let i = 0; i < answer.length; i += 1) {
    const letter = answer[i] as string;
    if (guess[i] === letter) {
      marks[i] = 'exact';
    } else {
      pool.set(letter, (pool.get(letter) ?? 0) + 1);
    }
  }
  for (let i = 0; i < guess.length; i += 1) {
    if (marks[i] === 'exact') continue;
    const letter = guess[i] as string;
    const left = pool.get(letter) ?? 0;
    if (left > 0) {
      marks[i] = 'present';
      pool.set(letter, left - 1);
    }
  }
  return marks;
}

const TILE: Record<MarkKind, string> = {
  exact: 'bg-tile-exact text-tile-exact-fg',
  present: 'bg-tile-present text-tile-present-fg',
  absent: 'bg-tile-absent text-tile-absent-fg',
};

const GLYPH: Record<MarkKind, string> = { exact: '●', present: '◐', absent: '○' };

function MockSurface({ state, compact = false }: { state: MockState; compact?: boolean }) {
  const rows = Array.from({ length: MAX_GUESSES }, (_, index) => state.guesses[index]);
  return (
    <div
      className={cn('grid w-fit gap-1', compact && 'gap-0.5')}
      role="img"
      aria-label={
        state.guesses.length === 0
          ? 'Empty board'
          : `Board after ${state.guesses.length} guesses: ${state.guesses
              .map((guess) => guess.word)
              .join(', ')}`
      }
    >
      {rows.map((guess, rowIndex) => (
        <div key={rowIndex} className={cn('flex gap-1', compact && 'gap-0.5')}>
          {Array.from({ length: 5 }, (_unused, colIndex) => {
            const letter = guess?.word[colIndex];
            const kind = guess?.marks[colIndex];
            return (
              <span
                key={colIndex}
                className={cn(
                  'grid place-items-center rounded-md border border-border font-semibold uppercase',
                  compact ? 'h-3.5 w-3.5 text-[7px]' : 'h-9 w-9 text-base',
                  kind ? TILE[kind] : 'bg-surface-2 text-muted',
                )}
              >
                {compact ? (kind ? GLYPH[kind] : '') : (letter ?? '')}
              </span>
            );
          })}
        </div>
      ))}
    </div>
  );
}

/* ------------------------------------------------------------- interactive */

/**
 * Answers the playable board can draw. Small on purpose: this is a fixture.
 *
 * Ordered so that seed 7 — the seed the recorded runs below use — draws the
 * same answer they were recorded against, which keeps Play and Watch showing
 * the same puzzle while the shell is being built.
 */
const PLAYABLE = ['SLATE', 'PRIZE', 'MOUND', ANSWER] as const;

/** Deterministic in the seed, as the contract requires. */
function initMock(seed: number): MockState {
  const answer = PLAYABLE[Math.abs(Math.trunc(seed)) % PLAYABLE.length] as string;
  return { answer, guesses: [], solved: false };
}

const LETTERS = /^[A-Za-z]*$/;

/**
 * The playable controls: type a five-letter word, submit, see it marked.
 *
 * Deliberately an input rather than an on-screen keyboard. The shell only ever
 * sees `onChange(next)`, so the fixture's job is to produce every state a real
 * demo's controls can — mid-word, illegal, solved, out of guesses — with as
 * little of its own machinery as possible.
 */
function MockControls({
  state,
  onChange,
  seed,
}: {
  state: MockState;
  onChange: (next: MockState) => void;
  seed: number;
}) {
  const [draft, setDraft] = useState('');
  const fieldId = useId();
  const over = state.solved || state.guesses.length >= MAX_GUESSES;
  const ready = draft.length === 5 && !over;

  const submit = useCallback(() => {
    if (!ready) return;
    const word = draft.toUpperCase();
    const guesses = [...state.guesses, { word, marks: mark(word, state.answer) }];
    onChange({ ...state, guesses, solved: word === state.answer });
    setDraft('');
  }, [draft, onChange, ready, state]);

  const status = state.solved
    ? `Solved in ${state.guesses.length} ${state.guesses.length === 1 ? 'guess' : 'guesses'}.`
    : over
      ? `Out of guesses. The word was ${state.answer}.`
      : `${MAX_GUESSES - state.guesses.length} guesses left.`;

  return (
    <div className="flex flex-col gap-3">
      <form
        className="flex flex-wrap items-center gap-2"
        onSubmit={(event) => {
          event.preventDefault();
          submit();
        }}
      >
        <label className="sr-only" htmlFor={fieldId}>
          Your five-letter guess
        </label>
        <input
          id={fieldId}
          value={draft}
          disabled={over}
          onChange={(event) => {
            const next = event.target.value.slice(0, 5);
            if (LETTERS.test(next)) setDraft(next.toUpperCase());
          }}
          autoComplete="off"
          autoCapitalize="characters"
          spellCheck={false}
          inputMode="text"
          placeholder="GUESS"
          className="tap min-w-0 flex-1 rounded-md border border-border bg-surface px-3 font-mono text-base uppercase tracking-[0.3em] text-fg placeholder:tracking-normal placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-50"
        />
        <Button type="submit" size="touch" disabled={!ready}>
          Guess
        </Button>
        <Button
          type="button"
          variant="outline"
          size="touch"
          onClick={() => {
            setDraft('');
            onChange(initMock(seed));
          }}
        >
          Reset
        </Button>
      </form>
      {/* The board is an image to a screen reader, so the outcome has to be
          said in words somewhere that announces itself. */}
      <p aria-live="polite" className="text-sm text-muted">
        {status}
      </p>
    </div>
  );
}

const REWARD_SOURCE = `import verifiers as vf


def reward_solved(state) -> float:
    """1.0 if the answer was found, else 0.0."""
    return 1.0 if state["solved"] else 0.0


# --8<-- efficiency
def reward_efficiency(state) -> float:
    """Pays for finding it early, and pays nothing for finding it late.

    This is the counterweight. Without it the highest-scoring policy is one
    that burns every guess it is allowed, because the objective alone cannot
    tell a lucky third guess from a grudging sixth.
    """
    if not state["solved"]:
        return 0.0
    used = len(state["guesses"])
    return max(0.0, (MAX_GUESSES - used + 1) / MAX_GUESSES)
# --8<--


def reward_legal(state) -> float:
    """Every guess was a real five-letter word in the allowed list."""
    return 1.0 if all(g in ALLOWED for g in state["guesses"]) else 0.0
`;

function turnBoard(episode: DemoEpisode, upto: number): MockState {
  const guesses: MockGuess[] = [];
  for (let i = 0; i <= upto && i < episode.turns.length; i += 1) {
    const info = episode.turns[i]?.info;
    const word = typeof info?.['guess'] === 'string' ? info['guess'] : null;
    if (!word) continue;
    guesses.push({ word, marks: mark(word, ANSWER) });
  }
  const last = guesses[guesses.length - 1];
  return { answer: ANSWER, guesses, solved: last?.word === ANSWER };
}

function adapt(episode: DemoEpisode): DemoStep<MockState>[] {
  return episode.turns.map((turn, index) => {
    const state = turnBoard(episode, index);
    const last = state.guesses[state.guesses.length - 1];
    const word = last?.word ?? 'no guess';
    const exact = last?.marks.filter((m) => m === 'exact').length ?? 0;
    return {
      index,
      state,
      reply: turn.reply,
      reasoning: turn.reasoning,
      call: turn.call,
      announce: state.solved
        ? `Guess ${index + 1}: ${word}. Solved.`
        : `Guess ${index + 1}: ${word}. ${exact} letters in the right place.`,
      caption: word.toLowerCase(),
    };
  });
}

function verify(episode: DemoEpisode): RewardValues | null {
  if (episode.truncated) return null;
  const final = turnBoard(episode, episode.turns.length - 1);
  const used = final.guesses.length;
  return {
    solved: final.solved ? 1 : 0,
    efficiency: final.solved ? Math.max(0, (MAX_GUESSES - used + 1) / MAX_GUESSES) : 0,
    legal: 1,
  };
}

export const mockDemo: DemoModule<MockState> = {
  meta: {
    slug: '__mock',
    title: 'Five-letter word game (mock)',
    tagline: 'An agent guesses a hidden five-letter word from letter feedback.',
    vertical: 'reference',
    status: 'live',
    order: 0,
    icon: 'Grid3x3',
    persona: 'Anyone deciding whether to fund an environment',
    rewardLine: 'Pays for solving; takes away for stalling.',
    ogImage: '/og/mock.png',
  },
  narrative: {
    thesis:
      'An environment is an eval you can take the gradient of. The task, the legal moves and the grader are all code — so you can change what "good" means and watch the number move.',
    anxiety: 'Is this a benchmark I read, or a thing I can actually change?',
    claims: {
      play: 'Play a round yourself, because the rest of this page is about what happened when a model played the same one.',
      watch: 'This is a recorded attempt, replayed one turn at a time, with the reasoning and the token cost of every turn attached.',
      reward: 'Move a single weight and the ranking of the runs re-orders underneath it. That is the whole product.',
      evidence: 'The grader is thirty lines of Python, printed here beside the command that ran it and the runs it produced.',
    },
    limits: [
      {
        text: 'A five-letter word has one right answer. Most business decisions do not, and a grader that pretends otherwise scores confidence rather than correctness.',
        answeredBy: 'claims-triage',
      },
      {
        text: 'Nothing here has a cost of being wrong. A real reward has to price the mistake, not just count the win.',
      },
    ],
  },
  reward: {
    components: [
      {
        key: 'solved',
        label: 'Found the word',
        description: 'One point if the hidden word was guessed within six tries.',
        weight: 1,
        role: 'objective',
      },
      {
        key: 'efficiency',
        label: 'Found it early',
        description: 'Pays more the fewer guesses it took. Zero if it never got there.',
        weight: 0.5,
        role: 'counterweight',
      },
      {
        key: 'legal',
        label: 'Played legal words',
        description: 'Every guess was a real five-letter word from the allowed list.',
        weight: 0.25,
        role: 'gate',
      },
    ],
    metrics: [
      { key: 'guesses_used', label: 'Guesses used', description: 'How many of the six were spent.' },
      { key: 'unique_letters', label: 'Unique letters tried', description: 'Breadth of the search.' },
    ],
    source: { path: 'envs/mock_five/mock_five/rewards.py', code: REWARD_SOURCE, marker: '--8<-- efficiency' },
  },
  provenance: {
    envPackage: 'wordle_five',
    tasksetId: 'wordle-five-v0-mock',
    verifiersVersion: '0.1.0',
    command: 'uv run vf-eval wordle-five -n 8 -m gpt-4.1-mini --seed 7',
    credits: [
      { label: 'verifiers', href: 'https://github.com/PrimeIntellect-ai/verifiers' },
    ],
  },
  anatomy: {
    task: 'Guess a hidden five-letter word in six tries, using the coloured feedback from each guess.',
    actions: 'One legal five-letter word per turn, drawn from the allowed list. Nothing else is a move.',
    grader: 'Deterministic Python: it marks the guess against the answer, checks legality, and scores the episode. No model judges it.',
    score: 'A weighted sum: one point for finding the word, half a point scaled by how early, a quarter as a legality gate.',
  },
  adapt,
  Surface: MockSurface,
  interactive: { init: initMock, Controls: MockControls },
  verify,
};

function call(prompt: number, completion: number, reasoning: number | null, ms: number) {
  return {
    promptTokens: prompt,
    completionTokens: completion,
    reasoningTokens: reasoning,
    durationMs: ms,
    finishReason: 'stop',
  };
}

export const mockEpisodes: Record<string, DemoEpisode> = {
  'mock-base': {
    runId: 'mock-base',
    seed: 7,
    model: 'gpt-4.1-mini',
    capturedAt: '2026-08-12',
    rewards: { solved: 1, efficiency: 0.5, legal: 1 },
    metrics: { guesses_used: 4, unique_letters: 14 },
    outcome: 'solved',
    turns: [
      {
        reply: 'SLATE',
        reasoning:
          'Opening with a word that covers three of the five most common letters and two common consonants. I want information, not a lucky hit.',
        call: call(412, 6, 84, 910),
        info: { guess: 'SLATE' },
      },
      {
        reply: 'TRACE',
        reasoning: null,
        call: call(486, 6, null, 640),
        info: { guess: 'TRACE' },
      },
      {
        reply: 'BRACE',
        reasoning:
          'A and E are placed. R is in the word but not where I put it. That leaves the first two positions to resolve.',
        call: call(551, 6, 121, 1180),
        info: { guess: 'BRACE' },
      },
      {
        reply: 'CRANE',
        reasoning: 'Only one word fits every constraint now.',
        call: call(618, 6, 96, 720),
        info: { guess: 'CRANE' },
      },
    ],
  },
  'mock-prompted': {
    runId: 'mock-prompted',
    seed: 7,
    model: 'gpt-4.1-mini',
    capturedAt: '2026-08-12',
    // Written as the expression the environment computes, not as a decimal
    // literal: a hand-rounded 0.8333 would fail our own verifier.
    rewards: { solved: 1, efficiency: 5 / 6, legal: 1 },
    metrics: { guesses_used: 2, unique_letters: 9 },
    outcome: 'solved',
    turns: [
      {
        reply: 'TRACE',
        reasoning: 'Told to open with maximum letter coverage and to commit early once a word fits.',
        call: call(455, 6, 140, 1020),
        info: { guess: 'TRACE' },
      },
      {
        reply: 'CRANE',
        reasoning: 'Every constraint is satisfied by exactly one candidate.',
        call: call(512, 6, 88, 660),
        info: { guess: 'CRANE' },
      },
    ],
  },
  'mock-truncated': {
    runId: 'mock-truncated',
    seed: 11,
    model: 'gpt-4.1-mini',
    capturedAt: '2026-08-12',
    // A run that hit the turn cap: the environment never reached a terminal
    // state, so two of the three components were never scored.
    rewards: { solved: null, efficiency: null, legal: 1 },
    metrics: { guesses_used: 2, unique_letters: 8 },
    truncated: true,
    outcome: 'aborted',
    turns: [
      { reply: 'AUDIO', reasoning: null, call: call(410, 6, null, 880), info: { guess: 'AUDIO' } },
      { reply: null, reasoning: null, call: null, info: {} },
    ],
  },
};

export const mockRuns: RunRef[] = [
  {
    id: 'mock-base',
    label: 'Out of the box',
    path: '/traces/__mock/base.json',
    kind: 'recorded',
    model: 'gpt-4.1-mini',
    capturedAt: '2026-08-12',
    seed: 7,
  },
  {
    id: 'mock-prompted',
    label: 'With a strategy prompt',
    path: '/traces/__mock/prompted.json',
    kind: 'intervened',
    intervention: 'System prompt rewritten',
    model: 'gpt-4.1-mini',
    capturedAt: '2026-08-12',
    seed: 7,
  },
  {
    id: 'mock-truncated',
    label: 'Hit the turn cap',
    path: '/traces/__mock/truncated.json',
    kind: 'recorded',
    model: 'gpt-4.1-mini',
    capturedAt: '2026-08-12',
    seed: 11,
  },
];
