const { useEffect, useRef, useState } = React;

const store = window.V3SessionStore;
const content = window.V3PrototypeContent;
const scenarioModel = window.V3ScenarioModel;
const {
  MascotPose,
  TargetBadge,
  VisualInstruction,
  PictureChoiceArt,
  EvidenceCounter,
  V3Pictograms,
  KioskIdleExperience,
} = window;
const gameModel = window.V3GameModel || null;
const gameVisuals = window.V3GameVisuals || {};
const LOGO_PATH = "assets/logo.png";

const ROLE_IDS = [
  "navigator",
  "signal_detective",
  "evidence_keeper",
  "storyteller",
];

const chapters = [
  { label: "集合", phases: ["prep", "welcome"] },
  { label: "猜想", phases: ["predict"] },
  { label: "安全转向", phases: ["preflight", "slew"] },
  { label: "找证据", phases: ["observe", "compare"] },
  { label: "说发现", phases: ["conclude", "result", "complete"] },
];

const preflightLabels = {
  zoneClear: {
    title: "周围空",
    detail: "老师确认周围没有障碍",
    asset: V3Pictograms.object.dish,
  },
  childrenBehindLine: {
    title: "线后站",
    detail: "孩子只观察，不操作设备",
    asset: V3Pictograms.mascot.safe,
  },
  routeReviewed: {
    title: "路线清",
    detail: "老师已经说明模拟路径",
    asset: null,
  },
  mockModeAcknowledged: {
    title: "只模拟",
    detail: "本页不连接真实设备",
    asset: V3Pictograms.object.signal,
  },
};

function activeScenario(state) {
  return scenarioModel.getScenario(
    state.runScenarioId || state.config.scenarioId,
  );
}

function childDispatch(state, type, payload) {
  store.dispatch({
    type,
    actor: "child",
    payload: {
      ...(payload || {}),
      expectedScenarioId: state.runScenarioId || state.config.scenarioId,
      expectedPhase: state.phase,
      expectedRunId: state.runId,
    },
  });
}

function useSessionState() {
  const [state, setState] = useState(() => store.getState());

  useEffect(() => store.subscribe(setState), []);
  return state;
}

function getChapterIndex(phase) {
  const index = chapters.findIndex((chapter) => chapter.phases.includes(phase));
  return index < 0 ? 0 : index;
}

function ChapterRibbon({ state }) {
  const phase = state.phase;
  const current = getChapterIndex(phase);
  return (
    <nav className="chapter-ribbon team-route" aria-label="小队解码路线">
      <span className="team-route-line" aria-hidden="true">
        <i
          style={{
            "--route-progress": `${
              (current / Math.max(1, chapters.length - 1)) * 100
            }%`,
          }}
        >
        </i>
      </span>
      {chapters.map((chapter, index) => {
        const stateClass = index < current
          ? "done"
          : index === current
          ? "current"
          : "";
        return (
          <span
            className={`chapter-node ${stateClass}`}
            aria-current={index === current ? "step" : undefined}
            key={chapter.label}
          >
            {index === current
              ? <img src={LOGO_PATH} alt="小海龟正在这里" />
              : null}
            {chapter.label}
          </span>
        );
      })}
    </nav>
  );
}

function ChildTopbar({ state }) {
  const ageMode = content.ageModes[state.config.ageBand] ||
    content.ageModes["6-8"];
  const scenario = activeScenario(state);
  const canPause = state.runState === "active" &&
    !["prep", "result", "complete"].includes(state.phase);

  return (
    <header className="child-topbar">
      <div className="child-brand">
        <img src={LOGO_PATH} alt="宇宙来电小海龟" />
        <div className="child-brand-copy">
          <strong>
            宇宙来电 <b>V3.2</b>
          </strong>
          <span>{ageMode.label}</span>
        </div>
      </div>
      <ChapterRibbon state={state} />
      <div className="child-top-actions">
        <TargetBadge scenario={scenario} compact />
        <span className="prototype-badge" data-tone="violet">教学模拟</span>
        <button
          className="child-pause-request"
          type="button"
          disabled={!canPause}
          onClick={() =>
            childDispatch(state, "REQUEST_SAFE_PAUSE", {
              reason: "孩子请求先停一下",
            })}
        >
          需要停一下
        </button>
      </div>
    </header>
  );
}

function StoryCopy({ eyebrow, title, body }) {
  return (
    <div className="story-column">
      <div className="stage-eyebrow">{eyebrow}</div>
      <h1>{title}</h1>
      <p>{body}</p>
    </div>
  );
}

function PrepScene({ scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "prep");
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label={`儿童屏·${scenario.shortLabel}待命`}
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div className="picture-side-panel">
        <TargetBadge scenario={scenario} />
        <StoryCopy
          eyebrow="看图就知道下一步"
          title={phaseCopy.childTitle}
          body={phaseCopy.childBody}
        />
      </div>
    </section>
  );
}

function claimedRoleIds(state) {
  return Array.isArray(state.responses && state.responses.roleClaims)
    ? state.responses.roleClaims
    : [];
}

function WelcomeScene({ state, scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "welcome");
  const claimed = claimedRoleIds(state);
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label="儿童屏·看图组队"
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div
        className="role-constellation picture-role-grid"
        aria-label="观测小队角色"
      >
        {scenario.roles.map((role, index) => {
          const roleId = ROLE_IDS[index] || `role-${index}`;
          const selected = claimed.includes(roleId);
          return (
            <button
              className={`role-star role-claim ${selected ? "claimed" : ""}`}
              type="button"
              aria-pressed={selected}
              disabled={state.responses.teamReady}
              key={role.name}
              onClick={() => childDispatch(state, "CLAIM_ROLE", { roleId })}
            >
              <img src={role.asset} alt="" aria-hidden="true" />
              <div>
                <strong>{role.name}</strong>
                {ageBand === "9-12" ? <small>{role.detail}</small> : null}
              </div>
              <span className="role-claim-state" aria-hidden="true">
                {selected ? "已认领" : "点我认领"}
              </span>
            </button>
          );
        })}
        {claimed.length && !state.responses.teamReady
          ? (
            <button
              className="role-reset"
              type="button"
              onClick={() => childDispatch(state, "CLEAR_ROLE_CLAIMS")}
            >
              重新分角色
            </button>
          )
          : null}
      </div>
    </section>
  );
}

function PredictScene({ state, scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "predict");
  const prediction = state.responses.prediction;
  const option = scenarioModel.getAnswers(scenario.id, "prediction", ageBand)
    .find((item) => item.value === prediction);
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label={`儿童屏·${scenario.shortLabel}预测`}
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div className="picture-side-panel prediction-picture-panel">
        <img src={scenario.targetAsset} alt="" aria-hidden="true" />
        <StoryCopy
          eyebrow="科学家会先猜一猜"
          title={phaseCopy.childTitle}
          body={phaseCopy.childBody}
        />
        <div
          className={`prediction-capsule ${option ? "sealed" : ""}`}
          aria-live="polite"
        >
          {option
            ? <PictureChoiceArt kind={option.art} />
            : <img src={V3Pictograms.mascot.think} alt="" aria-hidden="true" />}
          <span>
            <small>{option ? "猜想已经封存" : "等待小队放入猜想"}</small>
            <strong>{option ? option.label : "选一张图卡"}</strong>
          </span>
          <i aria-hidden="true">{option ? "封" : "?"}</i>
        </div>
      </div>
    </section>
  );
}

