// Main Solar System Explorer app — Three.js powered
const { useState, useEffect, useRef, useCallback } = React;

// Narration singleton lives in narration.js (loaded before any text/babel
// script in index.html) so every consumer can assume window.__narration
// exists. The hook below just adapts it to React.
function useSoundToggle() {
  const [on, setOn] = useState(() => window.__narration.isEnabled());
  useEffect(() => window.__narration.subscribe(setOn), []);
  return [on, (v) => window.__narration.setEnabled(v)];
}

function normalizeStorageKey(key) {
  if (typeof key !== "string") return "";
  return key.trim();
}

function readStorageValue(key, fallback) {
  const storageKey = normalizeStorageKey(key);
  if (!storageKey) return fallback;
  try {
    const value = localStorage.getItem(storageKey);
    return value === null ? fallback : value;
  } catch {
    return fallback;
  }
}

function readStorageFlag(key, fallback = false) {
  const value = readStorageValue(key, fallback ? "1" : "0");
  if (value === "1" || value === true) return true;
  if (value === "0" || value === false) return false;
  return Boolean(fallback);
}

function writeStorageFlag(key, value) {
  writeStorageValue(key, value ? "1" : "0");
}

function readStorageEnum(key, allowedValues, fallback) {
  const allowed = Array.isArray(allowedValues) ? allowedValues : [];
  const safeFallback = allowed.includes(fallback) ? fallback : allowed[0];
  const value = readStorageValue(key, safeFallback);
  return allowed.includes(value) ? value : safeFallback;
}

function readStorageInt(
  key,
  fallback = 0,
  min = Number.NEGATIVE_INFINITY,
  max = Number.POSITIVE_INFINITY,
) {
  const fallbackNumber = Number.isFinite(fallback) ? fallback : 0;
  const low = Number.isFinite(min) ? min : Number.NEGATIVE_INFINITY;
  const high = Number.isFinite(max) ? max : Number.POSITIVE_INFINITY;
  const lowerBound = Math.min(low, high);
  const upperBound = Math.max(low, high);
  const raw = readStorageValue(key, String(fallbackNumber));
  const parsed = Number.parseInt(raw, 10);
  const safe = Number.isFinite(parsed) ? parsed : fallbackNumber;
  return Math.max(lowerBound, Math.min(upperBound, safe));
}

function serializeStorageValue(value) {
  if (value === undefined || value === null) return "";
  if (typeof value === "string") return value;
  if (typeof value === "number" || typeof value === "boolean") {
    return String(value);
  }
  try {
    return JSON.stringify(value);
  } catch {
    return "";
  }
}

function writeStorageValue(key, value) {
  const storageKey = normalizeStorageKey(key);
  if (!storageKey) return;
  try {
    localStorage.setItem(storageKey, serializeStorageValue(value));
  } catch {}
}

function removeStorageValue(key) {
  const storageKey = normalizeStorageKey(key);
  if (!storageKey) return;
  try {
    localStorage.removeItem(storageKey);
  } catch {}
}

function readStorageJson(key, fallback) {
  const storageKey = normalizeStorageKey(key);
  if (!storageKey) return fallback;
  try {
    const raw = localStorage.getItem(storageKey);
    return raw ? JSON.parse(raw) : fallback;
  } catch {
    return fallback;
  }
}

function writeStorageJson(key, value) {
  const storageKey = normalizeStorageKey(key);
  if (!storageKey) return;
  try {
    const serialized = JSON.stringify(value);
    if (serialized === undefined) return;
    localStorage.setItem(storageKey, serialized);
  } catch {}
}

function safeArray(value) {
  return Array.isArray(value) ? value : [];
}

function normalizeNarrationClip(file) {
  if (typeof file !== "string") return "";
  const clip = file.trim();
  if (!clip || !/\.mp3$/i.test(clip)) return "";
  if (/^(?:[a-z]+:|\/|\\)/i.test(clip)) return "";
  if (clip.split(/[\\/]+/).includes("..")) return "";
  return clip;
}

function hasNarrationClip(file) {
  return Boolean(normalizeNarrationClip(file));
}

function playNarration(file) {
  if (!window.__narration) return;
  const clip = normalizeNarrationClip(file);
  if (!clip) return;
  window.__narration.play(clip);
}

function stopNarration() {
  if (window.__narration) window.__narration.stop();
}

const __gameRuntimeCleanups = new Set();
function registerGameRuntimeCleanup(cleanup) {
  if (typeof cleanup !== "function") return () => {};
  __gameRuntimeCleanups.add(cleanup);
  return () => __gameRuntimeCleanups.delete(cleanup);
}

function isSpacePerfEnabled() {
  try {
    const value = new URLSearchParams(window.location.search).get("perf");
    return value !== null && value !== "0" && value !== "false";
  } catch {
    return false;
  }
}

function roundPerfNumber(value) {
  return Math.round(value * 100) / 100;
}

function clampPerfNumber(value, fallback, min, max) {
  const parsed = Number(value);
  const safe = Number.isFinite(parsed) ? parsed : fallback;
  return Math.max(min, Math.min(max, safe));
}

function recordSpacePerfSample(sample) {
  try {
    window.__spacePerfSamples = window.__spacePerfSamples || [];
    window.__spacePerfSamples.push(sample);
  } catch {}
  try {
    console.info("[SpacePerf]", sample);
  } catch {}
}

function createSpacePerfProbe(sceneName, options = {}) {
  const enabled = isSpacePerfEnabled();
  const noop = {
    enabled: false,
    markInteractive: () => {},
    collectFrame: () => {},
  };
  if (!enabled || typeof performance === "undefined") return noop;

  const scene = String(sceneName || "space-scene").trim() || "space-scene";
  const sampleMs = clampPerfNumber(options.sampleMs, 5000, 1000, 20000);
  const startedAt = performance.now();
  let interactiveLogged = false;
  let lastFrameAt = 0;
  let sampleStartedAt = 0;
  let frameLogged = false;
  const frameTimes = [];

  const basePayload = () => ({
    scene,
    sampleMs,
    timestamp: new Date().toISOString(),
  });

  return {
    enabled: true,
    markInteractive(extra = {}) {
      if (interactiveLogged) return;
      interactiveLogged = true;
      const now = performance.now();
      recordSpacePerfSample({
        ...basePayload(),
        kind: "first-interactive",
        entryMs: roundPerfNumber(now - startedAt),
        ...(extra && typeof extra === "object" ? extra : {}),
      });
    },
    collectFrame(now = performance.now()) {
      if (frameLogged) return;
      if (!sampleStartedAt) sampleStartedAt = now;
      if (lastFrameAt) {
        const dt = now - lastFrameAt;
        if (Number.isFinite(dt) && dt > 0 && dt < 1000) frameTimes.push(dt);
      }
      lastFrameAt = now;
      if (now - sampleStartedAt < sampleMs || frameTimes.length < 2) return;

      frameLogged = true;
      const sorted = frameTimes.slice().sort((a, b) => a - b);
      const median = sorted[Math.floor(sorted.length / 2)];
      const p95 = sorted[Math.floor(sorted.length * 0.95)] || sorted[sorted.length - 1];
      const average = frameTimes.reduce((sum, dt) => sum + dt, 0) / frameTimes.length;
      recordSpacePerfSample({
        ...basePayload(),
        kind: "frame-median",
        frames: frameTimes.length,
        medianFrameMs: roundPerfNumber(median),
        averageFrameMs: roundPerfNumber(average),
        p95FrameMs: roundPerfNumber(p95),
      });
    },
  };
}

