function storyProgressKey(bookId) {
  return "planets_story_progress_" + bookId;
}

function resolveStoryBookId(bookId) {
  return STORYBOOKS[bookId] ? bookId : "starHome";
}

function isStoryPage(page) {
  return Boolean(page && (page.text || page.image || page.audio));
}

function storyPagesFor(story) {
  return story && Array.isArray(story.pages)
    ? story.pages.filter(isStoryPage)
    : [];
}

function clampStoryPage(page, pagesLength) {
  if (!pagesLength) return 0;
  const nextPage = Number.isFinite(page) ? page : 0;
  return Math.max(0, Math.min(pagesLength - 1, nextPage));
}

function storyCoverFor(story, pages) {
  return (story && story.coverImage) || (pages[0] && pages[0].image) || "";
}

function storyShelfStatus(pages, resumePage) {
  if (!pages.length) return "Loading";
  if (resumePage === pages.length - 1) return "Finished";
  if (resumePage > 0) return `Resume page ${resumePage + 1}`;
  return `${pages.length} pages`;
}

function readStoryProgress(bookId, maxPages) {
  if (!maxPages) return 0;
  return readStorageInt(storyProgressKey(bookId), 0, 0, maxPages - 1);
}

function StoryBookPage({ book, onExit }) {
  const bookId = resolveStoryBookId(book);
  const story = STORYBOOKS[bookId];
  const pages = storyPagesFor(story);
  const [isOpening, setIsOpening] = useState(true);
  const [page, setPage] = useState(() =>
    readStoryProgress(bookId, pages.length),
  );
  const current = pages[page] || pages[0];
  const bookClass = story.imageShape || "star-home";
  const hasAudio = Boolean(current && current.audio);
  const pageCue =
    page === 0
      ? "First page"
      : page === pages.length - 1
        ? "The end"
        : "Keep going";
  const bigNextLabel =
    page === pages.length - 1
      ? "Read again"
      : page === 0
        ? "Start turning"
        : "Next page";
  const go = (step) => {
    if (!pages.length) return;
    setPage((p) => clampStoryPage(p + step, pages.length));
    playKidSound("boop");
  };

  const goToPage = (nextPage) => {
    if (!pages.length) return;
    setPage(clampStoryPage(nextPage, pages.length));
    playKidSound("boop");
  };

  // Round-2 fix: track which bookId the current `page` state actually belongs
  // to so the persistence effect doesn't write the previous book's page number
  // into the newly-mounted book's key during the brief window between bookId
  // changing and setPage(readStoryProgress(...)) committing. We update the ref
  // only after `page` has been resynced to match the new bookId — which we
  // detect by comparing the post-render page against the freshly-loaded value.
  const persistedBookRef = useRef(bookId);
  useEffect(() => {
    const restoredPage = readStoryProgress(bookId, pages.length);
    setPage(restoredPage);
    setIsOpening(true);
    const timer = window.setTimeout(() => setIsOpening(false), 820);
    return () => window.clearTimeout(timer);
  }, [bookId, pages.length]);

  useEffect(() => {
    // Only persist once the in-state `page` is known to belong to the current
    // bookId. The first render after a bookId switch has the previous book's
    // page still in state; skip that one. After setPage commits, this effect
    // runs again with the correct page and bookId in sync.
    if (persistedBookRef.current !== bookId) {
      persistedBookRef.current = bookId;
      return;
    }
    writeStorageValue(
      storyProgressKey(bookId),
      String(clampStoryPage(page, pages.length)),
    );
  }, [bookId, page, pages.length]);

  useEffect(() => {
    const nearbyPages = [pages[page + 1], pages[page - 1]];
    nearbyPages.forEach((item) => {
      if (!item || !item.image) return;
      const image = new Image();
      image.decoding = "async";
      image.src = item.image;
    });
  }, [page, pages]);

  // Auto-play this page's narration, but let the child turn pages.
  // Stop any old clip immediately so the next page never speaks over stale audio.
  useEffect(() => {
    stopNarration();
    if (!current || !current.audio) return stopNarration;
    const timer = window.setTimeout(() => {
      playNarration(current.audio);
    }, STORY_NARRATION_DELAY_MS);
    return () => {
      window.clearTimeout(timer);
      stopNarration();
    };
  }, [page, current && current.audio]);

  useEffect(() => {
    return stopNarration;
  }, []);

  // Round-3: Escape now flows through the shared escape-stack helper so
  // App.jsx's base handler doesn't double-fire. Arrows/space/enter stay
  // on a per-storybook listener since they're local to the page nav UI.
  useEscapeHandler(onExit, true);
  useWindowKeyHandler((e) => {
    if (e.key === "ArrowRight" || e.key === " " || e.key === "Enter") {
      e.preventDefault();
      // Round-2 fix: keyboard parity with the bigNext button — wrap from the
      // last page back to the first instead of soft-locking on the last page.
      if (page === pages.length - 1) {
        goToPage(0);
      } else {
        go(1);
      }
    } else if (e.key === "ArrowLeft") {
      e.preventDefault();
      go(-1);
    }
  });

  const replay = () => {
    if (current && current.audio) playNarration(current.audio);
  };

  if (!current) {
    return (
      <div className="storybook-page">
        <Starfield count={180} />
        <button className="storybook-close" onClick={onExit}>
          ← Back to planets
        </button>
        <div className="storybook-empty" role="status">
          <strong>Story pages are loading.</strong>
          <span>Pick another book and try this one again.</span>
        </div>
      </div>
    );
  }

  return (
    <div className="storybook-page">
      <Starfield count={260} />
      <button className="storybook-close" onClick={onExit}>
        ← Back to planets
      </button>
      <div className={`storybook-shell ${bookClass}`}>
        <div className="storybook-title">{story.title}</div>
        <div className="storybook-counter" aria-live="polite">
          <strong>{pageCue}</strong>
          <span>
            {page + 1} / {pages.length}
          </span>
        </div>
        {story.characterAsset ? (
          <div className="storybook-character-badge" aria-hidden="true">
            <SafeAssetImage
              src={story.characterAsset}
              alt=""
              loading="lazy"
              decoding="async"
              context={`StoryBookCharacter:${bookId}`}
              fallbackClassName="storybook-badge-fallback"
              fallbackText=""
            />
          </div>
        ) : null}
        <button
          className="storybook-nav prev"
          onClick={() => go(-1)}
          disabled={page === 0}
          aria-label="Previous page"
        >
          ‹
        </button>
        <button
          className="storybook-nav next"
          onClick={() => go(1)}
          disabled={page === pages.length - 1}
          aria-label="Next page"
        >
          ›
        </button>
        <button
          className={`storybook-card storybook-open-book ${isOpening ? "opening" : ""}`}
          onClick={() => (page === pages.length - 1 ? goToPage(0) : go(1))}
          aria-label="Turn story page"
        >
          <span className="storybook-page-left">
            <SafeAssetImage
              src={current.image}
              alt=""
              decoding="async"
              context={`StoryBookPage:${bookId}:${page}`}
              fallbackClassName="storybook-image-fallback"
              fallbackText="Story picture is loading."
            />
          </span>
          <span className="storybook-book-gutter" aria-hidden="true" />
          <span className="storybook-page-right">
            <span className="storybook-page-text" aria-live="polite">
              {current.text}
            </span>
          </span>
        </button>
        <button
          className="storybook-big-next"
          onClick={() => {
            if (page === pages.length - 1) {
              goToPage(0);
            } else {
              go(1);
            }
          }}
        >
          {bigNextLabel}
        </button>
      </div>
      {hasAudio ? (
        <div className="storybook-audio-tools">
          <button
            className="storybook-replay"
            onClick={replay}
            aria-label="Read this page again"
          >
            🔊 Read again
          </button>
          <span>AI narrator</span>
        </div>
      ) : null}
      <div className="storybook-dots" aria-label="Story pages">
        {pages.map((_, i) => (
          <button
            key={i}
            className={i === page ? "on" : ""}
            type="button"
            aria-label={`Go to page ${i + 1}`}
            aria-current={i === page ? "page" : undefined}
            onClick={() => goToPage(i)}
          />
        ))}
      </div>
    </div>
  );
}