function PreflightScene({ state, scenario, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "preflight");
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label="儿童屏·老师安全预检"
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div className="preflight-stars" aria-label="老师预检进度，只读">
        {Object.entries(preflightLabels).map(([key, label]) => {
          const ready = Boolean(state.safety.preflight.checks[key]);
          const pictureAsset = key === "routeReviewed"
            ? scenario.targetAsset
            : label.asset;
          const displayTitle =
            key === "routeReviewed" && scenario.preflightRoute
              ? scenario.preflightRoute.childTitle
              : label.title;
          const displayDetail =
            key === "routeReviewed" && scenario.preflightRoute
              ? scenario.preflightRoute.childDetail
              : label.detail;
          return (
            <article
              className={`preflight-star ${ready ? "ready" : ""}`}
              key={key}
            >
              <img
                className="preflight-picture"
                src={pictureAsset}
                alt=""
                aria-hidden="true"
              />
              <div>
                <strong>{displayTitle}</strong>
                <small>{ready ? "老师已确认" : displayDetail}</small>
              </div>
              <span className="star-icon" aria-hidden="true">
                {ready ? "✓" : "·"}
              </span>
            </article>
          );
        })}
      </div>
    </section>
  );
}

function RfiSweepScene({ state, scenario, phaseCopy, ageBand }) {
  const progress = Math.round(state.movement.progress || 0);
  const arrived = Boolean(state.movement.arrived);
  const instruction = scenarioModel.instructionFor(scenario.id, "slew");
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label="儿童屏·展开 L 波段宽频谱"
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div
        className="rfi-sweep-panel"
        style={{ "--scan-progress": `${progress}%` }}
        aria-label={`L 波段模拟宽扫进度 ${progress}%`}
      >
        <div className="rfi-sweep-head">
          <div>
            <span>L-BAND · TEACHING PANORAMA</span>
            <strong>{arrived ? "频谱展开啦" : phaseCopy.childTitle}</strong>
          </div>
          <img src={V3Pictograms.object.dish} alt="" aria-hidden="true" />
        </div>
        <div className="rfi-frequency-window" aria-hidden="true">
          <span className="rfi-noise-floor"></span>
          <span className="rfi-peak short"></span>
          <span className="rfi-peak unknown"></span>
          <span className="rfi-wide-block"></span>
          <span className="rfi-scan-beam"></span>
        </div>
        <div className="rfi-frequency-labels" aria-hidden="true">
          <span>1000</span>
          <span>1250</span>
          <span>1500</span>
          <span>1750</span>
          <span>2000 MHz</span>
        </div>
        <div className="motion-readout rfi-motion-readout">
          <div className="motion-track" aria-hidden="true">
            <span style={{ "--motion-width": `${progress}%` }}></span>
          </div>
          <strong>{progress}%</strong>
        </div>
      </div>
    </section>
  );
}

function SlewScene({ state, scenario, phaseCopy, ageBand }) {
  if (scenario.kind === "rfi_scan") {
    return (
      <RfiSweepScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    );
  }
  const progress = Math.round(state.movement.progress || 0);
  const arrived = Boolean(state.movement.arrived);
  const instruction = scenarioModel.instructionFor(scenario.id, "slew");
  const isSatellite = scenario.kind === "satellite_pass";
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label={`儿童屏·模拟寻找${scenario.targetName}`}
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div
        className={`sky-path ${isSatellite ? "satellite-path" : ""}`}
        style={{
          "--scope-left": `${18 + progress * 0.42}%`,
          "--scope-bottom": `${12 + progress * 0.25}%`,
          "--satellite-left": `${12 + progress * 0.62}%`,
          "--satellite-bottom": `${10 + progress * 0.38}%`,
        }}
        aria-label={`模拟寻找${scenario.targetName}进度 ${progress}%`}
      >
        {isSatellite
          ? (
            <React.Fragment>
              <img
                className="sky-satellite"
                src={scenario.targetAsset}
                alt=""
                aria-hidden="true"
              />
              <img
                className="sky-dish"
                src={V3Pictograms.object.dish}
                alt=""
                aria-hidden="true"
              />
            </React.Fragment>
          )
          : <div className="sky-sun" aria-hidden="true" />}
        <div className="scope-marker" aria-hidden="true"></div>
        <div className="sky-phase-label">
          <strong>{arrived ? "到达啦" : phaseCopy.childTitle}</strong>
          <span>
            {arrived
              ? `等老师开始看${scenario.targetName}信号`
              : "只看屏幕，不碰设备"}
          </span>
        </div>
        <div className="motion-readout">
          <div className="motion-track" aria-hidden="true">
            <span style={{ "--motion-width": `${progress}%` }}></span>
          </div>
          <strong>{progress}%</strong>
        </div>
      </div>
    </section>
  );
}

function setupCanvas(canvas, draw, animate = true) {
  if (!canvas) return () => {};
  let frame = 0;
  let width = 0;
  let height = 0;
  let stopped = false;
  const reducedMotion =
    window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const resize = () => {
    const rect = canvas.getBoundingClientRect();
    width = Math.max(1, rect.width);
    height = Math.max(1, rect.height);
    const scale = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(width * scale);
    canvas.height = Math.round(height * scale);
    const context = canvas.getContext("2d");
    context.setTransform(scale, 0, 0, scale, 0, 0);
    draw(context, width, height, 0);
  };

  const loop = (time) => {
    if (stopped) return;
    const context = canvas.getContext("2d");
    draw(context, width, height, time);
    if (animate && !reducedMotion) frame = window.requestAnimationFrame(loop);
  };

  const observer = typeof ResizeObserver === "function"
    ? new ResizeObserver(resize)
    : null;
  if (observer) observer.observe(canvas);
  window.addEventListener("resize", resize);
  resize();
  if (animate && !reducedMotion) frame = window.requestAnimationFrame(loop);

  return () => {
    stopped = true;
    if (frame) window.cancelAnimationFrame(frame);
    if (observer) observer.disconnect();
    window.removeEventListener("resize", resize);
  };
}

function drawGrid(context, width, height, palette = {}) {
  const xAxis = palette.xAxis && typeof palette.xAxis === "object"
    ? palette.xAxis
    : { kind: "time", label: "时间" };
  const frequencyAxis = xAxis.kind === "frequency";
  const pad = { left: 68, right: 28, top: 30, bottom: frequencyAxis ? 58 : 48 };
  const plotWidth = Math.max(10, width - pad.left - pad.right);
  const plotHeight = Math.max(10, height - pad.top - pad.bottom);
  const lineColor = palette.line || "rgba(169, 183, 212, 0.16)";
  const textColor = palette.text || "#a9b7d4";
  const background = palette.background || "rgba(3, 11, 28, 1)";

  context.clearRect(0, 0, width, height);
  context.fillStyle = background;
  context.fillRect(0, 0, width, height);
  context.strokeStyle = lineColor;
  context.lineWidth = 1;
  context.font =
    "700 12px -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif";
  context.fillStyle = textColor;

  for (let row = 0; row <= 4; row += 1) {
    const y = pad.top + (plotHeight * row) / 4;
    context.beginPath();
    context.moveTo(pad.left, y);
    context.lineTo(width - pad.right, y);
    context.stroke();
    const value = 100 - row * 25;
    context.fillText(String(value), 27, y + 4);
  }
  const frequencyTicks = frequencyAxis && Array.isArray(xAxis.ticks) &&
      Number.isFinite(Number(xAxis.min)) && Number.isFinite(Number(xAxis.max))
    ? xAxis.ticks.map((tick) => ({
      label: String(tick),
      t: (Number(tick) - Number(xAxis.min)) /
        Math.max(1, Number(xAxis.max) - Number(xAxis.min)),
    })).filter((tick) => tick.t >= 0 && tick.t <= 1)
    : [];
  const columns = frequencyTicks.length
    ? frequencyTicks
    : Array.from({ length: 7 }, (_, index) => ({ label: null, t: index / 6 }));
  columns.forEach((column) => {
    const x = pad.left + plotWidth * column.t;
    context.beginPath();
    context.moveTo(x, pad.top);
    context.lineTo(x, height - pad.bottom);
    context.stroke();
    if (column.label) {
      context.textAlign = "center";
      context.fillText(column.label, x, height - 19);
    }
  });
  context.textAlign = "right";
  if (frequencyAxis) {
    context.fillText(
      `${xAxis.label || "频率"} / ${xAxis.unit || "MHz"}`,
      width - pad.right,
      20,
    );
  } else {
    context.fillText(
      `${xAxis.label || "时间"} →`,
      width - pad.right,
      height - 17,
    );
  }
  context.textAlign = "left";
  return { ...pad, plotWidth, plotHeight };
}

