// 赛博宝宝前端（2026-10-21 从 index.html 的内联 <script type="text/babel"> 原样搬出）：App 的动作处理：做饭、建造、探险出发/接回、七夕灯笼、随机故事。
// index.html 按顺序加载 app/*.jsx，前面文件定义的全局（函数、常量、组件）后面的文件直接用。
// App 拆成几段函数，按原来的顺序每次渲染都调用；上一段的局部变量放进 bag 递给下一段（hook 调用顺序不变）。
function appActions(bag) {
  const { advCollectFnRef, advCollectingRef, holdBubble, lampMagic, lanternSubmitting, lanternWish, lootResult, pendingStoriesRef, refresh,
    setActionLoading, setDoctorLetter, setLampMagic, setLanternStory, setLanternSubmitting, setLanternWish, setLootResult, setPanel, setStoryEvent,
    showToast, state, triggerOctoEating, triggerOctoReaction, weatherState } = bag;
  // ===== 动作处理 =====
  async function doAction(path, body = {}) {
    setActionLoading(true);
    try {
      const res = await api(path, 'POST', body);
      if (res.error) { showToast(res.error); return res; }
      await refresh();
      if (res.message) {
        holdBubble(res.message);
      }
      return res;
    } finally {
      setActionLoading(false);
    }
  }

  async function doQuickAction(path, body = {}) {
    const res = await api(path, 'POST', body);
    if (res.error) { showToast(res.error); return res; }
    if (res.message) {
      holdBubble(res.message);
    }
    if (path === '/feed') triggerOctoEating();
    if (path === '/pet') triggerOctoReaction('hearteye', 3000);
    if (path === '/clean') triggerOctoReaction('shocked', 2000);
    refresh();
    return res;
  }

  async function handleRevive() {
    setActionLoading(true);
    try {
      const res = await api('/revive', 'POST');
      if (res.error) { showToast(res.error); return; }
      setDoctorLetter(res.doctorLetter);
      await refresh();
    } finally {
      setActionLoading(false);
    }
  }

  async function handleCook(recipeId, placed) {
    const body = { recipe_id: recipeId };
    if (Array.isArray(placed) && placed.length) body.ingredients = placed;
    const res = await api('/cook', 'POST', body);
    if (res.error) { showToast(res.error); return; }
    if (res.message) {
      holdBubble(res.message);
    }
    triggerOctoEating();
    await refresh();
  }

  async function handleBuild(furnitureId) {
    const res = await doAction('/build', { furniture_id: furnitureId });
    if (res?.error) return res;
    if (res && !res.error) {
      await refresh();
      if (res.isFirstFurniture && window.STORY_EVENTS?.fixed) {
        const furnitureEvent = window.STORY_EVENTS.fixed.find(e => e.id === 'day12_furniture');
        if (furnitureEvent && !(state.completedEvents || []).includes('day12_furniture')) {
          const variant = furnitureEvent.variants?.[furnitureId];
          const variantLines = variant ? (Array.isArray(variant) ? variant : [variant]) : [];
          if (variantLines.length > 0) {
            const ev = { ...furnitureEvent, lines: variantLines, coins: 10, _cat: 'fixed' };
            setTimeout(() => setStoryEvent(ev), 800);
          }
        }
      }
    }
    return res;   // 建造面板要读 craftHint（第一次造轮子那句），不返回它就永远拿不到
  }

  async function handleLampUse() {
    if (!state.hasLamp || state.lampUses <= 0 || lampMagic) return;
    const res = await api('/lamp/use', 'POST');
    if (res.error) { showToast(res.error); return; }
    setLampMagic(true);
    setTimeout(async () => {
      setLampMagic(false);
      if (res.message) {
        holdBubble(res.message);
      }
      await refresh();
    }, 2800);
  }

  async function handleAdventureStart(selectedItems) {
    const res = await doAction('/adventure/start', { items: selectedItems || [] });
    if (res?.error) return res;
    // 尼尔斯线：这趟探险就是远征的一段路——回来后播下一章（2026-07-21 小玉定的机制）
    try { localStorage.setItem(`nils_leg_pending_${state.babyId}`, '1'); } catch (e) {}
    setPanel(null);
    return res;
  }

  async function handleAdventureTowel(selected) {
    const res = await api('/adventure/towel', 'POST', { selected });
    if (res.error) { showToast(res.error); return res; }
    await refresh();
    return res;
  }

  function advMapOpts() {
    return {
      adventure: state.adventure, youAre: state.youAre, babyName: state.name || '宝宝', partnerName: state.partnerName,
      canCollect: state.youAre === 'daddy' && state.adventure?.status === 'ready',
      onCollect: () => advCollectFnRef.current && advCollectFnRef.current(),
      itemInfo: advItemInfo,
      // 📷 地图上咔嚓了（或者打开地图时已经拍过）：照片还没人传过，就这台手机画好传上去
      onPhoto: (plan) => { const ph = state.adventure?.photo; if (ph && !ph.hasImage) ensureAdvPhoto(ph.adventureId, plan); },
    };
  }
  function openAdvMap() {
    if (window.AdvMap) window.AdvMap.open(advMapOpts());
  }
  advCollectFnRef.current = handleAdventureCollect;
  async function handleAdventureCollect() {
    // 📷 接回家之前先记下这趟的 plan（接完 status 里就没有了），拍过照、还没人传的，接完顺手画好传
    const photoPlan = state.adventure?.map?.plan || null, photoInfo = state.adventure?.photo || null;
    // 有地图的探险：地图上先演手掌接她（成长期东西先揭晓），演完再出收获卡；没地图的老探险照旧
    const withMap = !!(window.AdvMap && window.AdvMap.hasMap(state.adventure) && state.adventure.status !== 'failed');
    let res;
    if (withMap) {
      advCollectingRef.current = true;
      try {
        const p = doAction('/adventure/collect');
        const played = window.AdvMap.collect(p, advMapOpts());
        res = await p;
        try { await played; } catch (e) {}
      } finally { advCollectingRef.current = false; }
    } else {
      res = await doAction('/adventure/collect');
    }
    if (res.failed) {
      setLootResult({ failed: true, lost: res.lost || [], elapsed: res.elapsed_hours, gotBed: !!res.gotBed });
    } else if (res.loot) {
      const qixi = splitQixiLanternLoot(res.loot);
      setLootResult({ failed: false, loot: qixi.loot, hasLantern: qixi.hasLantern, elapsed: res.elapsed_hours, gotBed: res.gotBed, growthResolved: res.growthResolved || null, mapSummary: res.mapSummary || null, mealNotes: res.mealNotes || [], selfReturnNote: res.selfReturnNote || null });
      if (res.mapSummary?.photo && res.adventureId && !(photoInfo && photoInfo.hasImage)) ensureAdvPhoto(res.adventureId, photoPlan);
      // 稀有章节剧情现场播放（八音盒/神秘蛋等）：排在战利品弹窗后面
      const queue = [];
      if (res.triggeredStories && res.triggeredStories.length > 0) {
        for (const s of res.triggeredStories) {
          queue.push({
            id: s.eventId, title: s.title,
            lines: s.lines.map(t => ({ type: 'narration', text: t })),
            _cat: 'rare', coins: 0, _noComplete: true,
          });
        }
      }
      // 第一次探险得床：播 day5 的归来戏
      if (res.gotBed) {
        const day5 = window.STORY_EVENTS?.fixed?.find(e => e.id === 'day5_adventure');
        if (day5 && day5.return_lines) {
          queue.push({ id: 'day5_adventure_return', title: '回来了', lines: day5.return_lines, _cat: 'fixed', coins: 0, _noComplete: true });
        }
      }
      // day33「探险回来晚了」：探险≥8h 且到了 day33、还没播过，收获后触发（走正常完成流程发金币）
      const completedNow = new Set(state.completedEvents || []);
      if ((res.elapsed || res.elapsed_hours || 0) >= 8 && state.day >= 33 && !completedNow.has('day33_late')) {
        const e = window.STORY_EVENTS?.fixed?.find(ev => ev.id === 'day33_late');
        if (e) queue.push({ ...e, _cat: 'fixed', coins: e.coins || 10 });
      }
      pendingStoriesRef.current = queue;
    }
    setPanel(null);
  }

  function finishLootResult() {
    const foundLantern = !!lootResult?.hasLantern;
    setLootResult(null);
    api('/adventure/loot-seen', 'POST');
    if (foundLantern) {
      setLanternWish('');
      setLanternStory({ step: 'intro', babyWish: '' });
    } else {
      playPendingStories();
    }
  }

  async function submitLanternWish() {
    const wish = lanternWish.trim();
    if (!wish || lanternSubmitting) return;
    setLanternSubmitting(true);
    try {
      const res = await api('/coop/create', 'POST', {
        eventId: 'qixi_lantern',
        mamaResponse: wish,
      });
      if (res.error) { showToast(res.error); return; }
      const babyWish = res.babyWish || res.baby_wish || res.lanternWishes?.baby || res.lanternWishes?.baby_wish;
      if (!babyWish) { showToast(`${state.name || '宝宝'}的愿望还没回来，再试一次吧`); return; }
      try { localStorage.setItem(`qixi_lantern_${state.babyId}`, '1'); } catch (e) {}
      setLanternStory({ step: 'baby', babyWish });
      await refresh();
    } catch (e) {
      showToast('愿望没送出去，再试一次吧');
    } finally {
      setLanternSubmitting(false);
    }
  }

  function playPendingStories() {
    const q = pendingStoriesRef.current;
    if (q && q.length > 0) {
      const nextStory = q.shift();
      setTimeout(() => setStoryEvent(nextStory), 600);
    }
  }


  // 一句台词按当前拥有情况在 condText 候选里挑最贴合的一条（都没写就用 line.text 兜底）。
  // condText 从前到后按优先级排：need 里的都拥有、missing 里的都没有，才命中。
  // 2026-09-06 加，配合环境描写里的小章鱼/八音盒/花盆条件替换（见 pickRandomStoryEvent 里的 poss）。
  function resolveCondText(line, poss) {
    if (!Array.isArray(line.condText)) return line.text;
    for (const v of line.condText) {
      const needOk = (v.need || []).every(k => poss[k]);
      const missOk = (v.missing || []).every(k => !poss[k]);
      if (needOk && missOk) return v.text;
    }
    return line.text;
  }

  function pickRandomStoryEvent() {
    if (!window.STORY_EVENTS?.random) return null;
    const day = state.day;
    const completed = new Set(state.completedEvents || []);
    const progress = state.storylineProgress || {};
    const periodMatch = (period) => {
      if (period === 'early') return day <= 20;
      if (period === 'mid') return day >= 10 && day <= 60;
      if (period === 'late') return day >= 40;
      return true;
    };
    const needsAdventure = new Set(['A', 'B', 'D', 'F', 'G']);
    const canAdventure = day >= 5;
    // 2026-09-06：随机剧情文案里散落着"小章鱼""八音盒""花盆"这类环境描写，写的时候默认宝宝都有。
    // 没有的宝宝不该在台词里听见/看见自己没有的东西。故事线首章不能因此被挡住整条线（挡了后面全卡死），
    // 所以走文案条件替换：poss 记录当前实际拥有什么，下面按 ev.lines[].condText 挑对应半句。
    const ownedFurnitureIds = new Set((state.inventory || []).filter(i => i.item_type === 'furniture').map(i => i.item_id));
    const poss = {
      octopus: !!state.egg?.hatched,
      musicbox: completed.has('rare_glowing_stone_2'), // 八音盒之谜第8章：完整状态，偶尔自动响旋律
      flowerpot: completed.has('D1'), // D线第1章：种子种进花盆，窗台上多了一个小花盆
      wind_chime: ownedFurnitureIds.has('wind_chime'),
      swing: ownedFurnitureIds.has('swing'), // J1 院子秋千
      yard_pot: ownedFurnitureIds.has('yard_pot'), // J1 院子花盆（跟室内 D 线的窗台花盆是两个东西）
    };
    const w = weatherState;
    const weatherMatch = (trigger) => {
      if (!trigger) return true;
      if (trigger === 'after_rain_sunny' || trigger === 'daytime_sunny') return w === 'sunny';
      if (trigger === 'rainy') return w === 'rainy' || w === 'night_rain';
      // 下雨前后 12 小时都算（C4 用；?weather=rainy 调试参数也放行）
      if (trigger === 'rain_within_12h') return w === 'rainy' || w === 'night_rain' || rainWithin12h();
      if (trigger === 'night') return w.includes('night');
      if (trigger === 'night_clear') return w === 'night';
      if (trigger === 'rainy_or_random') return w === 'rainy' || w === 'night_rain' || Math.random() < 0.3;
      return true;
    };
    const candidates = [];
    const random = window.STORY_EVENTS.random;
    for (const [lineId, line] of Object.entries(random)) {
      if (needsAdventure.has(lineId) && !canAdventure) continue;
      if (lineId === 'K' && ![...completed].some(e => e.startsWith('hospital_'))) continue;
      // 整线级别的触发条件（如E线"夜晚的声音"只在夜里）
      if (line.trigger && !weatherMatch(line.trigger)) continue;
      const events = line.events || [];
      const ev = events.find(e => !completed.has(e.id));
      if (!ev) continue;
      // 尼尔斯线远征章节只走探险回来的钩子，不进摸头随机池（2026-07-21）
      if (ev.trigger === 'adventure_return') continue;
      // 小章鱼当主角的剧情（K4救发条小熊 / K8拖着大家游泳）没孵化不能演。
      // 整条线停在这儿等，不跳过去演后面的——跳了就乱序了（2026-07-28 小玉报的）
      if (ev.requires === 'octopus_hatched' && !state.egg?.hatched) continue;
      // 风铃的故事整段都靠风铃本身发生（H5"风好大"），不是场景描写，没有风铃这个故事没法演，只能挡住
      if (ev.requires === 'wind_chime' && !poss.wind_chime) continue;
      // period 只约束开线时机；已开播的线不受窗口限制，避免过窗后整线连手办一起卡死
      const lineStarted = events.some(e => completed.has(e.id));
      if (!lineStarted && !periodMatch(ev.period)) continue;
      if (ev.trigger && !weatherMatch(ev.trigger)) continue;
      const resolvedLines = (ev.lines || []).map(l => ({ ...l, text: resolveCondText(l, poss) }));
      candidates.push({ ...ev, lines: resolvedLines, _storyline: lineId, _cat: 'random' });
    }
    // I-line coop_random events (pet-triggered)
    const coopRandom = window.STORY_EVENTS?.coop_random || [];
    const role = state.youAre || 'mama';
    for (const evt of coopRandom) {
      if (evt.coopRequired) {
        if (completed.has(evt.id)) continue;
        if (evt.daddy_choices) {
          if (role === 'mama') {
            const resp = (state.coopResponses || []).find(r => r.eventId === evt.id);
            if (!resp) continue;
            const resolvedLines = (evt.mama_lines || []).map(l => ({
              ...l,
              text: l.text.replace('[爸爸选的开头]', resp.message || '')
            }));
            candidates.push({ ...evt, lines: resolvedLines, id: evt.id, _cat: 'coop-random', coins: 0, _coopResponse: resp });
            continue;
          }
          if (role === 'daddy') {
            if (completed.has(evt.id + '_daddy')) continue;
            candidates.push({ ...evt, lines: evt.daddy_lines, id: evt.id + '_daddy', _cat: 'coop-random', coins: 10, _daddyChoices: evt.daddy_choices });
            continue;
          }
        }
        if (role !== 'mama') continue;
      } else {
        if (completed.has(evt.id + '_' + role)) continue;
        if (evt.daddy_choices && evt.mama_choices) {
          if (!periodMatch(evt.period)) continue;
          if (evt.trigger === 'day_95_plus' && day < 95) continue;
          const myLines = role === 'daddy' ? evt.daddy_lines : evt.mama_lines;
          const myChoices = role === 'daddy' ? evt.daddy_choices : evt.mama_choices;
          if (!myLines || myLines.length === 0) continue;
          candidates.push({ ...evt, lines: myLines, id: evt.id + '_' + role, _cat: 'coop-random', coins: 10, _daddyChoices: myChoices, _choiceEventId: evt.id, _isCoopChoice: true });
          continue;
        }
      }
      if (!periodMatch(evt.period)) continue;
      if (evt.trigger === 'day_95_plus' && day < 95) continue;
      const lines = role === 'daddy' ? evt.daddy_lines : evt.mama_lines;
      if (!lines || lines.length === 0) continue;
      const resolvedId = evt.coopRequired ? evt.id : (evt.id + '_' + role);
      candidates.push({ ...evt, lines, id: resolvedId, _cat: 'coop-random', coins: evt.coopRequired ? 0 : 10 });
    }

    if (candidates.length === 0) return null;
    // 天气匹配的故事优先：带天气 trigger 的随机剧情（H 线、C4）能进候选就说明天气对上了，70% 概率选它。
    // 原来只优先 H 线；C4 要同时过「摸头 30% 掷骰」＋「正在下雨」＋「十来个候选里随机抽中」，等不到（2026-08-28）
    const weatherCands = candidates.filter(c => c._storyline && c.trigger);
    if (weatherCands.length > 0 && Math.random() < 0.7) {
      return weatherCands[Math.floor(Math.random() * weatherCands.length)];
    }
    return candidates[Math.floor(Math.random() * candidates.length)];
  }
  return { doAction, doQuickAction, finishLootResult, handleAdventureCollect, handleAdventureStart, handleAdventureTowel, handleBuild, handleCook,
    handleLampUse, handleRevive, openAdvMap, pickRandomStoryEvent, playPendingStories, submitLanternWish };
}