function stopSpaceGameRuntimes(reason = "exit") {
  stopNarration();
  try {
    document.querySelectorAll("audio, video").forEach((media) => {
      try {
        media.pause();
      } catch {}
    });
  } catch {}
  try {
    window.dispatchEvent(
      new CustomEvent("space-explorer:stop-game", { detail: { reason } }),
    );
  } catch {}
  Array.from(__gameRuntimeCleanups).forEach((cleanup) => {
    try {
      cleanup(reason);
    } catch (err) {
      reportSpaceError?.(err, { source: "stopSpaceGameRuntimes" });
    }
  });
}

function celebrateGameFinish(options = {}) {
  const source = options && typeof options === "object" ? options : {};
  const detail = {
    gameId: String(source.gameId || "space-game").trim() || "space-game",
    emoji: String(source.emoji || "⭐").trim().slice(0, 4) || "⭐",
    title:
      String(source.title || "Mission complete!")
        .replace(/\s+/g, " ")
        .trim()
        .slice(0, 48) || "Mission complete!",
    message:
      String(source.message || "You did it!")
        .replace(/\s+/g, " ")
        .trim()
        .slice(0, 96) || "You did it!",
  };
  try {
    window.dispatchEvent(
      new CustomEvent("space-explorer:game-finish-celebration", {
        detail,
      }),
    );
  } catch {}
  return detail;
}

// ---------- Helmet sticker decorator ----------
// Kid-picked emoji sticker that rides above the astronaut's helmet during flight.
// Persisted to localStorage so the choice carries across sessions.
const GENERATED_ASSETS = {
  helmet: "data/generated-assets/helmet-blank.png",
  sonAstronaut: "data/generated-assets/son-astronaut-character.png",
  astronautFlying: "data/sprites/astronaut-flying-v2-atlas.png",
  comet: "data/generated-assets/comet-friend.png",
  missionBadge: "data/generated-assets/mission-badge.png",
  storyShelf: "data/generated-assets/storybook-space-shelf-sixteen.png",
  soundBell: "data/generated-assets/sound-bell.png",
  rocket: "data/generated-assets/rocket-whoosh.png",
  rocketLabStage: "data/generated-assets/rocket-lab/rocket-lab-stage-bg.png",
  rocketLabRocket: "data/generated-assets/rocket-lab/rocket-cause-test-rocket.png",
  rocketLabExhaust: "data/generated-assets/rocket-lab/rocket-cause-exhaust.png",
  sparkleStar: "data/generated-assets/sparkle-star.png",
  alien: "data/generated-assets/alien-token.png",
  moon: "data/generated-assets/moon-charm.png",
  helmetLeft: "data/generated-assets/helmet-side-left.png",
  helmetBack: "data/generated-assets/helmet-back-round.png",
  helmetRight: "data/generated-assets/helmet-side-right.png",
  rocketNose: "data/generated-assets/rocket-builder/rocket-part-nose.png",
  rocketBody: "data/generated-assets/rocket-builder/rocket-part-body.png",
  rocketFins: "data/generated-assets/rocket-builder/rocket-part-fins.png",
  rocketBooster: "data/generated-assets/rocket-builder/rocket-part-booster.png",
  rocketWindow: "data/generated-assets/rocket-builder/rocket-part-window.png",
  rocketEngine: "data/generated-assets/rocket-builder/rocket-part-engine.png",
  rocketFuelTank:
    "data/generated-assets/rocket-builder/rocket-part-fuel-tank.png",
  rocketFuelPipes:
    "data/generated-assets/rocket-builder/rocket-part-fuel-pipes.png",
  rocketIgniter: "data/generated-assets/rocket-builder/rocket-part-igniter.png",
  rocketAntenna: "data/generated-assets/rocket-builder/rocket-part-antenna.png",
  rocketBadge: "data/generated-assets/rocket-builder/rocket-part-badge.png",
  rocketFire: "data/generated-assets/rocket-builder/rocket-launch-fire.png",
  pilotIssTarget: "data/generated-assets/pilot-school/iss-docking-target.png",
  pilotControlBoard:
    "data/generated-assets/pilot-school/control-board-panel.png",
  pilotTrainingRocket:
    "data/generated-assets/pilot-school/pilot-training-rocket.png",
  asteroidDodgeBg:
    "data/generated-assets/asteroid-dodge/asteroid-dodge-level-bg.png",
  asteroidDodgeRocket:
    "data/generated-assets/asteroid-dodge/asteroid-dodge-player-rocket.png",
  gravityAstronautJump:
    "data/generated-assets/gravity-jump/astronaut-moon-jump-sheet.png",
  gravityWorldsBg:
    "data/generated-assets/gravity-jump/gravity-worlds-full-bg.png",
  gravityBgMoon: "data/generated-assets/gravity-jump/gravity-bg-moon.png",
  gravityBgEarth: "data/generated-assets/gravity-jump/gravity-bg-earth-v2.png",
  gravityBgMars: "data/generated-assets/gravity-jump/gravity-bg-mars.png",
  gravityBgBig: "data/generated-assets/gravity-jump/gravity-bg-jupiter-v2.png",
  gravityAstronautJumpClean:
    "data/generated-assets/gravity-jump/astronaut-moon-jump-sheet-normalized.png",
  asteroidRock:
    "data/generated-assets/moon-rover/rocks/moon-rock-meteorite.png",
  asteroidIce: "data/generated-assets/moon-rover/rocks/moon-rock-ice.png",
};

function getGeneratedAsset(name, fallback = "") {
  if (!name || !Object.prototype.hasOwnProperty.call(GENERATED_ASSETS, name)) {
    return fallback;
  }
  return GENERATED_ASSETS[name] || fallback;
}