function drawScenarioSeries(context, plot, series, color, fillColor) {
  const points = series.map((point) => [
    plot.left + point.t * plot.plotWidth,
    plot.top + (1 - point.value / 100) * plot.plotHeight,
  ]);
  if (fillColor) {
    const gradient = context.createLinearGradient(
      0,
      plot.top,
      0,
      plot.top + plot.plotHeight,
    );
    gradient.addColorStop(0, fillColor);
    gradient.addColorStop(1, "rgba(41, 216, 242, 0.01)");
    context.beginPath();
    points.forEach(([x, y], index) =>
      index === 0 ? context.moveTo(x, y) : context.lineTo(x, y)
    );
    context.lineTo(plot.left + plot.plotWidth, plot.top + plot.plotHeight);
    context.lineTo(plot.left, plot.top + plot.plotHeight);
    context.closePath();
    context.fillStyle = gradient;
    context.fill();
  }
  context.beginPath();
  points.forEach(([x, y], index) =>
    index === 0 ? context.moveTo(x, y) : context.lineTo(x, y)
  );
  context.strokeStyle = color;
  context.lineWidth = 4;
  context.lineJoin = "round";
  context.lineCap = "round";
  context.stroke();
}

function SignalCanvas({ scenario, active, observation }) {
  const canvasRef = useRef(null);

  useEffect(() =>
    setupCanvas(canvasRef.current, (context, width, height) => {
      const plot = drawGrid(context, width, height, {
        xAxis: scenario.observe.xAxis,
      });
      const color = scenario.kind === "satellite_pass"
        ? "#6ee8ef"
        : scenario.kind === "rfi_scan"
        ? "#ff9a7c"
        : "#ffc857";
      const fillColor = scenario.kind === "satellite_pass"
        ? "rgba(41, 216, 242, 0.32)"
        : scenario.kind === "rfi_scan"
        ? "rgba(255, 124, 124, 0.3)"
        : "rgba(255, 200, 87, 0.28)";
      drawScenarioSeries(
        context,
        plot,
        scenario.observe.points,
        color,
        fillColor,
      );
      const startedAt = Number(observation && observation.startedAt);
      const durationMs = Number(observation && observation.durationMs) ||
        scenario.observe.durationMs;
      const elapsedRatio = startedAt > 0
        ? Math.max(0, (Date.now() - startedAt) / durationMs)
        : 0;
      const playhead = startedAt > 0
        ? scenario.kind === "rfi_scan"
          ? elapsedRatio % 1
          : Math.max(0, Math.min(1, elapsedRatio))
        : 0;
      const value = scenarioModel.sampleSeries(
        scenario.observe.points,
        playhead,
      );
      const x = plot.left + playhead * plot.plotWidth;
      const y = plot.top + (1 - value / 100) * plot.plotHeight;
      context.strokeStyle = "rgba(255,255,255,0.62)";
      context.lineWidth = 2;
      context.beginPath();
      context.moveTo(x, plot.top);
      context.lineTo(x, plot.top + plot.plotHeight);
      context.stroke();
      context.fillStyle = color;
      context.beginPath();
      context.arc(x, y, 7, 0, Math.PI * 2);
      context.fill();
    }, active), [
    active,
    scenario.id,
    observation && observation.startedAt,
    observation && observation.durationMs,
  ]);

  return (
    <canvas
      ref={canvasRef}
      role="img"
      aria-label={`${scenario.observe.signalLabel}，纵轴固定为 0 到 100。${scenario.observe.disclaimer}`}
    />
  );
}

function VisualModeToggle({ state }) {
  const ageDefault = state.config.ageBand === "9-12"
    ? "detective"
    : "discovery";
  const mode = state.visualization && state.visualization.mode
    ? state.visualization.mode
    : ageDefault;
  return (
    <div className="visual-mode-toggle" role="group" aria-label="数据画面深度">
      <button
        type="button"
        className={mode === "discovery" ? "active" : ""}
        aria-pressed={mode === "discovery"}
        onClick={() =>
          childDispatch(state, "SET_VISUAL_MODE", { mode: "discovery" })}
      >
        <span aria-hidden="true">◉</span> 看图发现
      </button>
      <button
        type="button"
        className={mode === "detective" ? "active" : ""}
        aria-pressed={mode === "detective"}
        onClick={() =>
          childDispatch(state, "SET_VISUAL_MODE", { mode: "detective" })}
      >
        <span aria-hidden="true">⌁</span> 数据侦探
      </button>
    </div>
  );
}

const CAPTURE_ROLE_STEPS = [
  { id: "navigator", action: "喊停·瞄准" },
  { id: "signal_detective", action: "选图案" },
  { id: "evidence_keeper", action: "锁星星" },
  { id: "storyteller", action: "来源待查" },
];

function getCapture(state) {
  return state.capture && typeof state.capture === "object" ? state.capture : {
    status: "scanning",
    cursorT: 0.5,
    selectedReasonId: null,
    activeRoleId: null,
    feedback: null,
  };
}

function clampCaptureT(value) {
  return Math.max(0, Math.min(1, Number(value) || 0));
}

function liveObservationT(state, scenario) {
  const startedAt = Number(state.observation && state.observation.startedAt);
  const duration = Math.max(
    800,
    Number(state.observation && state.observation.durationMs) ||
      Number(scenario.observe.durationMs) ||
      12000,
  );
  if (!Number.isFinite(startedAt) || startedAt <= 0) return 0.5;
  const elapsed = Math.max(0, Date.now() - startedAt) / duration;
  return scenario.kind === "rfi_scan" ? elapsed % 1 : clampCaptureT(elapsed);
}

function snapCaptureT(scenario, rawT, ageBand) {
  const points = Array.isArray(scenario.observe && scenario.observe.points)
    ? scenario.observe.points
    : [];
  const value = clampCaptureT(rawT);
  if (!points.length) return value;
  const nearest = points.reduce((best, point) => {
    const distance = Math.abs(Number(point.t) - value);
    return !best || distance < best.distance ? { point, distance } : best;
  }, null);
  const radius = ageBand === "9-12" ? 0.022 : 0.075;
  return nearest && nearest.distance <= radius
    ? clampCaptureT(nearest.point.t)
    : value;
}

function captureXAxisValue(scenario, cursorT) {
  const axis = scenario.observe && scenario.observe.xAxis;
  const min = Number(axis && axis.min);
  const max = Number(axis && axis.max);
  if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
    return null;
  }
  return min + (max - min) * clampCaptureT(cursorT);
}

function captureConfidence(scenario, cursorT) {
  const points = Array.isArray(scenario.observe && scenario.observe.points)
    ? scenario.observe.points
    : [];
  if (points.length < 3) return "待确认";
  const target = points.reduce(
    (best, point, index) =>
      Math.abs(Number(point.t) - cursorT) < best.distance
        ? { index, distance: Math.abs(Number(point.t) - cursorT) }
        : best,
    { index: 0, distance: Infinity },
  );
  const current = Number(points[target.index].value) || 0;
  const left = Number(points[Math.max(0, target.index - 1)].value) || current;
  const right =
    Number(points[Math.min(points.length - 1, target.index + 1)].value) ||
    current;
  const contrast = current - (left + right) / 2;
  if (current >= 68 || contrast >= 10) return "较明显";
  if (current >= 38 || Math.abs(contrast) >= 4) return "可复核";
  return "待比较";
}

