const { useCallback, useEffect, useMemo, useRef, useState } = React;

const STORE = window.V2SessionStore;
const CONTENT = window.V2PrototypeContent;
const MODEL = window.V2ScenarioModel;

const SELECTABLE_SCENARIO_IDS = ["solar-demo-v1", "leo-pass-demo-v1", "rfi-scan-demo-v1"];

const RUN_LABELS = {
  idle: "等待开始",
  active: "教学进行中",
  paused: "安全暂停",
  estop: "急停锁定",
  complete: "本轮完成",
};

const PREFLIGHT_ITEMS = [
  {
    key: "zoneClear",
    title: "运动区域无人和障碍物",
    detail: "查看真实设备周围，不以屏幕画面代替现场确认。",
  },
  {
    key: "childrenBehindLine",
    title: "孩子全部位于安全线后",
    detail: "由老师确认人数与站位，孩子不靠近机械结构。",
  },
  {
    key: "routeReviewed",
    title: "老师已说明转向方向",
    detail: "先指给孩子看设备会朝哪一边运动。",
  },
  {
    key: "mockModeAcknowledged",
    title: "确认当前仅为教学模拟",
    detail: "本原型不连接硬件，也不显示历史或实时数据。",
  },
];

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

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

function getActiveScenario(state) {
  return MODEL.getScenario(state.runScenarioId || state.config.scenarioId);
}

function getScenarioPhaseCopy(state, phase = state.phase) {
  const scenario = getActiveScenario(state);
  return MODEL.getPhaseCopy(scenario.id, phase) || CONTENT.phases[phase];
}

function displayAnswer(state, kind, value) {
  if (!value) return "等待儿童屏提交";
  const scenario = getActiveScenario(state);
  return MODEL.getAnswerLabel(scenario.id, kind, state.config.ageBand, value) || value;
}

function getPreflightItems(state) {
  const scenario = getActiveScenario(state);
  return PREFLIGHT_ITEMS.map((item) => {
    if (item.key !== "routeReviewed") return item;
    if (scenario.preflightRoute) {
      return {
        ...item,
        title: scenario.preflightRoute.title,
        detail: scenario.preflightRoute.detail,
      };
    }
    return {
      ...item,
      title: `老师已说明${scenario.targetName}的观测路径`,
      detail: `先向孩子指出${scenario.targetName}从哪里出现，以及模拟指向会怎样移动。`,
    };
  });
}

function getMotionCopy(scenario) {
  return {
    modeLabel: scenario.motion && scenario.motion.modeLabel ? scenario.motion.modeLabel : "模拟转向",
    progressLabel: scenario.motion && scenario.motion.progressLabel ? scenario.motion.progressLabel : "模拟转向",
    holdStart: scenario.motion && scenario.motion.holdStart ? scenario.motion.holdStart : `按住 0.6 秒后寻找${scenario.targetName}`,
    holding: scenario.motion && scenario.motion.holding ? scenario.motion.holding : `正在模拟指向${scenario.targetName} · 保持按住`,
    arrived: scenario.motion && scenario.motion.arrived ? scenario.motion.arrived : `已到达${scenario.targetName}观测位置 · 等待老师推进`,
    dockWaiting: scenario.motion && scenario.motion.dockWaiting ? scenario.motion.dockWaiting : `请在上方持续按住寻找${scenario.targetName}`,
    dockArrived: scenario.motion && scenario.motion.dockArrived ? scenario.motion.dockArrived : `确认到达${scenario.targetName} · 开始采集`,
    gateArrived: scenario.motion && scenario.motion.gateArrived ? scenario.motion.gateArrived : "已抵达，但不会自动进入采集",
  };
}

function observeEvidenceReady(scenario, markers) {
  const required = Math.max(1, Number(scenario.observe.minMarkers) || 1);
  if (markers.length < required) return false;
  const separation = Math.max(0, Number(scenario.observe.minMarkerSeparation) || 0);
  if (!separation) return true;
  const sorted = markers.map((marker) => Number(marker.sampleT)).filter(Number.isFinite).sort((a, b) => a - b);
  return sorted.filter((sampleT, index) => index === 0 || sampleT - sorted[index - 1] >= separation).length >= required;
}

function formatTime(timestamp) {
  if (!timestamp) return "—";
  return new Intl.DateTimeFormat("zh-CN", {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false,
  }).format(new Date(timestamp));
}

function phaseIndex(phase) {
  return Math.max(0, CONTENT.phaseOrder.indexOf(phase));
}

function preflightComplete(state) {
  return PREFLIGHT_ITEMS.every((item) => state.safety.preflight.checks[item.key]);
}

function isAuthorizationValid(state, now) {
  return Boolean(
    state.safety.preflight.confirmedAt &&
      state.safety.preflight.validUntil &&
      state.safety.preflight.validUntil > now
  );
}

