/**
 * The demo contract.
 *
 * Every demo in this repo is one directory under `src/demos/<slug>/` that
 * satisfies these interfaces and imports nothing from the shared shell except
 * `@/lib/demo-kit`. The shell renders any demo that does; a demo that reaches
 * deeper is caught by `scripts/check-demos.mjs`, not by review.
 *
 * There is no runtime code in this file. It is types only, so a demo can
 * depend on the contract without pulling the player, the registry or the
 * verifier into its chunk.
 */

/** A vertical the demo lineup is organised by. Purely presentational grouping. */
export type Vertical =
  | 'reference'
  | 'support'
  | 'healthcare'
  | 'insurance'
  | 'financial-crime'
  | 'energy'
  | 'logistics'
  | 'code'
  | 'retail'
  | 'telecom'
  | 'data'
  | 'legal';

/**
 * `live` demos are interactive and ship a real environment. `spec` demos are a
 * published specification with no interactive surface — a real task, action
 * set, grader, counterweight and eval command, not a coming-soon card.
 * `check-demos` refuses a `spec` whose specification is incomplete.
 */
export type DemoStatus = 'live' | 'spec';

/**
 * The eagerly-loaded half of a demo.
 *
 * This is imported for EVERY demo on every page load, because the header, the
 * gallery and the router are all generated from it. It must therefore be
 * serialisable and cheap: no React, no component imports, and `icon` is a
 * lucide icon NAME rather than a component, or one demo's icon would pull
 * lucide into the entry chunk for all of them.
 */
export interface DemoMeta {
  /** URL segment and registry key. Must equal the directory name. */
  slug: string;
  /** Shown in the header, the gallery card and the page title. */
  title: string;
  /** One line, exec-facing. Says what the agent DOES, not how it works. */
  tagline: string;
  vertical: Vertical;
  status: DemoStatus;
  /** Sort order within a vertical. Ties break on slug. */
  order: number;
  /** A lucide-react icon name, e.g. 'Grid3x3'. Resolved by the shell. */
  icon: string;
  /** The job title of the person this demo is aimed at. */
  persona: string;
  /** Six words on what the reward pays for, and what it takes away. */
  rewardLine: string;
  /** Absolute path to the social card, e.g. '/og/wordle.png'. */
  ogImage: string;
}

/** One weighted term of a reward, as the environment actually computes it. */
export interface RewardComponent {
  key: string;
  /** Plain English, as an executive would say it. Not the variable name. */
  label: string;
  /** What it measures, one sentence. Shown in the breakdown and the editor. */
  description: string;
  /** Shipped weight. The editor varies this; the environment ships this. */
  weight: number;
  /**
   * `objective` is what the task is for. `counterweight` is what stops the
   * objective being maximised the crude way, and must be in genuine tension
   * with it — a component every good policy also scores 1.0 on is a gate, not
   * a counterweight, and must be declared `gate`.
   */
  role: 'objective' | 'counterweight' | 'gate';
}

/** The reward, as a whole, plus the source that proves it. */
export interface RewardSpec {
  components: RewardComponent[];
  /** Unweighted diagnostics. Rendered, never summed. */
  metrics?: { key: string; label: string; description: string }[];
  /** The Python that computes it, imported with `?raw` and quoted verbatim. */
  source: { path: string; code: string; marker?: string };
}

/** One model call inside a step, straight off the recorded trace. */
export interface ModelCall {
  promptTokens: number | null;
  completionTokens: number | null;
  reasoningTokens: number | null;
  /** Real elapsed milliseconds, as recorded. Never invented for playback. */
  durationMs: number | null;
  finishReason: string | null;
}

/**
 * One turn of a recorded episode, already adapted to this demo's board type.
 *
 * `state` is whatever the demo's surface renders. The shell never inspects it.
 */
export interface DemoStep<TState = unknown> {
  index: number;
  /** The board AFTER this step. Steps are snapshots, not deltas. */
  state: TState;
  /** The model's visible reply, verbatim. */
  reply: string | null;
  /** Reasoning content, if the recorded run had thinking enabled. */
  reasoning: string | null;
  call: ModelCall | null;
  /**
   * What a screen reader announces when this step lands. REQUIRED, because
   * reduced-motion clamps every animation to nothing and colour alone cannot
   * carry the result.
   */
  announce: string;
  /** Rendered under the step chip. Six words at most. */
  caption?: string;
}

/** A scored reward on a recorded run. `null` means not scored, never zero. */
export type RewardValues = Record<string, number | null>;