function normalizeAssetSrc(src) {
  if (typeof src !== "string") return "";
  const value = src.trim();
  if (!value) return "";
  if (/^(https?:|data:|blob:)/.test(value)) return value;
  return value.replace(/^\.?\//, "");
}

function normalizeAssetContext(context, fallback = "asset") {
  const normalized = String(context || fallback).trim();
  return normalized || fallback;
}

function spaceErrorMessage(message) {
  if (message && typeof message === "object" && message.message) {
    return String(message.message).trim() || "Space Explorer error";
  }
  return String(message || "Space Explorer error").trim() || "Space Explorer error";
}

function spaceErrorSource(source, fallback = "app") {
  const normalized = String(source || fallback).trim();
  return normalized || fallback;
}

function spaceErrorDetail(detail = {}) {
  return detail && typeof detail === "object" ? detail : {};
}

function normalizeSpaceErrorRecord(message, detail = {}) {
  const messageRecord = message && typeof message === "object" ? message : {};
  const safeDetail = spaceErrorDetail(
    detail && Object.keys(spaceErrorDetail(detail)).length
      ? detail
      : messageRecord.detail,
  );
  const numericTime = Number(messageRecord.time);
  return {
    message: spaceErrorMessage(message),
    source: spaceErrorSource(safeDetail.source || messageRecord.source),
    detail: safeDetail,
    time: Number.isFinite(numericTime) ? numericTime : Date.now(),
  };
}

function reportSpaceError(message, detail = {}) {
  try {
    if (!window.__spaceErrors) window.__spaceErrors = [];
    window.__spaceErrors.push(normalizeSpaceErrorRecord(message, detail));
  } catch {}
}

function reportAssetLoadFailure(src, context = "asset") {
  reportSpaceError("Image asset failed to load.", {
    source: normalizeAssetContext(context),
    asset: src || "",
  });
}

function assetFallbackText(label, state = "loading") {
  const safeLabel = String(label || "Picture").trim() || "Picture";
  return state === "failed"
    ? `${safeLabel} could not load.`
    : `${safeLabel} is loading.`;
}

function safeAssetFallbackText(text, src, failed = false) {
  const message = typeof text === "string" ? text.trim() : "";
  if (message) return message;
  return failed || src
    ? assetFallbackText("Picture", "failed")
    : assetFallbackText("Picture", "loading");
}

const SafeAssetImage = React.forwardRef(function SafeAssetImage(
  {
    src,
    alt = "",
    className = "",
    fallbackClassName = "",
    fallbackText = "",
    context = "SafeAssetImage",
    onError,
    ...props
  },
  ref,
) {
  const [failed, setFailed] = useState(false);
  const safeSrc = normalizeAssetSrc(src);
  const safeContext = normalizeAssetContext(context, "SafeAssetImage");
  useEffect(() => {
    setFailed(false);
  }, [safeSrc]);
  if (!safeSrc || failed) {
    const fallbackClasses =
      `${className} asset-image-fallback ${fallbackClassName}`.trim();
    return (
      <span className={fallbackClasses} role="status">
        {safeAssetFallbackText(fallbackText, safeSrc, failed)}
      </span>
    );
  }
  return (
    <img
      {...props}
      ref={ref}
      className={className}
      src={safeSrc}
      alt={alt}
      onError={(event) => {
        setFailed(true);
        reportAssetLoadFailure(safeSrc, safeContext);
        if (typeof onError === "function") onError(event);
      }}
    />
  );
});

const GRAVITY_JUMP_FRAMES = [
  "data/generated-assets/gravity-jump/frames/astronaut-jump-0.png",
  "data/generated-assets/gravity-jump/frames/astronaut-jump-1.png",
  "data/generated-assets/gravity-jump/frames/astronaut-jump-2.png",
  "data/generated-assets/gravity-jump/frames/astronaut-jump-3.png",
  "data/generated-assets/gravity-jump/frames/astronaut-jump-4.png",
  "data/generated-assets/gravity-jump/frames/astronaut-jump-5.png",
];

const STAR_HOME_PAGES = [
  {
    image: "data/generated-assets/storybook/star-home-page-01.png",
    text: "Adam waved from his ship. Pip waved too.",
    audio: "story_01.mp3",
  },
  {
    image: "data/generated-assets/storybook/star-home-page-02.png",
    text: "A tiny star blinked beside the moon rocks.",
    audio: "story_02.mp3",
  },
  {
    image: "data/generated-assets/storybook/star-home-page-03.png",
    text: "Adam and Pip bounced across the quiet moon.",
    audio: "story_03.mp3",
  },
  {
    image: "data/generated-assets/storybook/star-home-page-04.png",
    text: "A golden comet showed them a sparkly path.",
    audio: "story_04.mp3",
  },
  {
    image: "data/generated-assets/storybook/star-home-page-05.png",
    text: "They zoomed past Saturn and its soft, glowing rings.",
    audio: "story_05.mp3",
  },
  {
    image: "data/generated-assets/storybook/star-home-page-06.png",
    text: "Pip shared moon cookies. The little star felt brave.",
    audio: "story_06.mp3",
  },
  {
    image: "data/generated-assets/storybook/star-home-page-07.png",
    text: "At last, the star found its bright family.",
    audio: "story_07.mp3",
  },
  {
    image: "data/generated-assets/storybook/star-home-page-08.png",
    text: "Adam and Pip smiled. Goodnight, little stars.",
    audio: "story_08.mp3",
  },
];

const MOON_SEED_PAGES = [
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-01.png",
    text: "Adam found a moon seed glowing in the dust.",
    audio: "moonseed_01.mp3",
  },
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-02.png",
    text: "He carried it carefully in his little bucket.",
    audio: "moonseed_02.mp3",
  },
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-03.png",
    text: "Pip helped pat the soft moon soil.",
    audio: "moonseed_03.mp3",
  },
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-04.png",
    text: "They watered the seed with drops of starlight.",
    audio: "moonseed_04.mp3",
  },
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-05.png",
    text: "A tiny glowing sprout peeked up and smiled.",
    audio: "moonseed_05.mp3",
  },
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-06.png",
    text: "Soon, star flowers twinkled in the garden.",
    audio: "moonseed_06.mp3",
  },
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-07.png",
    text: "The planets came close to see the shine.",
    audio: "moonseed_07.mp3",
  },
  {
    image: "data/generated-assets/storybook-moon-seed/moon-seed-page-08.png",
    text: "Adam and Pip rested under the star flower.",
    audio: "moonseed_08.mp3",
  },
];

const COMET_HAT_PAGES = [
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-01.png",
    text: "I found a tiny comet by my boots.",
    audio: "comethat_01.mp3",
  },
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-02.png",
    text: "I put it on my helmet like a hat.",
    audio: "comethat_02.mp3",
  },
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-03.png",
    text: "Hop, hop! My comet hat sparkled.",
    audio: "comethat_03.mp3",
  },
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-04.png",
    text: "I waved hello to Earth.",
    audio: "comethat_04.mp3",
  },
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-05.png",
    text: "My alien friend laughed with me.",
    audio: "comethat_05.mp3",
  },
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-06.png",
    text: "We followed little star crumbs.",
    audio: "comethat_06.mp3",
  },
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-07.png",
    text: "I helped the comet fly back home.",
    audio: "comethat_07.mp3",
  },
  {
    image: "data/generated-assets/storybook-comet-hat/comet-hat-page-08.png",
    text: "Then I smiled under the stars.",
    audio: "comethat_08.mp3",
  },
];