function selectedCaptureReason(scenario, capture) {
  if (!gameModel || typeof gameModel.getEvidenceReasonCards !== "function") {
    return null;
  }
  return gameModel.getEvidenceReasonCards(scenario.id).find((reason) =>
    reason.id === capture.selectedReasonId
  ) || null;
}

function CaptureRoleRelay({ state, scenario }) {
  const capture = getCapture(state);
  const statusRole = {
    scanning: "navigator",
    frozen: "signal_detective",
    reasoned: "evidence_keeper",
    locked: "storyteller",
    confirm: "storyteller",
  }[capture.status] || "navigator";
  const activeIndex = Math.max(
    0,
    CAPTURE_ROLE_STEPS.findIndex((role) => role.id === statusRole),
  );
  const claimed = claimedRoleIds(state);

  return (
    <div className="capture-role-relay" aria-label="小队接力路线">
      {CAPTURE_ROLE_STEPS.map((step, index) => {
        const role = scenario.roles[index] || {};
        const isCurrent = index === activeIndex;
        const isDone = index < activeIndex || capture.status === "confirm";
        return (
          <div
            className={`${isCurrent ? "current" : ""} ${isDone ? "done" : ""} ${
              claimed.length && !claimed.includes(step.id) ? "unclaimed" : ""
            }`}
            key={step.id}
            aria-current={isCurrent ? "step" : undefined}
          >
            <span className="capture-role-picture">
              <img
                src={role.asset || V3Pictograms.object.signal}
                alt=""
                aria-hidden="true"
              />
              <i aria-hidden="true">{isDone ? "✓" : index + 1}</i>
            </span>
            <span>
              <strong>{role.name || step.action}</strong>
              <small>{step.action}</small>
            </span>
          </div>
        );
      })}
    </div>
  );
}

function CaptureFeedback({ capture }) {
  const feedback = capture.feedback;
  const message = feedback && feedback.message ? feedback.message : "";
  if (!message) return null;
  return (
    <div
      className="capture-feedback-float"
      data-kind={feedback.kind || "info"}
      data-code={feedback.code || ""}
      role="status"
      aria-live="polite"
    >
      <span aria-hidden="true">
        {feedback.kind === "success"
          ? "✓"
          : feedback.kind === "warning"
          ? "!"
          : "i"}
      </span>
      <strong>{message}</strong>
    </div>
  );
}

function CaptureAimOverlay({ state, scenario, ageBand }) {
  const capture = getCapture(state);
  const canMove = ["frozen", "reasoned"].includes(capture.status);
  if (!["frozen", "reasoned", "locked"].includes(capture.status)) {
    return null;
  }
  const cursorT = clampCaptureT(capture.cursorT);
  const signalValue = scenarioModel.sampleSeries(
    scenario.observe.points,
    cursorT,
  );
  const cursorY = Math.max(10, Math.min(88, 100 - signalValue));
  const frequency = captureXAxisValue(scenario, cursorT);
  const confidence = captureConfidence(scenario, cursorT);

  const moveTo = (rawT, inputSource) => {
    if (!canMove) return;
    childDispatch(state, "SET_CAPTURE_CURSOR", {
      sampleT: snapCaptureT(scenario, rawT, ageBand),
      inputSource: inputSource || "touch",
      roleId: "navigator",
    });
  };
  const moveFromPointer = (event) => {
    if (!canMove) return;
    const rect = event.currentTarget.getBoundingClientRect();
    if (!rect.width) return;
    moveTo((event.clientX - rect.left) / rect.width, "touch");
  };

  return (
    <div
      className={`capture-aim-overlay capture-${capture.status}`}
      style={{
        "--capture-x": `${cursorT * 100}%`,
        "--capture-y": `${cursorY}%`,
      }}
      data-screen-label="数据磁力瞄准区"
    >
      <button
        className="capture-nudge capture-nudge-left"
        type="button"
        disabled={!canMove}
        aria-label="向左移动捕捉环"
        onClick={() =>
          moveTo(cursorT - (ageBand === "9-12" ? 0.018 : 0.045), "touch")}
      >
        <span aria-hidden="true">‹</span>
      </button>
      <div
        className="capture-aim-field"
        role="slider"
        tabIndex={canMove ? 0 : -1}
        aria-label="数据磁力捕捉环"
        aria-valuemin="0"
        aria-valuemax="100"
        aria-valuenow={Math.round(cursorT * 100)}
        aria-valuetext={ageBand === "9-12" && Number.isFinite(frequency)
          ? `${Math.round(frequency)} MHz，位置把握${confidence}`
          : `捕捉环在数据${Math.round(cursorT * 100)}% 处`}
        onPointerDown={(event) => {
          if (!canMove) return;
          event.currentTarget.setPointerCapture(event.pointerId);
          moveFromPointer(event);
        }}
        onPointerMove={(event) => {
          if (
            canMove && event.currentTarget.hasPointerCapture(event.pointerId)
          ) {
            moveFromPointer(event);
          }
        }}
      >
        <span className="capture-aim-line" aria-hidden="true"></span>
        <span className="capture-reticle" aria-hidden="true">
          <i></i>
          <img src={V3Pictograms.object.signal} alt="" />
        </span>
        <span className="capture-position-chip">
          {ageBand === "9-12" && Number.isFinite(frequency)
            ? (
              <React.Fragment>
                <strong>{Math.round(frequency)} MHz</strong>
                <small>位置把握：{confidence}</small>
              </React.Fragment>
            )
            : (
              <React.Fragment>
                <strong>吸住这里</strong>
                <small>看形状，不猜来源</small>
              </React.Fragment>
            )}
        </span>
      </div>
      <button
        className="capture-nudge capture-nudge-right"
        type="button"
        disabled={!canMove}
        aria-label="向右移动捕捉环"
        onClick={() =>
          moveTo(cursorT + (ageBand === "9-12" ? 0.018 : 0.045), "touch")}
      >
        <span aria-hidden="true">›</span>
      </button>
    </div>
  );
}

function CaptureReasonCards({ state, scenario, ageBand }) {
  const capture = getCapture(state);
  const cards =
    gameModel && typeof gameModel.getEvidenceReasonCards === "function"
      ? gameModel.getEvidenceReasonCards(scenario.id)
      : [];
  return (
    <div
      className="capture-reason-grid"
      role="group"
      aria-label="我看到的数据形状"
    >
      {cards.map((card) => {
        const selected = card.id === capture.selectedReasonId;
        const art = card.art === "peak" ? "pulse" : card.art;
        return (
          <button
            className={`capture-reason-card ${selected ? "selected" : ""}`}
            data-tone={card.tone || "cyan"}
            type="button"
            aria-pressed={selected}
            aria-label={`${card.label}。${card.description}`}
            key={card.id}
            onClick={() =>
              childDispatch(state, "SELECT_EVIDENCE_REASON", {
                reasonId: card.id,
                roleId: "signal_detective",
              })}
          >
            <PictureChoiceArt kind={art} />
            <span>
              <strong>{card.shortLabel || card.label}</strong>
              {ageBand === "9-12" ? <small>{card.description}</small> : null}
            </span>
            <i aria-hidden="true">{selected ? "✓" : ""}</i>
          </button>
        );
      })}
    </div>
  );
}

