/* ============================================================
   作成スタジオ — サイドバー「提案書作成」「見積書作成」。
   左＝AIチャット（指示すると即反映）／右＝案件と同じ編集画面
   （提案書=DeckEditor(inline)・見積書=QuoteSheet）。
   依存はすべてグローバル（render時解決）：useStore/Page/Icon/t/API/DeckEditor/QuoteSheet
   ============================================================ */

/* 左のチャット欄（提案書/見積書 共通） */
// Only system metadata and errors are localized; user instructions and AI prose remain verbatim.
function studioChatText(m) {
  if (m.role === 'user') return m.content;
  if (m.systemKey) return t(m.systemKey, m.systemVars) + (m.explanation ? '\n' + m.explanation : '');
  return m.err ? localizeMessage(m.content) : m.content;
}
function StudioChat({ msgs, busy, disabled, hint, onSend, maxChars }) {
  const [text, setText] = React.useState('');
  const [inputError, setInputError] = React.useState('');
  const boxRef = React.useRef(null);
  React.useEffect(() => { if (boxRef.current) boxRef.current.scrollTop = boxRef.current.scrollHeight; }, [msgs, busy]);
  const send = () => { const s = text.trim(); if (!s || busy || disabled) return; if (maxChars && s.length > maxChars) { setInputError(maxChars); return; } setInputError(''); setText(''); onSend(s); };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0 }}>
      <div ref={boxRef} style={{ flex: '1 1 auto', overflowY: 'auto', padding: '12px 12px 4px', display: 'flex', flexDirection: 'column', gap: 8 }}>
        {msgs.length === 0 && <div style={{ fontSize: 12, color: '#9aa1ab', lineHeight: 1.8, padding: '6px 2px', whiteSpace: 'pre-wrap' }}>{hint}</div>}
        {msgs.map((m, i) => (
          <div key={i} style={{ alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start', maxWidth: '88%', display: 'flex', flexDirection: 'column', alignItems: m.role === 'user' ? 'flex-end' : 'flex-start' }}>
            {m.role === 'user' && m.who && (
              <div style={{ fontSize: 10, color: '#a8aeb8', marginBottom: 2, padding: '0 4px' }}>{m.who}{m.at ? '・' + String(m.at).slice(5, 16).replace('T', ' ') : ''}</div>
            )}
            <div style={{
              background: m.role === 'user' ? '#4a5af0' : (m.err ? '#fdeeee' : '#f1f2f6'),
              color: m.role === 'user' ? '#fff' : (m.err ? '#b91c1c' : '#2a2e37'),
              fontSize: 12.5, lineHeight: 1.65, padding: '8px 11px', borderRadius: 12, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
              {studioChatText(m)}
              {m.note ? <div style={{ marginTop: 5, fontSize: 11, fontWeight: 700, color: '#15803d' }}>✓ {m.noteKey ? t(m.noteKey,m.systemVars) : m.note}</div> : null}
              {m.undoable && !busy && (
                <button onClick={() => onSend(t('studio.undoInstruction'))}
                  style={{ marginTop: 6, fontSize: 11, fontWeight: 700, padding: '3px 10px', borderRadius: 7, border: '1px solid #d9d5f2', background: '#fff', color: '#4a5af0', cursor: 'pointer', fontFamily: 'inherit' }}>↩ {t('st.undo')}</button>
              )}
            </div>
          </div>
        ))}
        {busy && <div style={{ alignSelf: 'flex-start', fontSize: 12, color: '#8b919b', padding: '6px 2px' }}>{t('st.thinking')}</div>}
      </div>
      {inputError && <div role="alert" style={{padding:'4px 10px',color:'#b91c1c',fontSize:12}}>{t('studio.maxChars',{n:inputError.toLocaleString()})}</div>}
      <div style={{ flex: '0 0 auto', padding: 10, borderTop: '1px solid #eceef2', display: 'flex', gap: 8, alignItems: 'flex-end' }}>
        <textarea value={text} rows={2} placeholder={disabled ? t('st.noCase') : t('st.placeholder')} disabled={disabled}
          onChange={e => setText(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(); } }}
          style={{ flex: 1, resize: 'none', fontFamily: 'inherit', fontSize: 12.5, lineHeight: 1.6, padding: '8px 10px', borderRadius: 10, border: '1px solid #dfe2e8', outline: 'none', background: disabled ? '#f6f7f9' : '#fff' }} />
        <button onClick={send} disabled={busy || disabled || !text.trim()}
          style={{ flex: '0 0 auto', fontSize: 12.5, fontWeight: 700, padding: '9px 14px', borderRadius: 10, border: 'none', cursor: (busy || disabled || !text.trim()) ? 'default' : 'pointer',
            background: (busy || disabled || !text.trim()) ? '#dcdde3' : '#4a5af0', color: '#fff', fontFamily: 'inherit' }}>{t('st.send')}</button>
      </div>
    </div>
  );
}

/* 案件セレクタ＝スタジオ共通ヘッダ。検索と同じ「入力して絞り込む」方式（ユーザー要望）。
   入力欄をフォーカスすると候補一覧（スタジオ作成分→案件の順）を表示し、文字で絞り込んで選ぶ */
function StudioCaseBar({ cases, drafts, caseId, onChange, onCreate, onPromote, onTrash }) {
  const sort = (arr) => arr.slice().sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
  const cur = [...drafts, ...cases].find(c => c.id === caseId);
  const [q, setQ] = React.useState('');
  const [open, setOpen] = React.useState(false);
  const boxRef = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const close = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) { setOpen(false); setQ(''); } };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, [open]);
  const s = q.trim().toLowerCase();
  const hit = (c) => !s || String(c.title || '').toLowerCase().includes(s);
  const gD = sort(drafts).filter(hit), gC = sort(cases).filter(hit);
  const pickIt = (id) => { setOpen(false); setQ(''); onChange(id); };
  const row = (c) => (
    <button key={c.id} onClick={() => pickIt(c.id)}
      onMouseEnter={e => { e.currentTarget.style.background = '#f4f4fb'; }} onMouseLeave={e => { e.currentTarget.style.background = c.id === caseId ? '#eef0fd' : 'none'; }}
      style={{ flex: '0 0 auto', textAlign: 'left', fontSize: 12.5, lineHeight: 1.5, padding: '7px 10px', borderRadius: 7, border: 'none', cursor: 'pointer', fontFamily: 'inherit',
        background: c.id === caseId ? '#eef0fd' : 'none', color: '#2a2e37', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
      {(c.title || c.id).slice(0, 60)}
    </button>
  );
  return (
    <div style={{ flex: '0 0 auto', padding: '10px 12px', borderBottom: '1px solid #eceef2', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
      <span style={{ fontSize: 12, fontWeight: 700, color: '#3b414b', flex: '0 0 auto' }}>{t('st.case')}</span>
      <span ref={boxRef} style={{ position: 'relative', flex: 1, minWidth: 0 }}>
        <input value={open ? q : ((cur && cur.title) || '')} placeholder={cur ? (cur.title || '') : t('st.searchCase')}
          onFocus={() => { setOpen(true); setQ(''); }}
          onChange={e => { setQ(e.target.value); if (!open) setOpen(true); }}
          onKeyDown={e => {
            if (e.key === 'Escape') { setOpen(false); setQ(''); e.currentTarget.blur(); }
            if (e.key === 'Enter') { const first = [...gD, ...gC][0]; if (open && first) { pickIt(first.id); e.currentTarget.blur(); } }
          }}
          style={{ width: '100%', boxSizing: 'border-box', fontSize: 12.5, fontFamily: 'inherit', padding: '7px 10px', borderRadius: 9, border: '1px solid #dfe2e8', background: '#fff', color: '#2a2e37', outline: 'none' }} />
        {open && (
          <div style={{ position: 'absolute', left: 0, right: 0, top: 36, zIndex: 80, background: '#fff', border: '1px solid #e6e4f5', borderRadius: 10,
            boxShadow: '0 10px 28px rgba(30,32,60,.14)', padding: 6, maxHeight: 300, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
            {gD.length > 0 && <div style={{ flex: '0 0 auto', fontSize: 10.5, fontWeight: 700, color: '#8b919b', padding: '4px 10px 2px' }}>{t('st.groupDrafts')}</div>}
            {gD.map(row)}
            {gC.length > 0 && <div style={{ flex: '0 0 auto', fontSize: 10.5, fontWeight: 700, color: '#8b919b', padding: '4px 10px 2px' }}>{t('st.groupCases')}</div>}
            {gC.map(row)}
            {gD.length + gC.length === 0 && <div style={{ fontSize: 12, color: '#a8aeb8', padding: '8px 10px' }}>{t('st.searchNoHit')}</div>}
          </div>
        )}
      </span>
      <button onClick={onCreate} title={t('st.newDoc')}
        style={{ flex: '0 0 auto', display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12, fontWeight: 700, padding: '8px 12px', borderRadius: 9,
          border: 'none', background: '#4a5af0', color: '#fff', cursor: 'pointer', fontFamily: 'inherit', whiteSpace: 'nowrap' }}>
        ＋ {t('st.newBtn')}
      </button>
      {cur && cur.studioDraft && (
        <>
          <button onClick={onPromote} title={t('st.toCaseConfirm')}
            style={{ flex: '0 0 auto', fontSize: 11.5, fontWeight: 600, padding: '6px 10px', borderRadius: 8, border: '1px solid #d9d5f2', background: '#fff', color: '#4a5af0', cursor: 'pointer', fontFamily: 'inherit' }}>{t('st.toCase')}</button>
          <button onClick={onTrash} title={t('st.delDraft')}
            style={{ flex: '0 0 auto', fontSize: 11.5, fontWeight: 600, padding: '6px 10px', borderRadius: 8, border: '1px solid #f2d9d9', background: '#fff', color: '#c0392b', cursor: 'pointer', fontFamily: 'inherit' }}>{t('st.delDraft')}</button>
        </>
      )}
    </div>
  );
}

/* 共通レイアウト＝左チャット・右エディタ */
function StudioLayout({ title, left, right }) {
  const isMobile = useIsMobile();
  return (
    <Page title={title} pad={false}>
      <div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 12, padding: isMobile ? '12px 10px 40px' : '14px 16px 16px',
        height: isMobile ? 'auto' : 'calc(100vh - 92px)' }}>
        <div style={{ flex: isMobile ? '0 0 auto' : '0 0 330px', minWidth: 0, height: isMobile ? 420 : 'auto',
          background: '#fff', border: '1px solid #e6e8ee', borderRadius: 12, display: 'flex', flexDirection: 'column', overflow: 'hidden', boxShadow: '0 1px 2px rgba(20,22,40,.04)' }}>
          {left}
        </div>
        <div style={{ flex: '1 1 auto', minWidth: 0, height: isMobile ? '78vh' : 'auto', minHeight: 0 }}>
          {right}
        </div>
      </div>
    </Page>
  );
}

function StudioEmpty({ icon, text, action, onAction }) {
  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10,
      background: '#fff', border: '1px dashed #d7dae1', borderRadius: 12, color: '#9aa1ab', padding: 24, textAlign: 'center' }}>
      <Icon name={icon} size={30} stroke={1.7} style={{ color: '#cbd0d7' }} />
      <div style={{ fontSize: 13, lineHeight: 1.8, maxWidth: 420 }}>{text}</div>
      {action && <button onClick={onAction} style={{ marginTop: 4, fontSize: 12.5, fontWeight: 700, padding: '9px 16px', borderRadius: 10, border: 'none', cursor: 'pointer', background: '#4a5af0', color: '#fff', fontFamily: 'inherit' }}>{action}</button>}
    </div>
  );
}

