/* ============================================================
   Episode browse · detail (persona selector) · submit · history
   ============================================================ */
const { useState: useSE, useMemo: useME, useEffect: useEE } = React;

/* selection helpers — selection = [{episodeId, personaLabel}] */
const selHas = (sel, id) => sel.some((s) => s.episodeId === id);
const selPersonaLabel = (sel, id) => (sel.find((s) => s.episodeId === id) || {}).personaLabel || null;
/* an episode's persona objects: prefer personas[] (4 fields), fall back to legacy persona1/2/3 */
const epPersonas = (ep) => {
  const arr = Array.isArray(ep && ep.personas) ? ep.personas : [];
  const fromNew = arr.filter((p) => p && String(p.title || '').trim());
  if (fromNew.length) return fromNew.map((p) => ({ title: String(p.title).trim(), description: p.description || '', buildup: p.buildup || '', solution: p.solution || '' }));
  return [ep && ep.persona1, ep && ep.persona2, ep && ep.persona3]
    .filter((x) => x && String(x).trim())
    .map((t) => ({ title: String(t).trim(), description: '', buildup: '', solution: '' }));
};
/* just the labels (titles) — used where only the name is needed */
const epPersonaOptions = (ep) => epPersonas(ep).map((p) => p.title);

/* ---- card list controls (shared by ② 카드 관리 + 라이브러리 피커) ---- */
// Split a free-text 설명 into bullet lines for card display. Honors explicit
// line breaks and bullet chars; otherwise breaks on sentence ends (다././?/!),
// so a single paragraph reads as scannable bullets. 문장 끝+공백을 개행으로 치환 후
// 분리 — 정규식 lookbehind는 구형 Safari/WebView에서 모듈 로드를 깨뜨려 쓰지 않는다.
const descBullets = (text) =>
  String(text || '')
    .split(/\n+/)
    .flatMap((line) => line.replace(/([.!?。])\s+/g, '$1\n').split('\n'))
    .map((s) => s.replace(/^[\s•·\-–—]+/, '').trim())
    .filter(Boolean);
// 설명을 "설명" 라벨 + 불릿 리스트로 — 카드 본문(EpisodeCard·라이브러리 피커 공용).
// 불릿이 하나도 없으면(빈/공백뿐 설명) 라벨까지 통째로 미렌더.
const EpDescBlock = ({ text }) => {
  const items = descBullets(text);
  return items.length === 0 ? null : (
    <div className="ep-desc-block">
      <div className="ep-desc-label">설명</div>
      <ul className="ep-desc-list">{items.map((b, i) => <li key={i}>{b}</li>)}</ul>
    </div>
  );
};
// how many themes a card is placed in (사용 유무 = placeCount > 0)
const placeCount = (c) => (c.placements || []).length;
// union of every product across the given cards, ko-collated.
// String() guards against non-string product values (products is a JSON column).
const cardProductOptions = (cards) => {
  const s = new Set();
  (cards || []).forEach((c) => (c.products || []).forEach((p) => p && s.add(p)));
  return Array.from(s).sort((a, b) => String(a).localeCompare(String(b), 'ko'));
};
// does a card pass the {q, product, usage} filter? (search over title·설명·텐션·제품·페르소나)
const matchCard = (c, { q, product, usage }) => {
  if (product && !(c.products || []).includes(product)) return false;
  if (usage === 'used' && placeCount(c) === 0) return false;
  if (usage === 'unused' && placeCount(c) > 0) return false;
  const needle = (q || '').trim().toLowerCase();
  if (!needle) return true;
  const hay = [c.title, c.description, c.tension, ...(c.products || []), ...epPersonaOptions(c)]
    .filter(Boolean).join(' ').toLowerCase();
  return hay.includes(needle);
};