function currentChildSignal(state) {
  const scenario = getActiveScenario(state);
  switch (state.phase) {
    case "prep":
      return `儿童屏正在等候「${scenario.taskTitle}」观测小队`;
    case "welcome":
      return state.responses.teamReady ? "观测小队已确认就绪" : "等待团队确认就绪";
    case "predict":
      return displayAnswer(state, "prediction", state.responses.prediction);
    case "preflight":
      return `儿童屏只读显示${scenario.targetName}任务的预检进度`;
    case "slew":
      {
        const motion = getMotionCopy(scenario);
      return state.movement.arrived
        ? `儿童屏显示：${motion.arrived}`
        : `儿童屏显示：${motion.progressLabel} · ${Math.round(state.movement.progress)}%`;
      }
    case "observe":
      return `${scenario.observe.signalLabel} · 已保存 ${state.responses.markers.length} 个儿童证据标记`;
    case "compare":
      return displayAnswer(state, "comparison", state.responses.comparison);
    case "conclude":
      return displayAnswer(state, "conclusion", state.responses.conclusion);
    case "result":
      return `儿童屏正在展示：${scenario.result.finding}`;
    case "complete":
      return `儿童屏正在感谢完成${scenario.shortLabel}的小小射电员`;
    default:
      return "双屏已同步";
  }
}

function useStrictHold(state, now) {
  const [pressStage, setPressStage] = useState("idle");
  const stateRef = useRef(state);
  const armTimerRef = useRef(null);
  const tickTimerRef = useRef(null);
  const startedRef = useRef(false);
  const pointerIdRef = useRef(null);

  stateRef.current = state;

  const clearTimers = useCallback(() => {
    if (armTimerRef.current) window.clearTimeout(armTimerRef.current);
    if (tickTimerRef.current) window.clearInterval(tickTimerRef.current);
    armTimerRef.current = null;
    tickTimerRef.current = null;
  }, []);

  const endHold = useCallback(
    (kind) => {
      const hadStarted = startedRef.current;
      startedRef.current = false;
      pointerIdRef.current = null;
      clearTimers();
      setPressStage("idle");
      if (hadStarted) {
        STORE.dispatch({ type: "HOLD_END", payload: { kind }, actor: "teacher" });
      }
    },
    [clearTimers]
  );

  const beginAfterIntentDelay = useCallback(() => {
    const latest = stateRef.current;
    const authorizationValid = isAuthorizationValid(latest, Date.now());
    if (
      latest.phase !== "slew" ||
      latest.runState !== "active" ||
      latest.movement.arrived ||
      !authorizationValid
    ) {
      setPressStage("idle");
      pointerIdRef.current = null;
      return;
    }

    const next = STORE.dispatch({ type: "HOLD_START", actor: "teacher" });
    if (!next.movement.holdActive) {
      setPressStage("idle");
      pointerIdRef.current = null;
      return;
    }

    startedRef.current = true;
    setPressStage("holding");
    const startedAt = performance.now();
    const startProgress = next.movement.progress;
    const remainingDuration = Math.max(900, (100 - startProgress) * 36);

    tickTimerRef.current = window.setInterval(() => {
      if (!startedRef.current) return;
      const elapsed = performance.now() - startedAt;
      const progress = Math.min(100, startProgress + (elapsed / remainingDuration) * (100 - startProgress));
      STORE.dispatch({ type: "HOLD_TICK", payload: { progress }, actor: "teacher" });
      if (progress >= 100) {
        startedRef.current = false;
        clearTimers();
        pointerIdRef.current = null;
        setPressStage("arrived");
      }
    }, 80);
  }, [clearTimers]);

  const armHold = useCallback(
    (pointerId) => {
      if (pressStage !== "idle" || startedRef.current || armTimerRef.current) return;
      const latest = stateRef.current;
      if (
        latest.phase !== "slew" ||
        latest.runState !== "active" ||
        latest.movement.arrived ||
        !isAuthorizationValid(latest, Date.now())
      ) {
        return;
      }
      pointerIdRef.current = pointerId;
      setPressStage("arming");
      armTimerRef.current = window.setTimeout(() => {
        armTimerRef.current = null;
        beginAfterIntentDelay();
      }, 600);
    },
    [beginAfterIntentDelay, pressStage]
  );

  useEffect(() => {
    const onBlur = () => endHold("window-blur");
    const onVisibilityChange = () => {
      if (document.visibilityState === "hidden") endHold("page-hidden");
    };
    window.addEventListener("blur", onBlur);
    document.addEventListener("visibilitychange", onVisibilityChange);
    return () => {
      window.removeEventListener("blur", onBlur);
      document.removeEventListener("visibilitychange", onVisibilityChange);
      clearTimers();
    };
  }, [clearTimers, endHold]);

  useEffect(() => {
    if (state.phase !== "slew" || state.runState !== "active" || state.movement.arrived) {
      clearTimers();
      startedRef.current = false;
      pointerIdRef.current = null;
      setPressStage(state.movement.arrived ? "arrived" : "idle");
    }
  }, [state.phase, state.runState, state.movement.arrived, clearTimers]);

  const canHold =
    state.phase === "slew" &&
    state.runState === "active" &&
    !state.movement.arrived &&
    isAuthorizationValid(state, now);

  return {
    pressStage,
    canHold,
    pointerHandlers: {
      onPointerDown(event) {
        if (!canHold || event.button !== 0) return;
        event.preventDefault();
        try {
          event.currentTarget.setPointerCapture(event.pointerId);
        } catch (error) {
          return;
        }
        armHold(event.pointerId);
      },
      onPointerUp(event) {
        event.preventDefault();
        endHold("released");
        try {
          if (event.currentTarget.hasPointerCapture(event.pointerId)) {
            event.currentTarget.releasePointerCapture(event.pointerId);
          }
        } catch (error) {
          // Lost capture is handled by the same fail-safe path.
        }
      },
      onPointerCancel() {
        endHold("pointer-cancel");
      },
      onLostPointerCapture() {
        endHold("lost-capture");
      },
      onPointerLeave() {
        if (pointerIdRef.current !== null) endHold("pointer-left-button");
      },
      onKeyDown(event) {
        if ((event.key === " " || event.key === "Enter") && !event.repeat) {
          event.preventDefault();
          armHold("keyboard");
        }
      },
      onKeyUp(event) {
        if (event.key === " " || event.key === "Enter") {
          event.preventDefault();
          endHold("key-released");
        }
      },
    },
  };
}

