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</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>
  );
}

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>
      {isRfi
        ? (
          <div className="rfi-clue-deck">
            <div className="rfi-pattern-strip" aria-label="L 波段图形线索">
              {scenario.observe.patternCards.map((card) => (
                <article key={card.label}>
                  <PictureChoiceArt kind={card.art} />
                  <span>
                    <strong>{card.label}</strong>
                    <small>{card.detail}</small>
                  </span>
                </article>
              ))}
              <div className="rfi-boundary-stamps" aria-label="观察边界">
                <span>只看强弱</span>
                <span>不听内容</span>
                <span>来源待查</span>
              </div>
            </div>
            <div className="rfi-reference-strip" aria-label="候选频率位置参考">
              {scenario.observe.referenceBands.map((band) => (
                <span key={band.label}>
                  <strong>{band.range}</strong>
                  <small>{band.label}</small>
                </span>
              ))}
            </div>
          </div>
        )
        : null}
      {MissionDataStage
        ? (
          <MissionDataStage
            scenario={scenario}
            state={state}
            ageBand={visualAgeBand}
            active={state.runState === "active"}
          />
        )
        : (
          <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 [view, setView] = useState(EvidenceReplay ? "replay" : "summary");
  return (
    <section
      className="child-scene 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">
        <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>
        {view === "summary"
          ? (
            <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>
                        {labelFor(
                          scenario,
                          "conclusion",
                          ageBand,
                          state.responses.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>
                        {labelFor(
                          scenario,
                          "prediction",
                          ageBand,
                          state.responses.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>
                        {labelFor(
                          scenario,
                          "conclusion",
                          ageBand,
                          state.responses.conclusion,
                        )}
                      </strong>
                    </div>
                  </div>
                )}
              <div className="role-seals" aria-label="小队完成的科学工作">
                <span>提出预测</span>
                <span>观察变化</span>
                <span>保存证据</span>
                <span>比较数据</span>
                <span>说明发现</span>
              </div>
            </article>
          )
          : (
            <div className="result-replay-panel">
              {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>
          )}
      </div>
    </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") {
    const count = state.responses.markers.length;
    const requiredMarkers = Math.max(
      1,
      Number(scenario.observe.minMarkers) || 1,
    );
    return (
      <div className="child-action-dock">
        <button
          className={`child-primary ${count > 0 ? "done" : ""}`}
          type="button"
          disabled={count >= 3}
          onClick={() => {
            const elapsed = state.observation.startedAt
              ? Date.now() - state.observation.startedAt
              : 0;
            const duration = Number(state.observation.durationMs) ||
              scenario.observe.durationMs;
            const rawSampleT = duration > 0 ? elapsed / duration : 0;
            childDispatch(state, "MARK_SIGNAL", {
              label: scenario.observe.markerLabel,
              sampleT: scenario.kind === "rfi_scan"
                ? rawSampleT % 1
                : Math.max(0, Math.min(1, rawSampleT)),
            });
          }}
        >
          <img
            className="primary-action-icon"
            src={V3Pictograms.object.evidence}
            alt=""
            aria-hidden="true"
          />
          <span>
            {scenario.kind === "rfi_scan"
              ? count === 0
                ? "标一处频率"
                : count < requiredMarkers
                ? "再找一种"
                : count < 3
                ? "还可再标"
                : "证据收好啦"
              : count === 0
              ? "发现变化"
              : count < 3
              ? "再找一个"
              : "证据收好啦"}
          </span>
          <EvidenceCounter count={count} />
        </button>
      </div>
    );
  }
  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 ChildApp() {
  const state = useSessionState();
  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 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-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 />);