function CaptureTeamDock({ state, scenario, ageBand }) {
  const capture = getCapture(state);
  const markerCount = state.responses.markers.length;
  const selectedReason = selectedCaptureReason(scenario, capture);
  const maxReached = markerCount >= 3;
  const cancelButton = !["scanning", "confirm"].includes(capture.status)
    ? (
      <button
        className="capture-cancel"
        type="button"
        onClick={() =>
          childDispatch(state, "CANCEL_CAPTURE", {
            message: "已取消这次误触，探照灯继续扫描。",
          })}
      >
        <span aria-hidden="true">×</span> 按错了·不保存
      </button>
    )
    : null;

  let controls = null;
  if (capture.status === "scanning") {
    controls = (
      <div className="capture-scan-controls">
        <button
          className={`capture-shout-button ${maxReached ? "complete" : ""}`}
          type="button"
          disabled={maxReached}
          onClick={() =>
            childDispatch(state, "FREEZE_SIGNAL", {
              sampleT: snapCaptureT(
                scenario,
                liveObservationT(state, scenario),
                ageBand,
              ),
              inputSource: "touch",
              roleId: "navigator",
            })}
        >
          <img src={V3Pictograms.mascot.point} alt="" aria-hidden="true" />
          <span className="capture-pause-symbol" aria-hidden="true">
            <i></i>
            <i></i>
          </span>
          <span>
            <strong>{maxReached ? "三颗证据星收好啦" : "我看到了"}</strong>
            <small>
              {maxReached ? "现在和老师一起比较" : "按下·让画面停住"}
            </small>
          </span>
          <EvidenceCounter count={markerCount} />
        </button>
        {markerCount > 0
          ? (
            <button
              className="capture-undo"
              type="button"
              onClick={() => childDispatch(state, "UNDO_LAST_MARKER")}
            >
              <span aria-hidden="true">↶</span> 撤回上一颗
            </button>
          )
          : null}
      </div>
    );
  } else if (capture.status === "frozen") {
    controls = (
      <CaptureReasonCards state={state} scenario={scenario} ageBand={ageBand} />
    );
  } else if (capture.status === "reasoned") {
    controls = (
      <div className="capture-lock-row">
        <div className="capture-selected-reason">
          <PictureChoiceArt
            kind={selectedReason && selectedReason.art === "peak"
              ? "pulse"
              : selectedReason && selectedReason.art}
          />
          <span>
            <small>信号侦探说</small>
            <strong>
              {selectedReason ? selectedReason.label : "已选图案"}
            </strong>
          </span>
          <button
            type="button"
            onClick={() =>
              childDispatch(state, "CANCEL_CAPTURE", {
                message: "已放回这次选择，可以重新瞄准。",
              })}
          >
            重选
          </button>
        </div>
        <button
          className="capture-lock-button"
          type="button"
          onClick={() =>
            childDispatch(state, "LOCK_EVIDENCE", {
              roleId: "evidence_keeper",
            })}
        >
          <img src={V3Pictograms.object.evidence} alt="" aria-hidden="true" />
          <span>
            <strong>把星星锁上去</strong>
            <small>证据记录员</small>
          </span>
        </button>
      </div>
    );
  } else if (capture.status === "locked") {
    controls = (
      <button
        className="capture-confirm-button"
        type="button"
        onClick={() =>
          childDispatch(state, "CONFIRM_TEAM_EVIDENCE", {
            roleId: "storyteller",
          })}
      >
        <span className="capture-question-shield" aria-hidden="true">?</span>
        <span>
          <strong>来源待查·大家确认</strong>
          <small>只保存位置、强弱和形状</small>
        </span>
        <img src={V3Pictograms.mascot.present} alt="" aria-hidden="true" />
      </button>
    );
  } else {
    controls = (
      <div className="capture-confirmed-row">
        <img src={V3Pictograms.object.evidence} alt="" aria-hidden="true" />
        <span>
          <small>全队完成</small>
          <strong>证据星已收好·来源仍待查</strong>
        </span>
        <button
          type="button"
          onClick={() =>
            childDispatch(state, "CANCEL_CAPTURE", {
              message: "小队开始寻找下一种形状。",
            })}
        >
          继续扫描
        </button>
        <button
          className="capture-undo"
          type="button"
          onClick={() => childDispatch(state, "UNDO_LAST_MARKER")}
        >
          撤回这颗
        </button>
      </div>
    );
  }

  return (
    <div
      className="child-action-dock capture-team-dock"
      data-capture-status={capture.status}
    >
      <CaptureRoleRelay state={state} scenario={scenario} />
      {controls}
      {cancelButton}
    </div>
  );
}

function ObserveScene({ state, scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "observe");
  const isRfi = scenario.kind === "rfi_scan";
  const MissionDataStage = gameVisuals.MissionDataStage;
  const visualAgeBand = state.visualization &&
      state.visualization.mode === "detective"
    ? "9-12"
    : "6-8";
  return (
    <section
      className="child-scene data-scene"
      data-scenario={scenario.kind}
      data-screen-label={`儿童屏·${scenario.shortLabel}捕获证据`}
    >
      <div className="data-header">
        <div className="visual-data-cue">
          <MascotPose pose={instruction.pose} />
          <div>
            <div className="stage-eyebrow">
              {scenario.shortLabel} · 固定模拟 · 0–100
            </div>
            <strong>{instruction.verb}</strong>
            <small>
              {ageBand === "9-12"
                ? phaseCopy.childBody
                : scenario.observe.disclaimer}
            </small>
          </div>
        </div>
        <div>
          <TargetBadge scenario={scenario} />
          <EvidenceCounter count={state.responses.markers.length} />
          <VisualModeToggle state={state} />
        </div>
      </div>
      {MissionDataStage
        ? (
          <div
            className={`capture-stage ${isRfi ? "rfi-capture-stage" : ""}`}
          >
            <MissionDataStage
              scenario={scenario}
              state={state}
              ageBand={visualAgeBand}
              active={state.runState === "active" &&
                getCapture(state).status === "scanning"}
            />
            <CaptureAimOverlay
              state={state}
              scenario={scenario}
              ageBand={ageBand}
            />
            <CaptureFeedback capture={getCapture(state)} />
          </div>
        )
        : (
          <div className="signal-stage" data-scenario={scenario.kind}>
            <div className="signal-axis-label">
              {scenario.observe.axisLabel}
            </div>
            <SignalCanvas
              scenario={scenario}
              active={state.runState === "active"}
              observation={state.observation}
            />
            <div
              className="signal-markers"
              aria-label={`已经留下 ${state.responses.markers.length} 个证据标记`}
            >
              {state.responses.markers.map((marker) => {
                const sampleT = Number.isFinite(marker.sampleT)
                  ? marker.sampleT
                  : 0.5;
                const value = scenarioModel.sampleSeries(
                  scenario.observe.points,
                  sampleT,
                );
                return (
                  <span
                    className="evidence-star"
                    key={marker.id}
                    aria-label={marker.label}
                    style={{
                      "--marker-x": `${sampleT * 100}%`,
                      "--marker-y": `${100 - value}%`,
                    }}
                  >
                    {isRfi && Number.isFinite(marker.sampleX)
                      ? <b>{Math.round(marker.sampleX)} MHz</b>
                      : null}
                  </span>
                );
              })}
            </div>
          </div>
        )}
    </section>
  );
}