function SafetyBar({ state }) {
  const phase = CONTENT.phases[state.phase];
  const runTone = state.runState === "estop" ? "danger" : state.runState === "paused" ? "warning" : "safe";
  const dataIsMock = state.config.dataMode === "fixed_mock";

  return (
    <header className="teacher-safety-bar">
      <div className="teacher-brand">
        <img src="assets/logo.png" alt="宇宙来电项目标志" />
        <div>
          <strong>宇宙来电 · 教学导演台</strong>
          <span>前端原型 · 不连接真实设备</span>
        </div>
      </div>

      <div className="safety-statuses" aria-label="当前安全状态">
        <span className="safety-pill safe">演示隔离</span>
        <span className={`safety-pill ${runTone}`}>{RUN_LABELS[state.runState] || state.runState}</span>
        <span className="safety-pill warning">{phase.short}</span>
        <span className={`safety-pill ${dataIsMock ? "safe" : "danger"}`}>
          {dataIsMock ? "教学模拟数据" : "未接入的数据模式"}
        </span>
      </div>

      <div className="teacher-safety-actions">
        <button
          type="button"
          className="pause-button"
          disabled={["idle", "paused", "estop", "complete"].includes(state.runState)}
          onClick={() =>
            STORE.dispatch({
              type: "PAUSE",
              payload: { reason: "老师从导演台暂停了教学流程" },
              actor: "teacher",
            })
          }
        >
          安全暂停
        </button>
        <button
          type="button"
          className="estop-button"
          disabled={state.safety.estop.latched}
          onClick={() =>
            STORE.dispatch({
              type: "ESTOP",
              payload: { reason: "老师启动原型急停" },
              actor: "teacher",
            })
          }
        >
          急停
        </button>
      </div>
    </header>
  );
}

function Timeline({ state }) {
  const activeIndex = phaseIndex(state.phase);
  const scenario = getActiveScenario(state);
  return (
    <aside className="teacher-panel teacher-timeline" aria-label="教学时间线">
      <div className="timeline-head">
        <span>TEACHING RUNBOOK</span>
        <strong>10 步导演流程</strong>
      </div>
      <div className="timeline-list">
        {CONTENT.phaseOrder.map((phaseKey, index) => {
          const baseMeta = CONTENT.phases[phaseKey];
          const scenarioCopy = MODEL.getPhaseCopy(scenario.id, phaseKey) || baseMeta;
          const stateClass = index < activeIndex ? "done" : index === activeIndex ? "current" : "";
          return (
            <div className={`timeline-step ${stateClass}`} key={phaseKey} aria-current={index === activeIndex ? "step" : undefined}>
              <span className="step-number">{String(index + 1).padStart(2, "0")}</span>
              <div>
                <strong>{baseMeta.short}</strong>
                <small>{scenarioCopy.teacherTitle}</small>
              </div>
            </div>
          );
        })}
      </div>
      <div className="timeline-foot">
        会话 {state.sessionId.slice(-8)}<br />
        同步版本 r{state.revision}
      </div>
    </aside>
  );
}

