/* ============================================================
   ROOY · Prototype shell
   ───────────────────────────────────────────────────────────
   Wraps the existing v3 screens (all expose `onNav` already) in
   an iOS-style tab-bar router with per-tab navigation stacks:
   each of the three tabs (home / ski / rooy) keeps its own push
   stack, so switching tabs and coming back restores where you
   were (UITabBarController behavior). Exposed as
   `window.RooyNav.{go,back}` so screens that already call
   `onNav(target)` route through us.

   All three tab containers stay mounted at all times — switching
   tabs only flips a visibility flag, so screens don't re-mount
   and their push-in CSS animations don't replay on every tab swap.
   ============================================================ */

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

/* Screen registry — `comp` is a lazy getter so it resolves AFTER
   all screen modules have attached themselves to window.
   - `tab`   which bottom-Tab the screen belongs to.
   - `kind`  'sheet' triggers bottom-sheet transition + embedded prop.
   - `group` screens with the same group transition with a fade
             replace instead of push (used for academy
             Segmented control: review ↔ course ↔ achievement). */
const SCREENS = {
  /* home flow */
  'home':               { comp: () => window.HomeScreenQ,         tab: 'home' },
  'home-firstrun':      { comp: () => window.HomeScreenFirstRun,  tab: 'home' },
  'settings':           { comp: () => window.SettingsScreen,      tab: 'home' },
  'settings-profile':   { comp: () => window.EditProfileScreen,   tab: 'home' },
  'season':             { comp: () => window.SeasonScreenB,       tab: 'home' },
  'history':            { comp: () => window.HistoryScreen,       tab: 'home' },
  'daily':              { comp: () => window.DailyScreen,         tab: 'home' },
  'review':             { comp: () => window.ReviewScreenN,       tab: 'home', group: 'academy' },
  'course':             { comp: () => window.CourseScreen,        tab: 'home', group: 'academy' },
  'course-detail':      { comp: () => window.CourseDetailScreenB, tab: 'home' },
  'rooy-pro':           { comp: () => window.RooyProScreen,       tab: 'home' },
  'achievement':        { comp: () => window.AchievementScreen,   tab: 'home', group: 'academy' },

  /* ski flow */
  'presession':         { comp: () => window.PreSessionScreenC,   tab: 'ski' },
  'presession-end':     { comp: () => window.PreSessionScreenEnd, tab: 'ski' },
  'ski-b':              { comp: () => window.SkiScreenB,          tab: 'ski' },
  'ski-c':              { comp: () => window.SkiScreenC,          tab: 'ski' },
  'ski-d':              { comp: () => window.SkiScreenD,          tab: 'ski' },

  /* rooy flow */
  'rooy':               { comp: () => window.RooyScreen,          tab: 'rooy' },
  'rooy-profile':       { comp: () => window.RooyProfileScreen,   tab: 'rooy' },
  'rooy-lens':          { comp: () => window.RooyLensSheet,       tab: 'rooy', kind: 'sheet' },

  /* onboarding (no tab, full-screen takeover) */
  'onboarding-welcome':   { comp: () => window.OnboardingWelcome   },
  'onboarding-basicinfo': { comp: () => window.OnboardingBasicInfo },
  'onboarding-goal':      { comp: () => window.OnboardingGoal     },
  'onboarding-level':     { comp: () => window.OnboardingLevel    },
  'onboarding-pair':      { comp: () => window.OnboardingPair     },

  /* pairing — bottom-sheet triad; embedded-prop contract. */
  'pairing-searching':  { comp: () => window.PairingSearching,  kind: 'sheet' },
  'pairing-found':      { comp: () => window.PairingFound,      kind: 'sheet' },
  'pairing-connected':  { comp: () => window.PairingConnected,  kind: 'sheet' },
};

const TAB_IDS = ['home', 'ski', 'rooy'];
const TAB_ROOT = { home: 'home', ski: 'presession', rooy: 'rooy' };
const ANIM_MS = 280;

/* `ski` is a special target id: from presession or presession-end
   it means "start / continue skiing" → push ski-b. Listing these
   here keeps the special-case branch readable. */