const ROCKET_PAJAMAS_COVER =
  "data/generated-assets/storybook-rocket-pajamas/rocket-pajamas-cover.png";
const ROCKET_PAJAMAS_PAGE_IMAGES = [
  "data/generated-assets/storybook-rocket-pajamas/rocket-pajamas-page-01.png",
  "data/generated-assets/storybook-rocket-pajamas/rocket-pajamas-page-02.png",
  "data/generated-assets/storybook-rocket-pajamas/rocket-pajamas-page-03.png",
  "data/generated-assets/storybook-rocket-pajamas/rocket-pajamas-page-04.png",
];
const SATURN_RING_COVER =
  "data/generated-assets/storybook-saturn-ring/saturn-ring-cover.png";
const SATURN_RING_PAGE_IMAGES = [
  "data/generated-assets/storybook-saturn-ring/saturn-ring-page-01.png",
  "data/generated-assets/storybook-saturn-ring/saturn-ring-page-02.png",
  "data/generated-assets/storybook-saturn-ring/saturn-ring-page-03.png",
  "data/generated-assets/storybook-saturn-ring/saturn-ring-page-04.png",
];
const BLACK_HOLE_PEEKABOO_COVER =
  "data/generated-assets/storybook-black-hole-peekaboo/black-hole-peekaboo-cover.png";
const BLACK_HOLE_PEEKABOO_PAGE_IMAGES = [
  "data/generated-assets/storybook-black-hole-peekaboo/black-hole-peekaboo-page-01.png",
  "data/generated-assets/storybook-black-hole-peekaboo/black-hole-peekaboo-page-02.png",
  "data/generated-assets/storybook-black-hole-peekaboo/black-hole-peekaboo-page-03.png",
  "data/generated-assets/storybook-black-hole-peekaboo/black-hole-peekaboo-page-04.png",
];
const MOON_MAILBOX_PAGE_IMAGES = [
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-01.png",
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-02.png",
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-03.png",
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-04.png",
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-05.png",
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-06.png",
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-07.png",
  "data/generated-assets/storybook-moon-mailbox/moon-mailbox-page-08.png",
];
const JUPITER_STORM_PAGE_IMAGES = [
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-01.png",
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-02.png",
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-03.png",
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-04.png",
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-05.png",
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-06.png",
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-07.png",
  "data/generated-assets/storybook-jupiter-storm/jupiter-storm-page-08.png",
];
const NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES = [
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-01.png",
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-02.png",
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-03.png",
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-04.png",
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-05.png",
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-06.png",
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-07.png",
  "data/generated-assets/storybook-neptune-night-light/neptune-night-light-page-08.png",
];
const MARS_KITE_DAY_PAGE_IMAGES = [
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-01.png",
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-02.png",
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-03.png",
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-04.png",
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-05.png",
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-06.png",
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-07.png",
  "data/generated-assets/storybook-mars-kite-day/mars-kite-day-page-08.png",
];
const VENUS_UMBRELLA_PARADE_PAGE_IMAGES = [
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-01.png",
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-02.png",
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-03.png",
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-04.png",
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-05.png",
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-06.png",
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-07.png",
  "data/generated-assets/storybook-venus-umbrella-parade/venus-umbrella-parade-page-08.png",
];
const MERCURY_SHADOW_TAG_PAGE_IMAGES = [
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-01.png",
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-02.png",
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-03.png",
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-04.png",
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-05.png",
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-06.png",
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-07.png",
  "data/generated-assets/storybook-mercury-shadow-tag/mercury-shadow-tag-page-08.png",
];
const URANUS_TILTED_TEA_PAGE_IMAGES = [
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-01.png",
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-02.png",
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-03.png",
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-04.png",
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-05.png",
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-06.png",
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-07.png",
  "data/generated-assets/storybook-uranus-tilted-tea/uranus-tilted-tea-page-08.png",
];
const PLUTO_SNOW_PARADE_PAGE_IMAGES = [
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-01.png",
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-02.png",
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-03.png",
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-04.png",
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-05.png",
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-06.png",
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-07.png",
  "data/generated-assets/storybook-pluto-snow-parade/pluto-snow-parade-page-08.png",
];
const MOON_WORD_WALK_PAGE_IMAGE =
  "data/generated-assets/storybook-moon-word-walk/moon-word-walk-page-01.png";
const ROCKET_WORD_RIDE_PAGE_IMAGE =
  "data/generated-assets/storybook-rocket-word-ride/rocket-word-ride-page-01.png";

const ROCKET_PAJAMAS_PAGES = [
  {
    image: ROCKET_PAJAMAS_PAGE_IMAGES[0],
    text: "My rocket put on stripey pajamas.",
    audio: "rocket-pajamas-1.mp3",
  },
  {
    image: ROCKET_PAJAMAS_PAGE_IMAGES[1],
    text: "It yawned a tiny puff of fire.",
    audio: "rocket-pajamas-2.mp3",
  },
  {
    image: ROCKET_PAJAMAS_PAGE_IMAGES[2],
    text: "We tucked it under a moon blanket.",
    audio: "rocket-pajamas-3.mp3",
  },
  {
    image: ROCKET_PAJAMAS_PAGE_IMAGES[3],
    text: "Goodnight, rocket. Dream of stars.",
    audio: "rocket-pajamas-4.mp3",
  },
];

const SATURN_RING_PAGES = [
  {
    image: SATURN_RING_PAGE_IMAGES[0],
    text: "Saturn looked around. One ring was missing.",
    audio: "saturn-lost-ring-1.mp3",
  },
  {
    image: SATURN_RING_PAGE_IMAGES[1],
    text: "The ring rolled past a sleepy moon.",
    audio: "saturn-lost-ring-2.mp3",
  },
  {
    image: SATURN_RING_PAGE_IMAGES[2],
    text: "Adam helped it loop back home.",
    audio: "saturn-lost-ring-3.mp3",
  },
  {
    image: SATURN_RING_PAGE_IMAGES[3],
    text: "Saturn spun a happy thank-you dance.",
    audio: "saturn-lost-ring-4.mp3",
  },
];

const BLACK_HOLE_PEEKABOO_PAGES = [
  {
    image: BLACK_HOLE_PEEKABOO_PAGE_IMAGES[0],
    text: "A little swirl whispered, peekaboo.",
    audio: "black-hole-peekaboo-1.mp3",
  },
  {
    image: BLACK_HOLE_PEEKABOO_PAGE_IMAGES[1],
    text: "Stars hid behind its purple curls.",
    audio: "black-hole-peekaboo-2.mp3",
  },
  {
    image: BLACK_HOLE_PEEKABOO_PAGE_IMAGES[2],
    text: "Adam counted one, two, three lights.",
    audio: "black-hole-peekaboo-3.mp3",
  },
  {
    image: BLACK_HOLE_PEEKABOO_PAGE_IMAGES[3],
    text: "The swirl smiled and let them shine.",
    audio: "black-hole-peekaboo-4.mp3",
  },
];