function SetupWorkspace({ state }) {
  const setConfig = (payload) => STORE.dispatch({ type: "CONFIGURE", payload, actor: "teacher" });
  const scenario = getActiveScenario(state);
  const scenarioOptions = SELECTABLE_SCENARIO_IDS.map((scenarioId) => MODEL.getScenario(scenarioId));
  return (
    <>
      <div className="scenario-selector" role="group" aria-label="选择本轮观测任务">
        {scenarioOptions.map((option) => {
          const selected = state.config.scenarioId === option.id;
          return (
            <button
              type="button"
              className={`scenario-option ${selected ? "selected" : ""}`}
              data-accent={option.accent}
              aria-pressed={selected}
              key={option.id}
              onClick={() => setConfig({ scenarioId: option.id })}
            >
              <span className="scenario-option-art" aria-hidden="true">
                <img src={option.targetAsset} alt="" />
              </span>
              <span className="scenario-option-copy">
                <small>OBSERVATION MISSION · {option.shortLabel}</small>
                <strong>{option.label}</strong>
                <span>{option.taskTitle}</span>
              </span>
              <em>{selected ? "本轮已选择" : "选择任务"}</em>
            </button>
          );
        })}
      </div>
      <div className="setup-grid">
        <div className="field-card">
          <label htmlFor="ageBand">年龄脚手架</label>
          <select id="ageBand" value={state.config.ageBand} onChange={(event) => setConfig({ ageBand: event.target.value })}>
            <option value="6-8">6–8 岁 · 发现模式</option>
            <option value="9-12">9–12 岁 · 探究模式</option>
          </select>
        </div>
        <div className="field-card">
          <label htmlFor="pace">课堂节奏</label>
          <select id="pace" value={state.config.pace} onChange={(event) => setConfig({ pace: event.target.value })}>
            <option value="quick8">快速体验 · 8 分钟</option>
            <option value="standard12">标准观测 · 12 分钟</option>
          </select>
        </div>
        <div className="field-card">
          <label htmlFor="dataMode">本轮数据来源</label>
          <select id="dataMode" value={state.config.dataMode} onChange={(event) => setConfig({ dataMode: event.target.value })}>
            <option value="fixed_mock">教学模拟 · 已启用</option>
            <option value="historical" disabled>真实历史数据 · 尚未接入</option>
            <option value="live_label" disabled>现场实时数据 · 尚未接入</option>
          </select>
        </div>
        <div className="field-card">
          <label htmlFor="groupSize">本组儿童人数</label>
          <input
            id="groupSize"
            type="number"
            min="1"
            max="8"
            value={state.config.groupSize}
            onChange={(event) => setConfig({ groupSize: Math.max(1, Math.min(8, Number(event.target.value) || 1)) })}
          />
        </div>
      </div>
      <div className="phase-brief">
        <span>本轮边界</span>
        <strong>{scenario.label} · 孩子参与科学判断，老师掌握所有流程推进</strong>
        <p>
          当前使用「{scenario.observe.sourceLabel}」。历史数据与实时数据尚未接入；三套任务各自使用独立的固定模拟数据、问题与答案，不能互相改名替代。
        </p>
      </div>
    </>
  );
}

function ResponseWorkspace({ state }) {
  const scenario = getActiveScenario(state);
  if (state.phase === "welcome") {
    return (
      <>
        <div className="response-panel">
          <div className="response-row"><span>儿童屏团队确认</span><strong>{state.responses.teamReady ? "已就绪" : "等待确认"}</strong></div>
          <div className="response-row"><span>角色建议</span><strong>{scenario.roles.slice(0, 3).map((role) => role.name).join(" · ")}</strong></div>
          <div className="response-row"><span>本组人数</span><strong>{state.config.groupSize} 人</strong></div>
        </div>
        <div className="status-note">孩子只确认“团队已就绪”。角色分配和流程推进均由老师口头组织。</div>
      </>
    );
  }

  if (state.phase === "predict") {
    return (
      <>
        <div className="response-panel">
          <div className="response-row"><span>儿童预测</span><strong>{displayAnswer(state, "prediction", state.responses.prediction)}</strong></div>
          <div className="response-row"><span>年龄脚手架</span><strong>{CONTENT.ageModes[state.config.ageBand].label}</strong></div>
          <div className="response-row"><span>导演提醒</span><strong>先追问理由，再进入预检</strong></div>
        </div>
        <div className="status-note">儿童提交预测后不会自动翻页。老师确认已经听到理由，再使用底部主操作推进。</div>
      </>
    );
  }

  return null;
}