// search box + 제품 select + 전체/사용중/미사용 segment. `children` = extra trailing
// control (e.g. the manager's sort select); picker passes none.
function CardFilterBar({ q, setQ, product, setProduct, productOptions, usage, setUsage, mb = 12, children }) {
  return (
    <div className="flex center wrap-flex" style={{ gap: 8, marginBottom: mb }}>
      <div style={{ position: 'relative', flex: '1 1 180px', minWidth: 150 }}>
        <Icon.search style={{ width: 14, height: 14, position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--ink-3, #9aa0a7)' }} />
        <input className="input" value={q} onChange={(e) => setQ(e.target.value)} placeholder="제목·설명·텐션·제품·페르소나 검색" style={{ paddingLeft: 30, paddingRight: q ? 28 : 12, width: '100%' }} />
        {q && <button className="icon-btn" onClick={() => setQ('')} title="검색 지우기" style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)', width: 22, height: 22 }}><Icon.x style={{ width: 12, height: 12 }} /></button>}
      </div>
      <select className="input" value={product} onChange={(e) => setProduct(e.target.value)} style={{ flex: '0 0 auto' }}>
        <option value="">전체 제품</option>
        {productOptions.map((p) => <option key={p} value={p}>{p}</option>)}
      </select>
      <div className="seg" style={{ marginBottom: 0 }}>
        <button className={usage === 'all' ? 'on red' : ''} onClick={() => setUsage('all')}>전체</button>
        <button className={usage === 'used' ? 'on red' : ''} onClick={() => setUsage('used')}>등록됨</button>
        <button className={usage === 'unused' ? 'on red' : ''} onClick={() => setUsage('unused')}>미등록</button>
      </div>
      {children}
    </div>
  );
}

// "조건에 맞는 카드가 없습니다" + 필터 초기화 — shown when a filter hides everything.
function CardFilterEmpty({ onReset }) {
  return <EmptyState icon={<Icon.search />} title="조건에 맞는 카드가 없습니다" desc="검색어나 필터를 바꿔보세요."
    action={<Btn variant="ghost" onClick={onReset}>필터 초기화</Btn>} />;
}

/* ============================================================
   EPISODE BROWSE  (view / select / submit — no authoring)
   ============================================================ */
function EpisodeBrowse({ project, month, user, navigate, toast }) {
  const { loading, error, data, reload } = useAsync(() => window.api.getContent(project.id), [project.id]);

  // month tab state — keyed by month.id; guard so tab switching works after reload
  const [curMonthId, setCurMonthId] = useSE(null);
  useEE(() => {
    if (data && (!curMonthId || !data.months.some((m) => m.id === curMonthId))) {
      setCurMonthId(data.months[0]?.id ?? null);
    }
  }, [data]);

  const [sel, setSel] = useSE([]);
  // session-only persona choices for not-yet-selected episodes: shown (blue) on
  // the card without selecting/submitting the episode. Carried into the
  // selection if/when the episode is later checked.
  const [personaDraft, setPersonaDraft] = useSE({});
  const [detail, setDetail] = useSE(null);      // episode being viewed
  const [submitOpen, setSubmitOpen] = useSE(false);
  const [submitting, setSubmitting] = useSE(false);

  useEE(() => { if (data) setSel(data.currentSelection || []); }, [data]);

  // per-month quota
  const cm = data ? data.months.find((m) => m.id === curMonthId) : null;
  const monthCap = cm ? (cm.maxSelections || 0) : 0;
  // build episode→monthId map from themes
  const epMonth = useME(() => {
    const map = {};
    if (data) data.themes.forEach((t) => t.episodes.forEach((ep) => { map[ep.id] = t.monthId; }));
    return map;
  }, [data]);
  const monthSelCount = sel.filter((s) => epMonth[s.episodeId] === curMonthId).length;

  const count = sel.length;
  const allPersona = sel.every((s) => s.personaLabel);

  const persist = (next) => { setSel(next); window.api.setSelection(project.id, null, next); };

  const toggleSelect = (ep) => {
    if (ep.status === 'paused') return;
    if (selHas(sel, ep.id)) {
      persist(sel.filter((s) => s.episodeId !== ep.id));
    } else {
      // enforce the per-month cap (0 = unlimited); mirrors backend MONTH_QUOTA
      if (monthCap > 0 && monthSelCount >= monthCap) {
        toast(`${cm?.label ?? '이 달'}은 최대 ${monthCap}개까지 선택할 수 있습니다`);
        return;
      }
      persist([...sel, { episodeId: ep.id, personaLabel: personaDraft[ep.id] ?? null }]);
    }
  };
  const assignPersona = (epId, personaLabel) => {
    // Picking a persona never selects the episode — selection goes through
    // toggleSelect, which enforces the month cap (mirrors backend MONTH_QUOTA).
    // Always remember the choice as a session draft (so it shows on the card);
    // if the episode is already selected, also persist it onto the snapshot.
    setPersonaDraft((d) => ({ ...d, [epId]: personaLabel }));
    if (selHas(sel, epId)) persist(sel.map((s) => (s.episodeId === epId ? { ...s, personaLabel } : s)));
  };

  const doSubmit = async (note) => {
    setSubmitting(true);
    await window.api.submit(project.id, null, { items: sel, note });
    setSubmitting(false); setSubmitOpen(false);
    toast(`에피소드 ${sel.length}개가 제출되었습니다`);
    navigate({ name: 'project', projectId: project.id, tab: user.role === 'LGE' ? 'episodes' : 'history' });
  };

  // flat episode map (for drawer/submit-modal episode lookups)
  const epById = useME(() => flatEpisodes(data ? data.themes : []), [data]);

  // themes for the selected month tab, showing ONLY 운영가이드 표기-ON episodes
  // (status !== 'paused'). Themes left with no visible episode are dropped so the
  // LGE guide never shows paused/hidden content; if nothing is left the month
  // renders the empty-state notice below.
  const visibleThemes = data
    ? data.themes
        .filter((t) => t.monthId === curMonthId)
        .map((t) => ({ ...t, episodes: t.episodes.filter((ep) => ep.status !== 'paused') }))
        .filter((t) => t.episodes.length > 0)
    : [];

  return (
    <div className="page-wrap">
      {/* sticky submit bar — hidden when the selected month has no 표기-ON content */}
      {visibleThemes.length > 0 && (
      <div className="submitbar">
        <div className="quota">
          {monthCap > 0
            ? <Ring value={monthSelCount} total={monthCap} />
            : <Ring value={count} total={Math.max(count, 1)} />}
          <div className="quota-txt">
            <div className="ql">에피소드 선택</div>
            {monthCap > 0
              ? <div className={`qs ${monthSelCount >= 1 ? 'ok' : 'under'}`}>
                  에피소드를 <b>{monthCap}개</b> 선택해주세요. (현재 <b> {monthSelCount}개)</b>
                </div>
              : <div className={`qs ${count >= 1 ? 'ok' : 'under'}`}>
                  <b>{count}개</b> 선택됨{count >= 1 ? ' · 제출 가능' : ' · 1개 이상 선택'}
                </div>}
          </div>
        </div>
        <div className="sb-actions">
          {!allPersona && count >= 1 && <span className="qs under hide-mobile" style={{ fontSize: 12 }}><Icon.alert style={{ width: 13, height: 13, verticalAlign: '-2px' }} /> 페르소나 미지정 {sel.filter((s) => !s.personaLabel).length}개</span>}
          {user.role !== 'LGE' && <Btn variant="ghost" icon={<Icon.clock />} className="hide-mobile" onClick={() => navigate({ name: 'project', projectId: project.id, tab: 'history' })}>제출 히스토리</Btn>}
          <Btn variant="primary" icon={<Icon.send />} disabled={count < 1} onClick={() => setSubmitOpen(true)}>
            제출{count > 0 ? ` (${count})` : ''}
          </Btn>
        </div>
      </div>
      )}

      {/* Month tabs */}
      {!loading && !error && data && data.months.length > 0 && (
        <div className="seg" style={{ marginBottom: 18 }}>
          {data.months.map((m) => (
            <button key={m.id} className={m.id === curMonthId ? 'on red' : ''} onClick={() => setCurMonthId(m.id)}>{m.label}</button>
          ))}
        </div>
      )}

      {loading && [0, 1].map((i) => (
        <div className="theme-box" key={i}><Skel w="40%" h={18} /><div className="ep-grid" style={{ marginTop: 16 }}>{[0, 1, 2].map((j) => <div key={j}><Skel h={120} r={12} /></div>)}</div></div>
      ))}
      {error && <ErrorState onRetry={reload} msg={error} />}
      {!loading && !error && visibleThemes.length === 0 && (
        <EmptyState icon={<Icon.layers />} title="표시할 콘텐츠가 없습니다"
          desc="이 달에 운영가이드 표기(ON)된 에피소드가 아직 없습니다." />
      )}

      {!loading && !error && visibleThemes.map((t, ti) => (
        <div className="theme-box" key={t.id}>
          <div className="theme-hd">
            <span className="theme-pill">Theme {String(ti + 1).padStart(2, '0')}</span>
            <div style={{ minWidth: 0 }}>
              <div className="theme-name">{t.name}</div>
              <div className="theme-sub">{t.subtitle}</div>
            </div>
          </div>
          <div className="ep-grid">
            {t.episodes.map((ep, ei) => {
              const ec = ep;
              return (
                <EpisodeCard key={ec.id} ep={ec} selected={selHas(sel, ec.id)}
                  personaLabel={selPersonaLabel(sel, ec.id) || personaDraft[ec.id] || null} isStaff={false}
                  onToggle={() => toggleSelect(ec)} onOpen={() => setDetail({ ...ec, themeId: t.id, themeName: t.name })} />
              );
            })}
          </div>
        </div>
      ))}

      {/* DETAIL DRAWER — no onEdit affordance */}
      {detail && (
        <EpisodeDetailDrawer episode={detail} project={project} user={user}
          selectedPersonaLabel={selPersonaLabel(sel, detail.id)} isSelected={selHas(sel, detail.id)}
          draftPersonaLabel={personaDraft[detail.id] || null}
          onClose={() => setDetail(null)} onToggleSelect={() => toggleSelect(detail)}
          onAssignPersona={(label) => assignPersona(detail.id, label)}
          onEdit={null} navigate={navigate} />
      )}

      {/* SUBMIT MODAL */}
      {submitOpen && (
        <SubmitModal sel={sel} episodes={epById} toast={toast}
          submitting={submitting} reSubmit={false}
          onAssign={assignPersona} onClose={() => setSubmitOpen(false)} onConfirm={doSubmit} />
      )}
    </div>
  );
}

/* ============================================================
   CONTENT DETAIL  (month tabs / themes / pool / edit modals —
   extracted from EpisodeAdmin so EpisodeAdmin stays a thin shell)
   ============================================================ */
function ContentDetail({ projectId, user, toast }) {
  const { loading, error, data, reload } = useAsync(
    () => projectId ? window.api.getContent(projectId) : Promise.resolve(null),
    [projectId]
  );

  // month tab state — keyed by month.id; guard so tab switching works after reload
  const [curMonthId, setCurMonthId] = useSE(null);
  useEE(() => {
    if (data && (!curMonthId || !data.months.some((m) => m.id === curMonthId))) {
      setCurMonthId(data.months[0]?.id ?? null);
    }
  }, [data]);

  const [editEp, setEditEp] = useSE(null);         // {themeId, episode} — edit an already-placed card
  const [pickTheme, setPickTheme] = useSE(null);   // {theme} — library picker target
  const [editTheme, setEditTheme] = useSE(null);   // {theme|null, defaultMonthId?}
  const [editMonth, setEditMonth] = useSE(null);   // {month|null} for month create/edit
  const [confirm, setConfirm] = useSE(null);
  const [importRef, setImportRef] = useSE(null);   // file input ref

  /* CNX mutations */
  // Editing a placed card edits the shared library card (reflected everywhere it's linked).
  const saveEpisode = async (themeId, body) => {
    await window.api.updateEpisode(body.id, {
      title: body.title, description: body.description,
      tension: body.tension, products: body.products, voc: body.voc, rtb: body.rtb, output: body.output,
      personas: body.personas,
    });
    setEditEp(null); toast('에피소드가 수정되었습니다'); reload();
  };
  const epStatus = async (ep, status) => { await window.api.updateEpisode(ep.id, { status }); toast(status === 'paused' ? '에피소드 일시중지됨' : '에피소드 활성화됨'); reload(); };
  // Removing a card from a theme UNLINKS it (the card stays in the library).
  const unlinkEpisode = async () => { const { themeId, episode } = confirm.ep; setConfirm(null); await window.api.removePlacement(episode.id, themeId); toast('테마에서 제거됨'); reload(); };
  const linkEpisodes = async (themeId, episodeIds) => {
    for (const eid of episodeIds) await window.api.addPlacement(eid, themeId);
    setPickTheme(null); toast(`${episodeIds.length}개 카드가 추가되었습니다`); reload();
  };
  const saveTheme = async (body) => {
    if (body.id) await window.api.updateTheme(body.id, body);
    else await window.api.createTheme(projectId, body);
    setEditTheme(null); toast(body.id ? '테마 수정됨' : '테마 추가됨'); reload();
  };
  const delTheme = async () => { const t = confirm.theme; setConfirm(null); await window.api.deleteTheme(t.id); toast('테마 삭제됨'); reload(); };

  const saveMonth = async (body) => {
    try {
      if (body.id) {
        await window.api.updateMonth(body.id, { label: body.label, maxSelections: body.maxSelections, sortOrder: body.sortOrder });
        toast('월 수정됨');
      } else {
        const created = await window.api.createMonth(projectId, { label: body.label, maxSelections: body.maxSelections, sortOrder: body.sortOrder ?? 0 });
        if (created && created.id) setCurMonthId(created.id);
        toast('월 추가됨');
      }
      setEditMonth(null); reload();
    } catch (ex) { toast('오류: ' + ex.message); }
  };
  const delMonth = async () => {
    const m = confirm.month; setConfirm(null);
    try { await window.api.deleteMonth(m.id); toast('월 삭제됨'); reload(); }
    catch (ex) { toast('오류: ' + ex.message); }
  };

  const handleImport = async (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    e.target.value = '';
    try {
      const r = await window.api.importEpisodes(file);
      toast(`${r.imported}개 생성` + (r.skipped && r.skipped.length ? `, ${r.skipped.length}개 스킵` : ''));
      reload();
    } catch (ex) { toast('오류: ' + ex.message); }
  };

  const visibleThemes = data ? data.themes.filter((t) => t.monthId === curMonthId) : [];

  return (
    <div>
      {/* Month tabs + 월 추가/편집 */}
      {!loading && !error && data && (
        <div className="flex center between wrap-flex" style={{ marginBottom: 14, gap: 8 }}>
          <div className="seg" style={{ marginBottom: 0 }}>
            {data.months.length === 0 && <span className="muted" style={{ fontSize: 12.5, padding: '0 8px' }}>월 없음 — 아래에서 추가하세요</span>}
            {data.months.map((m) => (
              <button key={m.id} className={m.id === curMonthId ? 'on red' : ''} onClick={() => setCurMonthId(m.id)}>{m.label}{m.maxSelections > 0 ? ` (최대 ${m.maxSelections})` : ''}</button>
            ))}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            {curMonthId && (() => {
              const curMonth = data.months.find((m) => m.id === curMonthId);
              return curMonth ? (
                <>
                  <button className="icon-btn" style={{ width: 28, height: 28 }} title="월 수정" onClick={() => setEditMonth({ month: curMonth })}><Icon.edit style={{ width: 14, height: 14 }} /></button>
                  <button className="icon-btn" style={{ width: 28, height: 28 }} title="월 삭제" onClick={() => setConfirm({ month: curMonth })}><Icon.trash style={{ width: 14, height: 14 }} /></button>
                </>
              ) : null;
            })()}
            <Btn size="sm" variant="ghost" icon={<Icon.plus />} onClick={() => setEditMonth({ month: null })}>월 추가</Btn>
          </div>
        </div>
      )}

      {/* CNX toolbar: add-theme + import (only when a month is selected) */}
      {!loading && !error && data && curMonthId && (
        <div className="flex between center" style={{ marginBottom: 14, gap: 8, flexWrap: 'wrap' }}>
          <span className="muted" style={{ fontSize: 12.5 }}>{visibleThemes.length}개 테마</span>
          <div className="flex center" style={{ gap: 6 }}>
            <Btn size="sm" variant="ghost" icon={<Icon.plus />} onClick={() => setEditTheme({ theme: null, defaultMonthId: curMonthId })}>테마 추가</Btn>
            <Btn size="sm" variant="ghost" onClick={() => window.api.exportEpisodeTemplate().catch((ex) => toast('오류: ' + ex.message))}>양식 다운로드</Btn>
            <Btn size="sm" variant="ghost" icon={<Icon.plus />} onClick={() => importRef && importRef.click()}>엑셀 업로드</Btn>
            <input type="file" accept=".xlsx" style={{ display: 'none' }} ref={(el) => setImportRef(el)} onChange={handleImport} />
          </div>
        </div>
      )}

      {loading && [0, 1].map((i) => (
        <div className="theme-box" key={i}><Skel w="40%" h={18} /><div className="ep-grid" style={{ marginTop: 16 }}>{[0, 1, 2].map((j) => <div key={j}><Skel h={120} r={12} /></div>)}</div></div>
      ))}
      {error && <ErrorState onRetry={reload} msg={error} />}

      {/* No month state */}
      {!loading && !error && data && data.months.length === 0 && (
        <EmptyState icon={<Icon.layers />} title="월이 없습니다"
          desc="먼저 월을 추가하고, 테마와 에피소드를 등록하세요."
          action={<Btn variant="primary" icon={<Icon.plus />} onClick={() => setEditMonth({ month: null })}>월 추가</Btn>} />
      )}

      {!loading && !error && data && curMonthId && visibleThemes.length === 0 && data.months.length > 0 && (
        <EmptyState icon={<Icon.layers />} title="등록된 테마가 없습니다"
          desc="테마를 추가하고 에피소드를 등록하세요."
          action={<Btn variant="primary" icon={<Icon.plus />} onClick={() => setEditTheme({ theme: null, defaultMonthId: curMonthId })}>테마 추가</Btn>} />
      )}

      {!loading && !error && visibleThemes.map((t, ti) => (
        <div className="theme-box" key={t.id}>
          <div className="theme-hd">
            <span className="theme-pill">Theme {String(ti + 1).padStart(2, '0')}</span>
            <div style={{ minWidth: 0 }}>
              <div className="theme-name">{t.name}</div>
              <div className="theme-sub">{t.subtitle}</div>
            </div>
            <div className="theme-actions">
              <Btn size="sm" variant="soft" icon={<Icon.plus />} onClick={() => setPickTheme({ theme: t })}>에피소드 추가</Btn>
              <Menu trigger={<button className="icon-btn"><Icon.dots /></button>}>
                <button onClick={() => setEditTheme({ theme: t })}><Icon.edit />테마 수정</button>
                <div className="menu-div" />
                <button className="danger" onClick={() => setConfirm({ theme: t })}><Icon.trash />테마 삭제</button>
              </Menu>
            </div>
          </div>
          <div className="ep-grid">
            {t.episodes.map((ep, ei) => {
              const ec = ep;
              return (
                <EpisodeCard key={ec.id} ep={ec} selected={false} isStaff={true}
                  onToggle={() => {}} onOpen={() => {}}
                  onEdit={() => setEditEp({ themeId: t.id, episode: ep })}
                  onPause={() => epStatus(ec, ec.status === 'paused' ? 'active' : 'paused')}
                  onDelete={() => setConfirm({ ep: { themeId: t.id, episode: ec } })} />
              );
            })}
            {t.episodes.length === 0 && <div className="muted" style={{ fontSize: 12.5, padding: '12px 0' }}>에피소드가 없습니다.</div>}
          </div>
        </div>
      ))}

      {/* EDIT MODALS */}
      {editEp && <EpisodeEditModal key={editEp.episode?.id || 'new'} data={editEp} onClose={() => setEditEp(null)} onSave={(body) => saveEpisode(editEp.themeId, body)} />}
      {pickTheme && (
        <LibraryPickerModal
          theme={pickTheme.theme}
          linkedIds={(pickTheme.theme.episodes || []).map((e) => e.id)}
          onClose={() => setPickTheme(null)}
          onConfirm={(ids) => linkEpisodes(pickTheme.theme.id, ids)} />
      )}
      {editTheme && (
        <ThemeEditModal
          theme={editTheme.theme}
          defaultMonthId={editTheme.defaultMonthId}
          months={data ? data.months : []}
          onClose={() => setEditTheme(null)}
          onSave={saveTheme} />
      )}
      {editMonth && <MonthEditModal month={editMonth.month} months={data ? data.months : []} onClose={() => setEditMonth(null)} onSave={saveMonth} />}
      {confirm && (
        <Modal
          title={confirm.month ? '월 삭제' : confirm.theme ? '테마 삭제' : '테마에서 제거'}
          sub={confirm.month ? confirm.month.label : confirm.theme ? confirm.theme.name : confirm.ep.episode.title}
          onClose={() => setConfirm(null)}
          footer={<><Btn onClick={() => setConfirm(null)}>취소</Btn><Btn variant="primary" icon={<Icon.trash />} onClick={confirm.month ? delMonth : confirm.theme ? delTheme : unlinkEpisode}>{confirm.ep ? '제거' : '삭제'}</Btn></>}>
          <p style={{ color: 'var(--ink-2)' }}>
            {confirm.month ? '이 월과 포함된 모든 테마가 삭제됩니다.' : confirm.theme ? '이 테마와 포함된 모든 배치가 해제됩니다.' : '이 카드를 테마에서 제거합니다. 카드는 라이브러리에 남습니다.'}
          </p>
        </Modal>
      )}
    </div>
  );
}

/* ============================================================
   LIBRARY PICKER  (① 테마에 라이브러리 카드를 링크로 추가)
   ============================================================ */
function LibraryPickerModal({ theme, linkedIds, onClose, onConfirm }) {
  const { loading, error, data, reload } = useAsync(() => window.api.getEpisodes(), []);
  const [picked, setPicked] = useSE([]);   // episodeId[]
  const [busy, setBusy] = useSE(false);
  // list controls — mirror ② 카드 관리: search · product · usage
  const [q, setQ] = useSE('');
  const [product, setProduct] = useSE('');
  const [usage, setUsage] = useSE('all');   // 'all' | 'used' | 'unused'

  const available = useME(() => { const linked = new Set(linkedIds || []); return (data || []).filter((e) => !linked.has(e.id)); }, [data, linkedIds]);
  const productOptions = useME(() => cardProductOptions(available), [available]);
  const shown = useME(() => available.filter((c) => matchCard(c, { q, product, usage })), [available, q, product, usage]);

  const filtered = !!(q.trim() || product || usage !== 'all');
  const resetFilters = () => { setQ(''); setProduct(''); setUsage('all'); };
  const toggle = (id) => setPicked((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
  return (
    <Modal wide title={`라이브러리에서 카드 추가 — ${theme.name}`} sub="이미 이 테마에 배치된 카드는 제외됩니다." onClose={onClose}
      footer={<><Btn onClick={onClose}>취소</Btn><Btn variant="primary" disabled={picked.length === 0 || busy} icon={<Icon.check />} onClick={async () => { setBusy(true); try { await onConfirm(picked); } finally { setBusy(false); } }}>{picked.length > 0 ? `${picked.length}개 추가` : '추가'}</Btn></>}>
      {loading && <div className="ep-grid">{[0,1,2].map((i) => <Skel key={i} h={110} r={12} />)}</div>}
      {error && <ErrorState onRetry={reload} msg={error} />}
      {!loading && !error && available.length === 0 && (
        <EmptyState icon={<Icon.layers />} title="추가할 카드가 없습니다" desc="라이브러리의 모든 카드가 이미 이 테마에 배치되어 있거나, 라이브러리가 비어 있습니다. ② 에피소드 카드 관리에서 카드를 추가하세요." />
      )}
      {!loading && !error && available.length > 0 && (
        <>
          <CardFilterBar q={q} setQ={setQ} product={product} setProduct={setProduct} productOptions={productOptions} usage={usage} setUsage={setUsage} />
          <div className="muted" style={{ fontSize: 11.5, marginBottom: 10 }}>
            {filtered ? `${shown.length} / ${available.length}개 표시` : `${available.length}개 추가 가능`} · 선택 {picked.length}개
          </div>
          {shown.length === 0
            ? <CardFilterEmpty onReset={resetFilters} />
            : (
              <div className="ep-grid">
                {shown.map((ec) => {
                  const on = picked.includes(ec.id);
                  return (
                    <div key={ec.id} className={`ep ${on ? 'sel' : ''}`} style={{ position: 'relative', cursor: 'pointer' }} onClick={() => toggle(ec.id)}>
                      <button className="ep-check" onClick={(e) => { e.stopPropagation(); toggle(ec.id); }} aria-label="선택"><Icon.check /></button>
                      <div className="ep-ttl">{ec.title}</div>
                      {(ec.products || []).length > 0 && <div className="ep-tags">{(ec.products || []).map((p, j) => <span className="tag" key={j}>{p}</span>)}</div>}
                      <EpDescBlock text={ec.description} />
                      <PlacementInfo placements={ec.placements} />
                    </div>
                  );
                })}
              </div>
            )}
        </>
      )}
    </Modal>
  );
}

/* ============================================================
   CONTENT CARD  (single card; lazy-loads its own counts)
   ============================================================ */
function ContentCard({ project, onOpen, onEdit, onDelete }) {
  const { data } = useAsync(() => window.api.getContent(project.id), [project.id]);
  const monthsN = data ? data.months.length : null;
  const themesN = data ? data.themes.length : null;
  const epN = data ? data.themes.reduce((a, t) => a + ((t.episodes && t.episodes.length) || 0), 0) : null;
  // Use periodStart — badge is start-based (due date is optional)
  const hasPeriod = !!project.periodStart;
  const fmtDate = (d) => d ? String(d).slice(0, 10) : '';
  const periodTxt = hasPeriod
    ? `${fmtDate(project.periodStart)}${project.periodEnd ? ` – ${fmtDate(project.periodEnd)}` : ' ~ 미정'}`
    : '기간 미설정';
  const isLive = project.status === 'active';
  const Badge = ({ on, children }) => (
    <span style={{ fontSize: 11.5, fontWeight: 600, padding: '3px 9px', borderRadius: 20,
      background: on ? 'var(--accent-50, #f1f2f4)' : 'var(--line, #f1f2f4)',
      color: on ? 'var(--accent, #16181d)' : 'var(--ink-3, #9aa0a7)' }}>{children}</span>
  );
  return (
    <div role="button" tabIndex={0} onClick={onOpen} style={{ position: 'relative', textAlign: 'left', cursor: 'pointer', padding: 15,
      border: isLive ? '2px solid var(--lg-red, #c0392b)' : '1px solid var(--line, #e2e4e8)',
      borderRadius: 12,
      background: isLive ? 'var(--lg-red-50, #fdecea)' : 'var(--surface, #fff)',
      boxShadow: isLive ? '0 0 0 2px var(--lg-red-50, #fdecea)' : 'none' }}>
      <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 3, paddingRight: 32, display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 4 }}>
        {project.name}
        {isLive && <span style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--lg-red, #c0392b)', background: 'var(--lg-red-50, #fdecea)', border: '1px solid var(--lg-red, #c0392b)', padding: '2px 7px', borderRadius: 20 }}>운영가이드 노출중</span>}
      </div>
      <div style={{ fontSize: 12, color: 'var(--ink-3, #8a9098)', marginBottom: 11 }}>{periodTxt}</div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
        <Badge on={hasPeriod}>{hasPeriod ? '기간 설정됨' : '기간 미설정'}</Badge>
        <Badge on={monthsN > 0}>월 {monthsN == null ? '·' : monthsN}</Badge>
        <Badge on={themesN > 0}>테마 {themesN == null ? '·' : themesN}</Badge>
        <Badge on={epN > 0}>에피소드 {epN == null ? '·' : epN}</Badge>
      </div>
      {(onEdit || onDelete) && (
        <div style={{ position: 'absolute', top: 10, right: 10, display: 'flex', gap: 5 }}>
          {onEdit && (
            <button title="콘텐츠 수정" onClick={(e) => { e.stopPropagation(); onEdit(project); }}
              style={{ width: 28, height: 28, borderRadius: 6, border: '1px solid var(--line, #e2e4e8)', background: 'var(--surface, #fff)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink-3, #8a9098)' }}>
              <Icon.edit style={{ width: 13, height: 13 }} />
            </button>
          )}
          {onDelete && (
            <button title="콘텐츠 삭제" onClick={(e) => { e.stopPropagation(); onDelete(project); }}
              style={{ width: 28, height: 28, borderRadius: 6, border: '1px solid var(--line, #e2e4e8)', background: 'var(--surface, #fff)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink-3, #8a9098)' }}>
              <Icon.trash style={{ width: 13, height: 13 }} />
            </button>
          )}
        </div>
      )}
    </div>
  );
}

/* ============================================================
   CONTENT GRID  (project card grid + 콘텐츠 추가)
   ============================================================ */
function ContentGrid({ onOpen, toast }) {
  const projects = useAsync(() => window.api.getProjects(), []);
  const [createProject, setCreateProject] = useSE(false);
  const [editProject, setEditProject] = useSE(null);
  const [delProject, setDelProject] = useSE(null);
  const [delBusy, setDelBusy] = useSE(false);
  return (
    <div>
      {projects.loading && <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 14 }}>{[0,1,2].map((i) => <Skel key={i} h={120} r={12} />)}</div>}
      {!projects.loading && (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 14 }}>
          {(projects.data || []).map((p) => <ContentCard key={p.id} project={p} onOpen={() => onOpen(p.id)} onEdit={(p) => setEditProject(p)} onDelete={(p) => setDelProject(p)} />)}
          <button onClick={() => setCreateProject(true)} style={{ border: '1px dashed var(--line, #e2e4e8)', borderRadius: 12, background: 'transparent', color: 'var(--accent, #16181d)', fontWeight: 600, cursor: 'pointer', minHeight: 120 }}>＋ 콘텐츠 추가</button>
        </div>
      )}
      {createProject && <ProjectCreateModal onClose={() => setCreateProject(false)} onSave={async (body) => {
        try { const p = await window.api.createProject(body); await projects.reload(); setCreateProject(false); toast('콘텐츠가 추가되었습니다'); if (p && p.id) onOpen(p.id); }
        catch (ex) { toast('오류: ' + ex.message); }
      }} />}
      {editProject && <ContentEditModal project={editProject} onClose={() => setEditProject(null)} onSave={async (body) => {
        try {
          const visChanged = (editProject.status === 'active') !== body.visible;
          await window.api.updateProject(editProject.id, { name: body.name, description: body.description, periodStart: body.periodStart, periodEnd: body.periodEnd });
          await window.api.setProjectVisible(editProject.id, body.visible);
          await projects.reload(); setEditProject(null); toast('콘텐츠가 수정되었습니다');
          // 운영중(표기) 변경은 운영가이드 뷰어의 프로젝트 선택(app.jsx projQ, [user] 캐시)에
          // 영향을 주므로 하드 리프레시로 바로 반영시킨다 (토스트가 잠깐 보인 뒤 리로드).
          if (visChanged) setTimeout(() => window.location.reload(), 500);
        } catch (ex) { toast('오류: ' + ex.message); }
      }} />}
      {delProject && (
        <Modal title="콘텐츠 삭제" sub={delProject.name} onClose={() => setDelProject(null)}
          footer={<><Btn onClick={() => setDelProject(null)}>취소</Btn><Btn variant="primary" disabled={delBusy} icon={<Icon.trash />} onClick={async () => {
            setDelBusy(true);
            try { await window.api.deleteProject(delProject.id); await projects.reload(); setDelProject(null); toast('콘텐츠가 삭제되었습니다'); }
            catch (ex) { toast('오류: ' + ex.message); }
            finally { setDelBusy(false); }
          }}>삭제</Btn></>}>
          <p style={{ color: 'var(--ink-2)' }}>이 콘텐츠와 포함된 월·테마·배치가 삭제됩니다. (에피소드 카드 자체는 라이브러리에 남습니다.)</p>
        </Modal>
      )}
    </div>
  );
}