function StoryShelfPage({ onRead, onExit }) {
  const [selectedBook, setSelectedBook] = useState(null);
  const books = STORY_SHELF_ORDER.filter((id) => STORYBOOKS[id]).map((id) => {
    const story = STORYBOOKS[id];
    const pages = storyPagesFor(story);
    const coverImage = storyCoverFor(story, pages);
    const resumePage = readStoryProgress(id, pages.length);
    return { id, story, pages, coverImage, resumePage };
  });
  const selected = selectedBook
    ? books.find((book) => book.id === selectedBook) || books[0] || null
    : null;
  const storyShelfAsset =
    window.SpaceExplorerFoundation?.getGeneratedAsset?.(
      "storyShelf",
      GENERATED_ASSETS.storyShelf,
    ) || GENERATED_ASSETS.storyShelf;

  const chooseBook = (id) => {
    const nextBook = books.some((book) => book.id === id) ? id : books[0]?.id;
    if (!nextBook) return;
    setSelectedBook(nextBook);
    playKidSound("boop");
  };

  if (!books.length) {
    return (
      <div className="story-shelf-page">
        <Starfield count={240} />
        <button className="storybook-close" onClick={onExit}>
          ← Back to planets
        </button>
        <div className="storybook-empty" role="status">
          <strong>Story shelf is loading.</strong>
          <span>Come back from the planets screen and try again.</span>
        </div>
      </div>
    );
  }

  return (
    <div className="story-shelf-page">
      <Starfield count={240} />
      <button className="storybook-close" onClick={onExit}>
        ← Back to planets
      </button>
      <section className="story-shelf-panel" aria-label="Story books">
        <div className="story-shelf-heading">
          <span>Adam's Books</span>
          <h1>Story shelf</h1>
        </div>
        <div className={`story-shelf-stage ${selected ? "has-selection" : ""}`}>
          <SafeAssetImage
            className="story-shelf-art"
            src={storyShelfAsset}
            alt=""
            decoding="async"
            context="StoryShelfPage:shelfArt"
            fallbackClassName="story-shelf-art-fallback"
            fallbackText="Story shelf is loading."
          />
          <div className="story-shelf-hotspots" aria-label="Choose a book">
            {books.map(({ id, story, coverImage }) => {
              const selectedClass = selectedBook === id ? "selected" : "";
              return (
                <button
                  key={id}
                  className={`story-shelf-hotspot ${story.imageShape} ${selectedClass}`}
                  onClick={() => chooseBook(id)}
                  aria-label={`Pull out ${story.title}`}
                  disabled={!coverImage}
                >
                  <span>{story.shelfTitle}</span>
                </button>
              );
            })}
          </div>
          {selected ? (
            <button
              className={`story-pulled-book ${selected.story.imageShape}`}
              onClick={() => onRead(selected.id)}
              disabled={!selected.coverImage}
              aria-label={`Open ${selected.story.title}`}
            >
              <div className="story-pulled-cover">
                {selected.coverImage ? (
                  <SafeAssetImage
                    src={selected.coverImage}
                    alt=""
                    loading="lazy"
                    decoding="async"
                    context={`StoryShelfCover:${selected.id}`}
                    fallbackClassName="story-cover-fallback"
                    fallbackText="Cover is loading."
                  />
                ) : null}
              </div>
              <div className="story-pulled-copy">
                <span>{selected.story.shelfIcon}</span>
                <strong>{selected.story.title}</strong>
                <small>{selected.story.shelfCue}</small>
                <em>{storyShelfStatus(selected.pages, selected.resumePage)}</em>
                <span className="story-open-cue">Tap to open</span>
              </div>
            </button>
          ) : null}
        </div>
        <div className="story-shelf-fallback">
          {books.map(({ id, story, pages, coverImage, resumePage }) => {
            return (
              <button
                key={id}
                className={`story-shelf-book ${story.imageShape}`}
                onClick={() => onRead(id)}
                aria-label={`Read ${story.title}`}
                disabled={!coverImage}
              >
                {coverImage ? (
                  <SafeAssetImage
                    src={coverImage}
                    alt=""
                    loading="lazy"
                    decoding="async"
                    context={`StoryShelfBook:${id}`}
                    fallbackClassName="story-cover-fallback"
                    fallbackText="Cover is loading."
                  />
                ) : null}
                <span className="story-shelf-icon">{story.shelfIcon}</span>
                <strong>{story.shelfTitle}</strong>
                <small>{story.shelfCue}</small>
                <em>{storyShelfStatus(pages, resumePage)}</em>
              </button>
            );
          })}
        </div>
      </section>
    </div>
  );
}

window.SpaceExplorerStorybooks = {
  resolveStoryBookId,
  clampStoryPage,
  storyPagesFor,
  storyCoverFor,
  readStoryProgress,
};