const MOON_MAILBOX_PAGES = [
  {
    image: MOON_MAILBOX_PAGE_IMAGES[0],
    text: "Adam found a shiny mailbox on the moon.",
    audio: "moon-mailbox-1.mp3",
  },
  {
    image: MOON_MAILBOX_PAGE_IMAGES[1],
    text: "Pip slipped in one glowing letter.",
    audio: "moon-mailbox-2.mp3",
  },
  {
    image: MOON_MAILBOX_PAGE_IMAGES[2],
    text: "The mailbox pointed toward the planets.",
    audio: "moon-mailbox-3.mp3",
  },
  {
    image: MOON_MAILBOX_PAGE_IMAGES[3],
    text: "They hopped across Mars with moon mail.",
    audio: "moon-mailbox-4.mp3",
  },
  {
    image: MOON_MAILBOX_PAGE_IMAGES[4],
    text: "Saturn caught a blue envelope on one ring.",
    audio: "moon-mailbox-5.mp3",
  },
  {
    image: MOON_MAILBOX_PAGE_IMAGES[5],
    text: "Jupiter sent back a golden cloud note.",
    audio: "moon-mailbox-6.mp3",
  },
  {
    image: MOON_MAILBOX_PAGE_IMAGES[6],
    text: "The little star read it and glowed.",
    audio: "moon-mailbox-7.mp3",
  },
  {
    image: MOON_MAILBOX_PAGE_IMAGES[7],
    text: "Adam and Pip rested by the mailbox.",
    audio: "moon-mailbox-8.mp3",
  },
];

const JUPITER_STORM_PAGES = [
  {
    image: JUPITER_STORM_PAGE_IMAGES[0],
    text: "A tiny storm smiled from Jupiter's clouds.",
    audio: "jupiter-storm-1.mp3",
  },
  {
    image: JUPITER_STORM_PAGE_IMAGES[1],
    text: "Pip brought it a soft moon ribbon.",
    audio: "jupiter-storm-2.mp3",
  },
  {
    image: JUPITER_STORM_PAGE_IMAGES[2],
    text: "The storm spun fast and sprayed stardust.",
    audio: "jupiter-storm-3.mp3",
  },
  {
    image: JUPITER_STORM_PAGE_IMAGES[3],
    text: "Adam held up a warm calm lantern.",
    audio: "jupiter-storm-4.mp3",
  },
  {
    image: JUPITER_STORM_PAGE_IMAGES[4],
    text: "The storm made slow round cloud rings.",
    audio: "jupiter-storm-5.mp3",
  },
  {
    image: JUPITER_STORM_PAGE_IMAGES[5],
    text: "Jupiter's stripes grew tidy and bright.",
    audio: "jupiter-storm-6.mp3",
  },
  {
    image: JUPITER_STORM_PAGE_IMAGES[6],
    text: "Everyone floated by a little moon.",
    audio: "jupiter-storm-7.mp3",
  },
  {
    image: JUPITER_STORM_PAGE_IMAGES[7],
    text: "The gentle storm slept in Jupiter's clouds.",
    audio: "jupiter-storm-8.mp3",
  },
];

const NEPTUNE_NIGHT_LIGHT_PAGES = [
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[0],
    text: "Adam found a lost night light star.",
    audio: "neptune-night-light-1.mp3",
  },
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[1],
    text: "Pip held it softly in both hands.",
    audio: "neptune-night-light-2.mp3",
  },
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[2],
    text: "Neptune opened curtains of blue cloud.",
    audio: "neptune-night-light-3.mp3",
  },
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[3],
    text: "They rode a glowing moonbeam path.",
    audio: "neptune-night-light-4.mp3",
  },
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[4],
    text: "A sleepy comet carried lantern light.",
    audio: "neptune-night-light-5.mp3",
  },
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[5],
    text: "The star lit Neptune's little moons.",
    audio: "neptune-night-light-6.mp3",
  },
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[6],
    text: "Neptune smiled at the sparkling circle.",
    audio: "neptune-night-light-7.mp3",
  },
  {
    image: NEPTUNE_NIGHT_LIGHT_PAGE_IMAGES[7],
    text: "Adam and Pip waved goodnight from their rocket.",
    audio: "neptune-night-light-8.mp3",
  },
];

const MARS_KITE_DAY_PAGES = [
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[0],
    text: "Adam found a folded red kite on Mars.",
    audio: "mars-kite-day-1.mp3",
  },
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[1],
    text: "Pip helped unfold it beside the rocks.",
    audio: "mars-kite-day-2.mp3",
  },
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[2],
    text: "A soft dust breeze lifted the kite.",
    audio: "mars-kite-day-3.mp3",
  },
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[3],
    text: "The kite danced above tiny rover tracks.",
    audio: "mars-kite-day-4.mp3",
  },
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[4],
    text: "Adam followed it past a little rover.",
    audio: "mars-kite-day-5.mp3",
  },
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[5],
    text: "Pip caught the string near a crater.",
    audio: "mars-kite-day-6.mp3",
  },
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[6],
    text: "The kite pointed to Earth like a dot.",
    audio: "mars-kite-day-7.mp3",
  },
  {
    image: MARS_KITE_DAY_PAGE_IMAGES[7],
    text: "They rested under the happy red kite.",
    audio: "mars-kite-day-8.mp3",
  },
];

const VENUS_UMBRELLA_PARADE_PAGES = [
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[0],
    text: "Adam opened a shiny umbrella on Venus.",
    audio: "venus-umbrella-parade-1.mp3",
  },
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[1],
    text: "Tiny cloud drops tapped a gentle beat.",
    audio: "venus-umbrella-parade-2.mp3",
  },
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[2],
    text: "A friendly cloud made a swirly path.",
    audio: "venus-umbrella-parade-3.mp3",
  },
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[3],
    text: "Adam and Pip marched in a parade.",
    audio: "venus-umbrella-parade-4.mp3",
  },
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[4],
    text: "The umbrella glowed like a sun shield.",
    audio: "venus-umbrella-parade-5.mp3",
  },
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[5],
    text: "They waved to a little floating probe.",
    audio: "venus-umbrella-parade-6.mp3",
  },
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[6],
    text: "The clouds turned peach, gold, and calm.",
    audio: "venus-umbrella-parade-7.mp3",
  },
  {
    image: VENUS_UMBRELLA_PARADE_PAGE_IMAGES[7],
    text: "Adam closed the umbrella with a smile.",
    audio: "venus-umbrella-parade-8.mp3",
  },
];

