// 赛博宝宝前端（2026-10-21 从 index.html 的内联 <script type="text/babel"> 原样搬出）：App 的 state / ref、推送、宝宝表情这些 hook（第一段）。
// index.html 按顺序加载 app/*.jsx，前面文件定义的全局（函数、常量、组件）后面的文件直接用。
// App 拆成几段函数，按原来的顺序每次渲染都调用；上一段的局部变量放进 bag 递给下一段（hook 调用顺序不变）。
function useAppState(bag) {
  const [state, setState] = useState(null);
  const [loading, setLoading] = useState(true);
  const [toast, setToast] = useState('');
  const [noticeSeen, setNoticeSeen] = useState(() => localStorage.getItem('xz_notice_seen') || '');
  // 图纸解锁播报，看过就不再弹。两位家长各自的设备各记各的（2026-08-03）
  const [blueprintSeen, setBlueprintSeen] = useState('');
  const [panel, setPanel] = useState(null); // 'cook' | 'adventure' | 'chat' | 'study' | 'diary' | 'message' | 'comfort' | 'debug'
  const [scene, setScene] = useState('room'); // 'room' | 'yard' 院子二期
  // 院子的风（2026-10-05）：摇摇马平时不动；真实风 ≥ 30 km/h 才每 25–45 秒被吹得晃一下
  const yardHorseRef = useRef(null);
  useEffect(() => {
    if (scene !== 'yard') return;
    let t;
    const loop = () => {
      t = setTimeout(() => {
        if (!document.hidden && (_weatherCache.windKmh || 0) >= 30) yardRockHorse(yardHorseRef.current, 'gust');
        loop();
      }, 25000 + Math.random() * 20000);
    };
    loop();
    return () => clearTimeout(t);
  }, [scene]);
  const [gardenPot, setGardenPot] = useState(null);   // 点开的花盆号（种植面板）
  const [flowerGiving, setFlowerGiving] = useState(false); // 送花选人面板
  const [flowerNote, setFlowerNote] = useState(''); // 送花赠言（2026-08-26 小玉：网页端送花不能写话，MCP 能）
  const [storyEvent, setStoryEvent] = useState(null);
  const pendingStoriesRef = useRef([]);
  // 2026-09-17：稀有章节补播的「这次会话已经排过」名单。补播标记要等弹窗关掉才删，
  // 中间任何一次 refresh 都会让调度 effect 重跑，没有这个 ref 就会被反复排上（小玉要的是补播一次）。
  const rareReplayQueuedRef = useRef(new Set());
  const justCompletedRef = useRef(new Set());
  const [speechBubble, setSpeechBubble] = useState('');
  const [pokeBounce, setPokeBounce] = useState(false);
  const pokeLastAtRef = useRef(0);
  // 2026-08-20 动作台词保护期：随机气泡定时器在这段时间里不许顶掉动作台词
  // （小玉 8/18 报：摸头/喂饭的 speech bubble 没读完就变了——原因是 doAction 先 refresh，
  //   state 一变随机气泡的 useEffect 重跑，立刻 setSpeechBubble(pickBubble()) 盖掉了台词）
  const bubbleHoldUntilRef = useRef(0);
  // 2026-10-16 她：吃饭的泡泡还没看完就没了——台词现在是 Haiku 现编的，长短不一，按字数算停留：至少 4 秒，每字 0.25 秒，最多 15 秒
  function holdBubble(msg, ms) {
    if (ms == null) ms = Math.min(15000, Math.max(4000, 1500 + [...String(msg || '')].length * 250));
    const until = Date.now() + ms;
    bubbleHoldUntilRef.current = until;
    setSpeechBubble(msg);
    setTimeout(() => {
      if (bubbleHoldUntilRef.current === until) setSpeechBubble('');
    }, ms);
  }
  const [blinkOpen, setBlinkOpen] = useState(true);
  const [advCountdown, setAdvCountdown] = useState('');
  const [lootResult, setLootResult] = useState(null);
  const advCollectingRef = useRef(false);   // 地图上正在演「接回家」：pendingLoot 那个自动弹卡先别弹
  const advCollectFnRef = useRef(null);
  const [actionLoading, setActionLoading] = useState(false);
  const [cookingRecipe, setCookingRecipe] = useState(null);
  const [eggHatching, setEggHatching] = useState(false);
  // 2026-09-06：初值必须先按墨尔本时间判断昼夜，不能等 open-meteo 回来才知道是不是晚上——
  // 之前固定 'cloudy'，天气接口一卡，房间就一直停在白天景（皮皮夜里23:50还是白天）。
  const [weatherState, setWeatherState] = useState(() => (isMelbNight() ? 'night' : 'cloudy'));
  const [doctorLetter, setDoctorLetter] = useState(null);
  const [endingData, setEndingData] = useState(null);
  const [lampMagic, setLampMagic] = useState(false);
  const [octoBlink, setOctoBlink] = useState(true);
  const [octoReaction, setOctoReaction] = useState(null);
  const [octoHeartFrame, setOctoHeartFrame] = useState(1);
  const [octoHungryFrame, setOctoHungryFrame] = useState(1);
  const [octoEating, setOctoEating] = useState(false);
  const [growthPopup, setGrowthPopup] = useState(null);
  const [growthResolvedPopup, setGrowthResolvedPopup] = useState(null);
  const growthResolvedSeenRef = useRef(new Set());
  const [lanternStory, setLanternStory] = useState(null); // intro | wish | baby | waiting
  const [lanternWish, setLanternWish] = useState('');
  const [lanternPanel, setLanternPanel] = useState(false);
  const [lanternSubmitting, setLanternSubmitting] = useState(false);

  const triggerOctoReaction = (type, duration = 2500) => {
    setOctoReaction(type);
    setTimeout(() => setOctoReaction(null), duration);
  };

  const triggerOctoEating = () => {
    setOctoEating(true);
    const t = setTimeout(() => setOctoEating(false), 4000);
    return () => clearTimeout(t);
  };

  const showToast = msg => { setToast(msg); setTimeout(() => setToast(''), 2500); };

  // 建造类成长故事保留到第二位家长确认看过；失败时留在眼前，下一次还能重试。
  const closeGrowthResolved = async () => {
    try {
      if (growthResolvedPopup?.needsMamaSeen) {
        const result = await api('/growth/seen', 'POST', { eventId: growthResolvedPopup.eventId });
        if (!result?.ok) { showToast(result?.error || '没能记下已读，请再试一次'); return; }
        growthResolvedSeenRef.current.add(`${growthResolvedPopup.babyId}:${growthResolvedPopup.eventId}`);
      }
      setGrowthResolvedPopup(null);
      refresh();
    } catch (e) {
      showToast('没能记下已读，请再试一次');
    }
  };

  // 加载状态
  const refresh = useCallback(async () => {
    try {
      const data = await api('/status');
      if (data.error) { showToast(data.error); return; }
      // 旧轮询可能在已读确认后才回包，不能把刚关掉的同一故事重新打开。
      if (data.growthResolved && !growthResolvedSeenRef.current.has(`${data.babyId}:${data.growthResolved.eventId}`)) {
        setGrowthResolvedPopup({ ...data.growthResolved, babyId: data.babyId });
      }
      setBlueprintSeen(localStorage.getItem(`xz_blueprint_seen_${data.babyId}`) || '');
      if (data.swingGift) showToast(`${data.name || '宝宝'}刚才去荡了秋千，心情+10 🎠`);
      if (data.storybookExpiresAt && data.storybookExpiresAt > Date.now()) {
        const today = new Date().toDateString();
        const lastShown = localStorage.getItem('xz_storybook_prompt');
        if (lastShown !== today) {
          localStorage.setItem('xz_storybook_prompt', today);
          setTimeout(() => showToast('📖 记得给宝宝讲个故事哦～'), 1500);
        }
      }
      if (data.growthLapsed?.name) showToast(`${data.growthLapsed.name}过完了`);
      setState(data);
    } catch (e) {
      showToast('连接失败……');
    }
  }, []);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const urlToken = params.get('token');
    if (urlToken) {
      localStorage.setItem('xz_token', urlToken);
      window.history.replaceState({}, '', window.location.pathname);
    }
    const token = localStorage.getItem('xz_token');
    if (!token) { setLoading(false); return; }
    refresh().then(() => setLoading(false));
    const urlWeather = new URLSearchParams(window.location.search).get('weather');
    if (urlWeather && ['sunny','cloudy','rainy','night','night_rain'].includes(urlWeather)) {
      setWeatherState(urlWeather);
    } else {
      checkWeather().then(setWeatherState);
    }
    const wt = setInterval(() => { if (!new URLSearchParams(window.location.search).get('weather')) checkWeather().then(setWeatherState); }, 1800000);
    return () => clearInterval(wt);
  }, []);

  // 推送状态
  const [pushSupported, setPushSupported] = useState(false);
  const [pushSubscribed, setPushSubscribed] = useState(false);

  useEffect(() => {
    if (!state) return;
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
    setPushSupported(true);
    navigator.serviceWorker.register('/sw.js').then(reg =>
      reg.pushManager.getSubscription().then(sub => { if (sub) setPushSubscribed(true); })
    ).catch(() => {});
  }, [state]);

  const subscribePush = async () => {
    try {
      const reg = await navigator.serviceWorker.ready;
      const res = await fetch(`${API}/push/vapid-key`);
      const { publicKey } = await res.json();
      if (!publicKey) { showToast('推送服务暂不可用'); return; }
      const sub = await reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: publicKey,
      });
      const subJson = sub.toJSON();
      await api('/push/subscribe', 'POST', {
        endpoint: subJson.endpoint,
        keys: subJson.keys,
      });
      setPushSubscribed(true);
      showToast('通知开启成功！');
    } catch (e) {
      if (Notification.permission === 'denied') {
        showToast('通知权限被拒绝了，请在浏览器设置中允许');
      } else {
        showToast('开启通知失败：' + e.message);
      }
    }
  };

  // 关闭通知（2026-10-21 小玉问怎么关）：退订浏览器这头＋删掉服务器那条
  const unsubscribePush = async () => {
    try {
      const reg = await navigator.serviceWorker.ready;
      const sub = await reg.pushManager.getSubscription();
      if (sub) {
        const endpoint = sub.endpoint;
        await sub.unsubscribe();
        try { await api('/push/subscribe', 'DELETE', { endpoint }); } catch (_) {}
      }
      setPushSubscribed(false);
      showToast('通知已关闭');
    } catch (e) {
      showToast('关闭通知失败：' + e.message);
    }
  };

  // 自动轮询（30秒）
  useEffect(() => {
    if (!state) return;
    const t = setInterval(refresh, 30000);
    return () => clearInterval(t);
  }, [state, refresh]);

  // 当前心情和阶段
  const currentMood = state ? (state.happiness < 25 ? 'sad' : state.happiness < 50 ? 'bored' : 'idle') : 'idle';
  const currentStage = state ? (state.day <= 20 ? 'baby' : state.day <= 60 ? 'growing' : 'mature') : 'baby';

  // 眨眼动画（生气=等权切换+闭眼颤抖，其他=快速眨眼）
  useEffect(() => {
    let timer;
    if (currentMood === 'sad') {
      const toggle = () => {
        setBlinkOpen(prev => !prev);
        timer = setTimeout(toggle, 1500 + Math.random() * 2000);
      };
      timer = setTimeout(toggle, 2000 + Math.random() * 1500);
    } else {
      const blink = () => {
        setBlinkOpen(false);
        setTimeout(() => setBlinkOpen(true), 180);
      };
      const schedule = () => { timer = setTimeout(() => { blink(); schedule(); }, 2500 + Math.random() * 3000); };
      schedule();
    }
    return () => clearTimeout(timer);
  }, [currentMood, currentStage]);

  // 章鱼眨眼
  useEffect(() => {
    if (!state?.egg?.hatched) return;
    let timer;
    const blink = () => {
      setOctoBlink(false);
      setTimeout(() => setOctoBlink(true), 200);
    };
    const schedule = () => { timer = setTimeout(() => { blink(); schedule(); }, 3000 + Math.random() * 4000); };
    schedule();
    return () => clearTimeout(timer);
  }, [state?.egg?.hatched]);

  // 章鱼帧动画（心心眼2帧）
  useEffect(() => {
    if (!state?.egg?.hatched) return;
    const t1 = setInterval(() => setOctoHeartFrame(f => f === 1 ? 2 : 1), 500);
    return () => { clearInterval(t1); };
  }, [state?.egg?.hatched]);

  // 章鱼吃饭帧动画（变速：frame1停900ms，2→5→2→5各120ms，循环）
  // 触发条件：喂饭动作后约4秒，与hunger数值脱钩
  const HUNGRY_SEQ = [
    {f:1,d:900},
    {f:2,d:120},{f:3,d:120},{f:4,d:120},{f:5,d:120},
    {f:2,d:120},{f:3,d:120},{f:4,d:120},{f:5,d:120},
  ];
  useEffect(() => {
    const isEating = state?.egg?.hatched && octoEating;
    if (!isEating) { setOctoHungryFrame(1); return; }
    let idx = 0;
    let timer;
    const step = () => {
      const { f, d } = HUNGRY_SEQ[idx];
      setOctoHungryFrame(f);
      idx = (idx + 1) % HUNGRY_SEQ.length;
      timer = setTimeout(step, d);
    };
    step();
    return () => clearTimeout(timer);
  }, [state?.egg?.hatched, octoEating]);

  // day100 恢复：如果 day100_here 已完成但 game_ended 没置位（coop 分支遗漏），补触发结局
  useEffect(() => {
    if (!state || state.gameEnded || endingData) return;
    if (!(state.completedEvents || []).includes('day100_here')) return;
    (async () => {
      const res = await api('/ending', 'POST', {});
      if (res.letter) setEndingData(res);
    })();
  }, [state?.gameEnded, state?.completedEvents]);

  // 探险结算自动弹出（后端有pending_loot时）
  useEffect(() => {
    if (!state?.pendingLoot || lootResult || advCollectingRef.current) return;
    const pl = state.pendingLoot;
    if (pl.failed) {
      setLootResult({ failed: true, lost: pl.lost || [], elapsed: pl.elapsed_hours, gotBed: !!pl.gotBed });
    } else if (pl.loot) {
      const qixi = splitQixiLanternLoot(pl.loot);
      setLootResult({ failed: false, loot: qixi.loot, hasLantern: qixi.hasLantern, elapsed: pl.elapsed_hours, gotBed: pl.gotBed, mapSummary: pl.mapSummary || null, mealNotes: pl.mealNotes || [], selfReturnNote: pl.selfReturnNote || null, growthResolved: pl.growthResolved || null });
      if (pl.mapSummary?.photo && pl.adventureId) ensureAdvPhoto(pl.adventureId);   // 📷 她自己走回家的那趟：没人在地图上看到咔嚓，这里补画
    }
  }, [state?.pendingLoot]);

  // 探险地图开着的时候，每次 refresh（30 秒一次）把最新的 choices 递进去：另一位家长选了什么，地图上接着演
  useEffect(() => {
    if (!window.AdvMap || !window.AdvMap.isOpen() || advCollectingRef.current) return;
    window.AdvMap.update(state?.adventure, { canCollect: state?.youAre === 'daddy' && state?.adventure?.status === 'ready' });
  }, [state?.adventure, state?.youAre]);

  // 尼尔斯线远征章节：她送出的探险结束后，下次打开播下一段（J15-J24）
  useEffect(() => {
    if (!state?.babyId || storyEvent || lootResult) return;
    if (state.adventure?.status && state.adventure.status !== 'idle') return;
    const flagKey = `nils_leg_pending_${state.babyId}`;
    let flag;
    try { flag = localStorage.getItem(flagKey); } catch (e) { return; }
    if (!flag) return;
    const jLine = window.STORY_EVENTS?.random?.J;
    if (!jLine) return;
    const completed = new Set(state.completedEvents || []);
    const jNext = (jLine.events || []).find(e => !completed.has(e.id));
    try { localStorage.removeItem(flagKey); } catch (e) {}
    if (jNext && jNext.trigger === 'adventure_return') {
      setTimeout(() => setStoryEvent({ ...jNext, _storyline: 'J', _cat: 'random', coins: jNext.coins || 10 }), 600);
    }
  }, [state?.adventure?.status, state?.completedEvents, storyEvent, lootResult]);

  // 成长期弹窗（每个期只弹一次）
  useEffect(() => {
    if (!state?.growthPeriod?.active) return;
    const gpId = state.growthPeriod.id;
    const key = `growth_seen_${state.babyId}_${gpId}`;
    if (localStorage.getItem(key)) return;
    const tips = {
      rebellion: { icon: '😤', title: '叛逆期到了', desc: '宝宝长大了，学会了好奇，也学会了叛逆。\n心情封顶50，会顶嘴，但其实心里还是爱你的。\n多和宝宝说说话吧～' },
      fragile: { icon: '🤒', title: '脆弱期到了', desc: '宝宝变得容易生病了。\n每天有小概率昏迷，不干净更容易倒下。\n要多照顾哦～' },
      selfish: { icon: '😼', title: '自私期到了', desc: '宝宝开始觉得自己最重要了。\n说话有点刻薄，探险收获减半。\n造一面镜子让宝宝照照自己吧～' },
      playful: { icon: '🛹', title: '贪玩期到了', desc: '宝宝只想出去玩！\n不去探险心情就归零。\n每天都要派宝宝出去溜达～' },
      puberty: { icon: '📔', title: '青春期到了', desc: '宝宝的情绪开始大起大落了。\n有时候会突然不理你。\n别担心，这只是在长大～' },
    };
    if (tips[gpId]) setGrowthPopup({ ...tips[gpId], key });
  }, [state?.growthPeriod?.id, state?.growthPeriod?.active]);
  return { actionLoading, closeGrowthResolved, advCollectFnRef, advCollectingRef, advCountdown, blinkOpen, blueprintSeen, bubbleHoldUntilRef, cookingRecipe, currentMood,
    doctorLetter, eggHatching, endingData, flowerGiving, flowerNote, gardenPot, growthPopup, growthResolvedPopup, holdBubble, justCompletedRef,
    lampMagic, lanternPanel, lanternStory, lanternSubmitting, lanternWish, loading, lootResult, noticeSeen, octoBlink, octoEating, octoHeartFrame,
    octoHungryFrame, octoReaction, panel, pendingStoriesRef, pokeBounce, pokeLastAtRef, pushSubscribed, pushSupported, rareReplayQueuedRef, refresh,
    scene, setActionLoading, setAdvCountdown, setBlueprintSeen, setDoctorLetter, setEggHatching, setEndingData, setFlowerGiving, setFlowerNote,
    setGardenPot, setGrowthPopup, setGrowthResolvedPopup, setLampMagic, setLanternPanel, setLanternStory, setLanternSubmitting, setLanternWish,
    setLoading, setLootResult, setNoticeSeen, setPanel, setPokeBounce, setScene, setSpeechBubble, setState, setStoryEvent, setToast, showToast,
    speechBubble, state, storyEvent, subscribePush, toast, triggerOctoEating, triggerOctoReaction, unsubscribePush, weatherState, yardHorseRef };
}