function PreflightWorkspace({ state, now }) {
  const scenario = getActiveScenario(state);
  const authorizationNoun = scenario.kind === "rfi_scan" ? "模拟扫描" : "模拟运动";
  const allChecked = preflightComplete(state);
  const authorizationValid = isAuthorizationValid(state, now);
  const remaining = authorizationValid ? Math.max(0, Math.ceil((state.safety.preflight.validUntil - now) / 1000)) : 0;
  const preflightItems = getPreflightItems(state);

  return (
    <>
      <div className="checklist">
        {preflightItems.map((item, index) => {
          const checked = Boolean(state.safety.preflight.checks[item.key]);
          return (
            <button
              type="button"
              className={`check-item ${checked ? "checked" : ""}`}
              key={item.key}
              aria-pressed={checked}
              onClick={() =>
                STORE.dispatch({
                  type: "SET_PREFLIGHT_CHECK",
                  payload: { key: item.key, value: !checked },
                  actor: "teacher",
                })
              }
            >
              <span className="check-box">{checked ? "✓" : index + 1}</span>
              <span><strong>{item.title}</strong><small>{item.detail}</small></span>
              <em>{checked ? "已确认" : "待检查"}</em>
            </button>
          );
        })}
      </div>

      <div className="authorization-card">
        <strong>
          {authorizationValid
            ? `${authorizationNoun}授权有效 · ${remaining} 秒`
            : state.safety.preflight.confirmedAt
              ? "授权已过期 · 请重新确认"
              : allChecked
                ? "四项已完成 · 等待老师确认授权"
                : `尚未获得${authorizationNoun}授权`}
        </strong>
        <span>授权只在全部项目确认后生成，有效 30 秒。暂停、急停、刷新或按压中断都会清除授权并返回预检。</span>
      </div>
    </>
  );
}

function SlewWorkspace({ state, now, hold }) {
  const scenario = getActiveScenario(state);
  const motion = getMotionCopy(scenario);
  const authorizationValid = isAuthorizationValid(state, now);
  const remaining = authorizationValid ? Math.max(0, Math.ceil((state.safety.preflight.validUntil - now) / 1000)) : 0;
  let holdLabel = motion.holdStart;
  if (hold.pressStage === "arming") holdLabel = "继续按住 · 正在确认操作意图";
  if (hold.pressStage === "holding" || state.movement.holdActive) holdLabel = motion.holding;
  if (state.movement.arrived) holdLabel = motion.arrived;
  if (!authorizationValid && !state.movement.arrived) holdLabel = "授权已失效 · 请返回重新预检";

  return (
    <>
      <div className="hold-panel">
        <div className="hold-summary">
          <div><span>模式</span><strong>{motion.modeLabel}</strong></div>
          <div><span>授权</span><strong>{authorizationValid ? `${remaining} 秒` : "无有效授权"}</strong></div>
          <div><span>进度</span><strong>{Math.round(state.movement.progress)}%</strong></div>
        </div>
        <div className="hold-progress" aria-label={`${motion.progressLabel}进度 ${Math.round(state.movement.progress)}%`}>
          <span style={{ "--hold-width": `${state.movement.progress}%` }} />
        </div>
        <button
          type="button"
          className={`hold-button ${hold.pressStage === "holding" || state.movement.holdActive ? "holding" : ""}`}
          disabled={!hold.canHold}
          aria-label={holdLabel}
          {...hold.pointerHandlers}
        >
          {holdLabel}
        </button>
      </div>
      <div className="status-note">
        {scenario.label} · {scenario.kind === "rfi_scan" ? "这不是接收机或云台控制" : "这不是设备控制"}：600 毫秒用于过滤误触；开始后约 3.6 秒完成{motion.progressLabel}。松手、取消指针捕获、移出按钮、窗口失焦或页面隐藏都会结束按压并锁定安全暂停。
      </div>
    </>
  );
}

function EvidenceWorkspace({ state }) {
  const scenario = getActiveScenario(state);
  if (state.phase === "observe") {
    const requiredMarkers = Math.max(1, Number(scenario.observe.minMarkers) || 1);
    return (
      <>
        <div className="response-panel">
          <div className="response-row"><span>数据来源</span><strong>{scenario.observe.sourceLabel}</strong></div>
          <div className="response-row"><span>当前儿童信号</span><strong>{scenario.observe.signalLabel}</strong></div>
          <div className="response-row"><span>儿童证据标记</span><strong>{state.responses.markers.length} / 3</strong></div>
          {state.responses.markers.map((marker, index) => (
            <div className="response-row" key={marker.id}><span>标记 {index + 1}</span><strong>{marker.label} · {formatTime(marker.at)}</strong></div>
          ))}
        </div>
        {scenario.kind === "rfi_scan" ? (
          <div className="reference-band-list" aria-label="教师候选频段参考">
            {scenario.observe.referenceBands.map((band) => (
              <div key={band.label}><span>{band.range}</span><strong>{band.label}</strong><small>{band.note}</small></div>
            ))}
          </div>
        ) : null}
        <div className="status-note">数据标签始终是“教学模拟”。至少在不同位置保存 {requiredMarkers} 个儿童标记后，老师才可进入比较。</div>
      </>
    );
  }

  if (state.phase === "compare") {
    return (
      <div className="response-panel">
        <div className="response-row"><span>儿童比较结论</span><strong>{displayAnswer(state, "comparison", state.responses.comparison)}</strong></div>
        <div className="response-row"><span>比较对象</span><strong>{scenario.compare.background.label} × {scenario.compare.target.label}</strong></div>
        <div className="response-row"><span>比较前提</span><strong>同一纵轴尺度 · {scenario.observe.sourceLabel}</strong></div>
        <div className="response-row"><span>保留证据</span><strong>{state.responses.markers.length} 个标记</strong></div>
      </div>
    );
  }

  if (state.phase === "conclude") {
    return (
      <div className="response-panel">
        <div className="response-row"><span>最初预测</span><strong>{displayAnswer(state, "prediction", state.responses.prediction)}</strong></div>
        <div className="response-row"><span>数据比较</span><strong>{displayAnswer(state, "comparison", state.responses.comparison)}</strong></div>
        <div className="response-row"><span>儿童证据结论</span><strong>{displayAnswer(state, "conclusion", state.responses.conclusion)}</strong></div>
      </div>
    );
  }

  return null;
}