const MERCURY_SHADOW_TAG_PAGES = [
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[0],
    text: "Adam saw a tiny shadow on Mercury.",
    audio: "mercury-shadow-tag-1.mp3",
  },
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[1],
    text: "Pip tapped the shadow with one paw.",
    audio: "mercury-shadow-tag-2.mp3",
  },
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[2],
    text: "The low Sun made the shadow long.",
    audio: "mercury-shadow-tag-3.mp3",
  },
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[3],
    text: "Adam jumped, and the shadow jumped too.",
    audio: "mercury-shadow-tag-4.mp3",
  },
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[4],
    text: "They hid beside a shiny crater rim.",
    audio: "mercury-shadow-tag-5.mp3",
  },
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[5],
    text: "A solar sparkle showed the way.",
    audio: "mercury-shadow-tag-6.mp3",
  },
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[6],
    text: "Adam and Pip waved at their shadows.",
    audio: "mercury-shadow-tag-7.mp3",
  },
  {
    image: MERCURY_SHADOW_TAG_PAGE_IMAGES[7],
    text: "Then they rested in cool crater shade.",
    audio: "mercury-shadow-tag-8.mp3",
  },
];

const URANUS_TILTED_TEA_PAGES = [
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[0],
    text: "Adam found a tiny tilted tea table.",
    audio: "uranus-tilted-tea-1.mp3",
  },
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[1],
    text: "Pip balanced one star cookie carefully.",
    audio: "uranus-tilted-tea-2.mp3",
  },
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[2],
    text: "A blue moon tilted the cups gently.",
    audio: "uranus-tilted-tea-3.mp3",
  },
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[3],
    text: "The teapot poured sparkles sideways.",
    audio: "uranus-tilted-tea-4.mp3",
  },
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[4],
    text: "They moved cushions to match the tilt.",
    audio: "uranus-tilted-tea-5.mp3",
  },
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[5],
    text: "Uranus rings glowed softly behind them.",
    audio: "uranus-tilted-tea-6.mp3",
  },
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[6],
    text: "Everyone leaned together, and nothing fell.",
    audio: "uranus-tilted-tea-7.mp3",
  },
  {
    image: URANUS_TILTED_TEA_PAGE_IMAGES[7],
    text: "Adam and Pip shared sleepy star tea.",
    audio: "uranus-tilted-tea-8.mp3",
  },
];

const PLUTO_SNOW_PARADE_PAGES = [
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[0],
    text: "Adam caught a tiny snow sparkle.",
    audio: "pluto-snow-parade-1.mp3",
  },
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[1],
    text: "Pip rolled a heart-shaped snowball.",
    audio: "pluto-snow-parade-2.mp3",
  },
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[2],
    text: "Little Pluto smiled in the sky.",
    audio: "pluto-snow-parade-3.mp3",
  },
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[3],
    text: "They made a line of tiny snow moons.",
    audio: "pluto-snow-parade-4.mp3",
  },
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[4],
    text: "A blue comet hummed parade music.",
    audio: "pluto-snow-parade-5.mp3",
  },
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[5],
    text: "Adam planted a flag by the path.",
    audio: "pluto-snow-parade-6.mp3",
  },
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[6],
    text: "Pip pulled a sled of star cookies.",
    audio: "pluto-snow-parade-7.mp3",
  },
  {
    image: PLUTO_SNOW_PARADE_PAGE_IMAGES[7],
    text: "They waved goodnight in gentle snow.",
    audio: "pluto-snow-parade-8.mp3",
  },
];

const MOON_WORD_WALK_PAGES = [
  {
    image: MOON_WORD_WALK_PAGE_IMAGE,
    text: "Moon. Adam and Pip take a moon walk.",
    audio: "moon-word-walk-1.mp3",
    words: [
      { label: "moon", audio: "word-moon.mp3" },
      { label: "walk", audio: "word-walk.mp3" },
    ],
  },
  {
    image: MOON_WORD_WALK_PAGE_IMAGE,
    text: "Rock. Pip taps a gray moon rock.",
    audio: "moon-word-walk-2.mp3",
    words: [{ label: "rock", audio: "word-rock.mp3" }],
  },
  {
    image: MOON_WORD_WALK_PAGE_IMAGE,
    text: "Star. A bright star shines up high.",
    audio: "moon-word-walk-3.mp3",
    words: [
      { label: "star", audio: "word-star.mp3" },
      { label: "high", audio: "word-high.mp3" },
    ],
  },
  {
    image: MOON_WORD_WALK_PAGE_IMAGE,
    text: "Home. They wave and walk home.",
    audio: "moon-word-walk-4.mp3",
    words: [
      { label: "home", audio: "word-home.mp3" },
      { label: "wave", audio: "word-wave.mp3" },
    ],
  },
];

const ROCKET_WORD_RIDE_PAGES = [
  {
    image: ROCKET_WORD_RIDE_PAGE_IMAGE,
    text: "Rocket. The rocket waits on the pad.",
    audio: "rocket-word-ride-1.mp3",
    words: [
      { label: "rocket", audio: "word-rocket.mp3" },
      { label: "pad", audio: "word-pad.mp3" },
    ],
  },
  {
    image: ROCKET_WORD_RIDE_PAGE_IMAGE,
    text: "Up. The rocket goes up.",
    audio: "rocket-word-ride-2.mp3",
    words: [{ label: "up", audio: "word-up.mp3" }],
  },
  {
    image: ROCKET_WORD_RIDE_PAGE_IMAGE,
    text: "Fast and slow. It zooms fast, then floats slow.",
    audio: "rocket-word-ride-3.mp3",
    words: [
      { label: "fast", audio: "word-fast.mp3" },
      { label: "slow", audio: "word-slow.mp3" },
    ],
  },
  {
    image: ROCKET_WORD_RIDE_PAGE_IMAGE,
    text: "Down. The rocket comes down softly.",
    audio: "rocket-word-ride-4.mp3",
    words: [{ label: "down", audio: "word-down.mp3" }],
  },
];

const STORY_NARRATION_DELAY_MS = 1400;