/* Where a library card is used (placements) — shown on each manager card.
   placement = {themeId, themeName, projectId, projectName, monthId, monthLabel} */
function PlacementInfo({ placements }) {
  const list = placements || [];
  // faint hairline above; compact "등록됨" label; 콘텐츠/테마 on separate label-rows
  const hdr = { fontSize: 10, fontWeight: 800, letterSpacing: '.04em', textTransform: 'uppercase' };
  return (
    <div className="ep-usage" style={{ marginTop: 8, paddingTop: 7, borderTop: '1px solid var(--line, #e7e9ee)' }}>
      {list.length === 0
        ? <div style={{ ...hdr, color: 'var(--lg-red, #C8102E)' }}>미등록</div>
        : (
          <>
            <div style={{ ...hdr, color: 'var(--cnx-blue, #1a6ec7)', marginBottom: 4 }}>등록됨{list.length > 1 ? ` · ${list.length}곳` : ''}</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {list.map((p, i) => (
                <div key={p.themeId || i} className="ep-meta" style={{ marginTop: 0, gap: 2 }}>
                  <div className="ep-meta-row"><span className="ep-meta-label">콘텐츠</span><span className="ep-meta-val">{p.projectName}</span></div>
                  <div className="ep-meta-row"><span className="ep-meta-label">테마</span><span className="ep-meta-val">{p.monthLabel ? `${p.monthLabel} ` : ''}{p.themeName}</span></div>
                </div>
              ))}
            </div>
          </>
        )}
    </div>
  );
}

