// 공지 관리 — 손님·사장님 공지 (/admin/notice/{user|merchant}). 목록·상세 + 등록·수정·삭제.
//
// 🔴 ID 필드명이 target 마다 다릅니다: adminUserNoticeId / adminMerchantNoticeId — noticeIdOf() 로 흡수.
// 🔴 앱 노출(backend 노출쿼리, 2026-10-05): (start IS NULL OR start<=now) AND (end IS NULL OR end>=now).
//    즉 기간을 비우면 상시 노출, 한쪽만 있으면 그 경계만. 상태 뱃지는 서버 날짜로 계산합니다.
// 🔴 쓰기 계약(backend 2026-10-05): 등록/수정은 **multipart** · **서버 검증 없음**(길이·날짜 화면이 막음,
//    title≤100·content≤400, STRICT 라 넘으면 500) · **수정은 부분**(안 보낸 필드 유지) · writerId 는 서버가 넣음.
//    삭제는 soft('Y'), 삭제분은 목록에 안 와 **되돌리는 화면 없음**. audit 남음.

const NOTICE_TARGETS = [
  { value: 'user', label: '사용자 공지' },
  { value: 'merchant', label: '사장님 공지' },
];
const targetAppName = (t) => (t === 'merchant' ? '사장님 앱' : '손님 앱');
const targetLabel = (t) => (NOTICE_TARGETS.find((x) => x.value === t) || {}).label || t;

const NOTICE_TITLE_MAX = 100;
const NOTICE_CONTENT_MAX = 400;

const noticeIdOf = (n) => (n ? (n.adminUserNoticeId != null ? n.adminUserNoticeId : n.adminMerchantNoticeId) : null);

// 게시 상태 — 서버 start/end 와 지금 시각으로 계산(지어내기 아님). parseDT 는 epoch ms 또는 null.
const noticeStatus = (n, nowMs) => {
  const s = parseDT(n.startDate);
  const e = parseDT(n.endDate);
  if (s == null && e == null) return { label: '상시 게시', tone: 'success', hint: '기간 제한 없이 항상 앱에 노출됩니다' };
  if (s != null && nowMs < s) return { label: '게시예정', tone: 'info', hint: `${fmtDT(n.startDate)}부터 노출됩니다` };
  if (e != null && nowMs > e) return { label: '게시종료', tone: 'neutral', hint: '게시 기간이 지났습니다' };
  return { label: '게시중', tone: 'success', hint: '지금 앱에 노출 중입니다' };
};
const noticePeriod = (n) => {
  if (!n.startDate && !n.endDate) return '상시 (기간 제한 없음)';
  return `${n.startDate ? fmtDT(n.startDate) : '처음부터'} ~ ${n.endDate ? fmtDT(n.endDate) : '제한 없음'}`;
};

// 날짜 변환: 서버 'yyyy-MM-dd HH:mm[:ss]' ↔ datetime-local 'yyyy-MM-ddTHH:mm'.
const toLocalInput = (s) => { const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/.exec(s || ''); return m ? `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}` : ''; };
const toServerDate = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(v || ''); return m ? `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:00` : ''; };

// 🔴 서버 검증이 없어 화면이 전부 막습니다. 통과하면 null, 아니면 사유 문자열.
const validateNotice = (f) => {
  const title = f.title.trim();
  const content = f.content.trim();
  if (!title) return '제목을 입력하세요.';
  if (title.length > NOTICE_TITLE_MAX) return `제목은 ${NOTICE_TITLE_MAX}자까지입니다. (현재 ${title.length}자)`;
  if (!content) return '본문을 입력하세요.';
  if (content.length > NOTICE_CONTENT_MAX) return `본문은 ${NOTICE_CONTENT_MAX}자까지입니다. (현재 ${content.length}자)`;
  const hasS = !!f.startDate, hasE = !!f.endDate;
  if (hasS !== hasE) return '게시 기간은 시작·종료를 둘 다 입력하거나 둘 다 비우세요. (한쪽만 두면 운영자가 헷갈립니다)';
  if (hasS && hasE) {
    const s = toServerDate(f.startDate), e = toServerDate(f.endDate);
    if (!s || !e) return '게시 기간 형식이 올바르지 않습니다.';
    if (s >= e) return '게시 종료는 시작보다 뒤여야 합니다.';
  }
  return null;
};
// FormData(multipart). 기간이 둘 다 있을 때만 날짜를 담습니다(둘 다 없으면 안 담아 상시).
const buildNoticeForm = (f, { create }) => {
  const fd = new FormData();
  fd.append('title', f.title.trim());
  fd.append('content', f.content.trim());
  if (f.startDate && f.endDate) { fd.append('startDate', toServerDate(f.startDate)); fd.append('endDate', toServerDate(f.endDate)); }
  if (create) fd.append('isDelete', 'N');   // 등록 때만 명시(안 보내면 null 저장될 수 있음). 수정 땐 안 보냄(유지).
  return fd;
};