const STORYBOOKS = {
  starHome: {
    title: "The Little Star Home",
    shelfTitle: "Little Star",
    shelfCue: "Help a tiny star get home.",
    shelfIcon: "⭐",
    pages: STAR_HOME_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "star-home",
  },
  moonSeed: {
    title: "The Moon Seed",
    shelfTitle: "Moon Seed",
    shelfCue: "Grow a glowing space flower.",
    shelfIcon: "🌱",
    pages: MOON_SEED_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "moon-seed",
  },
  cometHat: {
    title: "The Comet Hat",
    shelfTitle: "Comet Hat",
    shelfCue: "Wear a silly comet helmet.",
    shelfIcon: "☄",
    pages: COMET_HAT_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "comet-hat",
  },
  rocketPajamas: {
    title: "Rocket Pajamas",
    shelfTitle: "Rocket PJs",
    shelfCue: "Tuck a sleepy rocket into bed.",
    shelfIcon: "🚀",
    coverImage: ROCKET_PAJAMAS_COVER,
    pages: ROCKET_PAJAMAS_PAGES,
    imageShape: "rocket-pajamas",
  },
  saturnRing: {
    title: "Saturn's Lost Ring",
    shelfTitle: "Lost Ring",
    shelfCue: "Roll Saturn's ring back home.",
    shelfIcon: "🪐",
    coverImage: SATURN_RING_COVER,
    pages: SATURN_RING_PAGES,
    imageShape: "saturn-ring",
  },
  blackHolePeekaboo: {
    title: "Black Hole Peekaboo",
    shelfTitle: "Peekaboo",
    shelfCue: "Play gently with a starry swirl.",
    shelfIcon: "🌀",
    coverImage: BLACK_HOLE_PEEKABOO_COVER,
    pages: BLACK_HOLE_PEEKABOO_PAGES,
    imageShape: "black-hole-peekaboo",
  },
  moonMailbox: {
    title: "The Moon Mailbox",
    shelfTitle: "Moon Mail",
    shelfCue: "Send a glowing note through space.",
    shelfIcon: "✉",
    pages: MOON_MAILBOX_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "moon-mailbox",
  },
  jupiterStorm: {
    title: "Jupiter's Gentle Storm",
    shelfTitle: "Jupiter Storm",
    shelfCue: "Help a storm learn to slow down.",
    shelfIcon: "🌀",
    pages: JUPITER_STORM_PAGES,
    imageShape: "jupiter-storm",
  },
  neptuneNightLight: {
    title: "Neptune's Night Light",
    shelfTitle: "Night Light",
    shelfCue: "Carry a small star home.",
    shelfIcon: "🔦",
    pages: NEPTUNE_NIGHT_LIGHT_PAGES,
    imageShape: "neptune-night-light",
  },
  marsKiteDay: {
    title: "Mars Kite Day",
    shelfTitle: "Mars Kite",
    shelfCue: "Fly a red kite on Mars.",
    shelfIcon: "🪁",
    pages: MARS_KITE_DAY_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "mars-kite-day",
  },
  venusUmbrellaParade: {
    title: "Venus Umbrella Parade",
    shelfTitle: "Venus Parade",
    shelfCue: "March under golden clouds.",
    shelfIcon: "☂",
    pages: VENUS_UMBRELLA_PARADE_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "venus-umbrella-parade",
  },
  mercuryShadowTag: {
    title: "Mercury Shadow Tag",
    shelfTitle: "Shadow Tag",
    shelfCue: "Play with long Sun shadows.",
    shelfIcon: "☀",
    pages: MERCURY_SHADOW_TAG_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "mercury-shadow-tag",
  },
  uranusTiltedTea: {
    title: "Uranus Tilted Tea Party",
    shelfTitle: "Tilted Tea",
    shelfCue: "Lean with Uranus and sip stars.",
    shelfIcon: "🫖",
    pages: URANUS_TILTED_TEA_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "uranus-tilted-tea",
  },
  plutoSnowParade: {
    title: "Pluto Snow Parade",
    shelfTitle: "Pluto Snow",
    shelfCue: "Make a tiny snowy parade.",
    shelfIcon: "❄",
    pages: PLUTO_SNOW_PARADE_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "pluto-snow-parade",
  },
  moonWordWalk: {
    title: "Moon Word Walk",
    shelfTitle: "Moon Words",
    shelfCue: "Tap moon words and hear them.",
    shelfIcon: "🌙",
    coverImage: MOON_WORD_WALK_PAGE_IMAGE,
    pages: MOON_WORD_WALK_PAGES,
    characterAsset: GENERATED_ASSETS.sonAstronaut,
    imageShape: "moon-word-walk",
  },
  rocketWordRide: {
    title: "Rocket Word Ride",
    shelfTitle: "Rocket Words",
    shelfCue: "Tap rocket words and hear them.",
    shelfIcon: "🚀",
    coverImage: ROCKET_WORD_RIDE_PAGE_IMAGE,
    pages: ROCKET_WORD_RIDE_PAGES,
    imageShape: "rocket-word-ride",
  },
};

const STORY_SHELF_ORDER = [
  "starHome",
  "moonSeed",
  "cometHat",
  "rocketPajamas",
  "saturnRing",
  "blackHolePeekaboo",
  "moonMailbox",
  "jupiterStorm",
  "neptuneNightLight",
  "marsKiteDay",
  "venusUmbrellaParade",
  "mercuryShadowTag",
  "uranusTiltedTea",
  "plutoSnowParade",
  "moonWordWalk",
  "rocketWordRide",
];

const HELMET_STICKERS = [
  { id: "none", glyph: "", label: "None" },
  {
    id: "alien",
    glyph: "👽",
    label: "Alien ears",
    asset: GENERATED_ASSETS.alien,
  },
  { id: "dino", glyph: "🦖", label: "Dinosaur" },
  { id: "crown", glyph: "👑", label: "Crown" },
  { id: "flower", glyph: "🌸", label: "Flower" },
  {
    id: "star",
    glyph: "⭐",
    label: "Star",
    asset: GENERATED_ASSETS.sparkleStar,
  },
  {
    id: "rocket",
    glyph: "🚀",
    label: "Rocket",
    asset: GENERATED_ASSETS.rocket,
  },
  { id: "heart", glyph: "💛", label: "Heart" },
  { id: "moon", glyph: "🌙", label: "Moon", asset: GENERATED_ASSETS.moon },
  {
    id: "sparkle",
    glyph: "✨",
    label: "Sparkle",
    asset: GENERATED_ASSETS.sparkleStar,
  },
  { id: "smile", glyph: "🙂", label: "Smile" },
];
function useHelmetSticker() {
  const [id, setId] = useState(() => {
    try {
      return localStorage.getItem("planets_helmet") || "none";
    } catch {
      return "none";
    }
  });
  const update = (next) => {
    setId(next);
    try {
      localStorage.setItem("planets_helmet", next);
    } catch {}
  };
  return [id, update];
}

function useStickerPlacement() {
  const [place, setPlace] = useState(() => {
    try {
      return localStorage.getItem("planets_sticker_place") || "helmet";
    } catch {
      return "helmet";
    }
  });
  const update = (next) => {
    setPlace(next);
    try {
      localStorage.setItem("planets_sticker_place", next);
    } catch {}
  };
  return [place, update];
}

const HELMET_SIDES = [
  { id: "front", label: "Front", asset: GENERATED_ASSETS.helmet },
  { id: "right", label: "Right", asset: GENERATED_ASSETS.helmetRight },
  { id: "back", label: "Back", asset: GENERATED_ASSETS.helmetBack },
  { id: "left", label: "Left", asset: GENERATED_ASSETS.helmetLeft },
];