/* ============================================================
   EPISODE CARD MANAGER  (② 에피소드 카드 관리 — global card pool)
   ============================================================ */
function EpisodeCardManager({ user, toast }) {
  const { loading, error, data, reload } = useAsync(() => window.api.getEpisodes(), []);
  const [editEp, setEditEp] = useSE(null);   // { episode: ep|null }
  const [confirm, setConfirm] = useSE(null);  // { episode }
  const [importRef, setImportRef] = useSE(null);

  // list controls: text search · product filter · usage filter · sort
  const [q, setQ] = useSE('');
  const [product, setProduct] = useSE('');          // '' = 전체 제품
  const [usage, setUsage] = useSE('all');           // 'all' | 'used' | 'unused'
  const [sort, setSort] = useSE('default');         // 'default' | 'title' | 'usage'

  const allCards = data || [];
  const productOptions = useME(() => cardProductOptions(allCards), [allCards]);
  const usedN = useME(() => allCards.filter((c) => placeCount(c) > 0).length, [allCards]);

  const cards = useME(() => {
    let out = allCards.filter((c) => matchCard(c, { q, product, usage }));
    if (sort === 'title') out = [...out].sort((a, b) => String(a.title).localeCompare(String(b.title), 'ko'));
    else if (sort === 'usage') out = [...out].sort((a, b) => placeCount(b) - placeCount(a));
    return out;
  }, [allCards, q, product, usage, sort]);

  const filtered = !!(q.trim() || product || usage !== 'all');
  const resetFilters = () => { setQ(''); setProduct(''); setUsage('all'); };

  const saveEpisode = async (body) => {
    await window.api.updateEpisode(body.id, { title: body.title, description: body.description, tension: body.tension, products: body.products, voc: body.voc, rtb: body.rtb, output: body.output, personas: body.personas });
    setEditEp(null); toast('에피소드가 수정되었습니다'); reload();
  };
  const createCard = async (body) => { await window.api.createEpisode(body); setEditEp(null); toast('카드가 추가되었습니다'); reload(); };
  const delEpisode = async () => { const e = confirm.episode; setConfirm(null); await window.api.deleteEpisode(e.id); toast('에피소드 삭제됨'); reload(); };
  const handleImport = async (e) => {
    const file = e.target.files && e.target.files[0]; if (!file) return; e.target.value = '';
    try { const r = await window.api.importEpisodes(file); toast(`${r.imported}개 생성` + (r.skipped && r.skipped.length ? `, ${r.skipped.length}개 스킵` : '')); reload(); }
    catch (ex) { toast('오류: ' + ex.message); }
  };

  return (
    <div>
      <div className="flex center between wrap-flex" style={{ gap: 10, marginBottom: 12 }}>
        <span className="muted" style={{ fontSize: 12.5 }}>
          {filtered ? `${cards.length} / ${allCards.length}개 카드` : `${allCards.length}개 카드`} · 글로벌 라이브러리 · 등록됨 {usedN}개
        </span>
        <div className="flex center" style={{ gap: 6 }}>
          <Btn size="sm" variant="soft" icon={<Icon.plus />} onClick={() => setEditEp({ episode: null })}>카드 추가</Btn>
          <Btn size="sm" variant="ghost" onClick={() => window.api.exportEpisodeTemplate().catch((ex) => toast('오류: ' + ex.message))}>양식 다운로드</Btn>
          <Btn size="sm" variant="ghost" icon={<Icon.plus />} onClick={() => importRef && importRef.click()}>엑셀 업로드</Btn>
          <input type="file" accept=".xlsx" style={{ display: 'none' }} ref={(el) => setImportRef(el)} onChange={handleImport} />
        </div>
      </div>

      {/* search · product · usage · sort */}
      {!loading && !error && allCards.length > 0 && (
        <CardFilterBar q={q} setQ={setQ} product={product} setProduct={setProduct} productOptions={productOptions} usage={usage} setUsage={setUsage} mb={14}>
          <select className="input" value={sort} onChange={(e) => setSort(e.target.value)} style={{ flex: '0 0 auto' }}>
            <option value="default">기본 정렬</option>
            <option value="title">제목순</option>
            <option value="usage">사용 많은순</option>
          </select>
        </CardFilterBar>
      )}

      {loading && <div className="ep-grid">{[0,1,2].map((i) => <Skel key={i} h={120} r={12} />)}</div>}
      {error && <ErrorState onRetry={reload} msg={error} />}
      {!loading && !error && allCards.length === 0 && (
        <EmptyState icon={<Icon.layers />} title="카드가 없습니다" desc="카드를 추가하거나 엑셀로 업로드하세요." action={<Btn variant="primary" icon={<Icon.plus />} onClick={() => setEditEp({ episode: null })}>카드 추가</Btn>} />
      )}
      {!loading && !error && allCards.length > 0 && cards.length === 0 && (
        <CardFilterEmpty onReset={resetFilters} />
      )}
      {!loading && !error && cards.length > 0 && (
        <div className="ep-grid">
          {cards.map((ec) => (
            <div key={ec.id} className="ep" style={{ position: 'relative' }}>
              <div className="ep-ttl">{ec.title}</div>
              {ec.description && <div className="ep-desc">{ec.description}</div>}
              <div className="ep-tags">{(ec.products || []).map((p, j) => <span className="tag" key={j}>{p}</span>)}</div>
              {ec.tension && <div className="ep-ten"><b>핵심 텐션:</b> {ec.tension}</div>}
              {(ec.voc || ec.rtb || ec.output) && (
                <div className="ep-meta">
                  {ec.voc && <div className="ep-meta-row"><span className="ep-meta-label">VOC</span><span className="ep-meta-val">{ec.voc}</span></div>}
                  {ec.rtb && <div className="ep-meta-row"><span className="ep-meta-label">RTB</span><span className="ep-meta-val">{ec.rtb}</span></div>}
                  {ec.output && <div className="ep-meta-row"><span className="ep-meta-label">아웃풋</span><span className="ep-meta-val">{ec.output}</span></div>}
                </div>
              )}
              <PlacementInfo placements={ec.placements} />
              <div className="ep-foot" style={{ marginTop: 8, display: 'flex', gap: 6 }}>
                <Btn size="sm" variant="ghost" icon={<Icon.edit />} onClick={() => setEditEp({ episode: ec })}>편집</Btn>
                <Btn size="sm" variant="ghost" icon={<Icon.trash />} onClick={() => setConfirm({ episode: ec })}>삭제</Btn>
              </div>
            </div>
          ))}
        </div>
      )}

      {editEp && <EpisodeEditModal key={editEp.episode?.id || 'new'} data={{ themeId: null, episode: editEp.episode }} onClose={() => setEditEp(null)} onSave={(body) => (body.id ? saveEpisode(body) : createCard(body))} />}
      {confirm && (
        <Modal title="에피소드 삭제" sub={confirm.episode.title} onClose={() => setConfirm(null)}
          footer={<><Btn onClick={() => setConfirm(null)}>취소</Btn><Btn variant="primary" icon={<Icon.trash />} onClick={delEpisode}>삭제</Btn></>}>
          <p style={{ color: 'var(--ink-2)' }}>이 에피소드가 삭제됩니다.</p>
        </Modal>
      )}
    </div>
  );
}

