// 약관·정책 관리 — 실서버 /admin/terms (V18, 계약 yumyumpickup#173)
//
// 흐름: 문서 종류 → 판 목록(개정일·시행일·상태) → 작성 중 판 편집(제목·마크다운 + 미리보기) → 게시(개정일·시행일, 확인창).
// 게시된 판은 고칠 수 없습니다. 고치려면 "새 판 만들기"로 복사해 작성 중 판을 만듭니다(종류마다 작성 중 판은 1개).
// 시행 전(시행 예정) 판은 게시 취소로 작성 중으로 되돌릴 수 있습니다. 작성 중 판은 삭제할 수 있습니다.
// 판 상태(작성 중·시행 예정·시행 중·지난 판)는 서버가 요청마다 한국 날짜로 판정합니다 — 화면은 그대로 보여 줍니다.
// 공개 페이지는 파트너스가 그립니다. 시행 중·시행 예정 판은 "파트너스에서 보기"로 엽니다.
//
// 검증은 TEST_DOC(검증용) 종류로만 합니다 — 실제 약관 판은 게시·취소·삭제하지 마세요.

// TERMS_BANNER_RULE — 본문에 판 번호를 적지 않습니다 (사용자 결정 2026-09-30)
//
// 고객 약관 7종의 본문 첫 줄에 이런 배너가 있었습니다:
//   > **이 문서는 확정 전 초안이며 변경될 수 있습니다.** (제1판 · 확정본은 새 판으로 게시합니다)
// 판 번호가 **본문 안에** 박혀 있어서, 새 판을 낼 때 아무도 같이 고치지 않았습니다. 그래서 14개 판
// 전부가 "제1판"이라고 말했고, 그중 7개가 거짓이었습니다(USER_TERMS 3판이 "제1판"이라고 하는 식).
// 손님이 공개 API 로 그 거짓 배너를 그대로 받고 있었습니다.
//
// 게시된 판은 고칠 수 없으므로(서버 3085, 설계대로입니다) 되돌릴 방법이 없었습니다. 새 판을 내는
// 것이 유일한 길이었고, 그러면 배너를 "제3판"으로 맞추려다 실제로는 4판이 되는 자기모순이 됩니다.
// 그래서 **판 번호를 빼는 문구로** 새 판을 냈습니다:
//   > **이 문서는 확정 전 초안이며 변경될 수 있습니다.** (확정본은 새 판으로 게시합니다)
//
// 판 번호는 서버 `version` 필드가 들고 있고 이 화면(termsNo)과 파트너스가 그것으로 그립니다.
// 본문에서 빼면 **다시 어긋날 자리가 사라집니다.** 그게 이 선택의 요점입니다.
//
// 🔴 아직 옛 배너를 쓰는 판이 남아 있습니다 — USER_COLLECT · PHONE_VERIFY · USER_EFT 는 1판이라
//    "제1판"이 아직 참입니다. 낱말 두 개 때문에 법적 문서의 판 이력에 판을 얹지 않기로 했습니다.
//    **그 3종에 다음 판을 낼 때는 판 번호 없는 위 문구로 고쳐 주세요.** 지난 판(USER_TERMS 2판 등)은
//    법적으로 그대로 보여 줘야 하므로 건드리지 않습니다.


// 서버 필드 읽기 — 한 곳에 모았습니다.
const TF = {
  typeCode:  (t) => t.code,
  typeName:  (t) => t.title,
  audience:  (t) => t.audience,
  id:        (v) => v.versionId,
  no:        (v) => v.version,
  title:     (v) => v.title || '',
  status:    (v) => v.status,
  revised:   (v) => v.revisedAt,
  effective: (v) => v.effectiveAt,
  published: (v) => v.publishedAt,
  aid:       (v) => v.publishedBy,
  updated:   (v) => v.updateDate,
  body:      (v) => v.body || '',
};
const termsNo = (v) => (v && TF.no(v) != null ? `${TF.no(v)}판` : '');