function useHelmetSide(key, fallback = "front") {
  const [side, setSide] = useState(() => {
    try {
      return localStorage.getItem(key) || fallback;
    } catch {
      return fallback;
    }
  });
  const update = (next) => {
    setSide(next);
    try {
      localStorage.setItem(key, next);
    } catch {}
  };
  return [side, update];
}

function useStickerPosition() {
  const [pos, setPos] = useState(() => {
    try {
      const parsed = JSON.parse(
        localStorage.getItem("planets_sticker_pos") || "{}",
      );
      return {
        x: Number.isFinite(parsed.x) ? parsed.x : 50,
        y: Number.isFinite(parsed.y) ? parsed.y : 50,
      };
    } catch {
      return { x: 50, y: 50 };
    }
  });
  const update = (next) => {
    setPos(next);
    try {
      localStorage.setItem("planets_sticker_pos", JSON.stringify(next));
    } catch {}
  };
  return [pos, update];
}

function useHelmetDecorations() {
  const [items, setItems] = useState(() => {
    try {
      const raw = localStorage.getItem("planets_helmet_decorations_v2");
      const parsed = raw ? JSON.parse(raw) : [];
      if (!Array.isArray(parsed)) return [];
      return parsed.filter(
        (item) =>
          item &&
          typeof item.uid === "string" &&
          HELMET_STICKERS.some((s) => s.id === item.stickerId) &&
          HELMET_SIDES.some((s) => s.id === item.side),
      );
    } catch {
      return [];
    }
  });
  const update = (next) => {
    setItems((current) => {
      const resolved = typeof next === "function" ? next(current) : next;
      try {
        localStorage.setItem(
          "planets_helmet_decorations_v2",
          JSON.stringify(resolved),
        );
      } catch {}
      return resolved;
    });
  };
  return [items, update];
}

// ---------- Escape-stack helper ----------
// Round-3 cleanup. Before this, App.jsx had its own window keydown handler
// that called closeInteractiveSurfaces() on Escape, AND every per-level
// component (maze, storybook) also registered its own window keydown
// handler that called onExit() on Escape. Both fired on the same press —
// idempotent in practice but architecturally ambiguous about WHO closes
// what when modals are stacked. This helper lets the deepest registered
// handler win (LIFO), which is the conventional modal-stack contract.
const __escapeStack = [];
let __escapeListenerInstalled = false;
function __ensureEscapeListener() {
  if (__escapeListenerInstalled || typeof window === "undefined") return;
  __escapeListenerInstalled = true;
  window.addEventListener(
    "keydown",
    (e) => {
      if (e.key !== "Escape") return;
      const top = __escapeStack[__escapeStack.length - 1];
      if (!top) return;
      try {
        top();
      } catch (err) {
        reportSpaceError(err, { source: "escapeHandler" });
      }
    },
    true,
  );
}
function pushEscapeHandler(handler) {
  if (typeof handler !== "function") return () => {};
  __ensureEscapeListener();
  __escapeStack.push(handler);
  let popped = false;
  return () => {
    if (popped) return;
    popped = true;
    const idx = __escapeStack.lastIndexOf(handler);
    if (idx >= 0) __escapeStack.splice(idx, 1);
  };
}
function useEscapeHandler(handler, enabled = true) {
  // Wrap with useEffect so a parent unmount cleans up. The handler is
  // captured fresh on every render via a ref so callers don't have to
  // memoize.
  const ref = useRef(handler);
  useEffect(() => {
    ref.current = handler;
  });
  useEffect(() => {
    if (!enabled) return undefined;
    const dispose = pushEscapeHandler(() => {
      const fn = ref.current;
      if (typeof fn === "function") fn();
    });
    return dispose;
  }, [enabled]);
}

function shouldIgnoreWindowGameKey(event, options = {}) {
  if (!event) return true;
  const config = options && typeof options === "object" ? options : {};
  if (config.allowModifiedKeys !== true) {
    if (event.altKey || event.ctrlKey || event.metaKey) return true;
  }
  const tag = event.target && event.target.tagName;
  if (tag && /^(INPUT|SELECT|TEXTAREA)$/.test(tag)) return true;
  return Boolean(event.target && event.target.isContentEditable);
}

function useWindowKeyHandler(handler, enabled = true, options = {}) {
  const config = options && typeof options === "object" ? options : {};
  const ref = useRef(handler);
  useEffect(() => {
    ref.current = handler;
  });
  useEffect(() => {
    if (!enabled) return undefined;
    const onKey = (event) => {
      if (shouldIgnoreWindowGameKey(event, config)) return;
      const fn = ref.current;
      if (typeof fn === "function") fn(event);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [enabled, config.allowModifiedKeys]);
}

// ---------- One-time legacy localStorage migration ----------
// Round-3 cleanup. The helmet-decorations storage was migrated v1 → v2
// at some point in the project's history; the new key is read/written
// but the old "planets_helmet_decorations" key was left orphaned in
// users' localStorage. Same pattern for any future migrations: extend
// the LEGACY_KEYS_TO_REMOVE list and bump the migration marker.
const LEGACY_KEYS_TO_REMOVE = [
  "planets_helmet_decorations", // pre-v2; replaced by planets_helmet_decorations_v2
];
const STORAGE_MIGRATION_KEY = "planets_storage_migration_v1";
function migrateLegacyStorageKeys() {
  try {
    if (typeof localStorage === "undefined") return;
    if (localStorage.getItem(STORAGE_MIGRATION_KEY) === "done") return;
    for (const key of LEGACY_KEYS_TO_REMOVE) {
      try {
        localStorage.removeItem(key);
      } catch {}
    }
    localStorage.setItem(STORAGE_MIGRATION_KEY, "done");
  } catch {}
}
migrateLegacyStorageKeys();

window.SpaceExplorerFoundation = {
  readStorageValue,
  normalizeStorageKey,
  readStorageFlag,
  writeStorageFlag,
  readStorageEnum,
  readStorageInt,
  serializeStorageValue,
  writeStorageValue,
  removeStorageValue,
  readStorageJson,
  writeStorageJson,
  safeArray,
  normalizeNarrationClip,
  hasNarrationClip,
  playNarration,
  stopNarration,
  stopSpaceGameRuntimes,
  celebrateGameFinish,
  registerGameRuntimeCleanup,
  isSpacePerfEnabled,
  createSpacePerfProbe,
  pushEscapeHandler,
  useEscapeHandler,
  shouldIgnoreWindowGameKey,
  useWindowKeyHandler,
  migrateLegacyStorageKeys,
  getGeneratedAsset,
  normalizeAssetSrc,
  normalizeAssetContext,
  assetFallbackText,
  safeAssetFallbackText,
  spaceErrorMessage,
  spaceErrorSource,
  spaceErrorDetail,
  normalizeSpaceErrorRecord,
  reportSpaceError,
  reportAssetLoadFailure,
  SafeAssetImage,
};