function ResultWorkspace({ state }) {
  const scenario = getActiveScenario(state);
  const rows = [
    ["观测任务", scenario.label],
    ["数据来源", scenario.result.sourceLabel],
    ["团队预测", displayAnswer(state, "prediction", state.responses.prediction)],
    [scenario.result.evidenceLabel, `${state.responses.markers.length} 个`],
    ["曲线比较", displayAnswer(state, "comparison", state.responses.comparison)],
    ["证据结论", displayAnswer(state, "conclusion", state.responses.conclusion)],
    ["本轮发现", scenario.result.finding],
  ];
  return (
    <>
      <div className="response-panel">
        {rows.map(([label, value]) => (
          <div className="response-row" key={label}><span>{label}</span><strong>{value}</strong></div>
        ))}
      </div>
      <div className="phase-brief">
        <span>教师审核出口</span>
        <strong>先核对来源标签，再向全组展示成果</strong>
        <p>原型不包含打印、支付或儿童个人信息。需要拍照留存时，由老师或家长在主流程之外处理。</p>
      </div>
    </>
  );
}

function CompleteWorkspace({ state }) {
  const scenario = getActiveScenario(state);
  const motion = getMotionCopy(scenario);
  return (
    <>
      <div className="phase-brief">
        <span>本轮已结束</span>
        <strong>{scenario.label}已完成，儿童屏进入感谢与待命画面</strong>
        <p>本轮保存了 {state.responses.markers.length} 个「{scenario.observe.markerLabel}」。重置只会清空这个前端原型会话，不会向任何设备发送命令。</p>
      </div>
      <div className="response-panel">
        <div className="response-row"><span>运行状态</span><strong>本轮完成</strong></div>
        <div className="response-row"><span>{motion.progressLabel}</span><strong>已结束</strong></div>
        <div className="response-row"><span>下一步</span><strong>确认现场后，由老师重置</strong></div>
      </div>
    </>
  );
}

function Workspace({ state, now, hold }) {
  const meta = getScenarioPhaseCopy(state);
  const baseMeta = CONTENT.phases[state.phase];
  let content = null;
  if (state.phase === "prep") content = <SetupWorkspace state={state} />;
  if (["welcome", "predict"].includes(state.phase)) content = <ResponseWorkspace state={state} />;
  if (state.phase === "preflight") content = <PreflightWorkspace state={state} now={now} />;
  if (state.phase === "slew") content = <SlewWorkspace state={state} now={now} hold={hold} />;
  if (["observe", "compare", "conclude"].includes(state.phase)) content = <EvidenceWorkspace state={state} />;
  if (state.phase === "result") content = <ResultWorkspace state={state} />;
  if (state.phase === "complete") content = <CompleteWorkspace state={state} />;

  return (
    <main className="teacher-panel teacher-workspace" aria-live="polite">
      <div className="workspace-kicker">步骤 {String(phaseIndex(state.phase) + 1).padStart(2, "0")} · {baseMeta.short}</div>
      <h1 className="workspace-title">{meta.teacherTitle}</h1>
      <p className="workspace-subtitle">{meta.teacherPrompt}</p>
      {content}
      {state.lastError ? <div className="status-note" role="alert">状态机提示：{state.lastError}</div> : null}
    </main>
  );
}