// 판 상태별로 할 수 있는 일(계약 #173).
const TERMS_CAN = {
  edit:      (st) => st === 'DRAFT',
  publish:   (st) => st === 'DRAFT',
  remove:    (st) => st === 'DRAFT',
  unpublish: (st) => st === 'UPCOMING',
  view:      (st) => st === 'CURRENT' || st === 'UPCOMING',
};
const TERMS_TITLE_MAX = 100;

// 파트너스 공개 주소(partners 확정 2026-09-23): /terms/{종류} — 종류는 backend 코드를 소문자·하이픈으로
// (MERCHANT_FEE → merchant-fee). 특정 판은 /terms/{종류}/{판번호}. 시행 예정 판도 이 주소로 열리고 "시행 예정"이
// 붙습니다. 초안(미게시)은 공개 페이지에서 열리지 않습니다.
const termsSlug = (code) => String(code || '').toLowerCase().replace(/_/g, '-');
const termsPublicUrl = (type, versionNo) =>
  `${window.YUMYUM_CONFIG.partnersBaseUrl}/terms/${encodeURIComponent(termsSlug(type))}${versionNo != null ? `/${encodeURIComponent(versionNo)}` : ''}`;

// 미리보기 — 파트너스 공개 페이지와 **같은 렌더러·옵션**으로 그립니다(partners 확인 2026-09-23):
// marked@12.0.2 (gfm: true, breaks: false) → DOMPurify@3.1.6 (style·form·input 태그와 style 속성 금지).
// 지원: 제목·문단·번호/글머리 목록(중첩)·굵게·기울임·링크·GFM 표·인용·가로줄. 원시 HTML 은 걸러집니다.
// 라이브러리를 못 불러오면(CDN 실패) 원문을 이스케이프해 그대로 보여 줍니다 — 모양이 다르다는 안내와 함께.
const escapeHtml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const renderMd = (src) => {
  if (!window.marked || !window.DOMPurify) return { html: `<pre style="white-space:pre-wrap">${escapeHtml(src)}</pre>`, fallback: true };
  const raw = window.marked.parse(String(src || ''), { gfm: true, breaks: false });
  return { html: window.DOMPurify.sanitize(raw, { FORBID_TAGS: ['style', 'form', 'input'], FORBID_ATTR: ['style'] }), fallback: false };
};

const todayKst = () => new Date(Date.now() + 9 * 3600e3).toISOString().slice(0, 10);
// 시행일 하한: 오늘(KST)과 이미 게시된 판들의 가장 늦은 시행일 중 늦은 쪽(같은 날은 됩니다). 서버도 3087 로 막습니다.
const termsMinEffective = (rows) => rows
  .filter((r) => TF.status(r) !== 'DRAFT' && TF.effective(r))
  .map((r) => ymd(TF.effective(r)) || TF.effective(r))
  .reduce((a, b) => (b > a ? b : a), todayKst());

