/**
 * V3.3 daily science archive.
 * Official context lives outside the shared teacher/child session state and is
 * frozen once when the result scene mounts. RFI data may be injected later by
 * `window.V33EvidenceAdapter.getRfiSnapshot()` or the `v33:rfi-snapshot` event.
 */
(function () {
  "use strict";

  const { useEffect, useState } = React;
  const API_PATH = "/api/daily-evidence-v33/";
  const CACHE_PREFIX = "cosmic-call:v3.3:daily-evidence:v1";

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

  function shanghaiDate() {
    const parts = new Intl.DateTimeFormat("en-CA", {
      timeZone: "Asia/Shanghai",
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
    }).formatToParts(new Date());
    const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
    return `${byType.year}-${byType.month}-${byType.day}`;
  }

  function useShanghaiDate() {
    const [date, setDate] = useState(shanghaiDate);
    useEffect(() => {
      const update = () => setDate((current) => {
        const next = shanghaiDate();
        return current === next ? current : next;
      });
      const timer = window.setInterval(update, 60 * 1000);
      return () => window.clearInterval(timer);
    }, []);
    return date;
  }

  function dateLabel(value) {
    const date = new Date(`${value}T00:00:00+08:00`);
    if (Number.isNaN(date.getTime())) return value;
    return new Intl.DateTimeFormat("zh-CN", {
      timeZone: "Asia/Shanghai",
      year: "numeric",
      month: "long",
      day: "numeric",
    }).format(date);
  }

  function timeLabel(value) {
    if (!value) return "时间待更新";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return "时间待更新";
    return new Intl.DateTimeFormat("zh-CN", {
      timeZone: "Asia/Shanghai",
      month: "numeric",
      day: "numeric",
      hour: "2-digit",
      minute: "2-digit",
      hour12: false,
    }).format(date);
  }

  function cacheKey(kind, date) {
    return `${CACHE_PREFIX}:${kind}:${date}`;
  }

  function readCache(kind, date) {
    try {
      const raw = localStorage.getItem(cacheKey(kind, date));
      return raw ? JSON.parse(raw) : null;
    } catch (error) {
      return null;
    }
  }

  function writeCache(snapshot) {
    try {
      localStorage.setItem(
        cacheKey(snapshot.scenarioKind, snapshot.displayDate),
        JSON.stringify(snapshot),
      );
    } catch (error) {
      // A cache failure must never block the children's result page.
    }
  }

  function cachedSnapshot(snapshot) {
    return {
      ...snapshot,
      overallStatus: "latest",
      boundaryNote: `${snapshot.boundaryNote || ""} 当前网络暂不可用，以下为今天已缓存的最新一版。`.trim(),
      cards: Array.isArray(snapshot.cards)
        ? snapshot.cards.map((card) => ({
          ...card,
          status: ["unavailable", "simulation", "archive"].includes(card.status)
            ? card.status
            : "latest",
          statusLabel: ["unavailable", "simulation", "archive"].includes(card.status)
            ? card.statusLabel
            : "已缓存 · 最新可用",
        }))
        : [],
    };
  }

  function sampleSeries(points, t) {
    const series = Array.isArray(points) ? points : [];
    if (!series.length) return 0;
    const x = clamp(t);
    if (x <= Number(series[0].t)) return Number(series[0].value) || 0;
    for (let index = 1; index < series.length; index += 1) {
      const right = series[index];
      if (x <= Number(right.t)) {
        const left = series[index - 1];
        const ratio = (x - Number(left.t)) /
          Math.max(0.0001, Number(right.t) - Number(left.t));
        return Number(left.value) +
          (Number(right.value) - Number(left.value)) * ratio;
      }
    }
    return Number(series[series.length - 1].value) || 0;
  }

  function rfiSnapshotFromScenario(scenario, state, onsite) {
    const date = shanghaiDate();
    const sourcePoints = onsite && onsite.spectrum &&
        Array.isArray(onsite.spectrum.points)
      ? onsite.spectrum.points.map((point, index, points) => ({
        t: points.length > 1 ? index / (points.length - 1) : 0,
        value: Number(point.power) || 0,
        frequencyMHz: Number(point.frequencyMHz),
      }))
      : (scenario.observe && Array.isArray(scenario.observe.points)
        ? scenario.observe.points
        : []);
    const markers = state && state.responses && Array.isArray(state.responses.markers)
      ? state.responses.markers
      : [];
    const onsiteReady = Boolean(onsite && sourcePoints.length);
    const onsiteHistoryReady = Boolean(
      onsiteReady && Array.isArray(onsite.history) && onsite.history.length,
    );
    const onsiteDirectionReady = Boolean(
      onsiteReady && Array.isArray(onsite.direction) && onsite.direction.length,
    );
    const status = onsiteReady ? "onsite" : "simulation";
    const statusLabel = onsiteReady ? "本场实测" : "教学演示";
    const observedAt = onsiteReady && onsite.acquiredAt
      ? onsite.acquiredAt
      : new Date().toISOString();
    const footprints = markers.reduce((counts, marker) => {
      const label = marker.reasonLabel || marker.reasonId || "保存的变化";
      counts[label] = (counts[label] || 0) + 1;
      return counts;
    }, {});
    if (!Object.keys(footprints).length) footprints["等待孩子保存证据"] = 1;

    return {
      schemaVersion: 1,
      snapshotId: `rfi-${date}-${String(Date.now()).slice(-8)}`,
      scenarioKind: "rfi_scan",
      profileId: onsiteReady ? "rfi-onsite-v1" : "rfi-teaching-v1",
      displayDate: date,
      timezone: "Asia/Shanghai",
      generatedAt: new Date().toISOString(),
      overallStatus: status,
      boundaryNote: onsiteReady
        ? "这些图来自场馆接收机的聚合能量数据；信号形状不等于已经知道来源。"
        : "现场接收机尚未接入；以下三图来自本轮固定教学数据，不能称为现场实测。",
      cards: [
        {
          id: "session-spectrum-snapshot",
          type: "rfi-spectrum",
          kicker: "此刻频谱",
          title: "无线电天空长什么样",
          childCaption: "高高的尖塔，代表这一刻更强的频段",
          status,
          statusLabel,
          observedAt,
          source: {
            name: onsiteReady ? "场馆 L 波段接收机" : "V3.3 固定教学数据",
            url: "",
          },
          media: {
            points: sourcePoints,
            minMHz: onsiteReady ? onsite.spectrum.minMHz : scenario.observe.xAxis.min,
            maxMHz: onsiteReady ? onsite.spectrum.maxMHz : scenario.observe.xAxis.max,
          },
        },
        {
          id: "daily-waterfall-or-occupancy",
          type: onsiteReady && !onsiteHistoryReady
            ? "unavailable"
            : "rfi-waterfall",
          kicker: onsiteHistoryReady ? "今天热力图" : "本轮热力图",
          title: onsiteReady && !onsiteHistoryReady
            ? "现场时间序列尚未接入"
            : "什么时候最热闹",
          childCaption: onsiteHistoryReady
            ? "颜色更亮的时段，环境中的能量变化更多"
            : onsiteReady
            ? "已收到此刻频谱，但还没有真实的当天时间序列"
            : "这是一张教学瀑布图，现场接入后会换成当天实测",
          status: onsiteReady && !onsiteHistoryReady ? "unavailable" : status,
          statusLabel: onsiteReady && !onsiteHistoryReady
            ? "等待现场历史数据"
            : statusLabel,
          observedAt,
          source: {
            name: onsiteHistoryReady
              ? "场馆今日聚合数据"
              : onsiteReady
              ? "场馆接收机 · 历史数据未接入"
              : "V3.3 固定教学数据",
            url: "",
          },
          media: {
            points: sourcePoints,
            history: onsiteHistoryReady
              ? onsite.history
              : null,
          },
        },
        {
          id: "direction-or-signal-footprints",
          type: onsiteDirectionReady
            ? "rfi-direction"
            : onsiteReady
            ? "unavailable"
            : "rfi-footprints",
          kicker: onsiteDirectionReady ? "方向热图" : "信号脚印",
          title: onsiteDirectionReady
            ? "信号从哪个方向来"
            : onsiteReady
            ? "现场方向数据尚未接入"
            : "我们找到了哪种形状",
          childCaption: onsiteDirectionReady
            ? "亮的方向表示接收到的能量更强"
            : onsiteReady
            ? "不会用孩子的教学标记冒充现场方向测量"
            : `${markers.length} 颗证据星 · 形状分类不代表已经知道来源`,
          status: onsiteReady && !onsiteDirectionReady ? "unavailable" : status,
          statusLabel: onsiteReady && !onsiteDirectionReady
            ? "等待方向数据"
            : statusLabel,
          observedAt,
          source: {
            name: onsiteDirectionReady
              ? "场馆方向扫描"
              : onsiteReady
              ? "场馆接收机 · 方向数据未接入"
              : "本轮小队证据标记",
            url: "",
          },
          boundaryNote: "只比较能量和形状，不读取通信内容，也不确认具体设备。",
          media: {
            direction: onsiteDirectionReady ? onsite.direction : null,
            footprints,
          },
        },
      ],
    };
  }

  function unavailableCards(kind, date) {
    const solar = kind === "solar";
    const satellite = kind === "satellite_pass";
    const titles = solar
      ? ["卫星刚刚看见的太阳", "太阳今天在星空哪里", "太阳这周转了多少"]
      : satellite
      ? ["国际空间站长这样", "今天飞过哪里", "下一次从哪里出现"]
      : ["无线电天空长什么样", "什么时候最热闹", "我们找到了哪种形状"];
    return {
      schemaVersion: 1,
      snapshotId: `${kind}-${date}-unavailable`,
      scenarioKind: kind,
      displayDate: date,
      timezone: "Asia/Shanghai",
      generatedAt: new Date().toISOString(),
      overallStatus: "unavailable",
      boundaryNote: "今天的在线资料暂时没有收到；页面没有用模拟资料冒充实时数据。",
      cards: titles.map((title, index) => ({
        id: `${kind}-unavailable-${index}`,
        type: "unavailable",
        kicker: ["真实资料", "数据图", "相关图"][index],
        title,
        childCaption: "网络恢复后会自动显示最新可用资料",
        status: "unavailable",
        statusLabel: "暂未收到",
        observedAt: null,
        source: { name: "等待官方数据源", url: "" },
        media: {},
      })),
    };
  }

  function useDailyEvidenceSnapshot(scenario, state) {
    const date = useShanghaiDate();
    const kind = scenario ? scenario.kind : "solar";
    const [snapshot, setSnapshot] = useState(() =>
      kind === "rfi_scan" ? rfiSnapshotFromScenario(scenario, state, null) : null
    );
    const [loading, setLoading] = useState(kind !== "rfi_scan");

    useEffect(() => {
      let cancelled = false;
      if (!scenario) return undefined;

      if (kind === "rfi_scan") {
        const applyOnsite = (onsite) => {
          if (!cancelled) setSnapshot(rfiSnapshotFromScenario(scenario, state, onsite));
        };
        const eventHandler = (event) => applyOnsite(event.detail || null);
        window.addEventListener("v33:rfi-snapshot", eventHandler);
        Promise.resolve(
          window.V33EvidenceAdapter &&
              typeof window.V33EvidenceAdapter.getRfiSnapshot === "function"
            ? window.V33EvidenceAdapter.getRfiSnapshot()
            : null,
        ).then((onsite) => {
          if (onsite) applyOnsite(onsite);
          setLoading(false);
        }).catch(() => setLoading(false));
        return () => {
          cancelled = true;
          window.removeEventListener("v33:rfi-snapshot", eventHandler);
        };
      }

      const controller = new AbortController();
      const timer = window.setTimeout(() => controller.abort(), 18000);
      setLoading(true);
      fetch(`${API_PATH}?mission=${encodeURIComponent(kind)}&date=${date}&v=3`, {
        signal: controller.signal,
        headers: { Accept: "application/json" },
      }).then(async (response) => {
        if (!response.ok) throw new Error(`daily evidence ${response.status}`);
        return response.json();
      }).then((next) => {
        if (cancelled) return;
        writeCache(next);
        setSnapshot(next);
      }).catch(() => {
        if (cancelled) return;
        const cached = readCache(kind, date);
        setSnapshot(cached
          ? cachedSnapshot(cached)
          : unavailableCards(kind, date));
      }).finally(() => {
        window.clearTimeout(timer);
        if (!cancelled) setLoading(false);
      });

      return () => {
        cancelled = true;
        window.clearTimeout(timer);
        controller.abort();
      };
    }, [date, kind, scenario && scenario.id]);

    return { snapshot, loading };
  }

  const STATUS_COPY = {
    live: "近实时",
    today: "今日计算",
    onsite: "现场实测",
    recent: "最新预测",
    latest: "最新可用",
    archive: "档案资料",
    loading: "正在接收",
    unavailable: "暂未收到",
    simulation: "教学演示",
  };

  function EvidenceStatus({ card }) {
    const status = card.status || "latest";
    return (
      <span className="v33-evidence-status" data-status={status}>
        <i aria-hidden="true"></i>
        {card.statusLabel || STATUS_COPY[status] || "最新资料"}
      </span>
    );
  }

  function ImageVisual({ card }) {
    const [failed, setFailed] = useState(false);
    if (failed || !card.media || !card.media.imageUrl) {
      return <UnavailableVisual label="图片暂时没有收到" />;
    }
    return (
      <img
        className="v33-evidence-image"
        src={card.media.imageUrl}
        alt={card.title}
        onError={() => setFailed(true)}
      />
    );
  }

  function GalacticMap({ card }) {
    const data = card.media || {};
    const longitude = Number(data.galacticLongitude) || 0;
    const latitude = Number(data.galacticLatitude) || 0;
    const x = 30 + ((longitude % 360 + 360) % 360) / 360 * 540;
    const y = 145 - clamp(latitude, -90, 90) / 90 * 105;
    return (
      <svg className="v33-data-svg" viewBox="0 0 600 290" role="img" aria-label={`太阳银河经度 ${longitude.toFixed(1)} 度，纬度 ${latitude.toFixed(1)} 度`}>
        <rect x="0" y="0" width="600" height="290" rx="18"></rect>
        {[30, 165, 300, 435, 570].map((tick) => (
          <line className="grid" key={`x-${tick}`} x1={tick} x2={tick} y1="35" y2="250"></line>
        ))}
        {[40, 92, 145, 198, 250].map((tick) => (
          <line className="grid" key={`y-${tick}`} x1="30" x2="570" y1={tick} y2={tick}></line>
        ))}
        <path className="milky-band" d="M30 167 C125 108 190 198 282 137 S442 101 570 162"></path>
        <path className="milky-band soft" d="M30 185 C126 126 191 216 284 155 S444 119 570 180"></path>
        <circle className="sun-halo" cx={x} cy={y} r="23"></circle>
        <circle className="sun-point" cx={x} cy={y} r="10"></circle>
        <text className="data-label" x={Math.min(465, x + 16)} y={Math.max(35, y - 16)}>今天的太阳</text>
        <text className="axis-label" x="30" y="274">银河经度 0°</text>
        <text className="axis-label" x="570" y="274" textAnchor="end">360°</text>
        <text className="metric-label" x="570" y="24" textAnchor="end">l {longitude.toFixed(1)}° · b {latitude.toFixed(1)}°</text>
      </svg>
    );
  }

  function SolarRotation({ card }) {
    const points = card.media && Array.isArray(card.media.points)
      ? card.media.points
      : [];
    return (
      <div className="v33-rotation-strip" role="img" aria-label={card.childCaption}>
        {points.slice(0, 7).map((point, index) => (
          <span key={`${point.date}-${index}`}>
            <i style={{ "--turn": `${Number(point.longitude) || 0}deg` }}></i>
            <small>{index === points.length - 1 ? "今天" : `${index + 1}`}</small>
          </span>
        ))}
        <b>约 {Math.round(Number(card.media && card.media.rotationDegrees) || 0)}°</b>
      </div>
    );
  }

  function trackSegments(points) {
    const segments = [];
    let current = [];
    (Array.isArray(points) ? points : []).forEach((point) => {
      if (current.length && Math.abs(point.lon - current[current.length - 1].lon) > 180) {
        if (current.length > 1) segments.push(current);
        current = [];
      }
      current.push(point);
    });
    if (current.length > 1) segments.push(current);
    return segments;
  }

  function mapPoint(point) {
    return {
      x: 20 + ((Number(point.lon) + 180) / 360) * 560,
      y: 20 + ((90 - Number(point.lat)) / 180) * 250,
    };
  }

  function GroundTrack({ card }) {
    const media = card.media || {};
    const segments = trackSegments(media.points);
    const current = media.current ? mapPoint(media.current) : null;
    const site = media.site ? mapPoint(media.site) : null;
    return (
      <svg className="v33-data-svg v33-orbit-map" viewBox="0 0 600 290" role="img" aria-label="国际空间站最新地面轨迹预测">
        <rect x="0" y="0" width="600" height="290" rx="18"></rect>
        {[-120, -60, 0, 60, 120].map((lon) => {
          const x = mapPoint({ lon, lat: 0 }).x;
          return <line className="grid" key={`lon-${lon}`} x1={x} x2={x} y1="20" y2="270"></line>;
        })}
        {[-60, -30, 0, 30, 60].map((lat) => {
          const y = mapPoint({ lon: 0, lat }).y;
          return <line className="grid" key={`lat-${lat}`} x1="20" x2="580" y1={y} y2={y}></line>;
        })}
        {segments.map((segment, index) => (
          <polyline
            className="orbit-line"
            key={index}
            points={segment.map((point) => {
              const plotted = mapPoint(point);
              return `${plotted.x},${plotted.y}`;
            }).join(" ")}
          ></polyline>
        ))}
        {site ? <circle className="site-point" cx={site.x} cy={site.y} r="7"></circle> : null}
        {current ? (
          <React.Fragment>
            <circle className="orbit-halo" cx={current.x} cy={current.y} r="18"></circle>
            <circle className="orbit-point" cx={current.x} cy={current.y} r="8"></circle>
            <text className="data-label" x={Math.min(515, current.x + 13)} y={Math.max(24, current.y - 12)}>ISS</text>
          </React.Fragment>
        ) : null}
        <text className="axis-label" x="22" y="284">经纬网 · 最新轨道预测</text>
      </svg>
    );
  }

  function polarPoint(azimuth, elevation) {
    const radius = clamp((90 - elevation) / 90, 0, 1) * 104;
    const angle = (azimuth - 90) * Math.PI / 180;
    return { x: 150 + Math.cos(angle) * radius, y: 145 + Math.sin(angle) * radius };
  }

  function SkyPass({ card }) {
    const pass = card.media && card.media.pass;
    if (!pass || !Array.isArray(pass.points) || !pass.points.length) {
      return <UnavailableVisual label="轨道窗口内暂未算出过境" />;
    }
    const plotted = pass.points.map((point) => polarPoint(point.azimuth, point.elevation));
    const peak = pass.points.reduce((best, point) => point.elevation > best.elevation ? point : best, pass.points[0]);
    const peakPoint = polarPoint(peak.azimuth, peak.elevation);
    return (
      <svg className="v33-data-svg v33-sky-pass" viewBox="0 0 600 290" role="img" aria-label={card.childCaption}>
        <rect x="0" y="0" width="600" height="290" rx="18"></rect>
        {[104, 70, 35].map((radius) => <circle className="grid-ring" key={radius} cx="150" cy="145" r={radius}></circle>)}
        <line className="grid" x1="46" x2="254" y1="145" y2="145"></line>
        <line className="grid" x1="150" x2="150" y1="41" y2="249"></line>
        <polyline className="pass-line" points={plotted.map((point) => `${point.x},${point.y}`).join(" ")}></polyline>
        <circle className="orbit-point" cx={plotted[0].x} cy={plotted[0].y} r="7"></circle>
        <circle className="sun-point" cx={peakPoint.x} cy={peakPoint.y} r="8"></circle>
        <text className="axis-label" x="150" y="28" textAnchor="middle">北</text>
        <text className="axis-label" x="274" y="150">东</text>
        <text className="axis-label" x="150" y="275" textAnchor="middle">南</text>
        <text className="axis-label" x="26" y="150">西</text>
        <text className="metric-big" x="330" y="100">最高 {Math.round(pass.maxElevation)}°</text>
        <text className="metric-label" x="330" y="137">{pass.startDirection}出现 → {pass.endDirection}消失</text>
        <text className="metric-label" x="330" y="170">约 {pass.durationMinutes} 分钟</text>
        <text className="axis-label" x="330" y="214">几何过境预测</text>
      </svg>
    );
  }

  function seriesPath(points, width = 560, height = 210) {
    const series = Array.isArray(points) ? points : [];
    return series.map((point, index) => {
      const x = 20 + clamp(point.t) * width;
      const y = 245 - clamp(point.value, 0, 100) / 100 * height;
      return `${index ? "L" : "M"}${x.toFixed(1)},${y.toFixed(1)}`;
    }).join(" ");
  }

  function RfiSpectrum({ card }) {
    const points = card.media && card.media.points || [];
    return (
      <svg className="v33-data-svg v33-rfi-spectrum" viewBox="0 0 600 290" role="img" aria-label={card.childCaption}>
        <rect x="0" y="0" width="600" height="290" rx="18"></rect>
        {[55, 100, 145, 190, 235].map((y) => <line className="grid" key={y} x1="20" x2="580" y1={y} y2={y}></line>)}
        <path className="spectrum-area" d={`${seriesPath(points)} L580,255 L20,255 Z`}></path>
        <path className="spectrum-line" d={seriesPath(points)}></path>
        <text className="axis-label" x="20" y="278">{Math.round(card.media.minMHz || 0)} MHz</text>
        <text className="axis-label" x="580" y="278" textAnchor="end">{Math.round(card.media.maxMHz || 0)} MHz</text>
      </svg>
    );
  }

  function RfiWaterfall({ card }) {
    const points = card.media && card.media.points || [];
    const columns = 24;
    const rows = 10;
    const cells = [];
    const history = card.media && Array.isArray(card.media.history)
      ? card.media.history
      : null;
    const normalizedHistory = history
      ? history.slice(-rows).map((row) => {
        const values = Array.isArray(row)
          ? row
          : Array.isArray(row.points)
          ? row.points
          : Array.isArray(row.values)
          ? row.values
          : [];
        return values.map((value) => {
          if (typeof value === "number") return value;
          return Number(value.power ?? value.value ?? value.strength) || 0;
        });
      }).filter((row) => row.length)
      : [];

    if (card.status === "onsite" && !normalizedHistory.length) {
      return <UnavailableVisual label="现场时间序列尚未接入" />;
    }

    for (let row = 0; row < rows; row += 1) {
      for (let column = 0; column < columns; column += 1) {
        let value;
        if (normalizedHistory.length) {
          const sourceRow = normalizedHistory[
            Math.min(
              normalizedHistory.length - 1,
              Math.round(row / Math.max(1, rows - 1) *
                (normalizedHistory.length - 1)),
            )
          ];
          value = sourceRow[
            Math.min(
              sourceRow.length - 1,
              Math.round(column / Math.max(1, columns - 1) *
                (sourceRow.length - 1)),
            )
          ];
        } else {
          const t = column / Math.max(1, columns - 1);
          const base = sampleSeries(points, t);
          value = base + Math.sin((row + 1) * (column + 2) * 0.31) * 7;
        }
        cells.push({ row, column, value: clamp(value, 0, 100) });
      }
    }
    return (
      <div className="v33-waterfall" role="img" aria-label={card.childCaption}>
        {cells.map((cell) => (
          <i
            key={`${cell.row}-${cell.column}`}
            style={{
              "--heat": cell.value / 100,
              "--row": cell.row + 1,
              "--column": cell.column + 1,
            }}
          ></i>
        ))}
        <span>频率 →</span>
        <b>时间 ↓</b>
      </div>
    );
  }

  function RfiFootprints({ card }) {
    const footprints = card.media && card.media.footprints || {};
    return (
      <div className="v33-footprints" role="img" aria-label={card.childCaption}>
        {Object.entries(footprints).slice(0, 4).map(([label, count], index) => (
          <span key={label}>
            <i data-shape={index % 3}></i>
            <strong>{label}</strong>
            <b>{count} 次</b>
          </span>
        ))}
        <small>来源待查 · 不读取通信内容</small>
      </div>
    );
  }

  function RfiDirection({ card }) {
    const points = card.media && Array.isArray(card.media.direction)
      ? card.media.direction
      : [];
    return (
      <svg className="v33-data-svg" viewBox="0 0 600 290" role="img" aria-label={card.childCaption}>
        <rect x="0" y="0" width="600" height="290" rx="18"></rect>
        {[100, 70, 40].map((radius) => <circle className="grid-ring" key={radius} cx="300" cy="145" r={radius}></circle>)}
        {points.slice(0, 50).map((point, index) => {
          const plotted = polarPoint(point.azimuth, point.elevation);
          const shiftedX = plotted.x + 150;
          return <circle className="direction-point" key={index} cx={shiftedX} cy={plotted.y} r={4 + clamp(point.power, 0, 100) / 18}></circle>;
        })}
        <text className="axis-label" x="300" y="26" textAnchor="middle">北</text>
        <text className="axis-label" x="300" y="280" textAnchor="middle">能量方向图 · 来源待查</text>
      </svg>
    );
  }

  function UnavailableVisual({ label }) {
    return (
      <div className="v33-unavailable-visual">
        <span aria-hidden="true"></span>
        <strong>{label}</strong>
        <small>不会用模拟图冒充实时资料</small>
      </div>
    );
  }

  function CardVisual({ card }) {
    if (!card) return <UnavailableVisual label="资料卡暂不可用" />;
    const components = {
      image: ImageVisual,
      "galactic-map": GalacticMap,
      "solar-rotation": SolarRotation,
      "ground-track": GroundTrack,
      "sky-pass": SkyPass,
      "rfi-spectrum": RfiSpectrum,
      "rfi-waterfall": RfiWaterfall,
      "rfi-footprints": RfiFootprints,
      "rfi-direction": RfiDirection,
      unavailable: UnavailableVisual,
    };
    const Visual = components[card.type] || UnavailableVisual;
    return card.type === "unavailable"
      ? <UnavailableVisual label="今天暂时没收到这张图" />
      : <Visual card={card} />;
  }

  function DailyEvidenceCard({ card, onOpen, print = false }) {
    const Tag = print ? "article" : "button";
    return (
      <Tag
        className={print ? "v33-print-card" : "v33-daily-card"}
        data-status={card.status || "latest"}
        type={print ? undefined : "button"}
        onClick={print ? undefined : () => onOpen(card)}
      >
        <div className="v33-daily-visual">
          <CardVisual card={card} />
          {!print ? <EvidenceStatus card={card} /> : null}
        </div>
        <div className="v33-daily-copy">
          <small>{card.kicker}</small>
          <strong>{card.title}</strong>
          <span>{card.childCaption}</span>
        </div>
        <footer>
          <span>{card.source && card.source.name}</span>
          <time>{card.observedAt ? timeLabel(card.observedAt) : card.statusLabel}</time>
        </footer>
      </Tag>
    );
  }

  function EvidenceDialog({ card, onClose }) {
    if (!card) return null;
    return (
      <div className="v33-evidence-dialog" role="dialog" aria-modal="true" aria-label={`${card.title}资料放大镜`} onClick={onClose}>
        <article onClick={(event) => event.stopPropagation()}>
          <button className="v33-dialog-close" type="button" onClick={onClose} aria-label="关闭资料放大镜">×</button>
          <div className="v33-dialog-visual"><CardVisual card={card} /></div>
          <div className="v33-dialog-copy">
            <EvidenceStatus card={card} />
            <span>{card.kicker}</span>
            <h2>{card.title}</h2>
            <p>{card.childCaption}</p>
            {card.boundaryNote ? <small>{card.boundaryNote}</small> : null}
            {card.source && card.source.url
              ? <a href={card.source.url} target="_blank" rel="noopener">查看数据来源 · {card.source.name}</a>
              : <b>{card.source && card.source.name}</b>}
          </div>
        </article>
      </div>
    );
  }

  function DailyEvidenceSection({ snapshot, loading }) {
    const [openCard, setOpenCard] = useState(null);
    const cards = snapshot && Array.isArray(snapshot.cards) ? snapshot.cards : [];
    return (
      <section className="v33-daily-archive" data-screen-label="今日科学档案">
        <header className="v33-daily-header">
          <div>
            <span>今天的真实宇宙</span>
            <strong>{snapshot ? dateLabel(snapshot.displayDate) : "正在连接宇宙"}</strong>
          </div>
          <p>{snapshot ? snapshot.boundaryNote : "正在锁定今天的三张资料卡……"}</p>
          <b>{loading ? "正在接收" : `${cards.length} 张资料已锁定`}</b>
        </header>
        <div className="v33-daily-grid" aria-busy={loading}>
          {loading && !cards.length
            ? [0, 1, 2].map((index) => <div className="v33-daily-skeleton" key={index}></div>)
            : cards.slice(0, 3).map((card) => (
              <DailyEvidenceCard key={card.id} card={card} onOpen={setOpenCard} />
            ))}
        </div>
        <EvidenceDialog card={openCard} onClose={() => setOpenCard(null)} />
      </section>
    );
  }

  function PrintEvidenceChart({ scenario, markers }) {
    const points = scenario && scenario.observe && scenario.observe.points || [];
    const markerList = Array.isArray(markers) ? markers : [];
    return (
      <svg className="v33-print-evidence-chart" viewBox="0 0 1000 280" role="img" aria-label="本次教学证据曲线">
        <rect x="0" y="0" width="1000" height="280" rx="20"></rect>
        {[55, 105, 155, 205, 255].map((y) => <line key={y} x1="45" x2="970" y1={y} y2={y}></line>)}
        <path d={seriesPath(points, 925, 200).replace(/([ML])([\d.]+),([\d.]+)/g, (_, command, x, y) => `${command}${Number(x) + 25},${Number(y) - 10}`)}></path>
        {markerList.map((marker, index) => {
          const t = clamp(marker.sampleT);
          const x = 45 + t * 925;
          const y = 235 - clamp(sampleSeries(points, t), 0, 100) / 100 * 200;
          return <g key={marker.id || index}><circle cx={x} cy={y} r="9"></circle><text x={x + 12} y={y - 10}>证据 {index + 1}</text></g>;
        })}
      </svg>
    );
  }

  function PrintDiscoverySheet({ snapshot, scenario, state, resultSummary }) {
    if (!snapshot) return null;
    const cards = Array.isArray(snapshot.cards) ? snapshot.cards.slice(0, 3) : [];
    const markers = state && state.responses && state.responses.markers || [];
    return (
      <section className="v33-print-sheet" aria-label="宇宙来电今日发现卡打印版">
        <header>
          <img src="assets/logo.png" alt="宇宙来电小海龟" />
          <div>
            <span>宇宙来电 · 今日科学档案</span>
            <h1>{scenario.result.cardTitle}</h1>
            <p>{dateLabel(snapshot.displayDate)} · 北京时间 · 成果编号 {snapshot.snapshotId}</p>
          </div>
          <strong>猜想 · 证据 · 发现</strong>
        </header>
        <div className="v33-print-result-row">
          <article>
            <small>小队发现</small>
            <h2>{resultSummary.conclusion}</h2>
            <div className="v33-print-answer-key">
              <p>第 1 题：{resultSummary.prediction} → 正确答案：{resultSummary.correctPrediction}</p>
              <p>第 2 题：{resultSummary.comparison} → 正确答案：{resultSummary.correctComparison}</p>
              <p>第 3 题：{resultSummary.conclusion} → 正确答案：{resultSummary.correctConclusion}</p>
            </div>
            <p>保存：{markers.length} 颗证据星</p>
            <b>{scenario.result.sourceLabel}</b>
          </article>
          <PrintEvidenceChart scenario={scenario} markers={markers} />
        </div>
        <div className="v33-print-daily-grid">
          {cards.map((card) => <DailyEvidenceCard key={card.id} card={card} print />)}
        </div>
        <footer>
          <span>{snapshot.boundaryNote}</span>
          <b>生成：{timeLabel(snapshot.generatedAt)} · 数据时间以每张卡标注为准</b>
        </footer>
      </section>
    );
  }

  async function printDailyEvidence() {
    document.body.classList.add("v33-print-requested");
    const images = Array.from(document.querySelectorAll(".v33-print-sheet img"));
    await Promise.all(images.map((image) => {
      if (image.complete) return Promise.resolve();
      if (typeof image.decode === "function") return image.decode().catch(() => undefined);
      return new Promise((resolve) => {
        image.addEventListener("load", resolve, { once: true });
        image.addEventListener("error", resolve, { once: true });
      });
    }));
    window.setTimeout(() => window.print(), 60);
  }

  window.addEventListener("afterprint", () => {
    document.body.classList.remove("v33-print-requested");
  });

  Object.assign(window, {
    V33DailyEvidence: Object.freeze({
      DailyEvidenceSection,
      PrintDiscoverySheet,
      printDailyEvidence,
      useDailyEvidenceSnapshot,
    }),
  });
})();