/* ============================================================
   EPISODE ADMIN  (CNX / ADMIN only — authoring: create, edit,
   delete themes & episodes, Excel import, pool placement)
   ============================================================ */
function EpisodeAdmin({ user, toast, navigate }) {
  const [view, setView] = useSE('contents'); // 'contents' | 'cards'
  const [detailId, setDetailId] = useSE(null); // null = grid, id = detail

  return (
    <div className="page-wrap">
      <div className="page-head">
        <div className="page-title">에피소드 관리</div>
        <div className="page-desc">콘텐츠의 월·테마 관리와 라이브러리 카드 배치, 글로벌 에피소드 카드 생성·수정·삭제·엑셀 업로드 — CNX/ADMIN 전용.</div>
      </div>

      <div className="seg" style={{ marginBottom: 18 }}>
        <button className={view === 'contents' ? 'on red' : ''} onClick={() => { setView('contents'); setDetailId(null); }}>① 콘텐츠 관리</button>
        <button className={view === 'cards' ? 'on red' : ''} onClick={() => setView('cards')}>② 에피소드 카드 관리</button>
      </div>

      {view === 'contents' && (detailId
        ? (
          <div>
            <button onClick={() => setDetailId(null)} style={{ background: 'none', border: 'none', color: 'var(--accent, #16181d)', cursor: 'pointer', fontSize: 13, fontWeight: 600, marginBottom: 12, padding: 0 }}>← 콘텐츠 목록</button>
            <ContentDetail key={detailId} projectId={detailId} user={user} toast={toast} />
          </div>
        )
        : <ContentGrid onOpen={(id) => setDetailId(id)} toast={toast} />
      )}

      {view === 'cards' && <EpisodeCardManager user={user} toast={toast} />}
    </div>
  );
}

// Circled number derived from an episode's position within its theme (①②③…).
// Replaces the old hand-entered `code` so numbering always tracks order.
function epNum(i) { return i < 20 ? String.fromCharCode(0x2460 + i) : `${i + 1}.`; }

function flatEpisodes(themes) { const m = {}; (themes || []).forEach((t) => t.episodes.forEach((e, i) => { m[e.id] = { ...e, code: epNum(i), themeName: t.name }; })); return m; }

/* ---------------- Month tab bar (sub-nav inside a tab) ---------------- */
function MonthTabBar({ project, month, navigate, tab, onSelect }) {
  return (
    <div className="flex center between wrap-flex" style={{ marginBottom: 18, gap: 12 }}>
      <div className="seg">
        {project.months.map((m) => (
          <button key={m.month} className={`${m.month === month ? 'on red' : ''}`}
            onClick={() => onSelect ? onSelect(m.month) : navigate({ name: 'project', projectId: project.id, tab, month: m.month })}>
            {m.label}
            {m.isSubmitted && <Icon.checkCircle style={{ width: 14, height: 14, color: 'var(--pub-green)' }} />}
          </button>
        ))}
      </div>
    </div>
  );
}

/* personas → compact chip row (only non-empty); shared across card displays */
function PersonaChips({ ep }) {
  const list = epPersonaOptions(ep);
  if (list.length === 0) return null;
  return (
    <div className="ep-personas" style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 6 }}>
      {list.map((p, i) => (
        <span key={i} className="tag" style={{ fontSize: 10.5, background: 'var(--line, #f1f2f4)', color: 'var(--ink-2, #5a6068)' }}>
          <Icon.user style={{ width: 10, height: 10, verticalAlign: '-1px', marginRight: 3 }} />{p}
        </span>
      ))}
    </div>
  );
}

/* ---------------- Episode card ---------------- */
function EpisodeCard({ ep, selected, personaLabel, isStaff, onToggle, onOpen, onEdit, onPause, onDelete }) {
  const paused = ep.status === 'paused';
  return (
    <div className={`ep ${selected ? 'sel' : ''} ${paused ? 'paused-ep' : ''}`} onClick={onOpen}>
      {!paused && (
        <button className="ep-check" onClick={(e) => { e.stopPropagation(); onToggle(); }} aria-label="선택">
          <Icon.check />
        </button>
      )}
      {paused && <span className="ep-check" style={{ borderStyle: 'dashed' }} />}
      {paused && <div className="ep-wk">일시중지</div>}
      <div className="ep-ttl">{ep.title}</div>
      {(ep.products || []).length > 0 && <div className="ep-tags">{(ep.products || []).map((p, i) => <span className="tag" key={i}>{p}</span>)}</div>}
      <EpDescBlock text={ep.description} />
      <div className="ep-foot">
        {personaLabel
          ? <span className="ep-persona-chip"><Icon.user style={{ width: 14, height: 14 }} /><span className="pl" title={personaLabel}>{personaLabel}</span></span>
          : (selected
            ? <span className="ep-persona-chip none"><Icon.user style={{ width: 14, height: 14 }} />페르소나 미지정</span>
            : <span className="ep-persona-chip none" style={{ fontSize: 11.5 }}>선택 안 됨</span>)}
        {isStaff
          ? <Menu trigger={<button className="icon-btn" style={{ width: 26, height: 26, marginLeft: 'auto' }} onClick={(e) => e.stopPropagation()}><Icon.dots /></button>}>
              <button onClick={onEdit}><Icon.edit />수정</button>
              <button onClick={onPause}>{paused ? <><Icon.play />활성화</> : <><Icon.pause />일시중지</>}</button>
              <div className="menu-div" />
              <button className="danger" onClick={onDelete}><Icon.trash />삭제</button>
            </Menu>
          : <span className="ep-detail-link">상세 <Icon.chevR style={{ width: 13, height: 13 }} /></span>}
      </div>
    </div>
  );
}