const TermsAdmin = () => {
  const types = useLive(() => api.live.terms.types(), []);
  const typeList = types.data || [];
  const [code, setCode] = React.useState(null);
  React.useEffect(() => { if (!code && typeList.length) setCode(TF.typeCode(typeList[0])); }, [typeList.length]);
  const versions = useLive(() => (code ? api.live.terms.versions(code) : Promise.resolve([])), [code]);
  const rows = versions.data || [];
  const [openId, setOpenId] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const hasDraft = rows.some((v) => TF.status(v) === 'DRAFT');
  const current = rows.find((v) => TF.status(v) === 'CURRENT');
  const latestPublished = rows.find((v) => TF.status(v) !== 'DRAFT');
  const copySource = current || latestPublished;
  const typeName = (c) => { const t = typeList.find((x) => TF.typeCode(x) === c); return t ? TF.typeName(t) : c; };
  const reloadAll = () => { versions.reload(); types.reload(); };

  const createVersion = async () => {
    if (!window.confirm(copySource
      ? `${termsNo(copySource)}(${(TERMS_STATUS[TF.status(copySource)] || {}).label})을 복사해 새 판을 만들까요?\n새 판은 "작성 중"으로 만들어지고, 게시하기 전에는 공개되지 않습니다.`
      : '빈 새 판을 만들까요? 게시하기 전에는 공개되지 않습니다.')) return;
    setBusy(true); setError('');
    try {
      const made = await api.live.terms.create(code, copySource ? TF.id(copySource) : null);
      reloadAll();
      if (made && made.versionId != null) setOpenId(made.versionId);
    } catch (e) { setError(`새 판을 만들지 못했습니다: ${errorText(e)}`); } finally { setBusy(false); }
  };

  // 종류가 17개라 탭 대신 대상별로 묶은 선택 상자를 씁니다.
  const groups = typeList.reduce((acc, t) => { const a = TF.audience(t) || 'COMMON'; (acc[a] = acc[a] || []).push(t); return acc; }, {});
  const selected = typeList.find((t) => TF.typeCode(t) === code);

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1>서비스 관리 · 약관·정책</h1>
          <div className="sub">약관과 개인정보처리방침을 판 단위로 고치고 게시합니다. 게시된 판은 고칠 수 없습니다</div>
        </div>
        <div className="actions">
          {code && (
            <button className="btn btn-primary" onClick={createVersion} disabled={busy || hasDraft || versions.loading}
              title={hasDraft ? '작성 중인 판이 이미 있습니다' : ''}>
              <Icon name="plus" size={14}/>새 판 만들기
            </button>
          )}
        </div>
      </div>
      {error && <div style={{ marginBottom:12 }}><Notice tone="danger">{error}</Notice></div>}

      <LiveBlock state={types} empty={typeList.length === 0} emptyTitle="문서 종류가 없습니다">
        <div className="table-wrap">
          <div className="table-toolbar" style={{ flexWrap:'wrap', gap:12 }}>
            <select className="select" style={{ minWidth:260 }} value={code || ''} aria-label="문서 종류"
              onChange={(e) => { setCode(e.target.value); setOpenId(null); setError(''); }}>
              {Object.keys(groups).map((a) => (
                <optgroup key={a} label={TERMS_AUDIENCE[a] || a}>
                  {groups[a].map((t) => <option key={TF.typeCode(t)} value={TF.typeCode(t)}>{TF.typeName(t)}</option>)}
                </optgroup>
              ))}
            </select>
            {selected && (
              <span style={{ fontSize:13, color:'var(--text-3)' }}>
                {selected.currentVersion != null ? `시행 중 ${selected.currentVersion}판 (${ymd(selected.effectiveAt) || selected.effectiveAt} 시행)` : '시행 중인 판 없음'}
                {selected.upcomingVersion != null ? ` · 시행 예정 ${selected.upcomingVersion}판` : ''}
              </span>
            )}
          </div>
          {hasDraft && <div style={{ padding:'0 16px 10px', fontSize:12.5, color:'var(--text-3)' }}>작성 중인 판이 있어 새 판을 더 만들 수 없습니다. 그 판을 고쳐 게시하거나 삭제하세요.</div>}
          <LiveBlock state={versions} empty={rows.length === 0} emptyTitle="아직 판이 없습니다">
            <table className="table" style={{ opacity: versions.loading ? 0.45 : 1 }}>
              <thead><tr><th>판</th><th>제목</th><th>상태</th><th>개정일</th><th>시행일</th><th>게시</th><th></th></tr></thead>
              <tbody>
                {rows.map((v) => (
                  <tr key={TF.id(v)} onClick={() => setOpenId(TF.id(v))} style={{ cursor:'pointer' }}>
                    <td className="t-mono">{termsNo(v) || '—'}</td>
                    <td>{TF.title(v) || '—'}</td>
                    <td><StatusBadge map={TERMS_STATUS} value={TF.status(v)}/></td>
                    <td>{TF.revised(v) ? ymd(TF.revised(v)) || TF.revised(v) : '—'}</td>
                    <td>{TF.effective(v) ? ymd(TF.effective(v)) || TF.effective(v) : '—'}</td>
                    <td style={{ color:'var(--text-3)', fontSize:12.5 }}>{TF.published(v) ? `${fmtDT(TF.published(v))}${TF.aid(v) ? ` · 어드민 #${TF.aid(v)}` : ''}` : '—'}</td>
                    <td onClick={(e) => e.stopPropagation()}>
                      {TERMS_CAN.view(TF.status(v)) && (
                        <a className="btn btn-sm btn-ghost" href={termsPublicUrl(code, TF.no(v))} target="_blank" rel="noopener noreferrer">파트너스에서 보기</a>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </LiveBlock>
        </div>
      </LiveBlock>

      {openId != null && (
        <TermsVersionDrawer key={openId} versionId={openId} code={code} typeName={typeName(code)}
          minEffective={termsMinEffective(rows)} otherDraft={rows.some((r) => TF.status(r) === 'DRAFT' && TF.id(r) !== openId)}
          onClose={() => setOpenId(null)} onChanged={reloadAll}/>
      )}
    </div>
  );
};

const TermsVersionDrawer = ({ versionId, code, typeName, minEffective, otherDraft, onClose, onChanged }) => {
  const detail = useLive(() => api.live.terms.get(versionId), [versionId]);
  const v = detail.data && TF.id(detail.data) != null ? detail.data : null;
  const st = v ? TF.status(v) : null;
  const editable = !!v && TERMS_CAN.edit(st);
  const [bodyText, setBodyText] = React.useState(null);   // null = 서버 원문 그대로
  const [titleText, setTitleText] = React.useState(null);
  const [preview, setPreview] = React.useState(false);
  const [publishing, setPublishing] = React.useState(false);
  const [effective, setEffective] = React.useState(minEffective);
  const [revised, setRevised] = React.useState(todayKst());
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const [saved, setSaved] = React.useState('');
  const body = bodyText != null ? bodyText : (v ? TF.body(v) : '');
  const title = titleText != null ? titleText : (v ? TF.title(v) : '');
  const dirty = !!v && ((bodyText != null && bodyText !== TF.body(v)) || (titleText != null && titleText !== TF.title(v)));
  const titleOk = title.trim().length >= 1 && title.length <= TERMS_TITLE_MAX;
  const label = `${typeName} ${termsNo(v)}`.trim();

  const run = async (fn, after) => {
    setBusy(true); setError(''); setSaved('');
    try { await fn(); after && after(); } catch (e) { setError(errorText(e)); } finally { setBusy(false); }
  };
  const save = () => run(() => api.live.terms.save(versionId, title, body), () => {
    setBodyText(null); setTitleText(null); detail.reload(); onChanged(); setSaved('저장했습니다.');   // 서버 값으로 다시 그림
  });
  const publish = () => {
    if (dirty) { setError('저장하지 않은 수정이 있습니다. 먼저 저장하세요.'); return; }
    if (!effective || !revised) return;
    if (!window.confirm(`"${label}"을(를) 게시할까요?\n개정일 ${revised} · 시행일 ${effective}\n게시하면 이 판은 고칠 수 없습니다. 시행일 전까지는 게시 취소로 되돌릴 수 있습니다.`)) return;
    run(() => api.live.terms.publish(versionId, effective, revised), () => { setPublishing(false); detail.reload(); onChanged(); });
  };
  const unpublish = () => {
    if (!window.confirm(`"${label}"의 게시를 취소할까요?\n작성 중으로 돌아가고 개정일·시행일·게시자 기록이 지워집니다. 파트너스에 "시행 예정"으로 보이지 않게 됩니다.`)) return;
    run(() => api.live.terms.unpublish(versionId), () => { detail.reload(); onChanged(); });
  };
  const remove = () => {
    if (!window.confirm(`작성 중인 "${label}"을(를) 삭제할까요?\n삭제하면 되돌릴 수 없습니다.`)) return;
    run(() => api.live.terms.remove(versionId), () => { onChanged(); onClose(); });
  };

  const footer = <>
    {v && TERMS_CAN.remove(st) && !publishing && <button className="btn btn-danger-ghost" disabled={busy} onClick={remove} style={{ marginRight:'auto' }}>삭제</button>}
    {v && TERMS_CAN.unpublish(st) && (
      <button className="btn btn-danger-ghost" disabled={busy || otherDraft} onClick={unpublish} style={{ marginRight:'auto' }}
        title={otherDraft ? '작성 중인 판이 이미 있어 게시를 취소할 수 없습니다. 그 판을 먼저 삭제하세요.' : ''}>게시 취소</button>
    )}
    {editable && !publishing && <button className="btn btn-ghost" disabled={busy || !dirty || !titleOk} onClick={save}>{busy ? '저장 중…' : '저장'}</button>}
    {v && TERMS_CAN.publish(st) && !publishing && <button className="btn btn-primary" disabled={busy} onClick={() => { setPublishing(true); setError(''); }}>게시…</button>}
    {v && TERMS_CAN.publish(st) && publishing && <>
      <button className="btn btn-ghost" disabled={busy} onClick={() => setPublishing(false)}>취소</button>
      <button className="btn btn-primary" disabled={busy || !effective || !revised || revised > effective} onClick={publish}>{busy ? '게시 중…' : '게시'}</button>
    </>}
    {v && TERMS_CAN.view(st) && (
      <a className="btn btn-ghost" href={termsPublicUrl(code, TF.no(v))} target="_blank" rel="noopener noreferrer">파트너스에서 보기</a>
    )}
    <button className="btn btn-ghost" onClick={onClose}>닫기</button>
  </>;

  const dateOrDash = (x) => (x ? ymd(x) || x : '—');
  return (
    <Drawer open onClose={onClose} title={v ? label : `판 ${versionId}`} footer={footer}>
      {error && <div style={{ marginBottom:12 }}><Notice tone="danger">처리하지 못했습니다: {error}</Notice></div>}
      {saved && !error && <div style={{ marginBottom:12 }}><Notice>{saved}</Notice></div>}
      <LiveBlock state={detail} empty={!v} emptyTitle="판을 찾을 수 없습니다">
        {v && (
          <div>
            <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:12, flexWrap:'wrap' }}>
              <StatusBadge map={TERMS_STATUS} value={st}/>
              <span style={{ fontSize:13, color:'var(--text-3)' }}>
                개정일 {dateOrDash(TF.revised(v))} · 시행일 {dateOrDash(TF.effective(v))}
                {TF.published(v) ? ` · 게시 ${fmtDT(TF.published(v))}${TF.aid(v) ? ` 어드민 #${TF.aid(v)}` : ''}` : ''}
                {TF.updated(v) ? ` · 마지막 저장 ${fmtDT(TF.updated(v))}` : ''}
              </span>
            </div>
            {!editable && (
              <div style={{ marginBottom:12 }}><Notice>
                게시된 판이라 고칠 수 없습니다. 고치려면 목록에서 "새 판 만들기"로 복사하세요.
                {TERMS_CAN.unpublish(st) ? ' 아직 시행 전이라 게시 취소로 작성 중으로 되돌릴 수 있습니다.' : ''}
              </Notice></div>
            )}
            {publishing && (
              <div style={{ marginBottom:12, padding:12, border:'1px solid var(--border)', borderRadius:8 }}>
                <div style={{ display:'flex', gap:12, flexWrap:'wrap' }}>
                  <div>
                    <label style={{ display:'block', fontSize:13, fontWeight:600, marginBottom:6 }}>개정일</label>
                    <input className="input" type="date" value={revised} max={effective || undefined} onChange={(e) => setRevised(e.target.value)}/>
                  </div>
                  <div>
                    <label style={{ display:'block', fontSize:13, fontWeight:600, marginBottom:6 }}>시행일</label>
                    <input className="input" type="date" value={effective} min={minEffective} onChange={(e) => setEffective(e.target.value)}/>
                  </div>
                </div>
                <div style={{ fontSize:12, color:'var(--text-3)', marginTop:6, lineHeight:1.6 }}>
                  개정일은 문서에 적히는 "최종 개정일"이고, 시행일부터 이 판이 파트너스에 "현재 판"으로 보입니다.<br/>
                  시행일은 {minEffective} 이후로 고를 수 있습니다(오늘 또는 이미 게시된 판의 가장 늦은 시행일). 개정일은 시행일보다 늦을 수 없습니다.
                  오늘 시행으로 게시하면 게시 취소할 수 없습니다.
                </div>
                {revised > effective && <div style={{ fontSize:12, color:'var(--danger)', marginTop:6 }}>개정일이 시행일보다 늦습니다.</div>}
              </div>
            )}
            <label style={{ display:'block', fontSize:13, fontWeight:600, marginBottom:6 }}>제목</label>
            <input className="input" style={{ width:'100%', marginBottom:4, background: editable ? undefined : 'var(--bg)' }} readOnly={!editable}
              maxLength={TERMS_TITLE_MAX} value={title} onChange={(e) => setTitleText(e.target.value)}/>
            {editable && !titleOk && <div style={{ fontSize:12, color:'var(--danger)', marginBottom:8 }}>제목을 1~{TERMS_TITLE_MAX}자로 적어 주세요.</div>}
            <div style={{ display:'flex', gap:6, margin:'10px 0 8px' }}>
              <button type="button" className={`btn btn-sm ${!preview ? 'btn-primary' : 'btn-ghost'}`} onClick={() => setPreview(false)}>{editable ? '편집' : '원문'}</button>
              <button type="button" className={`btn btn-sm ${preview ? 'btn-primary' : 'btn-ghost'}`} onClick={() => setPreview(true)}>미리보기</button>
            </div>
            {preview ? (
              <div className="terms-preview" style={{ padding:'12px 16px', border:'1px solid var(--border)', borderRadius:8, lineHeight:1.7, fontSize:14 }}
                dangerouslySetInnerHTML={{ __html: renderMd(body).html }}/>
            ) : (
              <textarea className="input t-mono" readOnly={!editable}
                style={{ width:'100%', minHeight:420, padding:12, resize:'vertical', fontSize:13, lineHeight:1.6, background: editable ? undefined : 'var(--bg)' }}
                value={body} onChange={(e) => setBodyText(e.target.value)}/>
            )}
            {editable && !preview && (
              <div style={{ fontSize:12, color:'var(--text-3)', marginTop:6, lineHeight:1.7 }}>
                마크다운(GFM, 표 포함)으로 적습니다. HTML 태그와 javascript: 링크는 서버가 거절합니다.<br/>
                <strong>본문에 판 번호("제N판")를 적지 마세요.</strong> 판 번호는 이 화면과 파트너스가 서버 값으로 따로 그립니다.
                본문에 적으면 새 판을 낼 때마다 어긋나고, 게시한 뒤에는 고칠 수 없습니다(TERMS_BANNER_RULE 주석).
              </div>
            )}
            {preview && (renderMd(body).fallback
              ? <div style={{ fontSize:12, color:'var(--danger)', marginTop:6 }}>미리보기 렌더러를 불러오지 못해 원문을 그대로 보여 줍니다. 파트너스 화면과 모양이 다릅니다.</div>
              : <div style={{ fontSize:12, color:'var(--text-3)', marginTop:6 }}>파트너스와 같은 렌더러로 그렸습니다. 글꼴·간격 같은 스타일은 다를 수 있습니다.</div>)}
          </div>
        )}
      </LiveBlock>
    </Drawer>
  );
};

Object.assign(window, { TermsAdmin });