// 제목·본문·기간 입력 — 등록·수정이 공유합니다.
const NoticeFields = ({ f, set, disabled }) => (
  <div style={{ display: 'grid', gap: 14 }}>
    <div>
      <label style={{ fontSize: 13, fontWeight: 600 }}>제목</label>
      <input className="input" style={{ width: '100%', marginTop: 4 }} maxLength={NOTICE_TITLE_MAX}
        value={f.title} onChange={set('title')} disabled={disabled} placeholder="공지 제목"/>
      <div style={{ textAlign: 'right', fontSize: 11.5, color: f.title.length > NOTICE_TITLE_MAX ? 'var(--danger)' : 'var(--text-3)' }}>
        {f.title.length}/{NOTICE_TITLE_MAX}
      </div>
    </div>
    <div>
      <label style={{ fontSize: 13, fontWeight: 600 }}>본문</label>
      <textarea className="input" style={{ width: '100%', marginTop: 4, resize: 'vertical' }} rows={8} maxLength={NOTICE_CONTENT_MAX}
        value={f.content} onChange={set('content')} disabled={disabled} placeholder="공지 본문"/>
      <div style={{ textAlign: 'right', fontSize: 11.5, color: f.content.length > NOTICE_CONTENT_MAX ? 'var(--danger)' : 'var(--text-3)' }}>
        {f.content.length}/{NOTICE_CONTENT_MAX}
      </div>
    </div>
    <div>
      <label style={{ fontSize: 13, fontWeight: 600 }}>게시 기간</label>
      <div style={{ fontSize: 12, color: 'var(--text-3)', margin: '2px 0 6px' }}>비우면 상시 노출됩니다. 넣으면 그 기간에만 노출됩니다.</div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
        <input type="datetime-local" className="input" value={f.startDate} onChange={set('startDate')} disabled={disabled}/>
        <span>~</span>
        <input type="datetime-local" className="input" value={f.endDate} onChange={set('endDate')} disabled={disabled}/>
      </div>
    </div>
  </div>
);