/* 全社の作成履歴一覧（案件未選択時の右パネル）。クリックでその案件を開いて編集へ */
function StudioHistory({ title, items, emptyText, onOpen }) {
  const [q, setQ] = React.useState('');
  const s = q.trim().toLowerCase();
  const shown = !s ? items : items.filter(it => (it.title + ' ' + (it.company || '') + ' ' + (it.owner || '')).toLowerCase().includes(s));
  return (
    <div style={{ height: '100%', overflowY: 'auto', background: '#fff', border: '1px solid #e6e8ee', borderRadius: 12, padding: 16 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, flexWrap: 'wrap' }}>
        <span style={{ fontSize: 13.5, fontWeight: 700, color: '#1c1f26' }}>{title}</span>
        <span style={{ fontSize: 11.5, color: '#8b919b' }}>{t('st.histCount', { n: items.length })}</span>
        <input value={q} onChange={e => setQ(e.target.value)} placeholder={t('st.histSearch')}
          style={{ marginLeft: 'auto', fontSize: 12, fontFamily: 'inherit', padding: '6px 10px', borderRadius: 8, border: '1px solid #dfe2e8', width: 200 }} />
      </div>
      {items.length === 0 && <div style={{ padding: '48px 0', textAlign: 'center', color: '#9aa1ab', fontSize: 12.5, lineHeight: 1.9, whiteSpace: 'pre-wrap' }}>{emptyText}</div>}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(230px, 1fr))', gap: 10 }}>
        {shown.map(it => (
          <div key={it.id} onClick={() => onOpen(it.id)} className="lift"
            style={{ border: '1px solid #e8eaee', borderRadius: 11, padding: '12px 13px', cursor: 'pointer', background: '#fff', boxShadow: '0 1px 2px rgba(20,22,40,.04)' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
              <span style={{ fontSize: 12.5, fontWeight: 700, color: '#1c1f26', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>{it.title}</span>
              {it.draft && <span style={{ flex: '0 0 auto', fontSize: 10, fontWeight: 700, color: '#4a5af0', background: '#eef0fe', padding: '2px 7px', borderRadius: 999 }}>{t('st.badgeDraft')}</span>}
            </div>
            <div style={{ fontSize: 11.5, color: '#7b828d', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{it.company || '—'}</div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 7, fontSize: 11, color: '#9aa1ab' }}>
              <span>{t('st.madeBy')}：{it.madeBy || it.owner || '—'}{it.madeAt ? '・' + it.madeAt : (it.date ? '・' + it.date : '')}</span>
              {it.extra && <span style={{ marginLeft: 'auto', fontWeight: 700, color: '#2a6fdb' }}>{it.extra}</span>}
            </div>
            {it.editBy && (
              <div style={{ fontSize: 11, color: '#9aa1ab', marginTop: 2 }}>{t('st.editedBy')}：{it.editBy}{it.editAt ? '・' + it.editAt : ''}</div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

/* 履歴カード用のメタ情報（顧客名・担当名） */
function studioMeta(c) {
  const D = window.APP_DATA;
  const cust = (D.customers || []).find(x => x.id === c.customerId);
  const owner = (D.users || []).find(u => u.id === c.ownerId);
  return { company: (cust && (cust.shortName || cust.company)) || '', owner: (owner && (owner.short || owner.name)) || '' };
}

/* ドラフト案件（案件一覧に出ない器）を作る共通処理。studioDraft はストアの realCases で一括除外。
   削除はソフト方式（studioTrashed）＝どこにも出なくなるだけでデータは残る（誤操作も戻せる） */
function useStudioDrafts(saveCase, patchCase, allCases, currentUser) {
  const drafts = allCases.filter(c => c.studioDraft && !c.mergedInto && !c.studioTrashed);
  const create = (defTitle) => {
    const title = (window.prompt(t('st.newTitleAsk'), defTitle) || '').trim();
    if (!title) return null;
    return saveCase({ title, status: 'working', ownerId: (currentUser && currentUser.id) || null, customerId: null, studioDraft: true });
  };
  const promote = (id) => { if (window.confirm(t('st.toCaseConfirm'))) patchCase(id, { studioDraft: false }); };
  const trash = (id) => { if (!window.confirm(t('st.delDraftConfirm'))) return false; patchCase(id, { studioTrashed: true }); return true; };
  return { drafts, create, promote, trash };
}

/* チャット履歴の共有保存＝case.studioChat.deck / .quote に永続化（全員が見られ・続きから話せる）。
   直近100件。提案書の長い編集指示はAPIの上限40,000文字まで保持する */
function studioChatOf(c, key) { return (c && c.studioChat && Array.isArray(c.studioChat[key])) ? c.studioChat[key] : []; }
function studioChatSave(patchCase, kase, key, next) {
  if (!kase) return;
  const trimmed = next.slice(-100).map(m => ({ role: m.role, content: String(m.content || '').slice(0, key === 'deck' ? 40000 : 4000),
    note: m.note || '', systemKey:m.systemKey||'', systemVars:m.systemVars||{}, explanation:m.explanation||'', noteKey:m.noteKey||'', who: m.who || '', at: m.at || '', err: !!m.err }));
  patchCase(kase.id, { studioChat: { ...(kase.studioChat || {}), [key]: trimmed } });
}
function studioNow() { const d = new Date(); const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`; }

/* ---------- 提案書作成スタジオ ---------- */
function ProposalStudio() {
  const { cases, workspaceCases: allCases, route, navigate, saveCase, patchCase, syncProposalResult, currentUser } = useStore();
  const { drafts, create, promote, trash } = useStudioDrafts(saveCase, patchCase, allCases, currentUser);
  const [caseId, setCaseId] = React.useState(() => (route.id && allCases.some(c => c.id === route.id)) ? route.id : '');
  const [msgs, setMsgs] = React.useState(() => studioChatOf(allCases.find(c => c.id === (route.id || '')), 'deck'));
  const [busy, setBusy] = React.useState(false);
  const deckCtrl = React.useRef(null);
  const sending = React.useRef(false);
  const [changedPage, setChangedPage] = React.useState(0);
  const [deckKey, setDeckKey] = React.useState(0);   // チャット反映後に右のエディタを再読込
  const [deckBroken, setDeckBroken] = React.useState(false);  // 旧形式（HTML等・PPTXでない）で読めない→作り直し導線へ
  const kase = [...drafts, ...cases].find(c => c.id === caseId) || null;
  const skillSelection=useProposalSkillSelection(kase);
  const hasDeck = !!(kase && kase.proposalDesign && kase.proposalDesign.attId) && !deckBroken;

  const pick = (id) => { if (sending.current) return; setChangedPage(0); setCaseId(id); setDeckBroken(false); setMsgs(studioChatOf([...drafts, ...cases].find(c => c.id === id), 'deck')); navigate('proposalStudio', id || null); };

  /* デッキ未生成の案件へのチャット＝ゼロから自動生成（ブリーフ→デザインジョブ→完了までポーリング） */
  const generate = async (text) => {
    const r = await API.proposalBriefGen(caseId, text,skillSelection.value.brief);
    patchCase(caseId, { proposalBrief: { text: r.text, at: r.at || '', v: r.v || '', skill:r.skill, warnings: r.warnings || [] } });
    await API.proposalDesignStart(caseId, undefined,skillSelection.value.design);
    for (let i = 0; i < 40; i++) {                       // 8秒×40回＝最長5分強
      await new Promise(res => setTimeout(res, 8000));
      let s; try { s = await API.proposalDesignStatus(caseId); } catch (_) { continue; }  // 一時的な通信エラーは次で回復
      if (s.running) continue;
      if (s.error) throw new Error(s.error);
      if (s.attId) {
        const patch = { proposalDesign: s.design || { ...kase?.proposalDesign,attId: s.attId, at: s.at || '', v: s.v || '',skill:s.skill } };
        if (s.att && !((kase && kase.attachments) || []).some(x => x.id === s.att.id)) patch.attachments = [s.att, ...((kase && kase.attachments) || [])];
        patchCase(caseId, patch);
        setDeckKey(k => k + 1);
        return;
      }
      break;
    }
    throw new Error(t('st.fail'));
  };

  const send = async (text,{regenerate=false}={}) => {
    if (sending.current || !kase) return;
    if((!hasDeck||regenerate)&&(!skillSelection.readyFor('brief')||!skillSelection.readyFor('design')))return;
    /* 既存ブリーフのある通常案件をゼロから作り直す時は確認（案件タブ側の挙動と揃える・無断上書き防止） */
    if ((regenerate||!hasDeck&&kase&&!kase.studioDraft&&kase.proposalBrief?.text) && !window.confirm(t('st.genOverwriteConfirm'))) return;
    const who = (currentUser && (currentUser.short || currentUser.name)) || '';
    let next = [...msgs, { role: 'user', content: text, who, at: studioNow() }];
    sending.current = true; setMsgs(next); setBusy(true);
    try {
      if (text.length > 40000) throw new Error(t("studio.limit"));
      const expectedAttId = hasDeck ? deckCtrl.current?.prepare() : undefined;
      if (hasDeck && !expectedAttId) throw new Error(t("studio.waitLoad"));
      if (!hasDeck||regenerate) {
        next = [...next, { role: 'assistant', content: t('st.genStart'), systemKey: 'st.genStart' }]; setMsgs(next);
        await generate(text);
        setDeckBroken(false);   // 作り直し成功＝新形式PPTXになったので編集画面を有効化
        next = [...next, { role: 'assistant', content: t('st.genDone') }];
      } else {
        const history = msgs.slice(-8).map(m => ({ role: m.role, content: m.content }));
        const r = await API.deckChat(caseId, text, history, expectedAttId);
        next = [...next, { role: 'assistant', content: r.reply || t("studio.invalidReply"), note: r.applied ? t('st.appliedN', { n: r.applied }) : '',
          systemKey: r.outcome==='updated'?'studio.savedPages':r.outcome==='unchanged'?'studio.unchanged':r.outcome==='answer'?'studio.answerOnly':'',
          systemVars:{n:r.applied,pages:(r.changedPages||[]).map(p=>'p.'+p).join('、')},explanation:r.explanation||'',noteKey:r.applied?'st.appliedN':'' }];
        if (r.applied) {
          syncProposalResult(caseId, r);
          setChangedPage(Math.max(0, (r.changedPages?.[0] || 1) - 1));
          setDeckKey(k => k + 1);
        }
      }
    } catch (e) {
      next = [...next, { role: 'assistant', content: (e && e.message) || t('st.fail'), err: true }];
    }
    setMsgs(next); setBusy(false); sending.current = false;
    studioChatSave(patchCase, kase, 'deck', next);   // 案件に保存＝全員が履歴を見られ・続きから話せる
  };

  /* 全社の提案書履歴＝デザイン生成済みの案件（ドラフト含む）を新しい順に */
  const hist = [...drafts, ...cases]
    .filter(c => c.proposalDesign && c.proposalDesign.attId)
    .sort((a, b) => String((b.proposalDesign || {}).at || '').localeCompare(String((a.proposalDesign || {}).at || '')))
    .map(c => { const pd = c.proposalDesign || {};
      return { id: c.id, title: c.title || c.id, date: String(pd.at || '').slice(0, 10), draft: !!c.studioDraft,
        madeBy: pd.createdBy || '', madeAt: String(pd.createdAt || pd.at || '').slice(0, 10),
        editBy: pd.editBy || '', editAt: pd.editBy ? String(pd.at || '').slice(0, 10) : '', ...studioMeta(c) }; });

  return (
    <StudioLayout title={t('nav.deckStudio')}
      left={<>
        <fieldset disabled={busy} style={{border:0,padding:0,margin:0,minWidth:0}}>
        <StudioCaseBar cases={cases} drafts={drafts} caseId={caseId} onChange={pick}
          onCreate={() => { const id = create(t('st.newDeckDefault')); if (id) pick(id); }} onPromote={() => promote(caseId)}
          onTrash={() => { if (trash(caseId)) pick(''); }} />
        </fieldset>
        <ProposalSkillPicker selection={skillSelection} disabled={busy||!kase}/>
        {hasDeck&&<Button size="sm" disabled={busy||!skillSelection.readyFor('brief')||!skillSelection.readyFor('design')} onClick={()=>send(t('studio.regenerateInstruction'),{regenerate:true})}>{t("studio.regenerate")}</Button>}
        {hasDeck&&<div style={{fontSize:11.5,color:'#8a91a0',padding:'0 12px 8px'}}>{t("studio.regenerateHint")}</div>}
        {kase?.proposalBrief?.skill&&<UsedProposalSkill skill={kase.proposalBrief.skill}/>}
        {kase?.proposalDesign?.skill&&<UsedProposalSkill label={t("studio.designSkill")} skill={kase.proposalDesign.skill}/>}
        <StudioChat msgs={msgs} busy={busy} maxChars={40000} disabled={!kase||!hasDeck&&(!skillSelection.readyFor('brief')||!skillSelection.readyFor('design'))} hint={!kase ? t('st.deckHint') : (hasDeck ? t('st.deckHint') : t('st.deckHintNew'))} onSend={send} />
      </>}
      right={!kase
        ? <StudioHistory title={t('st.histDeck')} items={hist} emptyText={t('st.histEmptyDeck')} onOpen={pick} />
        : !hasDeck
          ? <StudioEmpty icon="edit" text={deckBroken ? t('st.oldDeck') : t('st.noDeckGen')}
              action={kase.studioDraft ? null : t('st.openProposalTab')} onAction={() => navigate('case', kase.id, 'proposal')} />
          : <DeckEditor key={kase.id + ':' + deckKey} caseData={kase} inline controlRef={deckCtrl} locked={busy} initialPage={changedPage} onLoadError={() => setDeckBroken(true)} />}
    />
  );
}

/* ---------- 見積書作成スタジオ ---------- */
function QuoteStudio() {
  const { cases, workspaceCases: allCases, route, navigate, patchCase, saveCase, currentUser } = useStore();
  const { drafts, create, promote, trash } = useStudioDrafts(saveCase, patchCase, allCases, currentUser);
  const [caseId, setCaseId] = React.useState(() => (route.id && allCases.some(c => c.id === route.id)) ? route.id : '');
  const [msgs, setMsgs] = React.useState(() => studioChatOf(allCases.find(c => c.id === (route.id || '')), 'quote'));
  const [busy, setBusy] = React.useState(false);
  const [qKey, setQKey] = React.useState(0);         // チャット反映後にQuoteSheetを再マウントして最新の明細を読ませる
  const sheetCtrl = React.useRef(null);              // QuoteSheetのflush窓口（チャット前に未保存の手入力を保存）
  const kase = [...drafts, ...cases].find(c => c.id === caseId) || null;

  const pick = (id) => { setCaseId(id); setMsgs(studioChatOf([...drafts, ...cases].find(c => c.id === id), 'quote')); navigate('quoteStudio', id || null); };
  const send = async (text) => {
    const who = (currentUser && (currentUser.short || currentUser.name)) || '';
    let next = [...msgs, { role: 'user', content: text, who, at: studioNow() }];
    setMsgs(next); setBusy(true);
    try {
      /* 入力途中（保存デバウンス待ち）の手直しを先にサーバへ反映＝AIが古い表を読んで巻き戻す事故を防ぐ */
      try { if (sheetCtrl.current) await sheetCtrl.current.flush(); } catch (_) {}
      const history = msgs.slice(-8).map(m => ({ role: m.role, content: m.content }));
      const r = await API.quoteChat(caseId, text, history);
      next = [...next, { role: 'assistant', content: r.reply || t('st.done'), note: r.sheet ? t('st.appliedQuote') : '', undoable: !!r.sheet }];
      if (r.sheet) { patchCase(caseId, { quoteSheet: r.sheet }); setQKey(k => k + 1); }  // サーバ保存済み＝ローカルstoreも同期して再表示
    } catch (e) {
      next = [...next, { role: 'assistant', content: (e && e.message) || t('st.fail'), err: true }];
    }
    setMsgs(next); setBusy(false);
    studioChatSave(patchCase, kase, 'quote', next);   // 案件に保存＝全員が履歴を見られ・続きから話せる
  };

  /* 全社の見積書履歴＝quoteSheet に明細か件名がある案件（ドラフト含む）。合計金額も表示 */
  const hist = [...drafts, ...cases]
    .filter(c => c.quoteSheet && ((c.quoteSheet.items || []).length > 0 || String(c.quoteSheet.subject || '').trim()))
    .sort((a, b) => String((b.quoteSheet || {}).quotationDate || '').localeCompare(String((a.quoteSheet || {}).quotationDate || '')))
    .map(c => {
      const items = (c.quoteSheet.items || []).filter(it => it.type !== 'text');
      const total = items.reduce((s, it) => s + (parseFloat(it.unitPrice) || 0) * (parseFloat(it.qty) || 0), 0);
      const qs = c.quoteSheet || {};
      return { id: c.id, title: qs.subject || c.title || c.id, date: qs.quotationDate || '', draft: !!c.studioDraft,
        madeBy: qs.createdBy || '', madeAt: String(qs.createdAt || '').slice(0, 10) || (qs.quotationDate || ''),
        editBy: qs.updatedBy || '', editAt: String(qs.updatedAt || '').slice(0, 10),
        extra: total > 0 ? '¥' + Math.round(total).toLocaleString('ja-JP') : '', ...studioMeta(c) };
    });

  return (
    <StudioLayout title={t('nav.quoteStudio')}
      left={<>
        <StudioCaseBar cases={cases} drafts={drafts} caseId={caseId} onChange={pick}
          onCreate={() => { const id = create(t('st.newQuoteDefault')); if (id) pick(id); }} onPromote={() => promote(caseId)}
          onTrash={() => { if (trash(caseId)) pick(''); }} />
        <StudioChat msgs={msgs} busy={busy} disabled={!kase} hint={t('st.quoteHint')} onSend={send} />
      </>}
      right={!kase
        ? <StudioHistory title={t('st.histQuote')} items={hist} emptyText={t('st.histEmptyQuote')} onOpen={pick} />
        : <div key={kase.id + ':' + qKey} style={{ height: '100%', overflowY: 'auto', background: '#fff', border: '1px solid #e6e8ee', borderRadius: 12, padding: 16 }}>
            <QuoteSheet caseData={kase} ctrlRef={sheetCtrl} />
          </div>}
    />
  );
}

Object.assign(window, { ProposalStudio, QuoteStudio });