function StaticChart({ chart, scenario }) {
  const canvasRef = useRef(null);

  useEffect(() =>
    setupCanvas(canvasRef.current, (context, width, height) => {
      const plot = drawGrid(context, width, height, {
        background: "#f6f2e8",
        line: "rgba(11, 20, 48, 0.11)",
        text: "#667186",
        xAxis: scenario.observe.xAxis,
      });
      const isTarget = chart.id === scenario.compare.target.id;
      const targetColor = scenario.kind === "rfi_scan" ? "#df725f" : "#e69328";
      drawScenarioSeries(
        context,
        plot,
        chart.points,
        isTarget ? targetColor : "#2b8193",
      );
      const average = chart.points.reduce((sum, point) =>
        sum + point.value, 0) / chart.points.length;
      const averageY = plot.top + (1 - average / 100) * plot.plotHeight;
      context.setLineDash([8, 8]);
      context.strokeStyle = isTarget
        ? scenario.kind === "rfi_scan"
          ? "rgba(223, 114, 95, 0.62)"
          : "rgba(230, 147, 40, 0.62)"
        : "rgba(43, 129, 147, 0.62)";
      context.lineWidth = 2;
      context.beginPath();
      context.moveTo(plot.left, averageY);
      context.lineTo(plot.left + plot.plotWidth, averageY);
      context.stroke();
      context.setLineDash([]);
    }, false), [scenario.id, chart.id]);

  return (
    <article className="comparison-chart">
      <header>
        <strong>{chart.label}</strong>
        <span>{chart.subtitle}</span>
      </header>
      <canvas
        ref={canvasRef}
        role="img"
        aria-label={`${chart.label}教学模拟曲线，和另一张图使用相同的 0 到 100 纵轴`}
      />
    </article>
  );
}

function CompareScene({ state, scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "compare");
  const CompareLens = gameVisuals.CompareLens;
  const visualAgeBand = state.visualization &&
      state.visualization.mode === "detective"
    ? "9-12"
    : "6-8";
  return (
    <section
      className="child-scene data-scene"
      data-scenario={scenario.kind}
      data-screen-label={`儿童屏·比较${scenario.shortLabel}数据`}
    >
      <div className="data-header">
        <div className="visual-data-cue">
          <MascotPose pose={instruction.pose} />
          <div>
            <div className="stage-eyebrow">同一刻度 · 看图比较</div>
            <strong>{instruction.verb}</strong>
            <small>
              {ageBand === "9-12" ? phaseCopy.childBody : "先看图，再选答案"}
            </small>
          </div>
        </div>
        <div>
          <TargetBadge scenario={scenario} />
          <VisualModeToggle state={state} />
        </div>
      </div>
      {CompareLens
        ? <CompareLens scenario={scenario} ageBand={visualAgeBand} />
        : (
          <div className="comparison-grid">
            <StaticChart
              chart={scenario.compare.background}
              scenario={scenario}
            />
            <StaticChart chart={scenario.compare.target} scenario={scenario} />
          </div>
        )}
    </section>
  );
}

function ConcludeScene({ state, scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "conclude");
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label="儿童屏·形成证据结论"
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div className="picture-side-panel">
        <img
          className="large-evidence-icon"
          src={V3Pictograms.object.evidence}
          alt=""
          aria-hidden="true"
        />
        <StoryCopy
          eyebrow="发现 + 证据"
          title={phaseCopy.childTitle}
          body={phaseCopy.childBody}
        />
        <div
          className="evidence-reasoning-trail"
          aria-label="从猜想到证据再到发现"
        >
          <span>
            <small>我们先猜</small>
            <strong>
              {labelFor(
                scenario,
                "prediction",
                ageBand,
                state.responses.prediction,
              )}
            </strong>
          </span>
          <i aria-hidden="true">→</i>
          <span>
            <small>我们留下</small>
            <strong>{state.responses.markers.length} 颗证据星</strong>
          </span>
          <i aria-hidden="true">→</i>
          <span>
            <small>现在要</small>
            <strong>根据证据说发现</strong>
          </span>
        </div>
      </div>
    </section>
  );
}

function labelFor(scenario, kind, ageBand, value) {
  if (!value) return "等待小队选择";
  return scenarioModel.getAnswerLabel(scenario.id, kind, ageBand, value) ||
    value;
}

function ResultScene({ state, scenario, phaseCopy, ageBand }) {
  const isRfi = scenario.kind === "rfi_scan";
  const EvidenceReplay = gameVisuals.EvidenceReplay;
  const dailyEvidence = window.V32DailyEvidence;
  const [view, setView] = useState(EvidenceReplay ? "replay" : "summary");
  const { snapshot, loading } = dailyEvidence.useDailyEvidenceSnapshot(
    scenario,
    state,
  );
  const resultSummary = {
    prediction: labelFor(
      scenario,
      "prediction",
      ageBand,
      state.responses.prediction,
    ),
    conclusion: labelFor(
      scenario,
      "conclusion",
      ageBand,
      state.responses.conclusion,
    ),
  };
  const archive = (
    <dailyEvidence.DailyEvidenceSection
      snapshot={snapshot}
      loading={loading}
    />
  );
  return (
    <section
      className="child-scene result-scene v32-result-scene"
      data-screen-label="儿童屏·团队发现卡"
    >
      <div className="result-copy">
        <MascotPose pose="present" />
        <div className="stage-eyebrow">猜想 · 证据 · 发现</div>
        <h1>{phaseCopy.childTitle}</h1>
        {ageBand === "9-12" ? <p>{phaseCopy.childBody}</p> : null}
      </div>
      <div className="v3-result-workspace">
        <div className="v32-result-toolbar">
          <nav className="v3-result-tabs" aria-label="成果查看方式">
            <button
              type="button"
              className={view === "replay" ? "active" : ""}
              aria-pressed={view === "replay"}
              onClick={() => setView("replay")}
              disabled={!EvidenceReplay}
            >
              证据回放
            </button>
            <button
              type="button"
              className={view === "summary" ? "active" : ""}
              aria-pressed={view === "summary"}
              onClick={() => setView("summary")}
            >
              小队发现卡
            </button>
          </nav>
          <button
            type="button"
            className="v32-print-button"
            disabled={!snapshot}
            onClick={dailyEvidence.printDailyEvidence}
          >
            打印今日发现卡
          </button>
        </div>
        {view === "summary"
          ? (
            <div className="v32-result-summary-stack">
              <article
                className="result-card"
                aria-label={`${scenario.result.cardTitle}发现卡`}
              >
                <header className="result-card-head">
                  <div>
                    <span>宇宙来电 · 团队发现记录</span>
                    <strong>{scenario.result.cardTitle}</strong>
                    <small>{scenario.result.sourceLabel}</small>
                  </div>
                  <img
                    src={scenario.targetAsset}
                    alt={`${scenario.targetName}任务图标`}
                  />
                </header>
                {isRfi
                  ? (
                    <div className="result-evidence rfi-report-evidence">
                      <div>
                        <img
                          src={V3Pictograms.object.signal}
                          alt=""
                          aria-hidden="true"
                        />
                        <span>固定模拟包含</span>
                        <strong>局部频段增强和间歇窄峰</strong>
                      </div>
                      <div>
                        <img
                          src={V3Pictograms.object.evidence}
                          alt=""
                          aria-hidden="true"
                        />
                        <span>小队的结论</span>
                        <strong>
                          {resultSummary.conclusion} ·{" "}
                          {state.responses.markers.length} 个频率证据点
                        </strong>
                      </div>
                      <div>
                        <img
                          src={V3Pictograms.mascot.present}
                          alt=""
                          aria-hidden="true"
                        />
                        <span>还不能说</span>
                        <strong>一定来自某台手机，或知道通信内容</strong>
                      </div>
                    </div>
                  )
                  : (
                    <div className="result-evidence">
                      <div>
                        <img
                          src={V3Pictograms.mascot.think}
                          alt=""
                          aria-hidden="true"
                        />
                        <span>先猜</span>
                        <strong>{resultSummary.prediction}</strong>
                      </div>
                      <div>
                        <img
                          src={V3Pictograms.object.evidence}
                          alt=""
                          aria-hidden="true"
                        />
                        <span>找证据</span>
                        <strong>
                          {state.responses.markers.length} 颗证据星 ·{" "}
                          {scenario.result.evidenceText}
                        </strong>
                      </div>
                      <div>
                        <img
                          src={V3Pictograms.mascot.present}
                          alt=""
                          aria-hidden="true"
                        />
                        <span>说发现</span>
                        <strong>{resultSummary.conclusion}</strong>
                      </div>
                    </div>
                  )}
                <div className="role-seals" aria-label="小队完成的科学工作">
                  <span>提出预测</span>
                  <span>观察变化</span>
                  <span>保存证据</span>
                  <span>比较数据</span>
                  <span>说明发现</span>
                </div>
              </article>
              {archive}
            </div>
          )
          : (
            <div className="v32-result-story">
              <div className="v32-replay-slot">
                {EvidenceReplay
                  ? (
                    <EvidenceReplay
                      scenario={scenario}
                      markers={state.responses.markers}
                      ageBand={ageBand}
                      compact
                      durationMs={state.visualization &&
                          state.visualization.replay &&
                          state.visualization.replay.durationMs
                        ? state.visualization.replay.durationMs
                        : 7000}
                    />
                  )
                  : null}
              </div>
              {archive}
            </div>
          )}
      </div>
      <dailyEvidence.PrintDiscoverySheet
        snapshot={snapshot}
        scenario={scenario}
        state={state}
        resultSummary={resultSummary}
      />
    </section>
  );
}