/* ---------------- Episode detail drawer (PERSONA SELECTOR) ---------------- */
function EpisodeDetailDrawer({ episode, project, user, selectedPersonaLabel, isSelected, draftPersonaLabel, onClose, onToggleSelect, onAssignPersona, onEdit, navigate }) {
  const isStaff = user.role === 'ADMIN' || user.role === 'CNX';
  const personaList = epPersonas(episode);              // [{title,description,buildup,solution}]
  const [expanded, setExpanded] = useSE(null);          // index of expanded persona (펼침 ≠ 선택)
  const [refChip, setRefChip] = useSE(null);            // product/feature chip whose references are open (레퍼런스 모달)

  // Picking a persona never selects the episode — selection goes through the
  // card / "이 에피소드 선택" button (which enforces the month cap). For a
  // not-yet-selected episode the choice is kept as a session draft and shown
  // (blue) on the card; the highlight reads from the selection or that draft.
  const activePersona = isSelected ? selectedPersonaLabel : draftPersonaLabel;

  return (
    <Drawer onClose={onClose}
      head={
        <div>
          <div className="flex between center">
            <span className="det-wk">{episode.themeName}</span>
            <button className="icon-btn" onClick={onClose}><Icon.x /></button>
          </div>
          <div className="det-title">{episode.title}</div>
          <div className="det-tags">{episode.products.map((p, i) => (
            <span className="tag" key={i} onClick={() => setRefChip(p)} style={{ cursor: 'pointer' }} title="레퍼런스 보기">{p} ›</span>
          ))}</div>
        </div>
      }
      footer={
        <div className="flex between center gap12">
          <span className="muted" style={{ fontSize: 12 }}>
            {isSelected ? (selectedPersonaLabel ? '선택됨 · 페르소나 지정 완료' : '선택됨 · 페르소나 미지정') : (draftPersonaLabel ? '선택 안 됨 · 페르소나 표시만' : '선택되지 않음')}
          </span>
          {episode.status === 'paused'
            ? <span className="pill pill-paused"><span className="dot" />일시중지된 에피소드</span>
            : (
              <div className="flex center gap8">
                {isSelected
                  ? <Btn variant="ghost" icon={<Icon.x />} onClick={onToggleSelect}>선택 해제</Btn>
                  : <Btn variant="ghost" icon={<Icon.check />} onClick={onToggleSelect}>이 에피소드 선택</Btn>}
                <Btn variant="primary" icon={<Icon.check />} onClick={onClose}>확인</Btn>
              </div>
            )}
        </div>
      }>
      {episode.description && (
        <div className="det-section">
          <div className="dsh">설명</div>
          <div style={{ fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.6 }}>{episode.description}</div>
        </div>
      )}
      <div className="det-section">
        <div className="dsh">핵심 텐션</div>
        <div className="det-tension">”{episode.tension}”</div>
      </div>
      <div className="det-section">
        <div className="dsh">콘텐츠 상세</div>
        <div className="det-field"><span className="dfl">VOC</span><span className="dfv">{episode.voc}</span></div>
        <div className="det-field"><span className="dfl">RTB</span><span className="dfv">{episode.rtb}</span></div>
        <div className="det-field"><span className="dfl">아웃풋</span><span className="dfv">{episode.output}</span></div>
      </div>

      {/* PERSONA SELECTOR — choose ONE; click chevron to EXPAND 4 fields (expand ≠ select) */}
      <div className="det-section">
        <div className="flex between center" style={{ marginBottom: 9 }}>
          <div className="dsh" style={{ margin: 0 }}>페르소나 {isSelected && !selectedPersonaLabel && <span className="qs under" style={{ fontSize: 10 }}>· 지정 필요</span>}</div>
        </div>
        <div className="persona-list">
          {personaList.map((p, i) => {
            const open = expanded === i;
            const hasDetail = p.description || p.buildup || p.solution;
            return (
              <div key={i} className={`persona-acc ${activePersona === p.title ? 'sel' : ''} ${open ? 'open' : ''}`}>
                <div className="persona-acc-head">
                  <button type="button" className="persona-pick" onClick={() => onAssignPersona(p.title)} aria-label="이 페르소나 선택">
                    <span className="radio" />
                    <span className="pname">{p.title}</span>
                  </button>
                  {hasDetail && (
                    <button type="button" className="persona-toggle" onClick={() => setExpanded(open ? null : i)} aria-label="상세 펼치기">
                      <Icon.chevD style={{ width: 16, height: 16, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
                    </button>
                  )}
                </div>
                {open && (
                  <div className="persona-acc-body">
                    {p.description && <div className="persona-field"><span className="pfl">설명</span><span className="pfv">{p.description}</span></div>}
                    {p.buildup && <div className="persona-field"><span className="pfl">빌드업</span><span className="pfv">{p.buildup}</span></div>}
                    {p.solution && <div className="persona-field"><span className="pfl">LG AI 해결책</span><span className="pfv">{p.solution}</span></div>}
                  </div>
                )}
              </div>
            );
          })}
          {personaList.length === 0 && <div className="muted" style={{ fontSize: 12.5 }}>이 에피소드에 등록된 페르소나가 없습니다.</div>}
        </div>
        {!isStaff && personaList.length > 0 && <p className="fhint">에피소드를 선택한 뒤 타깃 페르소나를 1개 지정하세요. 행을 펼치면 설명·빌드업·LG AI 해결책을 볼 수 있습니다.</p>}
      </div>

      {isStaff && onEdit && (
        <div className="det-section">
          <Btn variant="ghost" icon={<Icon.edit />} onClick={onEdit}>에피소드 내용 편집</Btn>
        </div>
      )}
      {refChip && <FeatureRefModal chip={refChip} onClose={() => setRefChip(null)} navigate={navigate} />}
    </Drawer>
  );
}

/* Feature reference popup — click a product/feature chip to see the curated note,
   related PDP products and supporting LG Newsroom articles for that feature. */
function FeatureRefModal({ chip, onClose, navigate }) {
  const [st, setSt] = useSE({ loading: true, error: null, data: null });
  useEE(() => {
    let alive = true;
    window.api.featureReferences(chip)
      .then((data) => { if (alive) setSt({ loading: false, error: null, data }); })
      .catch((e) => { if (alive) setSt({ loading: false, error: e.message || '불러오기 실패', data: null }); });
    return () => { alive = false; };
  }, [chip]);

  const d = st.data;
  const note = d && d.note;
  const hasNote = note && (note.techText || note.problemText || note.solutionText);
  const products = (d && d.products) || [];
  const articles = (d && d.articles) || [];
  const N = 12;
  const thS = { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid var(--line)', fontSize: 11, color: 'var(--ink-3)', fontWeight: 650 };
  const tdS = { padding: '6px 8px', borderBottom: '1px solid var(--line)', verticalAlign: 'top' };

  return (
    <Modal wide title={`레퍼런스 — ${chip}`}
      sub={d && d.matched ? `매칭 키워드: ${d.tag.label}` : null} onClose={onClose}
      footer={<Btn variant="primary" onClick={onClose}>닫기</Btn>}>
      <div className="muted" style={{ fontSize: 12, marginBottom: 14, lineHeight: 1.5 }}>이 콘텐츠 기능과 연관된 LG 제품·근거 기사입니다. 콘텐츠 제작 시 출처로 활용하세요.</div>
      {st.loading && <div style={{ padding: 4 }}><Skel h={16} /><Skel h={16} style={{ marginTop: 8 }} /><Skel h={16} style={{ marginTop: 8 }} /></div>}
      {st.error && <div style={{ fontSize: 13, color: 'var(--danger, #c0392b)' }}>{st.error}</div>}
      {d && !d.matched && <div className="muted" style={{ fontSize: 13 }}>이 항목과 연결된 레퍼런스가 아직 없습니다.</div>}
      {d && d.matched && (
        <>
          {hasNote && (
            <div className="det-section">
              <div className="dsh">큐레이션</div>
              <div style={{ fontSize: 14, lineHeight: 1.6 }}>
                <b>{note.techText}</b>를 통해 {note.problemText} 문제를 {note.solutionText}(으)로 해결합니다.
              </div>
            </div>
          )}
          <div className="det-section">
            <div className="dsh">연관 제품{products.length ? ` (${products.length})` : ''}</div>
            <div className="muted" style={{ fontSize: 11.5, marginBottom: 6 }}>이 기능이 적용된 실제 LG 제품입니다. 행을 누르면 제품 페이지로 이동합니다.</div>
            {products.length === 0 && <div className="muted" style={{ fontSize: 12.5 }}>없음</div>}
            {products.length > 0 && (
              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5 }}>
                <thead><tr><th style={thS}>모델명</th><th style={thS}>이름</th></tr></thead>
                <tbody>
                  {products.slice(0, N).map((p) => (
                    <tr key={p.productId} onClick={() => p.url && window.open(p.url, '_blank', 'noopener')}
                      style={{ cursor: 'pointer' }} title={p.url || ''}>
                      <td style={{ ...tdS, whiteSpace: 'nowrap', fontWeight: 600, color: 'var(--brand, #C8102E)' }}>{p.model}</td>
                      <td style={tdS}>{(p.title || '').replace(/\s*\|\s*LG\b.*$/i, '')}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
            {products.length > N && navigate && (
              <div style={{ marginTop: 8 }}>
                <a style={{ fontSize: 12.5, fontWeight: 700, cursor: 'pointer', color: 'var(--accent, #C8102E)' }}
                  onClick={() => navigate({ name: 'refs', chip, kind: 'pdp', label: d.tag.label })}>더보기 — 전체 {products.length}개 →</a>
              </div>
            )}
          </div>
          <div className="det-section">
            <div className="dsh">근거 기사 · LG 뉴스룸{articles.length ? ` (${articles.length})` : ''} · 최신순</div>
            <div className="muted" style={{ fontSize: 11.5, marginBottom: 6 }}>이 기능을 다룬 LG 뉴스룸 기사입니다. 최신순이며, 누르면 기사로 이동합니다.</div>
            {articles.length === 0 && <div className="muted" style={{ fontSize: 12.5 }}>없음</div>}
            {articles.slice(0, 10).map((a) => (
              <div key={a.pagePath} style={{ fontSize: 13, marginBottom: 4 }}>
                <a href={'https://www.lg.com' + a.pagePath.replace('/content/lge/en', '')} target="_blank" rel="noreferrer">{a.title}</a>
                {a.postedDate && <span className="muted" style={{ fontSize: 11, marginLeft: 6 }}>{a.postedDate}</span>}
              </div>
            ))}
            {articles.length > 10 && navigate && (
              <div style={{ marginTop: 8 }}>
                <a style={{ fontSize: 12.5, fontWeight: 700, cursor: 'pointer', color: 'var(--accent, #C8102E)' }}
                  onClick={() => navigate({ name: 'refs', chip, kind: 'newsroom', label: d.tag.label })}>더보기 — 전체 {articles.length}개 →</a>
              </div>
            )}
          </div>
        </>
      )}
    </Modal>
  );
}

/* Full-list page for a feature's references — reached from the modal's "더보기".
   kind='pdp' → all related products, kind='newsroom' → all source articles. */
function RefListScreen({ chip, kind, label, navigate }) {
  const [st, setSt] = useSE({ loading: true, error: null, data: null });
  useEE(() => {
    let alive = true;
    window.api.featureReferences(chip)
      .then((data) => { if (alive) setSt({ loading: false, error: null, data }); })
      .catch((e) => { if (alive) setSt({ loading: false, error: e.message || '불러오기 실패', data: null }); });
    return () => { alive = false; };
  }, [chip]);

  const d = st.data;
  const isPdp = kind === 'pdp';
  const items = d && d.matched ? (isPdp ? d.products : d.articles) : [];
  const thS = { textAlign: 'left', padding: '7px 8px', borderBottom: '1px solid var(--line)', fontSize: 11.5, color: 'var(--ink-3)', fontWeight: 650 };
  const tdS = { padding: '7px 8px', borderBottom: '1px solid var(--line)', verticalAlign: 'top' };

  return (
    <div className="page-wrap">
      <div className="flex center gap12" style={{ marginBottom: 6 }}>
        <Btn variant="ghost" icon={<Icon.x />} onClick={() => navigate({ name: 'project', tab: 'episodes' })}>돌아가기</Btn>
        <h2 style={{ fontSize: 18, fontWeight: 750, margin: 0 }}>{isPdp ? '연관 제품' : '근거 기사 · LG 뉴스룸'} 전체{items.length ? ` (${items.length})` : ''}</h2>
      </div>
      <div className="muted" style={{ fontSize: 12.5, marginBottom: 16 }}>매칭 키워드: {label || chip}{isPdp ? ' · 행을 누르면 제품 페이지로 이동합니다.' : ' · 최신순이며, 누르면 기사로 이동합니다.'}</div>

      {st.loading && <CardSkel />}
      {st.error && <div style={{ fontSize: 13, color: 'var(--danger, #c0392b)' }}>{st.error}</div>}
      {d && !d.matched && <div className="muted" style={{ fontSize: 13 }}>연결된 레퍼런스가 없습니다.</div>}

      {d && d.matched && isPdp && (
        <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
          <thead><tr><th style={thS}>모델명</th><th style={thS}>이름</th></tr></thead>
          <tbody>
            {items.map((p) => (
              <tr key={p.productId} onClick={() => p.url && window.open(p.url, '_blank', 'noopener')} style={{ cursor: 'pointer' }} title={p.url || ''}>
                <td style={{ ...tdS, whiteSpace: 'nowrap', fontWeight: 600, color: 'var(--brand, #C8102E)' }}>{p.model}</td>
                <td style={tdS}>{(p.title || '').replace(/\s*\|\s*LG\b.*$/i, '')}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}

      {d && d.matched && !isPdp && (
        <div>
          {items.map((a) => (
            <div key={a.pagePath} style={{ fontSize: 13.5, marginBottom: 9 }}>
              <a href={'https://www.lg.com' + a.pagePath.replace('/content/lge/en', '')} target="_blank" rel="noreferrer">{a.title}</a>
              {a.postedDate && <span className="muted" style={{ fontSize: 11.5, marginLeft: 8 }}>{a.postedDate}</span>}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function PersonaQuickAdd({ onClose, onSave }) {
  const [name, setName] = useSE(''); const [segment, setSegment] = useSE(''); const [description, setDescription] = useSE('');
  const [busy, setBusy] = useSE(false);
  return (
    <Modal title="페르소나 추가" onClose={onClose}
      footer={<><Btn onClick={onClose}>취소</Btn><Btn variant="blue" disabled={!name.trim() || busy} onClick={async () => { setBusy(true); await onSave({ name: name.trim(), segment, description }); }}>추가</Btn></>}>
      <div className="frow"><label className="flabel">이름</label><input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="예: 효율러 민지" /></div>
      <div className="frow"><label className="flabel">세그먼트</label><input className="input" value={segment} onChange={(e) => setSegment(e.target.value)} placeholder="예: 1인 가구 · 직장인" /></div>
      <div className="frow"><label className="flabel">설명</label><textarea className="textarea" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="페르소나 특성·니즈" /></div>
    </Modal>
  );
}

/* ---------------- Submit modal ---------------- */
function SubmitModal({ sel, episodes, toast, submitting, reSubmit, onAssign, onClose, onConfirm }) {
  const [note, setNote] = useSE('');
  const missing = sel.filter((s) => !s.personaLabel).length;
  // Episodes that COULD have a persona (options exist) but none is chosen.
  // Episodes with no registered personas can't be assigned, so they don't block.
  const needsPersona = sel.some((s) => !s.personaLabel && epPersonaOptions(episodes[s.episodeId] || {}).length > 0);
  const tryConfirm = () => {
    if (needsPersona) { toast('페르소나를 지정해주세요'); return; }
    onConfirm(note);
  };
  return (
    <Modal wide title="에피소드 제출" sub={reSubmit ? '재제출입니다. 이전 제출 이력은 보존됩니다.' : `선택한 ${sel.length}개 에피소드를 제출합니다.`} onClose={onClose}
      footer={
        <>
          <Btn onClick={onClose}>취소</Btn>
          <Btn variant="primary" icon={submitting ? <Icon.clock className="spin" /> : <Icon.send />} disabled={submitting} onClick={tryConfirm}>
            {submitting ? '제출 중…' : reSubmit ? '재제출' : '제출 확정'}
          </Btn>
        </>
      }>
      {missing > 0 && (
        <div className="tw-item lge" style={{ marginBottom: 16, display: 'flex', gap: 9, alignItems: 'center' }}>
          <Icon.alert style={{ width: 18, height: 18, color: 'var(--lg-red)', flexShrink: 0 }} />
          <span style={{ fontSize: 12.5 }}>페르소나가 지정되지 않은 에피소드 <b>{missing}개</b>가 있습니다. 아래에서 지정할 수 있습니다.</span>
        </div>
      )}
      <div>
        {sel.map((s, i) => {
          const ep = episodes[s.episodeId];
          if (!ep) return null;
          const opts = epPersonaOptions(ep);
          return (
            <div className="submit-item" key={s.episodeId}>
              <span className="si-num">{i + 1}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="si-t">{ep.title}</div>
                <div className="muted" style={{ fontSize: 11.5, marginTop: 2 }}>{ep.themeName}</div>
                <div className="si-p" style={{ marginTop: 6, display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
                  <Icon.user style={{ width: 13, height: 13, flexShrink: 0 }} />
                  {opts.length === 0
                    ? <span className="muted" style={{ fontSize: 12 }}>등록된 페르소나 없음</span>
                    : opts.map((label, j) => {
                        const on = s.personaLabel === label;
                        return (
                          <button type="button" key={j} title={label}
                            className={`prod-chip ${on ? 'on' : ''}`}
                            onClick={() => onAssign(s.episodeId, on ? null : label)}>
                            {label}
                          </button>
                        );
                      })}
                </div>
              </div>
            </div>
          );
        })}
      </div>
      <div className="frow" style={{ marginTop: 18, marginBottom: 0 }}>
        <label className="flabel">메모 (선택)</label>
        <textarea className="textarea" value={note} onChange={(e) => setNote(e.target.value)} placeholder="제출에 대한 코멘트를 남길 수 있습니다." style={{ minHeight: 60 }} />
      </div>
    </Modal>
  );
}

/* Selectable products — the Product column of the RTB sheet
   (reference/LG Product_RTB_260522_공유.xlsx, 3rd sheet). Edit here to change the list. */
const PRODUCTS = [
  'Washing Machine', 'Dryer', 'Styler', 'Robot Vacuum', 'Refrigerator',
  'Dishwasher', 'Oven', 'Air Conditioner', 'Air purifier', 'AI TV (webOS)', 'ThinQ AI',
];

/* ---------------- CNX: Episode edit ---------------- */
function EpisodeEditModal({ data, onClose, onSave }) {
  const ep = data.episode;
  // lazy initializer: 폼 상태(페르소나 3슬롯 패딩 포함)는 마운트 시 1회만 계산
  const [f, setF] = useSE(() => {
    const base = ep ? epPersonas(ep) : [];
    return {
      id: ep?.id, title: ep?.title || '', code: ep?.code || '', description: ep?.description || '',
      products: ep?.products || [], tension: ep?.tension || '', voc: ep?.voc || '', rtb: ep?.rtb || '', output: ep?.output || 'IG 캐러셀 × 6 / LinkedIn / LG.com 뉴스룸',
      personas: [0, 1, 2].map((i) => base[i] || { title: '', description: '', buildup: '', solution: '' }),
    };
  });
  const set = (k) => (e) => setF((p) => ({ ...p, [k]: e.target.value }));
  const setPersona = (i, k) => (e) => setF((p) => ({ ...p, personas: p.personas.map((pp, idx) => idx === i ? { ...pp, [k]: e.target.value } : pp) }));
  const [busy, setBusy] = useSE(false);
  const [custom, setCustom] = useSE('');
  const addCustom = () => {
    const v = custom.trim();
    if (!v) return;
    setF((s) => (s.products.includes(v) ? s : { ...s, products: [...s.products, v] }));
    setCustom('');
  };
  return (
    <Modal wide title={ep ? '에피소드 수정' : '에피소드 추가'} onClose={onClose}
      footer={<><Btn onClick={onClose}>취소</Btn><Btn variant="primary" disabled={!f.title.trim() || busy} icon={<Icon.check />} onClick={async () => { setBusy(true); await onSave({ ...f }); }}>{ep ? '저장' : '추가'}</Btn></>}>
      <div className="frow"><label className="flabel">제목</label><input className="input" value={f.title} onChange={set('title')} /></div>
      <div className="frow"><label className="flabel">설명</label><textarea className="input" rows={2} value={f.description} onChange={set('description')} placeholder="에피소드 요약 설명" /></div>
      <div className="frow">
        <label className="flabel">제품 태그</label>
        <div className="prod-pick">
          {[...PRODUCTS, ...f.products.filter((p) => !PRODUCTS.includes(p))].map((p) => {
            const on = f.products.includes(p);
            return (
              <button type="button" key={p} className={`prod-chip ${on ? 'on' : ''}`}
                onClick={() => setF((s) => ({ ...s, products: on ? s.products.filter((x) => x !== p) : [...s.products, p] }))}>
                {p}
              </button>
            );
          })}
        </div>
        <div className="prod-add">
          <input className="input" value={custom} onChange={(e) => setCustom(e.target.value)}
            placeholder="직접 입력 (목록에 없는 제품)"
            onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addCustom(); } }} />
          <Btn size="sm" variant="soft" icon={<Icon.plus />} disabled={!custom.trim()} onClick={addCustom}>추가</Btn>
        </div>
      </div>
      <div className="frow"><label className="flabel">핵심 텐션</label><input className="input" value={f.tension} onChange={set('tension')} /></div>
      <div className="frow"><label className="flabel">VOC</label><textarea className="textarea" value={f.voc} onChange={set('voc')} /></div>
      <div className="frow"><label className="flabel">RTB</label><input className="input" value={f.rtb} onChange={set('rtb')} /></div>
      <div className="frow"><label className="flabel">아웃풋</label><input className="input" value={f.output} onChange={set('output')} /></div>
      {f.personas.map((pp, i) => (
        <div key={i} className="persona-form-block">
          <div className="persona-form-head">페르소나 {i + 1}</div>
          <div className="frow"><label className="flabel">제목</label><input className="input" value={pp.title} onChange={setPersona(i, 'title')} placeholder="예: 알레르기 있는 아이 키우는 엄마" /></div>
          <div className="frow"><label className="flabel">설명</label><textarea className="input" rows={2} value={pp.description} onChange={setPersona(i, 'description')} placeholder="이 페르소나는 누구인가" /></div>
          <div className="frow"><label className="flabel">빌드업</label><textarea className="input" rows={2} value={pp.buildup} onChange={setPersona(i, 'buildup')} placeholder="이 페르소나의 맥락·긴장 전개" /></div>
          <div className="frow" style={{ marginBottom: 0 }}><label className="flabel">LG AI 해결책</label><textarea className="input" rows={2} value={pp.solution} onChange={setPersona(i, 'solution')} placeholder="LG AI가 어떻게 해결하는가" /></div>
        </div>
      ))}
    </Modal>
  );
}

/* ---------------- CNX: Theme edit (monthId-based) ---------------- */
function ThemeEditModal({ theme, defaultMonthId, months, onClose, onSave }) {
  const [name, setName] = useSE(theme?.name || '');
  const [subtitle, setSubtitle] = useSE(theme?.subtitle || '');
  const [monthId, setMonthId] = useSE(theme?.monthId || defaultMonthId || '');
  const [busy, setBusy] = useSE(false);
  return (
    <Modal title={theme ? '테마 수정' : '테마 추가'} onClose={onClose}
      footer={<><Btn onClick={onClose}>취소</Btn><Btn variant="primary" disabled={!name.trim() || busy} icon={<Icon.check />} onClick={async () => { setBusy(true); await onSave({ id: theme?.id, name: name.trim(), subtitle, monthId: monthId || null }); }}>{theme ? '저장' : '추가'}</Btn></>}>
      <div className="frow"><label className="flabel">테마명</label><input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="예: 에코라이프" /></div>
      <div className="frow"><label className="flabel">부제</label><input className="input" value={subtitle} onChange={(e) => setSubtitle(e.target.value)} placeholder="테마 설명" /></div>
      {months && months.length > 0 && (
        <div className="frow" style={{ marginBottom: 0 }}>
          <label className="flabel">월</label>
          <select className="select-sm" style={{ width: '100%', padding: '7px 12px' }} value={monthId} onChange={(e) => setMonthId(e.target.value)}>
            <option value="">월 선택…</option>
            {months.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
          </select>
        </div>
      )}
    </Modal>
  );
}

/* ---------------- CNX: Month create/edit (1~12 click picker) ---------------- */
function MonthEditModal({ month, months, onClose, onSave }) {
  // Derive the currently-edited month number from its label/sortOrder (e.g. "6월" → 6).
  const numFromMonth = (m) => {
    if (!m) return null;
    const mm = /(\d+)/.exec(m.label || '');
    return mm ? Number(mm[1]) : (m.sortOrder || null);
  };
  const [picked, setPicked] = useSE(numFromMonth(month));
  const [maxSel, setMaxSel] = useSE(month?.maxSelections != null ? String(month.maxSelections) : '0');
  const [busy, setBusy] = useSE(false);
  // months already created (exclude the one being edited) → disabled in picker.
  const usedNums = new Set((months || [])
    .filter((m) => !month || m.id !== month.id)
    .map((m) => numFromMonth(m))
    .filter((n) => n != null));
  return (
    <Modal title={month ? '월 수정' : '월 추가'} onClose={onClose}
      footer={<><Btn onClick={onClose}>취소</Btn><Btn variant="primary" disabled={!picked || busy} icon={<Icon.check />} onClick={async () => { setBusy(true); await onSave({ id: month?.id, label: `${picked}월`, maxSelections: Number(maxSel) || 0, sortOrder: picked }); }}>{month ? '저장' : '추가'}</Btn></>}>
      <div className="frow">
        <label className="flabel">월 선택</label>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: 6 }}>
          {Array.from({ length: 12 }, (_, i) => i + 1).map((n) => {
            const used = usedNums.has(n);
            const on = picked === n;
            return (
              <button type="button" key={n} disabled={used}
                onClick={() => setPicked(n)}
                title={used ? '이미 추가된 월입니다' : ''}
                style={{
                  padding: '8px 0', borderRadius: 8, fontWeight: 700, fontSize: 13,
                  cursor: used ? 'not-allowed' : 'pointer',
                  border: on ? '2px solid var(--accent, #16181d)' : '1px solid var(--line, #e2e4e8)',
                  background: used ? 'var(--line, #f1f2f4)' : on ? 'var(--accent-50, #f1f2f4)' : 'var(--surface, #fff)',
                  color: used ? 'var(--ink-3, #b0b6bd)' : on ? 'var(--accent, #16181d)' : 'var(--ink-1, #2a2f36)',
                  opacity: used ? 0.6 : 1,
                }}>
                {n}월
              </button>
            );
          })}
        </div>
        <div className="fhint">이미 추가된 월은 선택할 수 없습니다.</div>
      </div>
      <div className="frow" style={{ marginBottom: 0 }}>
        <label className="flabel">최대 선택 수 (n)</label>
        <input className="input" type="number" min="0" value={maxSel} onChange={(e) => setMaxSel(e.target.value)} placeholder="0 = 제한 없음" />
        <div className="fhint">0이면 선택 제한 없음. LGE가 이 달에 선택할 수 있는 최대 에피소드 수.</div>
      </div>
    </Modal>
  );
}

/* ---------------- CNX: 콘텐츠(Project) 수정 ---------------- */
function ContentEditModal({ project, onClose, onSave }) {
  const [name, setName] = useSE(project.name || '');
  const [description, setDescription] = useSE(project.description || '');
  const [periodStart, setPeriodStart] = useSE((project.periodStart || '').slice(0, 10));
  const [periodEnd, setPeriodEnd] = useSE((project.periodEnd || '').slice(0, 10));
  const [visible, setVisible] = useSE(project.status === 'active');
  const [busy, setBusy] = useSE(false);
  return (
    <Modal title="콘텐츠 수정" onClose={onClose}
      footer={<><Btn onClick={onClose}>취소</Btn><Btn variant="primary" disabled={!name.trim() || !periodStart || busy} icon={<Icon.check />}
        onClick={async () => { setBusy(true); try { await onSave({ name: name.trim(), description, periodStart, periodEnd, visible }); } finally { setBusy(false); } }}>저장</Btn></>}>
      <div className="frow"><label className="flabel">콘텐츠명</label><input className="input" value={name} onChange={(e) => setName(e.target.value)} /></div>
      <div className="frow"><label className="flabel">설명 (선택)</label><input className="input" value={description} onChange={(e) => setDescription(e.target.value)} /></div>
      <div className="frow"><label className="flabel">시작일</label><input className="input" type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} /></div>
      <div className="frow"><label className="flabel">종료일 (선택)</label><input className="input" type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} /></div>
      <div className="frow" style={{ marginBottom: 0, display: 'flex', alignItems: 'center', gap: 10 }}>
        <label className="flabel" style={{ margin: 0 }}>운영가이드 표기</label>
        <button type="button" onClick={() => setVisible((v) => !v)} style={{ cursor: 'pointer', padding: '5px 12px', borderRadius: 20, border: '1px solid var(--line)', fontWeight: 700, fontSize: 12.5, background: visible ? 'var(--lg-red-50, #fdecea)' : 'var(--surface, #fff)', color: visible ? 'var(--lg-red, #c0392b)' : 'var(--ink-3, #8a9098)' }}>{visible ? 'ON · 노출중' : 'OFF · 숨김'}</button>
        <span className="muted" style={{ fontSize: 11.5 }}>ON이면 LGE 운영가이드에 노출됩니다</span>
      </div>
    </Modal>
  );
}

/* ---------------- CNX: 콘텐츠(Project) 추가 ---------------- */
function ProjectCreateModal({ onClose, onSave }) {
  const [name, setName] = useSE('');
  const [description, setDescription] = useSE('');
  const [periodStart, setPeriodStart] = useSE('');
  const [periodEnd, setPeriodEnd] = useSE('');
  const [busy, setBusy] = useSE(false);
  return (
    <Modal title="콘텐츠 추가" onClose={onClose}
      footer={<><Btn onClick={onClose}>취소</Btn><Btn variant="primary" disabled={!name.trim() || busy} icon={<Icon.check />} onClick={async () => { setBusy(true); await onSave({ name: name.trim(), description, periodStart: periodStart || undefined, periodEnd: periodEnd || undefined }); }}>추가</Btn></>}>
      <div className="frow"><label className="flabel">프로젝트명</label><input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="예: LG AI 365 · 2026 하반기" /></div>
      <div className="frow"><label className="flabel">설명 (선택)</label><input className="input" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="간단한 설명" /></div>
      <div className="frow"><label className="flabel">기간 시작</label><input className="input" type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} /></div>
      <div className="frow" style={{ marginBottom: 0 }}><label className="flabel">기간 종료 (선택)</label><input className="input" type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} /></div>
    </Modal>
  );
}

/* ============================================================
   SUBMIT HISTORY (tab)
   ============================================================ */
function SubmitHistory({ project, user, navigate }) {
  // Items are now self-describing (enriched by backend): no client-side episode resolution.
  const { loading, error, data, reload } = useAsync(
    () => window.api.getSubmissions(project.id).then((subs) => ({ subs })),
    [project.id]
  );

  const [detail, setDetail] = useSE(null);   // clicked submission

  return (
    <div className="page-wrap">
      <div style={{ marginBottom: 14 }}>
        <Btn size="sm" variant="ghost" icon={<Icon.chevL />} onClick={() => navigate({ name: 'project', projectId: project.id, tab: 'episodes' })}>에피소드 선택으로</Btn>
      </div>
      <div className="page-head"><div className="page-title" style={{ fontSize: 19 }}>제출 히스토리</div>
        <div className="page-desc">누가 · 언제 · 어떤 테마의 어떤 에피소드를 제출했는지 시간순으로 보여줍니다. 재제출 시 이전 이력은 보존됩니다.</div>
      </div>

      {loading && <div className="card" style={{ padding: 24 }}>{[0, 1].map((i) => <div key={i} style={{ marginBottom: 16 }}><Skel w="30%" /><Skel w="60%" h={12} style={{ marginTop: 8 }} /></div>)}</div>}
      {error && <ErrorState onRetry={reload} msg={error} />}
      {!loading && !error && data.subs.length === 0 && (
        <EmptyState icon={<Icon.clock />} title="제출 이력이 없습니다" desc="에피소드 탭에서 선택을 완료하고 제출하면 여기에 기록됩니다."
          action={<Btn variant="primary" onClick={() => navigate({ name: 'project', projectId: project.id, tab: 'episodes' })}>에피소드 선택하기</Btn>} />
      )}
      {!loading && !error && data.subs.length > 0 && (
        <div className="log-list">
          {data.subs.map((s, i) => {
            const first = s.items[0] || {};
            const firstTitle = first.episodeTitle || (first.episodeId ? '(삭제된 에피소드)' : '');
            return (
              <button className="log-row" key={s.id} onClick={() => setDetail(s)}>
                <Avatar name={s.actor} color={s.role === 'CNX' ? '#1a6ec7' : '#C8102E'} size={36} />
                <div className="lr-id">
                  <div className="lr-who">
                    <span className="who">{s.actor}</span><RoleBadge role={s.role} />
                    {i === 0 && <span className="pill pill-active" style={{ fontSize: 10 }}><span className="dot" />최신</span>}
                  </div>
                  <div className="log-when">{fmtDateTime(s.at)}</div>
                  {firstTitle && <div className="lr-preview">{firstTitle}{s.items.length > 1 ? ` 외 ${s.items.length - 1}건` : ''}</div>}
                </div>
                <span className="sub-count">에피소드 {s.items.length}개</span>
                <Icon.chevR className="lr-chev" style={{ width: 18, height: 18 }} />
              </button>
            );
          })}
        </div>
      )}

      {detail && (() => {
        const s = detail;
        const personaN = s.items.filter((it) => it.personaName).length;
        return (
          <Drawer onClose={() => setDetail(null)}
            head={
              <div>
                <div className="flex between center">
                  <span className="det-wk">제출 상세</span>
                  <button className="icon-btn" onClick={() => setDetail(null)}><Icon.x /></button>
                </div>
                <div className="log-dh-who" style={{ marginTop: 10 }}>
                  <Avatar name={s.actor} color={s.role === 'CNX' ? '#1a6ec7' : '#C8102E'} size={36} />
                  <div><span className="who">{s.actor}</span> <RoleBadge role={s.role} /></div>
                </div>
                <div className="log-dh-when">{fmtDateTime(s.at)}</div>
              </div>
            }
            footer={
              <span className="muted" style={{ fontSize: 12.5 }}>총 에피소드 {s.items.length}개 · 페르소나 지정 {personaN}개</span>
            }>
            {s.note && (
              <div className="log-note">
                <div className="ln-label">소비자 메시지</div>
                <div className="ln-quote">“{s.note}”</div>
              </div>
            )}
            <div className="log-content-title"><Icon.layers style={{ width: 16, height: 16 }} />{project.name}</div>
            {groupByMonthTheme(s.items).map(([key, rows]) => (
              <div className="log-group" key={key}>
                <div className="log-group-head"><Icon.layers style={{ width: 13, height: 13 }} />{key}</div>
                {rows.map((it, ri) => <EpisodeDetailBlock key={(it.episodeId || '') + ri} it={it} />)}
              </div>
            ))}
          </Drawer>
        );
      })()}
    </div>
  );
}

// Group enriched submission items by `${monthLabel} · ${themeName}` preserving order.
function groupByMonthTheme(items) {
  const groups = []; const idx = {};
  (items || []).forEach((it) => {
    const key = `${it.monthLabel || '월 미지정'} · ${it.themeName || '기타'}`;
    if (idx[key] === undefined) { idx[key] = groups.length; groups.push([key, []]); }
    groups[idx[key]][1].push(it);
  });
  return groups;
}

// One episode rendered as a clear block: bold title + blue persona chip + compact detail.
// Shared visual contract with the EpisodeLog drawer in screens-admin.jsx.
function EpisodeDetailBlock({ it }) {
  const title = it.episodeTitle || '(삭제된 에피소드)';
  const products = Array.isArray(it.products) ? it.products : (it.products ? [it.products] : []);
  const hasDetail = it.tension || products.length || it.voc || it.rtb || it.output;
  return (
    <div className="log-ep-block">
      <div className="log-ep-head">
        <Icon.checkCircle style={{ width: 15, height: 15, color: 'var(--pub-green)', flexShrink: 0 }} />
        <span className="lel-t">{title}</span>
        {it.personaName
          ? <span className="log-ep-chip"><Icon.user style={{ width: 11, height: 11 }} />{it.personaName}</span>
          : <span className="log-ep-chip none">페르소나 미지정</span>}
      </div>
      {hasDetail && (
        <div className="ep-meta log-ep-meta">
          {it.tension && <div className="ep-meta-row"><span className="ep-meta-label">핵심텐션</span><span className="ep-meta-val">{it.tension}</span></div>}
          {products.length > 0 && (
            <div className="ep-meta-row"><span className="ep-meta-label">제품</span>
              <span className="ep-meta-val ep-tags">{products.map((p, j) => <span className="tag" key={j}>{p}</span>)}</span>
            </div>
          )}
          {it.voc && <div className="ep-meta-row"><span className="ep-meta-label">VOC</span><span className="ep-meta-val">{it.voc}</span></div>}
          {it.rtb && <div className="ep-meta-row"><span className="ep-meta-label">RTB</span><span className="ep-meta-val">{it.rtb}</span></div>}
          {it.output && <div className="ep-meta-row"><span className="ep-meta-label">아웃풋</span><span className="ep-meta-val">{it.output}</span></div>}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { EpisodeBrowse, EpisodeAdmin, SubmitHistory, MonthTabBar, EpisodeDetailBlock, groupByMonthTheme, RefListScreen });