function TeacherSide({ state }) {
  const scenario = getActiveScenario(state);
  const meta = getScenarioPhaseCopy(state);
  const baseMeta = CONTENT.phases[state.phase];
  const cue = scenario.teacherCues[state.phase];
  return (
    <aside className="teacher-panel teacher-side">
      <section className="child-preview" aria-label="儿童共享屏只读预览">
        <div className="preview-inner">
          <div>
            <span>儿童共享屏 · {baseMeta.short} · {scenario.shortLabel}</span>
            <strong>{meta.childTitle}</strong>
            <small>{meta.childBody}</small>
          </div>
          <img src={scenario.targetAsset} alt={`${scenario.targetName}卡通任务图标`} />
        </div>
      </section>
      <section className="teacher-cue-card">
        <span>TEACHER CUE · 老师怎么说</span>
        <blockquote>“{cue.say}”</blockquote>
        <hr />
        <strong>现在观察</strong>
        <p>{cue.watch}</p>
        <hr />
        <strong>儿童屏信号</strong>
        <p>{currentChildSignal(state)}</p>
      </section>
    </aside>
  );
}

function getDockPlan(state, now) {
  const scenario = getActiveScenario(state);
  const motion = getMotionCopy(scenario);
  const authorizationValid = isAuthorizationValid(state, now);
  const allChecked = preflightComplete(state);
  const mockOnly = state.config.dataMode === "fixed_mock";
  const plans = {
    prep: {
      label: `开始${scenario.shortLabel} · 邀请孩子集合`,
      disabled: !mockOnly,
      gate: mockOnly ? "开始后，儿童屏进入团队集合页" : "请选择唯一可用的“教学模拟”数据来源",
      action: () => STORE.dispatch({ type: "START_SESSION", actor: "teacher" }),
    },
    welcome: {
      label: "听到团队回应 · 进入预测",
      disabled: !state.responses.teamReady,
      gate: state.responses.teamReady ? "儿童屏已确认团队就绪" : "等待儿童屏提交“我们准备好了”",
      action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
    },
    predict: {
      label: "已追问理由 · 进入安全预检",
      disabled: !state.responses.prediction,
      gate: state.responses.prediction ? `已记录：${displayAnswer(state, "prediction", state.responses.prediction)}` : "等待儿童屏提交预测",
      action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
    },
    preflight: authorizationValid
      ? {
          label: `授权有效 · 进入${motion.modeLabel}`,
          disabled: false,
          gate: `授权还剩 ${Math.max(0, Math.ceil((state.safety.preflight.validUntil - now) / 1000))} 秒`,
          action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
        }
      : {
          label: state.safety.preflight.confirmedAt ? "重新确认 30 秒授权" : "确认预检并生成 30 秒授权",
          disabled: !allChecked,
          gate: allChecked ? "确认后仍需由老师明确进入下一阶段" : "请逐项完成四项现场预检",
          action: () => STORE.dispatch({ type: "CONFIRM_PREFLIGHT", actor: "teacher" }),
        },
    slew: !authorizationValid && !state.movement.arrived
      ? {
          label: "返回并重新完成预检",
          disabled: false,
          gate: "模拟运动授权已失效",
          action: () => STORE.dispatch({ type: "RESUME_TO_PREFLIGHT", actor: "teacher" }),
        }
      : {
          label: state.movement.arrived ? motion.dockArrived : motion.dockWaiting,
          disabled: !state.movement.arrived,
          gate: state.movement.arrived ? motion.gateArrived : `${motion.progressLabel} ${Math.round(state.movement.progress)}%`,
          action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
        },
    observe: {
      label: "已捕获证据 · 进入比较",
      disabled: !observeEvidenceReady(scenario, state.responses.markers),
      gate: observeEvidenceReady(scenario, state.responses.markers)
        ? `已保存 ${state.responses.markers.length} 个儿童标记`
        : `等待儿童屏在不同位置保存 ${Math.max(1, Number(scenario.observe.minMarkers) || 1)} 个证据标记`,
      action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
    },
    compare: {
      label: "已指出曲线差异 · 形成结论",
      disabled: !state.responses.comparison,
      gate: state.responses.comparison ? `已记录：${displayAnswer(state, "comparison", state.responses.comparison)}` : "等待儿童屏提交比较",
      action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
    },
    conclude: {
      label: "已听到证据句 · 查看成果",
      disabled: !state.responses.conclusion,
      gate: state.responses.conclusion ? `已记录：${displayAnswer(state, "conclusion", state.responses.conclusion)}` : "等待儿童屏完成证据结论",
      action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
    },
    result: {
      label: "审核无误 · 完成本轮",
      disabled: false,
      gate: "成果出口仅由老师处理",
      action: () => STORE.dispatch({ type: "ADVANCE", actor: "teacher" }),
    },
    complete: {
      label: "确认现场后 · 重置下一轮",
      disabled: false,
      gate: "重置只清除本地前端原型状态",
      action: () => STORE.dispatch({ type: "RESET_SESSION", actor: "teacher" }),
    },
  };
  return plans[state.phase];
}