const Notices = () => {
  const [target, setTarget] = React.useState('user');
  const [openId, setOpenId] = React.useState(null);
  const [creating, setCreating] = React.useState(false);
  useMinuteTick();   // "게시중/예정/종료"가 시간에 걸쳐 있어 1분마다 다시 계산
  const now = Date.now();
  const list = useLive(() => api.live.notices.list(target), [target]);
  const rows = listRows(list.data);
  React.useEffect(() => { setOpenId(null); setCreating(false); }, [target]);

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1>공지 관리</h1>
          <div className="sub">손님·사장님 앱에 띄우는 공지를 관리합니다</div>
        </div>
        <div className="actions">
          <button className="btn btn-ghost btn-sm" onClick={list.reload} disabled={list.loading}>
            <Icon name="refresh" size={14}/>새로고침
          </button>
          <button className="btn btn-primary btn-sm" onClick={() => setCreating(true)}>
            <Icon name="plus" size={14}/>공지 등록
          </button>
        </div>
      </div>

      <Tabs items={NOTICE_TARGETS} value={target} onChange={setTarget}/>

      <div style={{ margin: '12px 0' }}>
        <Notice>
          게시 기간을 <strong>비우면 {targetAppName(target)}에 상시 노출</strong>됩니다.
          기간을 넣으면 그 기간에만 노출되고, 한쪽만 넣으면 그 경계만 적용됩니다.
        </Notice>
      </div>

      <div className="table-wrap" data-loading={list.loading ? 'true' : 'false'}>
        <LiveBlock state={list} empty={rows.length === 0} emptyTitle={`${targetLabel(target)}이(가) 없습니다`}>
          <table className="table">
            <thead><tr>
              <th>제목</th>
              <th>게시 상태</th>
              <th>게시 기간</th>
              <th>등록일</th>
            </tr></thead>
            <tbody>
              {rows.map((n) => {
                const st = noticeStatus(n, now);
                return (
                  <tr key={noticeIdOf(n)} onClick={() => setOpenId(noticeIdOf(n))} style={{ cursor: 'pointer' }}>
                    <td style={{ fontWeight: 600, maxWidth: 420 }}>
                      <div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.title || '(제목 없음)'}</div>
                    </td>
                    <td><Badge tone={st.tone} title={st.hint}>{st.label}</Badge></td>
                    <td style={{ whiteSpace: 'nowrap', fontSize: 12.5, color: 'var(--text-3)' }}>{noticePeriod(n)}</td>
                    <td style={{ whiteSpace: 'nowrap' }}>{fmtDT(n.regDate)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </LiveBlock>
        {!list.error && (
          <div className="table-foot">
            <span style={{ fontSize: 13, color: 'var(--text-3)' }}>
              {list.data ? `${fmtNum(rows.length)}건 (등록 최신순)` : ''}
            </span>
          </div>
        )}
      </div>

      {creating && (
        <NoticeCreateDrawer target={target} onClose={() => setCreating(false)} onCreated={() => { setCreating(false); list.reload(); }}/>
      )}
      {openId != null && (
        <NoticeDrawer target={target} id={openId} onClose={() => setOpenId(null)}
          onChanged={list.reload} onRemoved={() => { setOpenId(null); list.reload(); }}/>
      )}
    </div>
  );
};

// 등록 — 새 공지. 성공 후 목록 재조회(가짜 성공 금지: 서버 성공을 받고서야 닫고 재조회).
const NoticeCreateDrawer = ({ target, onClose, onCreated }) => {
  const [f, setF] = React.useState({ title: '', content: '', startDate: '', endDate: '' });
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const set = (k) => (e) => setF((prev) => ({ ...prev, [k]: e.target.value }));
  const problem = validateNotice(f);

  const submit = async () => {
    if (problem) { setError(problem); return; }
    const period = f.startDate && f.endDate ? `게시 기간: ${toServerDate(f.startDate)} ~ ${toServerDate(f.endDate)}` : '상시 노출(기간 없음)';
    if (!window.confirm(`${targetLabel(target)}를 등록할까요?\n\n${f.title.trim()}\n${period}\n\n등록하면 ${targetAppName(target)}에 노출됩니다. 변경 이력에 남습니다.`)) return;
    setBusy(true); setError('');
    try {
      await api.live.notices.create(target, buildNoticeForm(f, { create: true }));
      onCreated();
    } catch (e) { setError(errorText(e)); }
    finally { setBusy(false); }
  };

  return (
    <Drawer open onClose={onClose} title={`${targetLabel(target)} 등록`}
      footer={<>
        <button className="btn btn-primary" disabled={busy || !!problem} onClick={submit}>{busy ? '등록 중…' : '등록'}</button>
        <button className="btn btn-ghost" onClick={onClose}>닫기</button>
      </>}>
      {error && <div style={{ marginBottom: 12 }}><Notice tone="danger">등록하지 못했습니다: {error}</Notice></div>}
      <NoticeFields f={f} set={set} disabled={busy}/>
      {problem && <div style={{ marginTop: 10, fontSize: 12.5, color: 'var(--text-3)' }}>{problem}</div>}
    </Drawer>
  );
};

// 상세 보기 + 수정 + 삭제.
const NoticeDrawer = ({ target, id, onClose, onChanged, onRemoved }) => {
  const detail = useLive(() => api.live.notices.get(target, id), [target, id]);
  const n = detail.data;
  const [editing, setEditing] = React.useState(false);
  const [f, setF] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  React.useEffect(() => { setEditing(false); setError(''); }, [id]);

  const startEdit = () => {
    setF({ title: n.title || '', content: n.content || '', startDate: toLocalInput(n.startDate), endDate: toLocalInput(n.endDate) });
    setError(''); setEditing(true);
  };
  const set = (k) => (e) => setF((prev) => ({ ...prev, [k]: e.target.value }));
  const problem = editing && f ? validateNotice(f) : null;
  // 🔴 기존에 기간이 있던 공지를 "둘 다 비움"(상시)으로 바꾸는 것은 부분수정으론 null 로 못 지웁니다
  //    (안 보내면 유지, 빈값 보내면 500). 가짜 성공·에러를 막으려 저장을 막고 안내합니다(backend 확인중).
  const hadPeriod = !!(n && (n.startDate || n.endDate));
  const clearingPeriod = editing && f && hadPeriod && !f.startDate && !f.endDate;

  const save = async () => {
    if (problem) { setError(problem); return; }
    if (clearingPeriod) { setError('기간을 완전히 비워 상시로 바꾸는 것은 아직 지원되지 않습니다. 삭제 후 다시 등록해 주세요.'); return; }
    if (!window.confirm(`이 ${targetLabel(target)}를 수정할까요?\n\n${f.title.trim()}\n\n변경 이력에 남습니다.`)) return;
    setBusy(true); setError('');
    try {
      await api.live.notices.update(target, id, buildNoticeForm(f, { create: false }));
      setEditing(false);
      detail.reload();
      onChanged && onChanged();
    } catch (e) { setError(errorText(e)); }
    finally { setBusy(false); }
  };

  const remove = async () => {
    if (!window.confirm(`이 ${targetLabel(target)}를 삭제할까요?\n\n${(n && n.title) || '(제목 없음)'}\n\n목록과 ${targetAppName(target)}에서 바로 사라집니다. 서버에는 삭제 표시로 남지만 되돌리는 화면은 없습니다.`)) return;
    setBusy(true); setError('');
    try {
      await api.live.notices.remove(target, id);
      onRemoved && onRemoved();
    } catch (e) { setError(errorText(e)); }
    finally { setBusy(false); }
  };

  const st = n && !editing ? noticeStatus(n, Date.now()) : null;

  return (
    <Drawer open onClose={onClose} title={`${targetLabel(target)} ${id}`}
      footer={editing ? <>
        <button className="btn btn-primary" disabled={busy || !!problem || clearingPeriod} onClick={save}>{busy ? '저장 중…' : '저장'}</button>
        <button className="btn btn-ghost" disabled={busy} onClick={() => { setEditing(false); setError(''); }}>편집 취소</button>
      </> : <>
        {n && <button className="btn btn-danger-ghost" style={{ marginRight: 'auto' }} disabled={busy} onClick={remove}>삭제</button>}
        {n && <button className="btn btn-primary" onClick={startEdit}>수정</button>}
        <button className="btn btn-ghost" onClick={onClose}>닫기</button>
      </>}>
      <div data-loading={detail.loading ? 'true' : 'false'}>
        {error && <div style={{ marginBottom: 12 }}><Notice tone="danger">{error}</Notice></div>}
        {detail.error && <Notice tone="danger">불러오지 못했습니다: {errorText(detail.error)}</Notice>}
        {n && editing && f && (
          <>
            <NoticeFields f={f} set={set} disabled={busy}/>
            {clearingPeriod && (
              <div style={{ marginTop: 10 }}>
                <Notice tone="danger">기간을 완전히 비워 상시로 바꾸는 것은 아직 지원되지 않습니다. 상시로 바꾸려면 삭제 후 다시 등록해 주세요.</Notice>
              </div>
            )}
            {problem && !clearingPeriod && <div style={{ marginTop: 10, fontSize: 12.5, color: 'var(--text-3)' }}>{problem}</div>}
          </>
        )}
        {n && !editing && (
          <>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
              <Badge tone={st.tone} title={st.hint}>{st.label}</Badge>
              <span style={{ marginLeft: 'auto', fontSize: 13, color: 'var(--text-3)' }}>{fmtDT(n.regDate)} 등록</span>
            </div>
            <h3 style={{ margin: '0 0 12px', fontSize: 16 }}>{n.title || '(제목 없음)'}</h3>
            <dl className="spec">
              <dt>대상</dt><dd>{targetLabel(target)}</dd>
              <dt>게시 기간</dt><dd>{noticePeriod(n)}</dd>
              <dt>작성자</dt><dd className="t-mono">관리자 #{n.writerId != null ? n.writerId : '—'}</dd>
            </dl>
            <h4 style={{ margin: '16px 0 8px', fontSize: 13, color: 'var(--text-3)', fontWeight: 600 }}>본문</h4>
            <div style={{ whiteSpace: 'pre-wrap', fontSize: 14, lineHeight: 1.6, padding: '12px 14px', border: '1px solid var(--border)', borderRadius: 8 }}>
              {n.content || '(본문 없음)'}
            </div>
          </>
        )}
      </div>
    </Drawer>
  );
};

Object.assign(window, {
  Notices, NoticeDrawer, NoticeCreateDrawer, NoticeFields,
  noticeIdOf, noticeStatus, noticePeriod, validateNotice, buildNoticeForm,
  NOTICE_TITLE_MAX, NOTICE_CONTENT_MAX,
});