const SKI_START_FROM = new Set(['presession', 'presession-end']);

/* First-run onboarding lives on its own stack outside the tab
   system — TabBar isn't visible during onboarding. Completing
   onboarding (any nav to home or presession) flips off the flag
   and drops the user on home tab with full state, not first-run. */
const ONBOARDING_IDS = new Set([
  'onboarding-welcome', 'onboarding-basicinfo',
  'onboarding-goal', 'onboarding-level', 'onboarding-pair',
]);
const ONBOARDING_EXIT = new Set(['home', 'presession']);

function App() {
  const seqRef = useRef(1);
  /* `originTab` records which tab the user was on when this layer
     was pushed. Used by `back` to restore the caller's tab on pop
     when the layer was pushed via a cross-tab jump (e.g., 02a → 10
     daily lives on home tab but back should return to ski tab). */
  const mk = useCallback((id, anim, originTab) => {
    const meta = SCREENS[id];
    const a = anim || 'push';
    return {
      id,
      key: 's' + (seqRef.current++),
      kind: meta?.kind || 'screen',
      anim: a,
      originTab: originTab || null,
      /* `entering` drives the underlying layer's parallax push-out
         and the top's push-in/fade-in. A useEffect below clears it
         ANIM_MS after `createdAt` so a later pop doesn't replay
         push-in on this layer. `createdAt` is wall-clock time so
         the timer fires at the correct moment even if the stack
         re-renders mid-animation (e.g., the academy swap prune
         changing tabStacks at t=ANIM_MS would otherwise reset a
         deps-only timer and leak fade-in past its window). */
      entering: a === 'push' || a === 'fade' || a === 'sheet',
      createdAt: Date.now(),
    };
  }, []);

  const [activeTab, setActiveTab] = useState('home');
  const [tabStacks, setTabStacks] = useState(() => ({
    home: [{ id: 'home',       key: 'h-root', kind: 'screen', anim: 'none' }],
    ski:  [{ id: 'presession', key: 'k-root', kind: 'screen', anim: 'none' }],
    rooy: [{ id: 'rooy',       key: 'r-root', kind: 'screen', anim: 'none' }],
  }));

  const [inOnboarding, setInOnboarding] = useState(true);
  const [onboardingStack, setOnboardingStack] = useState(() => [
    { id: 'onboarding-welcome', key: 'ob-root', kind: 'screen', anim: 'none' },
  ]);

  /* Two-phase back: mark top `leaving` so render attaches the
     pop-out animation, then prune from stack once it completes.
     If the popped layer was cross-tab-pushed (has originTab),
     restore the caller's tab so e.g. 02a → daily → back returns
     to 02a (on ski tab) rather than home tab root.
     Defined before `go` because go references it. */
  /* When marking the top as leaving, also clear `entering` on the
     layer that's about to be revealed. That layer becomes the new
     top after the prune, and we don't want isTop && entering to
     replay push-in on it. */
  const markTopLeaving = (arr) => arr.map((s, i) => {
    if (i === arr.length - 1 && !s.leaving) return { ...s, leaving: true };
    if (i === arr.length - 2 && s.entering) return { ...s, entering: false };
    return s;
  });

  /* rooyEntryTabRef: only the ROOY chat page (rooy tab root) supports
     "swipe back → return to previous tab". We record the tab the user
     came from when they switch INTO rooy, and consume it on back from
     rooy root. Other tab roots don't have this — they're terminal. */
  const rooyEntryTabRef = useRef(null);

  const back = useCallback(() => {
    if (inOnboarding) {
      const cur = onboardingStack;
      if (cur.length <= 1) return;
      const top = cur[cur.length - 1];
      if (top.leaving) return;
      setOnboardingStack(prev => markTopLeaving(prev));
      setTimeout(() => {
        setOnboardingStack(prev => prev.filter(s => !s.leaving));
      }, ANIM_MS);
      return;
    }

    const cur = tabStacks[activeTab];
    /* ROOY chat root only: back returns to the tab the user came
       from (fallback: home). Other tab roots have nothing to do on
       back — there's no logical "previous" for those. */
    if (cur.length <= 1) {
      if (activeTab === 'rooy') {
        const prev = rooyEntryTabRef.current || 'home';
        rooyEntryTabRef.current = null;
        if (prev !== 'rooy') setActiveTab(prev);
      }
      return;
    }
    const top = cur[cur.length - 1];
    if (top.leaving) return;
    const restoreTab = top.originTab && top.originTab !== activeTab
      ? top.originTab : null;

    setTabStacks(prev => ({
      ...prev,
      [activeTab]: markTopLeaving(prev[activeTab]),
    }));
    setTimeout(() => {
      setTabStacks(prev => ({
        ...prev,
        [activeTab]: prev[activeTab].filter(s => !s.leaving),
      }));
      if (restoreTab) setActiveTab(restoreTab);
    }, ANIM_MS);
  }, [activeTab, tabStacks, inOnboarding, onboardingStack]);

  /* 50ms micro-window dedupe. React 18 dev mode double-invokes
     impure state updaters; SlideToStart's setProgress updater
     schedules `setTimeout(onStart, 280)` as a side effect, which
     gets registered twice and fires `go('ski')` twice in the same
     tick. 50ms is below any plausible human double-tap (≥ 100ms)
     but well above React's same-tick duplicate. */
  const lastGoRef = useRef({ target: null, time: 0 });

  /* Navigate. Resolution order:
     1. `'back'` — explicit pop on current tab stack.
     2. Tab id ('home' / 'ski' / 'rooy'):
        · 'ski' from presession / presession-end → push ski-b.
        · Different tab → switch active tab (per-tab stack kept).
        · Same tab → rewrite target to tab root, fall through.
     3. Unknown screen id → warn and ignore.
     4. Target lives in another tab → push to that tab + activate.
     5. Same-group as current top → replace top (Segmented swap).
     6. Target already in current stack → pop to that index
        (UINavigationController-style "go back to existing").
     7. Sheet kind → push with sheet transition.
     8. Otherwise → regular push. */
  const go = useCallback((target) => {
    const now = Date.now();
    if (lastGoRef.current.target === target && now - lastGoRef.current.time < 50) {
      return;
    }
    lastGoRef.current = { target, time: now };

    if (target === 'back') { back(); return; }

    /* Onboarding takes over the screen until the user reaches a
       real-app destination. Internal pushes / pop-to-existing
       happen on `onboardingStack`; pairing sheets ride on top of
       it; any nav to a home/ski exit dismisses onboarding and
       drops onto the home tab with full state. */
    if (inOnboarding) {
      if (ONBOARDING_EXIT.has(target)) {
        setInOnboarding(false);
        setActiveTab('home');
        setOnboardingStack([]);
        return;
      }
      if (ONBOARDING_IDS.has(target)) {
        const idx = onboardingStack.findIndex(s => s.id === target);
        if (idx === onboardingStack.length - 1) return;
        if (idx >= 0) {
          setOnboardingStack(prev => {
            if (prev[prev.length - 1].leaving) return prev;
            return prev.map((s, i, arr) =>
              i === arr.length - 1 ? { ...s, leaving: true } : s);
          });
          setTimeout(() => {
            setOnboardingStack(prev => prev.slice(0, idx + 1));
          }, ANIM_MS);
          return;
        }
        setOnboardingStack(prev => [...prev, mk(target, 'push')]);
        return;
      }
      if (SCREENS[target]?.kind === 'sheet') {
        setOnboardingStack(prev => [...prev, mk(target, 'sheet')]);
        return;
      }
      console.warn('[onboarding] unhandled target:', target);
      return;
    }

    const curStack = tabStacks[activeTab];
    const top = curStack[curStack.length - 1];

    if (TAB_IDS.includes(target)) {
      if (target === 'ski' && SKI_START_FROM.has(top.id)) {
        setTabStacks(prev => ({
          ...prev,
          ski: [...prev.ski, mk('ski-b', 'push')],
        }));
        return;
      }
      if (activeTab !== target) {
        if (target === 'rooy') rooyEntryTabRef.current = activeTab;
        setActiveTab(target);
        return;
      }
      /* Ski tab tap while a ski-flow screen is already on top: stay put.
         Without this, top=ski-b/ski-c falls through to pop-to-root
         (presession), and the next tap re-pushes ski-b via the
         SKI_START_FROM branch above — user sees presession ↔ ski-b
         ping-ponging on every tap. iOS would also no-op a tab-tap when
         the user is mid-flow within that tab. */
      if (target === 'ski' && (top.id === 'ski-b' || top.id === 'ski-c')) {
        return;
      }
      target = TAB_ROOT[target];
    }

    /* ski-b / ski-c are "long-session" screens that conceptually belong
       to the ski tab. When invoked from another tab (e.g. rooy chat
       "开始上课" → ski-c), the default per-tab-stack rule would push them
       onto the caller's stack — so switching to home and back to ski
       drops the user on presession (ski tab's untouched root) instead of
       the session they just started. Re-route: switch to ski tab and
       ensure the target sits on top of ski's own stack. Other cross-tab
       targets (daily, review, etc.) stay on the caller's stack as before. */
    if ((target === 'ski-b' || target === 'ski-c') && activeTab !== 'ski') {
      setTabStacks(prev => {
        const skiStack = prev.ski;
        const idx = skiStack.findIndex(s => s.id === target);
        if (idx === skiStack.length - 1) return prev;
        if (idx >= 0) {
          return { ...prev, ski: skiStack.slice(0, idx + 1) };
        }
        return { ...prev, ski: [...skiStack, mk(target, 'push')] };
      });
      setActiveTab('ski');
      return;
    }

    if (!SCREENS[target]) {
      console.warn('[RooyNav] unknown screen:', target);
      return;
    }

    const targetMeta = SCREENS[target];

    /* Pushes stay on the current tab's stack, even when the target
       screen's logical home is a different tab (e.g., ski →
       presession-end → daily; daily belongs to home tab but is
       pushed onto ski here). The previous design switched activeTab
       to the target's home tab on push and back on pop, but the
       back animation ran ON the target tab — the user saw a flash
       of the target-tab root (home) before the activeTab swap
       returned them to the caller (ski). Per-tab-local stacks
       eliminate the flash and match iOS UINavigationController:
       each tab is its own independent navigation hierarchy. */

    /* Same-group replace: review ↔ course ↔ achievement Segmented
       control. Snap-replace with no animation — iOS UISegmentedControl
       semantics: the segments themselves animate (pill slide), the
       content panel underneath swaps instantly. Cross-fading two
       partially-transparent screens stacks both contents at 0.5 each
       and reads as a double-image; fading in over the layer beneath
       would flash whatever's under the academy stack (home). Snap is
       the only clean option for sibling-tab content. */
    const topMeta = SCREENS[top.id];
    if (topMeta?.group && topMeta.group === targetMeta.group && top.id !== target) {
      setTabStacks(prev => ({
        ...prev,
        [activeTab]: [
          ...prev[activeTab].slice(0, -1),
          mk(target, 'none'),
        ],
      }));
      return;
    }

    /* Pop to existing screen in current stack. */
    const existingIdx = curStack.findIndex(s => s.id === target);
    if (existingIdx >= 0) {
      if (existingIdx === curStack.length - 1) return;
      setTabStacks(prev => ({
        ...prev,
        [activeTab]: prev[activeTab].slice(0, existingIdx + 1),
      }));
      return;
    }

    if (targetMeta.kind === 'sheet') {
      setTabStacks(prev => ({
        ...prev,
        [activeTab]: [...prev[activeTab], mk(target, 'sheet')],
      }));
      return;
    }

    setTabStacks(prev => ({
      ...prev,
      [activeTab]: [...prev[activeTab], mk(target, 'push')],
    }));
  }, [activeTab, tabStacks, mk, back, inOnboarding, onboardingStack]);

  useEffect(() => {
    const stack = inOnboarding ? onboardingStack : tabStacks[activeTab];
    window.RooyNav = {
      go, back,
      current: () => stack[stack.length - 1].id,
      tab: () => inOnboarding ? 'onboarding' : activeTab,
    };
  }, [go, back, tabStacks, activeTab, inOnboarding, onboardingStack]);

  /* Clear `entering` after the entry animation completes. We need
     this gate because the top's `isTop && entering && anim==='push'`
     branch drives push-in: if `entering` stuck around forever, a
     later pop that re-promotes this layer to top would replay
     push-in. Clearing it after ANIM_MS lets the post-animation top
     fall through to a no-class state (natural 0/1) — matching where
     push-in ended, so no visible jump.
     The wait is aligned to the oldest entering layer's createdAt
     (not a flat ANIM_MS) so re-renders during the window — e.g.,
     the academy swap's prune at t=ANIM_MS — re-schedule the timer
     to the same wall-clock target instead of pushing it out a
     second ANIM_MS. */
  useEffect(() => {
    const stacks = inOnboarding ? [onboardingStack] : TAB_IDS.map(t => tabStacks[t]);
    let minStart = Infinity;
    for (const s of stacks) for (const item of s) {
      if (item.entering && item.createdAt < minStart) minStart = item.createdAt;
    }
    if (minStart === Infinity) return;
    const wait = Math.max(0, minStart + ANIM_MS - Date.now());
    const timer = setTimeout(() => {
      /* Small fudge above ANIM_MS handles wake-up jitter so layers
         scheduled in the same tick all clear together. */
      const cutoff = Date.now() - ANIM_MS + 16;
      const clear = (s) => s.some(it => it.entering && it.createdAt <= cutoff)
        ? s.map(it => it.entering && it.createdAt <= cutoff ? { ...it, entering: false } : it)
        : s;
      if (inOnboarding) {
        setOnboardingStack(prev => clear(prev));
      } else {
        setTabStacks(prev => {
          let changed = false;
          const next = { ...prev };
          for (const tab of TAB_IDS) {
            const after = clear(next[tab]);
            if (after !== next[tab]) { next[tab] = after; changed = true; }
          }
          return changed ? next : prev;
        });
      }
    }, wait);
    return () => clearTimeout(timer);
  }, [tabStacks, onboardingStack, inOnboarding]);

  /* iOS edge-swipe back gesture (only meaningful when active tab
     has sub-pages on its stack). */
  const swipeRef = useRef(null);
  const onTouchStart = (e) => {
    if (inOnboarding) {
      if (onboardingStack.length <= 1) return;
    } else {
      const stackLen = tabStacks[activeTab].length;
      /* Allow swipe on ROOY chat root — back() always has a target. */
      if (stackLen <= 1 && activeTab !== 'rooy') return;
    }
    const t = e.touches[0];
    if (t.clientX > 24) return;
    swipeRef.current = { x0: t.clientX, y0: t.clientY };
  };
  const onTouchEnd = (e) => {
    const s = swipeRef.current;
    swipeRef.current = null;
    if (!s) return;
    const t = e.changedTouches[0];
    const dx = t.clientX - s.x0;
    const dy = Math.abs(t.clientY - s.y0);
    if (dx > 60 && dy < 40) back();
  };

  /* Stack renderer. Takes a stack array directly so we can reuse
     it for the tab stacks (mounted simultaneously, visibility
     toggle keeps push animations tied to real pushes, not tab
     swaps) and for the onboarding stack (single full-screen
     overlay before the user reaches the real app). */
  function renderStack(stack) {
    const topIdx = stack.length - 1;
    const top = stack[topIdx];
    /* Any opaque top covers the under and should hold it in the
       dimmed parallax state — that includes the snap-replaced
       academy top (anim:'none'). Only sheets don't, since they
       float partway over the under with a translucent backdrop. */
    const topDrivesUnderlay = top && top.kind !== 'sheet';

    return stack.map((item, i) => {
      const meta = SCREENS[item.id];
      if (!meta) return null;
      const Comp = meta.comp();
      if (!Comp) return null;
      const isTop = i === topIdx;
      const isSheet = item.kind === 'sheet';

      if (isSheet) {
        const bdClass  = item.leaving ? 'anim-fade-out'  : 'anim-fade-in';
        const panClass = item.leaving ? 'anim-sheet-out' : 'anim-sheet-in';
        return (
          <div key={item.key} className="screen-layer" style={{
            zIndex: i + 1, background: 'transparent', pointerEvents: 'auto',
          }}>
            <div
              className={bdClass}
              onClick={back}
              style={{
                position: 'absolute', inset: 0,
                background: 'rgba(0,0,0,0.42)',
              }}
            />
            <div
              className={panClass}
              style={{
                position: 'absolute',
                /* iOS large detent ≈ 75% 屏高。max 720pt 锁高屏不过分,
                   矮屏退回到 22% 留出层级感. */
                top: 'max(22%, calc(100% - 720px))',
                left: 0, right: 0, bottom: 0,
                borderRadius: '20px 20px 0 0',
                overflow: 'hidden',
                boxShadow: '0 -8px 28px oklch(17% 0.005 250 / 0.22)',
                background: 'var(--r-bg)',
              }}
            >
              <Comp onNav={go} embedded/>
            </div>
          </div>
        );
      }

      /* Animation resolution:
         · leaving layer: pop-out for push-style departure, fade-out
           for cross-fade departure (academy swap marks oldTop with
           anim:'fade' before flipping leaving).
         · top entering: push-in or fade-in based on item.anim.
           Gated on `entering` so this only plays once on the actual
           push — a later pop that re-promotes the layer won't replay.
         · layer directly under a push top: push-out (held at -30/0.6
           by `animation-fill-mode: both`) while top is in/at-rest,
           pop-in while top is leaving. Keeping the same keyframe
           start/end values across push-out → pop-in means no jump
           when the user reverses direction. */
      let animClass = '';
      if (item.leaving) {
        animClass = item.anim === 'fade' ? 'anim-fade-out' : 'anim-pop-out';
      } else if (isTop) {
        if (item.entering) {
          if (item.anim === 'fade') animClass = 'anim-fade-in';
          else if (item.anim === 'push') animClass = 'anim-push-in';
        }
      } else if (i === topIdx - 1 && topDrivesUnderlay) {
        animClass = top.leaving ? 'anim-pop-in' : 'anim-push-out';
      }

      /* Hide layers deeper than the under. Without this, push-out's
         opacity 0.6 on the under (and pop-in's opacity-from-0.6
         start) leaks content of any deeper layer through the
         translucent zone — visible as "other screens flashing
         through" during onboarding pushes, ski-flow transitions,
         and any cross-tab nav like history → daily. The deeper
         layers are fully covered by the under in static state, so
         hiding them is invisible at rest; they unhide whenever the
         stack reshuffles.
         IMPORTANT: only set `visibility: hidden` when we actually
         want to hide. NEVER set `visibility: visible` explicitly —
         that would override the parent tab container's own
         `visibility: hidden` (used for inactive tabs), causing
         every tab's layers to paint on top of the active tab. */
      const isHidden = i < topIdx - 1;
      const layerStyle = { zIndex: i + 1 };
      if (isHidden) {
        layerStyle.visibility = 'hidden';
        layerStyle.pointerEvents = 'none';
      }

      return (
        <div
          key={item.key}
          className={`screen-layer ${animClass}`}
          style={layerStyle}
        >
          <div style={{
            position: 'absolute', inset: 0,
            background: 'var(--r-bg)',
          }}>
            <Comp onNav={go}/>
          </div>
        </div>
      );
    });
  }

  return (
    <div className="screen-stage" onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
      {inOnboarding ? (
        <div style={{ position: 'absolute', inset: 0 }}>
          {renderStack(onboardingStack)}
        </div>
      ) : (
        TAB_IDS.map(tabId => {
          const isActive = activeTab === tabId;
          return (
            <div
              key={tabId}
              style={{
                position: 'absolute', inset: 0,
                visibility: isActive ? 'visible' : 'hidden',
                pointerEvents: isActive ? 'auto' : 'none',
              }}
              aria-hidden={!isActive}
            >
              {renderStack(tabStacks[tabId])}
            </div>
          );
        })
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('app-root')).render(<App/>);