function Dock({ state, now }) {
  const plan = getDockPlan(state, now);
  const error = state.lastError;
  return (
    <footer className="teacher-dock">
      <div className="dock-secondary">
        <button type="button" className="teacher-button" onClick={() => window.open("child-stage.html", "_blank", "noopener")}>打开儿童屏</button>
        {state.phase === "predict" ? (
          <button type="button" className="teacher-button" onClick={() => STORE.dispatch({ type: "BACK", actor: "teacher" })}>返回集合</button>
        ) : null}
      </div>
      <div className={`dock-gate ${error ? "error" : ""}`} role={error ? "alert" : undefined}>
        {error ? error : plan.gate}
      </div>
      <button type="button" className="teacher-button primary" disabled={plan.disabled} onClick={plan.action}>
        {plan.label}
      </button>
    </footer>
  );
}

function SafetyOverlay({ state }) {
  const scenario = getActiveScenario(state);
  const motion = getMotionCopy(scenario);
  if (state.safety.estop.latched) {
    const returnsToEarlyStep = ["welcome", "predict"].includes(state.safety.estop.fromPhase);
    return (
      <div className="teacher-overlay" role="dialog" aria-modal="true" aria-labelledby="estopTitle">
        <div className="teacher-modal danger">
          <h2 id="estopTitle">急停已锁定</h2>
          <p>{state.safety.estop.reason}。所有模拟运动已停止，运动授权已清除。不能通过重置绕过此状态。</p>
          <div className="status-note">
            {state.safety.estop.acknowledged
              ? returnsToEarlyStep
                ? "已确认现场检查。清除急停后会回到刚才的教学步骤，不会跳过团队就绪或预测。"
                : "已确认现场检查。下一步清除急停后，系统会统一回到四项安全预检。"
              : "请老师离开屏幕检查现场、儿童站位与设备周围，再进行确认。"}
          </div>
          <div className="modal-actions">
            {!state.safety.estop.acknowledged ? (
              <button type="button" className="teacher-button sun" autoFocus onClick={() => STORE.dispatch({ type: "ACK_ESTOP", actor: "teacher" })}>
                我已检查现场
              </button>
            ) : (
              <button type="button" className="teacher-button primary" autoFocus onClick={() => STORE.dispatch({ type: "CLEAR_ESTOP", actor: "teacher" })}>
                {returnsToEarlyStep ? "清除急停并回到本步" : "清除急停并返回预检"}
              </button>
            )}
          </div>
        </div>
      </div>
    );
  }

  if (state.safety.pause.latched || state.runState === "paused") {
    const returnsToEarlyStep = ["welcome", "predict"].includes(state.safety.pause.fromPhase);
    return (
      <div className="teacher-overlay" role="dialog" aria-modal="true" aria-labelledby="pauseTitle">
        <div className="teacher-modal">
          <h2 id="pauseTitle">教学流程已安全暂停</h2>
          <p>{state.safety.pause.reason || `${motion.progressLabel}和课堂流程已经停止。`}</p>
          <div className="status-note">
            {returnsToEarlyStep
              ? "恢复后会回到刚才的教学步骤，不会跳过团队就绪或预测。"
              : `恢复不会继续刚才的执行进度。系统将清除授权与${motion.progressLabel}进度，统一回到四项安全预检。`}
          </div>
          <div className="modal-actions">
            <button type="button" className="teacher-button primary" autoFocus onClick={() => STORE.dispatch({ type: "RESUME_TO_PREFLIGHT", actor: "teacher" })}>
              {returnsToEarlyStep ? "回到本步继续" : "返回预检后恢复"}
            </button>
          </div>
        </div>
      </div>
    );
  }

  return null;
}

function TeacherApp() {
  const state = useSessionState();
  const [now, setNow] = useState(Date.now());

  useEffect(() => {
    const timer = window.setInterval(() => setNow(Date.now()), 250);
    return () => window.clearInterval(timer);
  }, []);

  const hold = useStrictHold(state, now);
  const scenario = getActiveScenario(state);
  const meta = useMemo(
    () => getScenarioPhaseCopy(state),
    [state.phase, state.runScenarioId, state.config.scenarioId]
  );

  useEffect(() => {
    document.title = `宇宙来电 V2 · ${scenario.shortLabel} · ${CONTENT.phases[state.phase].short} · 老师导演台`;
  }, [scenario.shortLabel, state.phase]);

  return (
    <div className="teacher-shell" data-screen-label="teacher-director-console" data-phase={state.phase} data-run-state={state.runState}>
      <SafetyBar state={state} />
      <div className="teacher-main">
        <Timeline state={state} />
        <Workspace state={state} now={now} hold={hold} />
        <TeacherSide state={state} />
      </div>
      <Dock state={state} now={now} />
      <SafetyOverlay state={state} />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("teacherRoot")).render(<TeacherApp />);