/** Pointer to one recorded run, listed in `public/traces/manifest.json`. */
export interface RunRef {
  id: string;
  /** Exec-facing arm label, e.g. 'Out of the box'. */
  label: string;
  /** Where the JSON lives, absolute from the site root. */
  path: string;
  /**
   * `recorded` is a plain rollout. `intervened` had something done to it, and
   * `intervention` is then REQUIRED — so a prompt change can never be
   * presented as a training result by omission.
   */
  kind: 'recorded' | 'intervened' | 'generated';
  intervention?: string;
  model: string;
  /** ISO date. Rendered in the permanent provenance badge. */
  capturedAt: string;
  seed: number;
}

/** A recorded episode as it sits on disk. */
export interface DemoEpisode {
  runId: string;
  seed: number;
  model: string;
  capturedAt: string;
  /** Verbatim from the environment. `null` values render 'not scored'. */
  rewards: RewardValues;
  /** Unweighted diagnostics from the environment. */
  metrics?: Record<string, number | null>;
  /** True when the run ended without reaching a terminal state. */
  truncated?: boolean;
  outcome: 'solved' | 'failed' | 'aborted';
  /** Raw turns. The demo's adapter turns these into DemoStep[]. */
  turns: {
    reply: string | null;
    reasoning: string | null;
    call: ModelCall | null;
    info?: Record<string, unknown>;
  }[];
}

/**
 * The four tabs every demo page has, in order.
 *
 * `play` is the landing tab and the reason the page exists: a visitor should be
 * doing the task within one click of choosing an environment, not reading about
 * it. The other three are what they reach for once they have felt it.
 *
 * A demo with no `interactive` mode has no `play` tab and opens on `watch`;
 * the shell works that out, not the demo.
 */
export type DemoTabId = 'play' | 'watch' | 'reward' | 'evidence';

export const DEMO_TABS: readonly DemoTabId[] = ['play', 'watch', 'reward', 'evidence'];

/** What this demo deliberately does not teach, and which demo answers it. */
export interface Limit {
  text: string;
  /** Slug of the demo that closes this gap, if one is planned. */
  answeredBy?: string;
}

export interface Narrative {
  /** The one-paragraph thesis, exec-facing. */
  thesis: string;
  /** The question in the buyer's head when they land. */
  anxiety: string;
  /**
   * One sentence per tab, asserted as a claim the tab then demonstrates.
   *
   * Required for every tab, including ones this demo may not render — writing
   * the claim is how an author works out whether the tab has anything to say.
   * A tab whose claim is hard to write is usually a tab with nothing in it.
   */
  claims: Record<DemoTabId, string>;
  limits: Limit[];
}

/** Everything needed to reproduce the recorded runs. Rendered verbatim. */
export interface Provenance {
  /** The environment package, e.g. 'wordle_five'. */
  envPackage: string;
  tasksetId: string;
  verifiersVersion: string;
  /** The literal command that produced the fixtures. Copy-pasteable. */
  command: string;
  /** Upstream work this mirrors or builds on, with URLs. */
  credits: { label: string; href: string }[];
}

/**
 * The lazily-loaded half of a demo: everything that costs bundle size.
 *
 * `TState` is the demo's own board type. The shell is generic over it and
 * never reaches inside.
 */
export interface DemoModule<TState = unknown> {
  meta: DemoMeta;
  narrative: Narrative;
  reward: RewardSpec;
  provenance: Provenance;
  /** The four boxes: task, legal moves, grader, and the score that moves. */
  anatomy: { task: string; actions: string; grader: string; score: string };
  /** Turns a recorded episode into renderable steps. Pure. */
  adapt: (episode: DemoEpisode) => DemoStep<TState>[];
  /** Renders one board state. Used by play, replay AND the gallery card. */
  Surface: React.ComponentType<{ state: TState; compact?: boolean }>;
  /**
   * Optional interactive mode: lets the visitor play rather than watch. A demo
   * without one still renders every other surface.
   */
  interactive?: {
    /** Fresh state for a seed. Must be deterministic in the seed. */
    init: (seed: number) => TState;
    /** The keyboard/controls component, wired to the demo's own state. */
    Controls: React.ComponentType<{
      state: TState;
      onChange: (next: TState) => void;
      seed: number;
    }>;
  };
  /**
   * Re-derives the reward from a recorded episode, in the browser, so the page
   * can prove the recorded numbers rather than assert them. Returning null
   * means 'unverifiable' — a truncated trace — and must never render as zero.
   */
  verify?: (episode: DemoEpisode) => RewardValues | null;
  /**
   * Extra panels this demo adds. NOT extra top-level tabs: the page's four
   * tabs are fixed and derived, so these ride in the step-detail strip inside
   * `watch`, beside Reasoning and Model call, where they sit next to the step
   * they are almost always about. A demo with no recording has no step-detail
   * strip, and they fall to the bottom of `evidence` instead.
   */
  tabs?: { id: string; label: string; Component: React.ComponentType }[];
}
