// public/story-page.jsx: window.StoryPage, the Strategy and Vision story at the top of the page
// (content/story.json via window.STORY; one renderer per slide type in story-slides.jsx). It owns the
// site header (fixed at the top over the story and the offerings alike: the logo, Strategy and Vision /
// Opportunities / FAQ with the section on screen highlighted, the confidentiality note and the one
// EN/ES switch), the side rail and the contents menu, which slide is current, where the page opens
// (public/story-rules.js), the email gate hand-off when the visitor reaches the offerings, and the
// deck_* analytics the image deck used to send. Mounted by DesktopApp (app.jsx) and MobileApp
// (mobile/mobile-app.jsx) right above the offerings, which carry id="r-offerings".
(function () {
  const { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } = React;
  const R = window.StoryRules;
  const DECK_ID = "reurbano-story-page";
  const SEEN_KEY = "r-deck-seen";
  const POS_KEY = "r-story-pos";
  const HEAD = 48; // the story header's height; --rs-head in story.css
  const pad2 = (n) => (n < 10 ? "0" : "") + n;
  const reduced = () => !!(window.MOTION && window.MOTION.reduce);

  // Seen means seen at the story's current revision (StoryRules.isSeen).
  function isSeen() {
    try { return R.isSeen(localStorage.getItem(SEEN_KEY), window.STORY); } catch (_) { return false; }
  }
  function readPos() {
    try { return JSON.parse(sessionStorage.getItem(POS_KEY) || "null"); } catch (_) { return null; }
  }
  function writePos(key) {
    try { sessionStorage.setItem(POS_KEY, JSON.stringify({ key: key, href: window.location.href })); } catch (_) {}
  }

  function StoryPage({ lang, setLang, shell }) {
    const story = window.STORY;
    const slides = story.slides;
    const labels = story.labels;
    const keys = useMemo(() => slides.map((s) => s.key), [slides]);
    // The rail and the contents menu list every slide after the cover that has a title, in order, by
    // its short rail label where it has one (Chris, 2026-09-24), else by its title.
    const listed = useMemo(() => slides.filter((s, i) => i > 0 && s.title), [slides]);
    const L = (field) => window.pickLang(field, lang);
    const T = (window.STRINGS && window.STRINGS[lang]) || {};

    const [current, setCurrent] = useState(null);
    const [inView, setInView] = useState({});
    const [settled, setSettled] = useState(null);
    const [menuOpen, setMenuOpen] = useState(false);

    const els = useRef({});
    const inViewRef = useRef({});
    const target = useRef(null);   // where the page opened, followed until the visitor scrolls
    const source = useRef("page"); // deck_opened source: page, deeplink or header
    const opened = useRef(false);
    const reached = useRef(false);
    const viewed = useRef([]);
    const lastViewed = useRef(0);
    const completed = useRef(false);
    const frame = useRef(0);
    const closeRef = useRef(null);

    const offerings = () => document.getElementById("r-offerings");

    // Page position of a slide's top or of the offerings, both under the fixed site header.
    const yOf = useCallback((key) => {
      const el = key === R.OFFERINGS ? offerings() : els.current[key];
      if (!el) return null;
      return Math.max(0, el.getBoundingClientRect().top + window.scrollY - HEAD);
    }, []);

    const go = useCallback((key, smooth) => {
      const y = yOf(key);
      if (y == null) return;
      target.current = null;
      window.scrollTo({ top: y, behavior: smooth && !reduced() ? "smooth" : "auto" });
    }, [yOf]);

    const measure = useCallback(() => {
      frame.current = 0;
      const vh = window.innerHeight;
      const rects = keys.map((k) => (els.current[k] ? els.current[k].getBoundingClientRect() : null));
      const off = offerings();
      const offRect = off ? off.getBoundingClientRect() : null;
      const tops = rects.map((r) => (r ? r.top : Infinity));
      if (offRect) tops.push(offRect.top);
      const idx = R.currentIndex(tops, HEAD, vh);
      const cur = idx < keys.length ? keys[idx] : R.OFFERINGS;
      setCurrent((c) => (c === cur ? c : cur));

      // In view with some hysteresis: in at 30% visible, out only when fully off screen.
      const next = { ...inViewRef.current };
      let changed = false;
      rects.forEach((r, i) => {
        if (!r) return;
        const k = keys[i], ratio = R.visibleRatio(r.top, r.bottom, 0, vh);
        const v = ratio >= 0.3 ? true : ratio === 0 ? false : !!next[k];
        if (v !== !!next[k]) { next[k] = v; changed = true; }
      });
      if (changed) { inViewRef.current = next; setInView(next); }

      const r = idx < keys.length ? rects[idx] : null;
      const st = r && R.visibleRatio(r.top, r.bottom, HEAD, vh) >= R.SETTLED_RATIO ? cur : null;
      setSettled((s) => (s === st ? s : st));

      // The story is on screen for the first time in this page load.
      if (!opened.current && keys.some((k) => next[k])) {
        opened.current = true;
        window.visitorTracker?.onDeckOpen?.(DECK_ID, { slide_count: keys.length, source: source.current });
      }
      // The visitor reached the offerings: a first visit ends its story here, so ask for the email.
      if (!reached.current && offRect && R.visibleRatio(offRect.top, offRect.bottom, 0, vh) >= 0.5) {
        reached.current = true;
        if (!isSeen()) {
          try { localStorage.setItem(SEEN_KEY, R.seenMark(window.STORY)); } catch (_) {}
          try { window.dispatchEvent(new CustomEvent("reurbano:request-gate")); } catch (_) {}
        }
        if (opened.current) window.visitorTracker?.onDeckDismiss?.(DECK_ID, lastViewed.current);
      }
    }, [keys]);

    // Open where the visit rules say, before the first paint. Fonts and late layout can still move
    // things, so keep that spot in place until the visitor moves the page themselves (or 2.5 s pass).
    // Any scroll position other than the one this effect set means the visitor moved it: the wheel,
    // a key, a touch, a scrollbar drag, a link.
    useLayoutEffect(() => {
      try { window.history.scrollRestoration = "manual"; } catch (_) {}
      const t = R.initialTarget({ search: window.location.search, href: window.location.href, stored: readPos(), seen: isSeen(), keys });
      source.current = t.source;
      target.current = t.key;
      let placed = null;
      const put = () => {
        const y = yOf(t.key);
        if (y == null) return;
        window.scrollTo(0, y);
        placed = window.scrollY;
      };
      put();
      const again = () => {
        if (target.current !== t.key) return;
        if (placed != null && Math.abs(window.scrollY - placed) > 2) { target.current = null; return; }
        const y = yOf(t.key);
        if (y != null && Math.abs(window.scrollY - y) > 1) put();
      };
      window.addEventListener("load", again, { once: true });
      if (document.fonts && document.fonts.ready) document.fonts.ready.then(again);
      const late = setTimeout(() => { again(); if (target.current === t.key) target.current = null; }, 2500);
      return () => {
        clearTimeout(late);
        window.removeEventListener("load", again);
      };
    }, []);

    useEffect(() => {
      const onScroll = () => { if (!frame.current) frame.current = requestAnimationFrame(measure); };
      window.addEventListener("scroll", onScroll, { passive: true });
      window.addEventListener("resize", onScroll);
      onScroll();
      return () => {
        window.removeEventListener("scroll", onScroll);
        window.removeEventListener("resize", onScroll);
        cancelAnimationFrame(frame.current);
        frame.current = 0;
      };
    }, [measure]);

    // Viewed: one second as the settled slide. Completed: the last story slide was viewed.
    useEffect(() => {
      if (!settled) return undefined;
      const key = settled;
      const t = setTimeout(() => {
        const index = keys.indexOf(key) + 1;
        lastViewed.current = index;
        if (viewed.current.indexOf(key) >= 0) return;
        viewed.current.push(key);
        window.visitorTracker?.onDeckSlideView?.(DECK_ID, index, keys.length, key);
        if (!completed.current && R.isCompleted(viewed.current, keys)) {
          completed.current = true;
          window.visitorTracker?.onDeckComplete?.(DECK_ID);
        }
      }, R.VIEW_DWELL_MS);
      return () => clearTimeout(t);
    }, [settled, keys]);

    // Remember the spot for a reload or a switch between the desktop and phone layouts.
    useEffect(() => { if (current) writePos(current); }, [current]);

    // The offerings header's "Strategy and Vision" button (header.jsx, mobile-feed.jsx) calls this.
    useEffect(() => {
      window.openStoryDeck = () => {
        if (!opened.current) source.current = "header";
        go(keys[0], true);
      };
      return () => { delete window.openStoryDeck; };
    }, [go, keys]);

    useEffect(() => {
      const html = document.documentElement;
      html.classList.add("r-snap");
      return () => html.classList.remove("r-snap");
    }, []);

    useEffect(() => {
      if (!menuOpen) return undefined;
      const html = document.documentElement;
      html.classList.add("r-overlay-open");
      const onKey = (e) => { if (e.key === "Escape") setMenuOpen(false); };
      document.addEventListener("keydown", onKey);
      if (closeRef.current) closeRef.current.focus();
      return () => {
        html.classList.remove("r-overlay-open");
        document.removeEventListener("keydown", onKey);
      };
    }, [menuOpen]);

    const jump = (key) => (e) => {
      if (e) e.preventDefault();
      if (menuOpen) {
        setMenuOpen(false);
        requestAnimationFrame(() => go(key, true));
      } else {
        go(key, true);
      }
    };
    const openFaq = () => {
      if (window.posthog) window.posthog.capture("faq_opened", { source: "story" });
      if (window.openFaq) window.openFaq();
    };

    const atOfferings = current === R.OFFERINGS;
    const Logo = window.ReurbanoMark;

    const langButtons = (
      <div className="rs-lang" role="group" aria-label="Language">
        {[["en", "EN"], ["es", "ES"]].map(([v, t]) => (
          <button key={v} type="button" aria-pressed={lang === v} onClick={() => {
            if (lang !== v && window.posthog) window.posthog.capture("language_switched", { language: v });
            if (setLang) setLang(v);
          }}>{t}</button>
        ))}
      </div>
    );

    const header = (
      <header className="rs-top">
        <div className="rs-hbar">
          <button type="button" className="rs-logo" aria-label="Reurbano" onClick={jump(keys[0])}>{Logo ? <Logo symbol /> : "Reurbano"}</button>
          <nav className="rs-nav" aria-label={L(labels.contents)}>
            <a href={"?slide=" + keys[0]} className={atOfferings ? undefined : "rs-cur"} aria-current={atOfferings ? undefined : "true"} onClick={jump(keys[0])}>{L(labels.story)}</a>
            <a href="#r-offerings" className={atOfferings ? "rs-cur" : undefined} aria-current={atOfferings ? "true" : undefined} onClick={jump(R.OFFERINGS)}>{L(labels.opportunities)}</a>
            <button type="button" onClick={openFaq}>{L(labels.faq)}</button>
          </nav>
          <span className="rs-where"><b>{L(atOfferings ? labels.opportunities : labels.story)}</b></span>
          {T.confidentialityNote ? <p className="rs-conf">{T.confidentialityNote}</p> : null}
          <div className="rs-hright">
            {langButtons}
            <button type="button" className="rs-menu-btn" aria-haspopup="dialog" aria-expanded={menuOpen} onClick={() => setMenuOpen(true)}>{L(labels.menu)}</button>
          </div>
        </div>
      </header>
    );

    const rail = (
      <nav className="rs-rail" aria-label={L(labels.contents)}>
        <ol>
          {listed.map((s) => (
            <li key={s.key}>
              <a href={"?slide=" + s.key} className={current === s.key ? "rs-cur" : undefined}
                aria-current={current === s.key ? "true" : undefined} onClick={jump(s.key)}>
                <span>{pad2(keys.indexOf(s.key) + 1)}</span><span>{L(s.rail || s.title)}</span>
              </a>
            </li>
          ))}
          <li className="rs-rail-off">
            <a href="#r-offerings" className={atOfferings ? "rs-cur" : undefined} onClick={jump(R.OFFERINGS)}>
              <span>→</span><span>{L(labels.opportunities)}</span>
            </a>
          </li>
        </ol>
      </nav>
    );

    const menu = menuOpen ? (
      <div className="rs-idx" role="dialog" aria-modal="true" aria-label={L(labels.contents)}>
        <button type="button" ref={closeRef} className="rs-idx-close" aria-label={L(labels.close)} onClick={() => setMenuOpen(false)}>×</button>
        <p className="rs-kick rs-idx-title"><a href={"?slide=" + keys[0]} onClick={jump(keys[0])}>{L(labels.story)}</a></p>
        <ol className="rs-idx-list">
          {listed.map((s) => (
            <li key={s.key}><a href={"?slide=" + s.key} onClick={jump(s.key)}><span>{pad2(keys.indexOf(s.key) + 1)}</span><span>{L(s.rail || s.title)}</span></a></li>
          ))}
        </ol>
        <div className="rs-idx-grp">
          <div className="rs-idx-n">→</div>
          <div><h3><a href="#r-offerings" onClick={jump(R.OFFERINGS)}>{L(labels.opportunities)}</a></h3></div>
        </div>
        <div className="rs-idx-grp">
          <div className="rs-idx-n">?</div>
          <div><h3><button type="button" onClick={() => { setMenuOpen(false); openFaq(); }}>{L(labels.faq)}</button></h3>{langButtons}</div>
        </div>
        {T.confidentialityNote ? <p className="rs-idx-conf">{T.confidentialityNote}</p> : null}
      </div>
    ) : null;

    const S = window.StorySlides || {};
    const renderSlide = (s, i) => {
      const Comp = S[s.type];
      const cls = "rs-slide rs-bg-" + s.bg + " rs-t-" + s.type + (inView[s.key] ? " rs-in" : " rs-off");
      return (
        <section key={s.key} className={cls} data-key={s.key} ref={(el) => { els.current[s.key] = el; }}>
          {Comp ? (
            <Comp slide={s} lang={lang} inView={!!inView[s.key]} labels={labels}
              go={(k) => go(k, true)} next={keys[i + 1] || R.OFFERINGS} />
          ) : null}
          <div className="rs-sno" aria-hidden="true">{pad2(i + 1) + " / " + keys.length}</div>
        </section>
      );
    };

    return (
      <div className="r-story" data-shell={shell}>
        {header}
        {renderSlide(slides[0], 0)}
        <div className="rs-frame">
          {rail}
          <div className="rs-slides">{slides.slice(1).map((s, i) => renderSlide(s, i + 1))}</div>
        </div>
        {menu}
      </div>
    );
  }

  window.StoryPage = StoryPage;
})();