function CompleteScene({ state, scenario, phaseCopy, ageBand }) {
  const instruction = scenarioModel.instructionFor(scenario.id, "complete");
  const scienceActions = ["敢先猜", "会观察", "存证据", "能比较", "讲边界"];
  return (
    <section
      className="child-scene picture-story-scene"
      data-screen-label="儿童屏·任务完成"
    >
      <VisualInstruction
        scenario={scenario}
        instruction={instruction}
        ageBand={ageBand}
      />
      <div className="picture-side-panel">
        <TargetBadge scenario={scenario} />
        <StoryCopy
          eyebrow="小队安全结束"
          title={phaseCopy.childTitle}
          body={phaseCopy.childBody}
        />
        <div
          className="mission-complete-constellation"
          aria-label="小队完成的五种科学行动"
        >
          {scienceActions.map((action, index) => (
            <span key={action} style={{ "--star-delay": `${index * 90}ms` }}>
              <i aria-hidden="true">✦</i>
              <strong>{action}</strong>
            </span>
          ))}
          <small>
            {claimedRoleIds(state).length} 个角色共同完成 · 不计速度和排名
          </small>
        </div>
      </div>
    </section>
  );
}

function ChildScene({ state }) {
  const scenario = activeScenario(state);
  const phaseCopy = scenarioModel.getPhaseCopy(scenario.id, state.phase);
  const ageBand = state.config.ageBand === "9-12" ? "9-12" : "6-8";
  const scenes = {
    prep: (
      <PrepScene scenario={scenario} phaseCopy={phaseCopy} ageBand={ageBand} />
    ),
    welcome: (
      <WelcomeScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    predict: (
      <PredictScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    preflight: (
      <PreflightScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    slew: (
      <SlewScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    observe: (
      <ObserveScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    compare: (
      <CompareScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    conclude: (
      <ConcludeScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    result: (
      <ResultScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
    complete: (
      <CompleteScene
        state={state}
        scenario={scenario}
        phaseCopy={phaseCopy}
        ageBand={ageBand}
      />
    ),
  };
  return (
    <React.Fragment key={state.phase}>
      {scenes[state.phase] || scenes.prep}
    </React.Fragment>
  );
}

function ChoiceDock({ state, options, selected, actionType }) {
  return (
    <div className="child-action-dock">
      <div
        className="choice-grid"
        style={{ "--choice-count": options.length }}
        role="group"
        aria-label="小队看图选择"
      >
        {options.map((option) => (
          <button
            className={`child-choice picture-choice ${
              selected === option.value ? "selected" : ""
            }`}
            type="button"
            aria-pressed={selected === option.value}
            aria-label={`${option.label}。${option.help || ""}`}
            key={option.value}
            onClick={() =>
              childDispatch(state, actionType, { value: option.value })}
          >
            <PictureChoiceArt kind={option.art} />
            <span className="choice-copy">
              <strong>{option.label}</strong>
              <small>{option.help}</small>
            </span>
            <span className="choice-check" aria-hidden="true">
              {selected === option.value ? "✓" : "·"}
            </span>
          </button>
        ))}
      </div>
    </div>
  );
}

function WaitingDock({ title, detail }) {
  return (
    <div className="child-action-dock waiting" aria-live="polite">
      <div>
        <strong>{title}</strong>
        <span>{detail}</span>
      </div>
    </div>
  );
}

function ActionDock({ state }) {
  const ageBand = state.config.ageBand === "9-12" ? "9-12" : "6-8";
  const scenario = activeScenario(state);
  if (state.phase === "welcome") {
    const roleCount = claimedRoleIds(state).length;
    return (
      <div className="child-action-dock">
        <button
          className={`child-primary ${state.responses.teamReady ? "done" : ""}`}
          type="button"
          disabled={state.responses.teamReady || roleCount === 0}
          onClick={() => childDispatch(state, "TEAM_READY")}
        >
          <img
            className="primary-action-icon"
            src={V3Pictograms.mascot.ready}
            alt=""
            aria-hidden="true"
          />
          <span>
            {state.responses.teamReady
              ? "小队集合完成"
              : roleCount === 0
              ? "先认领一个角色"
              : `${roleCount} 个角色已认领 · 我们准备好了`}
          </span>
        </button>
      </div>
    );
  }
  if (state.phase === "predict") {
    return (
      <ChoiceDock
        state={state}
        options={scenarioModel.getAnswers(scenario.id, "prediction", ageBand)}
        selected={state.responses.prediction}
        actionType="SUBMIT_PREDICTION"
      />
    );
  }
  if (state.phase === "observe") {
    return (
      <CaptureTeamDock state={state} scenario={scenario} ageBand={ageBand} />
    );
  }
  if (state.phase === "compare") {
    return (
      <ChoiceDock
        state={state}
        options={scenarioModel.getAnswers(scenario.id, "comparison", ageBand)}
        selected={state.responses.comparison}
        actionType="SUBMIT_COMPARISON"
      />
    );
  }
  if (state.phase === "conclude") {
    return (
      <ChoiceDock
        state={state}
        options={scenarioModel.getAnswers(scenario.id, "conclusion", ageBand)}
        selected={state.responses.conclusion}
        actionType="SUBMIT_CONCLUSION"
      />
    );
  }

  const instruction = scenarioModel.instructionFor(scenario.id, state.phase);
  const waitingCopy = {
    prep: [instruction.verb, "老师准备好后带大家开始"],
    preflight: ["线后等", "这一步只由老师完成"],
    slew: [
      state.movement.arrived
        ? scenario.kind === "rfi_scan" ? "频谱展开啦" : "到达啦"
        : instruction.verb,
      "只看屏幕，不碰设备",
    ],
    result: ["讲给同伴听", "老师审核后安全结束"],
    complete: ["完成啦", "谢谢每一位小小射电员"],
  };
  const copy = waitingCopy[state.phase] ||
    ["等待老师继续", "小队可以先交换刚才看到的证据"];
  return <WaitingDock title={copy[0]} detail={copy[1]} />;
}

function SafetyOverlay({ state }) {
  if (!state.safety.estop.latched && state.runState !== "paused") return null;
  const estop = state.safety.estop.latched;
  return (
    <div className="paused-overlay" role="alert" aria-live="assertive">
      <article className="paused-card" data-estop={estop ? "true" : "false"}>
        <img src={V3Pictograms.mascot.safe} alt="小海龟站在安全线后" />
        <h2>{estop ? "安全停" : "先停一下"}</h2>
        <p>
          {estop
            ? "请大家留在安全线后，不靠近设备。老师检查完现场后会重新开始预检。"
            : "保持原来的位置，回想刚才看到了什么。只有老师完成安全检查后，任务才会继续。"}
        </p>
      </article>
    </div>
  );
}

function EvidenceCaptureToast({ marker, scenario }) {
  if (!marker) return null;
  const description =
    gameModel && typeof gameModel.describeMarker === "function"
      ? gameModel.describeMarker(scenario.id, marker)
      : scenario.kind === "rfi_scan" && Number.isFinite(Number(marker.sampleX))
      ? `保存了 ${Math.round(Number(marker.sampleX))} MHz 附近`
      : "一颗证据星已经吸附到数据上";
  return (
    <div className="evidence-capture-toast" role="status" aria-live="polite">
      <img src={V3Pictograms.object.evidence} alt="" aria-hidden="true" />
      <span>
        <small>证据保存成功</small>
        <strong>{description}</strong>
      </span>
    </div>
  );
}

function routeCaptureInput(state, scenario, detail) {
  if (
    !detail ||
    state.phase !== "observe" ||
    state.runState !== "active"
  ) {
    return;
  }
  const capture = getCapture(state);
  const inputSource = detail.inputSource === "joystick"
    ? "joystick"
    : detail.inputSource === "keyboard"
    ? "keyboard"
    : "touch";

  if (
    detail.intent === "move" &&
    ["frozen", "reasoned"].includes(capture.status)
  ) {
    const ageBand = state.config.ageBand === "9-12" ? "9-12" : "6-8";
    const baseStep = ageBand === "9-12" ? 0.018 : 0.045;
    const strength = Math.max(0.55, Math.min(1, Number(detail.strength) || 1));
    const direction = Number(detail.direction) < 0 ? -1 : 1;
    childDispatch(state, "SET_CAPTURE_CURSOR", {
      sampleT: snapCaptureT(
        scenario,
        clampCaptureT(capture.cursorT + direction * baseStep * strength),
        ageBand,
      ),
      inputSource,
      roleId: "navigator",
    });
    return;
  }

  if (detail.intent === "activate") {
    if (capture.status === "scanning") {
      childDispatch(state, "FREEZE_SIGNAL", {
        sampleT: snapCaptureT(
          scenario,
          liveObservationT(state, scenario),
          state.config.ageBand,
        ),
        inputSource,
        roleId: "navigator",
      });
    } else if (capture.status === "reasoned") {
      childDispatch(state, "LOCK_EVIDENCE", {
        roleId: "evidence_keeper",
      });
    } else if (capture.status === "locked") {
      childDispatch(state, "CONFIRM_TEAM_EVIDENCE", {
        roleId: "storyteller",
      });
    } else if (capture.status === "confirm") {
      childDispatch(state, "CANCEL_CAPTURE", {
        message: "小队开始寻找下一种形状。",
      });
    }
    return;
  }

  if (detail.intent === "cancel") {
    if (capture.status === "confirm") {
      childDispatch(state, "UNDO_LAST_MARKER");
    } else if (capture.status !== "scanning") {
      childDispatch(state, "CANCEL_CAPTURE", {
        message: "已取消这次误触，探照灯继续扫描。",
      });
    }
  }
}

function resultPreviewState(baseState) {
  const params = new URLSearchParams(window.location.search);
  if (params.get("preview") !== "result") return baseState;
  const scenarioParam = params.get("scenario");
  const scenarioIds = {
    solar: "solar-demo-v1",
    satellite: "leo-pass-demo-v1",
    rfi: "rfi-scan-demo-v1",
  };
  const scenarioId = scenarioIds[scenarioParam] || "solar-demo-v1";
  const ageBand = params.get("age") === "9-12" ? "9-12" : "6-8";
  const prediction = scenarioModel.getAnswers(
    scenarioId,
    "prediction",
    ageBand,
  )[0];
  const conclusion = scenarioModel.getAnswers(
    scenarioId,
    "conclusion",
    ageBand,
  )[0];
  const markerTs = scenarioId === "rfi-scan-demo-v1"
    ? [0.09, 0.48, 0.745]
    : scenarioId === "leo-pass-demo-v1"
    ? [0.28, 0.52, 0.76]
    : [0.22, 0.62, 0.91];
  return {
    ...baseState,
    phase: "result",
    runState: "active",
    config: {
      ...baseState.config,
      scenarioId,
      ageBand,
    },
    responses: {
      ...baseState.responses,
      prediction: prediction && prediction.value,
      conclusion: conclusion && conclusion.value,
      markers: markerTs.map((sampleT, index) => ({
        id: `preview-marker-${scenarioId}-${index}`,
        scenarioId,
        sampleT,
        sampleX: scenarioId === "rfi-scan-demo-v1"
          ? 1000 + sampleT * 1000
          : undefined,
        xUnit: scenarioId === "rfi-scan-demo-v1" ? "MHz" : undefined,
        label: `证据 ${index + 1}`,
        reasonLabel: ["整体抬升", "局部峰值", "间歇变化"][index],
        sourceCertainty: "unconfirmed",
        teamConfirmed: true,
      })),
    },
  };
}

function ChildApp() {
  const sessionState = useSessionState();
  const state = resultPreviewState(sessionState);
  const stateRef = useRef(state);
  stateRef.current = state;
  const scenario = activeScenario(state);
  const screenLabel = `儿童共享舞台 · ${scenario.shortLabel} · ${
    content.phases[state.phase].short
  }`;
  const visualMode = state.visualization && state.visualization.mode
    ? state.visualization.mode
    : state.config.ageBand === "9-12"
    ? "detective"
    : "discovery";
  const [capturedMarker, setCapturedMarker] = useState(null);
  const previousMarkerCountRef = useRef(state.responses.markers.length);
  const markerCount = state.responses.markers.length;
  const latestMarkerId = markerCount
    ? state.responses.markers[markerCount - 1].id
    : null;

  useEffect(() => {
    const eventName =
      window.V32InputAdapter && window.V32InputAdapter.EVENT_NAME
        ? window.V32InputAdapter.EVENT_NAME
        : "v32:capture-input";
    const handleInput = (event) => {
      const currentState = stateRef.current;
      routeCaptureInput(
        currentState,
        activeScenario(currentState),
        event.detail || {},
      );
    };
    window.addEventListener(eventName, handleInput);
    return () => window.removeEventListener(eventName, handleInput);
  }, []);

  useEffect(() => {
    const count = markerCount;
    if (count > previousMarkerCountRef.current) {
      const latest = state.responses.markers[count - 1];
      setCapturedMarker(latest || null);
      const timer = window.setTimeout(() => setCapturedMarker(null), 2200);
      previousMarkerCountRef.current = count;
      return () => window.clearTimeout(timer);
    }
    previousMarkerCountRef.current = count;
    if (count === 0) setCapturedMarker(null);
    return undefined;
  }, [markerCount, latestMarkerId]);

  return (
    <KioskIdleExperience state={state}>
      <main
        className="child-shell picture-first"
        data-phase={state.phase}
        data-run-state={state.runState}
        data-visual-mode={visualMode}
        data-capture-input={state.phase === "observe" &&
            state.runState === "active"
          ? "active"
          : "inactive"}
        data-screen-label={screenLabel}
        aria-label={screenLabel}
      >
        <ChildTopbar state={state} />
        <div className="child-stage">
          <ChildScene state={state} />
          <EvidenceCaptureToast marker={capturedMarker} scenario={scenario} />
        </div>
        <ActionDock state={state} />
        <SafetyOverlay state={state} />
      </main>
    </KioskIdleExperience>
  );
}

ReactDOM.createRoot(document.getElementById("childRoot")).render(<ChildApp />);
