/* ════════════════════════════════════════════════════════════════
   문서 탐색기 (Tab 16) — 프로젝트 폴더 트리 탐색 + txt/md/pdf 뷰어 + 인라인 편집/관리
   백엔드: /api/docs/*  (변경 작업은 X-Kb-Token admin)
   esc() / kbToken 은 app.js(선로딩) 전역 재사용
   ════════════════════════════════════════════════════════════════ */

const DOCS_TEXT_EXT = ['txt', 'log', 'text', 'csv', 'sql', 'py'];
const DOCS_MD_EXT = ['md', 'markdown'];

const docsState = {
  inited: false,
  canManage: false,
  root: '',
  curFile: null,   // { path, ext }
  editing: false,
};

/* ── 진입 ──────────────────────────────────────────────────── */
paneInit.docs = docsInit;
async function docsInit() {
  await docsLoadConfig();
  if (!docsState.inited) {
    docsState.inited = true;
    document.getElementById('docs-tree').addEventListener('click', docsTreeClick);
    await docsRefresh();
  }
}

async function docsLoadConfig() {
  try {
    const r = await fetch('/api/docs/config', { headers: docsHeaders() });
    const d = await r.json();
    docsState.canManage = !!d.canManage;
    docsState.root = d.root || '';
    document.getElementById('docs-root-label').textContent = d.root || '';
    docsUpdateAdminUi();
  } catch (e) { /* noop */ }
}

/* ── 인증 헤더 (KB admin 토큰 재사용) ───────────────────────── */
function docsToken() { return sessionStorage.getItem('kbToken') || ''; }
function docsHeaders(extra) {
  const h = extra || {};
  const t = docsToken();
  if (t) h['X-Kb-Token'] = t;
  return h;
}

/* ── 관리 UI 토글 ──────────────────────────────────────────── */
function docsUpdateAdminUi() {
  const on = docsState.canManage;
  document.getElementById('docs-mkdir-btn').hidden = !on;
  document.getElementById('docs-upload-btn').hidden = !on;
  document.getElementById('docs-tree').classList.toggle('can-manage', on);
  document.getElementById('docs-admin-state').textContent = on ? '🔓 관리 가능' : '🔒 읽기 전용';
  document.getElementById('docs-unlock-btn').textContent = on ? '잠금' : '관리 잠금해제';
  if (docsState.curFile) docsRenderViewerActions();
}

async function docsUnlock() {
  if (docsState.canManage) {
    try { await fetch('/api/kb/auth/logout', { method: 'POST', headers: docsHeaders() }); } catch (e) {}
    sessionStorage.removeItem('kbToken');
    try { kbToken = ''; } catch (e) {}
    await docsLoadConfig();
    return;
  }
  const pw = prompt('관리자 비밀번호');
  if (!pw) return;
  try {
    const r = await fetch('/api/kb/auth/login', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ password: pw }),
    });
    const d = await r.json();
    if (!d.success) { alert('로그인 실패: ' + (d.error || '')); return; }
    sessionStorage.setItem('kbToken', d.token);
    try { kbToken = d.token; } catch (e) {}
    await docsLoadConfig();
    docsToast('관리 잠금해제됨');
  } catch (e) { alert('로그인 오류: ' + e.message); }
}

/* ── 트리 ──────────────────────────────────────────────────── */
async function docsFetchTree(path) {
  const r = await fetch('/api/docs/tree?path=' + encodeURIComponent(path || ''), { headers: docsHeaders() });
  const d = await r.json();
  if (!d.success) throw new Error(d.error || '목록 조회 실패');
  return d.entries;
}

async function docsRefresh() {
  const host = document.getElementById('docs-tree');
  host.innerHTML = '<div class="docs-loading">불러오는 중…</div>';
  try {
    const entries = await docsFetchTree('');
    const ul = document.createElement('ul');
    ul.className = 'docs-ul';
    docsRenderInto(ul, entries, 0);
    host.innerHTML = '';
    host.appendChild(ul);
    docsApplyFilter();
  } catch (e) {
    host.innerHTML = '<div class="docs-error">' + esc(e.message) + '</div>';
  }
}

function docsRenderInto(ul, entries, depth) {
  for (const e of entries) {
    const li = document.createElement('li');
    li.className = 'docs-li';
    li.appendChild(docsNodeRow(e, depth));
    if (e.type === 'dir') {
      const kids = document.createElement('ul');
      kids.className = 'docs-children';
      kids.hidden = true;
      li.appendChild(kids);
    }
    ul.appendChild(li);
  }
}

function docsNodeRow(entry, depth) {
  const row = document.createElement('div');
  row.className = 'docs-node ' + (entry.type === 'dir' ? 'is-dir' : 'is-file');
  row.style.paddingLeft = (8 + depth * 14) + 'px';
  row.dataset.path = entry.path;
  row.dataset.type = entry.type;
  row.dataset.name = (entry.name || '').toLowerCase();
  row.dataset.ext = entry.ext || '';
  row.dataset.depth = depth;

  const ic = entry.type === 'dir' ? '▸' : docsFileIcon(entry.ext);
  const size = entry.type === 'file' ? `<span class="docs-node-size">${docsFmtSize(entry.size)}</span>` : '';
  const acts = `<span class="docs-node-act">
      <button class="docs-act-rename" title="이름변경">✎</button>
      <button class="docs-act-delete" title="삭제">🗑</button>
    </span>`;
  row.innerHTML =
    `<span class="docs-node-ic">${ic}</span>` +
    `<span class="docs-node-name">${esc(entry.name)}</span>` +
    size + acts;
  return row;
}

async function docsTreeClick(ev) {
  const renameBtn = ev.target.closest('.docs-act-rename');
  const deleteBtn = ev.target.closest('.docs-act-delete');
  const row = ev.target.closest('.docs-node');
  if (!row) return;

  if (renameBtn) { ev.stopPropagation(); return docsRenamePrompt(row.dataset.path); }
  if (deleteBtn) { ev.stopPropagation(); return docsDeletePrompt(row.dataset.path, row.dataset.type); }

  if (row.dataset.type === 'dir') return docsToggleDir(row);
  // 파일
  document.querySelectorAll('#docs-tree .docs-node.active').forEach(n => n.classList.remove('active'));
  row.classList.add('active');
  docsOpenPath(row.dataset.path);
}

async function docsToggleDir(row) {
  const li = row.parentElement;
  const kids = li.querySelector(':scope > .docs-children');
  if (!kids) return;
  if (kids.hidden) {
    if (kids.dataset.loaded !== '1') {
      kids.innerHTML = '<li class="docs-loading">…</li>';
      try {
        const entries = await docsFetchTree(row.dataset.path);
        kids.innerHTML = '';
        docsRenderInto(kids, entries, (+row.dataset.depth) + 1);
        kids.dataset.loaded = '1';
        docsApplyFilter();
      } catch (e) {
        kids.innerHTML = '<li class="docs-error">' + esc(e.message) + '</li>';
      }
    }
    kids.hidden = false;
    row.classList.add('open');
  } else {
    kids.hidden = true;
    row.classList.remove('open');
  }
}

/* ── 파일 열기 / 뷰어 디스패치 ─────────────────────────────── */
function docsOpenPath(path) {
  const ext = (path.split('.').pop() || '').toLowerCase();
  docsState.curFile = { path, ext };
  docsState.editing = false;
  document.getElementById('docs-cur-path').textContent = path;
  docsRenderViewerActions();
  if (DOCS_MD_EXT.includes(ext)) docsViewMarkdown(path);
  else if (ext === 'pdf') docsViewPdf(path);
  else if (DOCS_TEXT_EXT.includes(ext)) docsViewText(path);
  else docsViewUnsupported(path, ext);
}

async function docsFetchText(path) {
  const r = await fetch('/api/docs/text?path=' + encodeURIComponent(path), { headers: docsHeaders() });
  const d = await r.json();
  if (!d.success) throw new Error(d.error || '파일 읽기 실패');
  return d;
}

function docsViewer() { return document.getElementById('docs-viewer'); }
function docsViewerError(err) {
  docsViewer().className = 'docs-viewer';
  docsViewer().innerHTML = '<div class="docs-error">' + esc(err.message || String(err)) + '</div>';
}
function docsTruncBanner(size) {
  const b = document.createElement('div');
  b.className = 'docs-trunc';
  b.textContent = `⚠ 파일이 커서 일부만 표시합니다 (전체 ${docsFmtSize(size)}). 다운로드로 전체 확인하세요.`;
  return b;
}

async function docsViewText(path) {
  const v = docsViewer();
  v.className = 'docs-viewer mode-text';
  v.innerHTML = '<div class="docs-loading">불러오는 중…</div>';
  try {
    const d = await docsFetchText(path);
    v.innerHTML = '';
    if (d.truncated) v.appendChild(docsTruncBanner(d.size));
    const pre = document.createElement('pre');
    pre.className = 'docs-text-pre';
    pre.textContent = d.text;
    v.appendChild(pre);
  } catch (e) { docsViewerError(e); }
}

function docsViewPdf(path) {
  const v = docsViewer();
  v.className = 'docs-viewer mode-pdf';
  const src = '/api/docs/raw?path=' + encodeURIComponent(path);
  v.innerHTML = `<iframe class="docs-pdf-frame" src="${src}"></iframe>`;
}

async function docsViewMarkdown(path) {
  const v = docsViewer();
  v.className = 'docs-viewer mode-md';
  v.innerHTML = '<div class="docs-loading">렌더링 준비 중…</div>';
  try {
    await docsEnsureMdLibs();
    const d = await docsFetchText(path);
    v.innerHTML = '';
    if (d.truncated) v.appendChild(docsTruncBanner(d.size));
    const body = document.createElement('div');
    body.className = 'md-body';
    v.appendChild(body);
    docsRenderMarkdownInto(body, d.text);
  } catch (e) { docsViewerError(e); }
}

function docsViewUnsupported(path, ext) {
  const v = docsViewer();
  v.className = 'docs-viewer mode-unsupported';
  v.innerHTML = `<div class="docs-unsupported">
    <div class="docs-unsupported-ic">${docsFileIcon(ext)}</div>
    <p><b>.${esc(ext || '?')}</b> 형식은 미리보기를 지원하지 않습니다.</p>
    <p class="docs-muted">지원 뷰어: txt · md · pdf</p>
    <a class="btn-a btn-sm" href="/api/docs/raw?path=${encodeURIComponent(path)}&download=true">⬇ 다운로드</a>
  </div>`;
}

/* ── PDF 내보내기 (브라우저 인쇄 → "PDF로 저장") ──────────────
   이미 렌더된 결과(md=marked/KaTeX/mermaid SVG/코드강조, txt=pre)를
   깨끗한 새 창에 담아 print(). 폐쇄망 OK — 외부 리소스 없이 /lib 로컬 CSS만 사용. */
const DOCS_PRINT_CSS = `
*{box-sizing:border-box}
body{font:15px/1.7 'Malgun Gothic','Noto Sans KR',sans-serif;color:#24292f;max-width:900px;margin:0 auto;padding:24px}
h1,h2,h3,h4,h5,h6{font-weight:700;line-height:1.3;margin:24px 0 12px;color:#1f2328}
h1{font-size:1.9em;border-bottom:1px solid #d8dee4;padding-bottom:.3em}
h2{font-size:1.5em;border-bottom:1px solid #d8dee4;padding-bottom:.3em}
h3{font-size:1.25em}h4{font-size:1.05em}
p{margin:0 0 12px}a{color:#0969da;text-decoration:none}
ul,ol{margin:0 0 12px;padding-left:2em}li{margin:3px 0}
blockquote{margin:0 0 12px;padding:2px 16px;color:#57606a;border-left:4px solid #d0d7de}
hr{border:0;border-top:1px solid #d8dee4;margin:20px 0}
img{max-width:100%}
code{font-family:'D2Coding','Consolas',monospace;font-size:85%;background:rgba(175,184,193,.2);padding:.2em .4em;border-radius:6px}
pre{background:#f6f8fa;border:1px solid #e4e8ec;border-radius:8px;padding:14px;overflow:auto;line-height:1.5}
pre code{background:none;padding:0;font-size:13px}
table{border-collapse:collapse;margin:0 0 14px;max-width:100%}
th,td{border:1px solid #d0d7de;padding:6px 13px}th{background:#f6f8fa}
tr:nth-child(2n) td{background:#f6f8fa}
.mermaid{text-align:center;margin:0 0 14px}.mermaid svg{max-width:100%;height:auto}
pre.txt{white-space:pre-wrap;word-break:break-word;font-family:'D2Coding','Consolas',monospace}
@page{margin:15mm}
@media print{body{max-width:none;padding:0}pre,table,img,.mermaid,blockquote{break-inside:avoid}h1,h2,h3,h4{break-after:avoid}}
`;

function docsExportPdf() {
  const f = docsState.curFile;
  if (!f) return;
  const isMd = DOCS_MD_EXT.includes(f.ext);
  const isTxt = DOCS_TEXT_EXT.includes(f.ext);
  if (!isMd && !isTxt) return;

  // 파일명(확장자 제거) → PDF 기본 파일명으로 사용됨
  const base = (f.path.split('/').pop() || f.path).replace(/\.[^.]+$/, '');

  let contentHtml = '';
  let cssLinks = '';
  if (isMd) {
    const body = document.querySelector('#docs-viewer .md-body');
    if (!body) { alert('먼저 문서를 연 뒤 시도하세요.'); return; }
    contentHtml = `<div class="md-body">${body.innerHTML}</div>`;
    // 코드강조/수식 스타일 (로컬 /lib). KaTeX 폰트는 이 CSS의 상대경로로 로컬 로드됨
    cssLinks =
      '<link rel="stylesheet" href="/lib/highlight-github.min.css">' +
      '<link rel="stylesheet" href="/lib/katex/katex.min.css">';
  } else {
    const pre = document.querySelector('#docs-viewer .docs-text-pre');
    contentHtml = `<pre class="txt">${esc(pre ? pre.textContent : '')}</pre>`;
  }

  const w = window.open('', '_blank');
  if (!w) { alert('팝업이 차단되었습니다. 팝업을 허용한 뒤 다시 시도하세요.'); return; }
  w.document.write(
    `<!doctype html><html lang="ko"><head><meta charset="utf-8">` +
    `<title>${esc(base)}</title>${cssLinks}<style>${DOCS_PRINT_CSS}</style></head>` +
    `<body>${contentHtml}` +
    `<script>window.addEventListener('load',function(){` +
    `(document.fonts&&document.fonts.ready?document.fonts.ready:Promise.resolve())` +
    `.then(function(){setTimeout(function(){window.focus();window.print();},100);});});` +
    `window.onafterprint=function(){window.close();};<\/script>` +
    `</body></html>`
  );
  w.document.close();
}

/* ── 마크다운 렌더 파이프라인 ──────────────────────────────── */

/// box-drawing 문자(┌─┬┐│├─┼┤└─┴┘) 라인을 감지하여 표는 GFM pipe table(|---|)로,
/// 일반 box line은 ``` fence로 감싸 고정폭 렌더링 보장
function docsWrapBoxDrawing(text) {
  const BOX_RE = /^[ \t]*[┌┐└┘├┤┬┴┼│─━]/;
  const lines = text.split('\n');
  const out = [];
  let inBlock = false;
  let buf = [];
  for (const line of lines) {
    const isBox = BOX_RE.test(line);
    if (isBox && !inBlock) { inBlock = true; buf = [line]; }
    else if (isBox && inBlock) { buf.push(line); }
    else {
      if (inBlock) { out.push(docsBoxBlockToGfm(buf)); inBlock = false; buf = []; }
      out.push(line);
    }
  }
  if (inBlock) out.push(docsBoxBlockToGfm(buf));
  return out.join('\n');
}

/// box-drawing block이 표(┬┴┼ + │)면 GFM pipe table로, 아니면 ``` fence로
function docsBoxBlockToGfm(buf) {
  var joined = buf.join('');
  if (!/[┬┴┼]/.test(joined) || !/│/.test(joined))
    return '```\n' + buf.join('\n') + '\n```';

  // 각 데이터 행별로 자신의 │ 위치로 셀 추출 (행 간 컬럼 경계 불일치 대응)
  var allRows = [];
  for (var i = 0; i < buf.length; i++) {
    if (!/^[ \t]*│/.test(buf[i])) continue;
    var poses = [];
    for (var j = 0; j < buf[i].length; j++) {
      if (buf[i][j] === '│') poses.push(j);
    }
    if (poses.length < 2) continue;
    var cells = [];
    for (var c = 0; c < poses.length - 1; c++)
      cells.push(buf[i].substring(poses[c] + 1, poses[c + 1]).trim());
    allRows.push(cells);
  }
  if (allRows.length === 0) return '```\n' + buf.join('\n') + '\n```';

  // 여분 줄 병합 (≤1 non-empty cell)
  var merged = [allRows[0]];
  for (var r = 1; r < allRows.length; r++) {
    var nonEmpty = 0, lastNec = -1;
    var numCols = allRows[r].length;
    for (var c = 0; c < numCols; c++) { if (allRows[r][c] !== '') { nonEmpty++; lastNec = c; } }
    if (nonEmpty <= 1) {
      if (lastNec >= 0 && merged[merged.length - 1][lastNec] !== '')
        merged[merged.length - 1][lastNec] += ' ' + allRows[r][lastNec];
    } else {
      merged.push(allRows[r]);
    }
  }

  var result = [];
  var hdr = merged[0];
  result.push('| ' + hdr.join(' | ') + ' |');
  result.push('|' + ' --- |'.repeat(hdr.length));
  for (var r = 1; r < merged.length; r++) {
    result.push('| ' + merged[r].join(' | ') + ' |');
  }
  return result.join('\n');
}

function docsRenderMarkdownInto(el, text) {
  text = docsWrapBoxDrawing(text);
  const rawHtml = marked.parse(text, { gfm: true, breaks: false });
  el.innerHTML = DOMPurify.sanitize(rawHtml, { ADD_TAGS: ['details', 'summary'], ADD_ATTR: ['target'] });
  docsExternalLinks(el);
  docsExtractMermaid(el);   // mermaid 코드블록 → div.mermaid (highlight 전)
  docsHighlight(el);
  docsRenderMath(el);
  docsRunMermaid(el);
}

function docsExternalLinks(el) {
  el.querySelectorAll('a[href]').forEach(a => {
    const href = a.getAttribute('href') || '';
    if (/^https?:/i.test(href)) {
      a.target = '_blank'; a.rel = 'noopener noreferrer';
    } else if (href && !href.startsWith('#') && docsState.curFile) {
      // 상대 링크 → 탐색기 내에서 열기
      const rel = docsResolveRel(docsState.curFile.path, href);
      if (rel != null) {
        a.addEventListener('click', (ev) => { ev.preventDefault(); docsOpenPath(rel); });
      }
    }
  });
}

function docsResolveRel(basePath, href) {
  try {
    const clean = href.split('#')[0].split('?')[0];
    if (!clean) return null;
    const baseDir = basePath.includes('/') ? basePath.slice(0, basePath.lastIndexOf('/')) : '';
    const parts = (baseDir ? baseDir.split('/') : []).concat(clean.split('/'));
    const stack = [];
    for (const p of parts) {
      if (p === '' || p === '.') continue;
      if (p === '..') stack.pop();
      else stack.push(p);
    }
    return stack.join('/');
  } catch (e) { return null; }
}

function docsExtractMermaid(el) {
  el.querySelectorAll('code.language-mermaid').forEach(code => {
    const div = document.createElement('div');
    div.className = 'mermaid';
    div.textContent = code.textContent;
    (code.closest('pre') || code).replaceWith(div);
  });
}

function docsHighlight(el) {
  if (typeof hljs === 'undefined') return;
  el.querySelectorAll('pre code').forEach(c => { try { hljs.highlightElement(c); } catch (e) {} });
}

function docsRenderMath(el) {
  if (typeof renderMathInElement === 'undefined') return;
  try {
    renderMathInElement(el, {
      delimiters: [
        { left: '$$', right: '$$', display: true },
        { left: '$', right: '$', display: false },
        { left: '\\[', right: '\\]', display: true },
        { left: '\\(', right: '\\)', display: false },
      ],
      throwOnError: false,
    });
  } catch (e) {}
}

async function docsRunMermaid(el) {
  const nodes = el.querySelectorAll('.mermaid');
  if (!nodes.length || typeof mermaid === 'undefined') return;
  try { await mermaid.run({ nodes }); } catch (e) { /* 실패 시 원문 텍스트 유지 */ }
}

/* ── 라이브러리 lazy 로더 (md 첫 진입 시 1회) ──────────────── */
let _docsMdLibs = null;
function docsLoadScript(src) {
  return new Promise((res, rej) => {
    const s = document.createElement('script');
    s.src = src; s.onload = res; s.onerror = () => rej(new Error('스크립트 로드 실패: ' + src));
    document.head.appendChild(s);
  });
}
function docsLoadCss(href) {
  return new Promise((res) => {
    const l = document.createElement('link');
    l.rel = 'stylesheet'; l.href = href; l.onload = res; l.onerror = res;
    document.head.appendChild(l);
  });
}
function docsEnsureMdLibs() {
  if (_docsMdLibs) return _docsMdLibs;
  _docsMdLibs = (async () => {
    docsLoadCss('/lib/highlight-github.min.css');
    docsLoadCss('/lib/katex/katex.min.css');
    await docsLoadScript('/lib/marked.min.js');
    // raw HTML escape — 터미널 덤프에 포함된 <div class="hidden"> 등이 DOM을 깨는 것 방지
    marked.use({
      renderer: { html: s => s.replace(/</g, '&lt;').replace(/>/g, '&gt;') }
    });
    await docsLoadScript('/lib/purify.min.js');
    await docsLoadScript('/lib/highlight.min.js');
    await docsLoadScript('/lib/katex/katex.min.js');
    await docsLoadScript('/lib/katex/auto-render.min.js');
    await docsLoadScript('/lib/mermaid.min.js');
    try { mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'strict' }); } catch (e) {}
  })();
  return _docsMdLibs;
}

/* ── 뷰어 툴바 액션 ────────────────────────────────────────── */
function docsRenderViewerActions() {
  const host = document.getElementById('docs-viewer-actions');
  const f = docsState.curFile;
  if (!f) { host.innerHTML = ''; return; }
  const isText = DOCS_TEXT_EXT.includes(f.ext) || DOCS_MD_EXT.includes(f.ext);
  let h = '';
  if (docsState.editing) {
    h += `<button class="btn-a btn-sm" onclick="docsEditSave()">💾 저장</button>`;
    h += `<button class="btn-b btn-sm" onclick="docsEditCancel()">취소</button>`;
  } else {
    if (isText && docsState.canManage) h += `<button class="btn-b btn-sm" onclick="docsEditStart()">✎ 편집</button>`;
    if (isText) h += `<button class="btn-b btn-sm" onclick="docsExportPdf()" title="인쇄 창에서 'PDF로 저장' 선택">🖨 PDF</button>`;
    h += `<a class="btn-b btn-sm" href="/api/docs/raw?path=${encodeURIComponent(f.path)}&download=true">⬇ 다운로드</a>`;
  }
  host.innerHTML = h;
}

/* ── 인라인 편집 ───────────────────────────────────────────── */
async function docsEditStart() {
  const f = docsState.curFile;
  if (!f || !docsState.canManage) return;
  let d;
  try { d = await docsFetchText(f.path); } catch (e) { alert(e.message); return; }
  docsState.editing = true;
  docsRenderViewerActions();
  const isMd = DOCS_MD_EXT.includes(f.ext);
  const v = docsViewer();
  v.className = 'docs-viewer mode-edit' + (isMd ? ' is-md' : '');
  v.innerHTML =
    `<div class="docs-edit-wrap">
       <textarea id="docs-edit-ta" class="docs-edit-ta" spellcheck="false"></textarea>
       ${isMd ? '<div id="docs-edit-prev" class="md-body docs-edit-prev"></div>' : ''}
     </div>`;
  const ta = document.getElementById('docs-edit-ta');
  ta.value = d.text;
  if (isMd) {
    await docsEnsureMdLibs();
    const prev = document.getElementById('docs-edit-prev');
    const upd = docsDebounce(() => docsRenderMarkdownInto(prev, ta.value), 250);
    ta.addEventListener('input', upd);
    docsRenderMarkdownInto(prev, ta.value);
  }
}

async function docsEditSave() {
  const f = docsState.curFile;
  const ta = document.getElementById('docs-edit-ta');
  if (!f || !ta) return;
  try {
    const r = await fetch('/api/docs/text', {
      method: 'PUT',
      headers: docsHeaders({ 'Content-Type': 'application/json' }),
      body: JSON.stringify({ path: f.path, content: ta.value }),
    });
    const d = await r.json();
    if (!d.success) { alert('저장 실패: ' + (d.error || r.status)); return; }
    docsState.editing = false;
    docsOpenPath(f.path);
    docsToast('저장됨');
  } catch (e) { alert('저장 오류: ' + e.message); }
}

function docsEditCancel() {
  docsState.editing = false;
  if (docsState.curFile) docsOpenPath(docsState.curFile.path);
}

/* ── 관리: 새 폴더 / 업로드 / 이름변경 / 삭제 ──────────────── */
async function docsMkdirPrompt() {
  const path = prompt('새 폴더 경로 (루트 기준, 예: docs/notes)');
  if (!path) return;
  try {
    const r = await fetch('/api/docs/mkdir', {
      method: 'POST', headers: docsHeaders({ 'Content-Type': 'application/json' }),
      body: JSON.stringify({ path }),
    });
    const d = await r.json();
    if (!d.success) { alert('실패: ' + (d.error || '')); return; }
    await docsRefresh();
    docsToast('폴더 생성됨');
  } catch (e) { alert(e.message); }
}

function docsUploadPrompt() {
  document.getElementById('docs-upload-input').click();
}

async function docsUploadDo() {
  const input = document.getElementById('docs-upload-input');
  const file = input.files && input.files[0];
  if (!file) return;
  const dir = prompt('업로드할 대상 폴더 (루트 기준, 비우면 루트)', '') || '';
  const fd = new FormData();
  fd.append('file', file);
  fd.append('path', dir);
  try {
    const r = await fetch('/api/docs/upload', { method: 'POST', headers: docsHeaders(), body: fd });
    const d = await r.json();
    input.value = '';
    if (!d.success) { alert('업로드 실패: ' + (d.error || '')); return; }
    await docsRefresh();
    docsToast('업로드됨: ' + d.path);
  } catch (e) { input.value = ''; alert(e.message); }
}

async function docsRenamePrompt(path) {
  if (!docsState.canManage) return;
  const next = prompt('새 경로 (루트 기준)', path);
  if (!next || next === path) return;
  try {
    const r = await fetch('/api/docs/rename', {
      method: 'POST', headers: docsHeaders({ 'Content-Type': 'application/json' }),
      body: JSON.stringify({ from: path, to: next }),
    });
    const d = await r.json();
    if (!d.success) { alert('실패: ' + (d.error || '')); return; }
    await docsRefresh();
    docsToast('이름 변경됨');
  } catch (e) { alert(e.message); }
}

async function docsDeletePrompt(path, type) {
  if (!docsState.canManage) return;
  const isDir = type === 'dir';
  const msg = isDir
    ? `폴더와 그 안의 모든 내용을 삭제합니다:\n${path}\n\n되돌릴 수 없습니다. 계속할까요?`
    : `파일을 삭제합니다:\n${path}\n\n계속할까요?`;
  if (!confirm(msg)) return;
  try {
    const url = '/api/docs?path=' + encodeURIComponent(path) + (isDir ? '&recursive=true' : '');
    const r = await fetch(url, { method: 'DELETE', headers: docsHeaders() });
    const d = await r.json();
    if (!d.success) { alert('삭제 실패: ' + (d.error || '')); return; }
    if (docsState.curFile && docsState.curFile.path === path) {
      docsState.curFile = null;
      document.getElementById('docs-cur-path').textContent = '파일을 선택하세요';
      document.getElementById('docs-viewer-actions').innerHTML = '';
      docsViewer().className = 'docs-viewer';
      docsViewer().innerHTML = '<div class="docs-empty">← 왼쪽에서 문서를 선택하세요</div>';
    }
    await docsRefresh();
    docsToast('삭제됨');
  } catch (e) { alert(e.message); }
}

/* ── 필터 ──────────────────────────────────────────────────── */
function docsApplyFilter() {
  const q = (document.getElementById('docs-filter').value || '').toLowerCase().trim();
  document.querySelectorAll('#docs-tree .docs-li').forEach(li => {
    const row = li.querySelector(':scope > .docs-node');
    if (!row) return;
    if (row.dataset.type === 'dir') { li.style.display = ''; return; }
    li.style.display = (!q || row.dataset.name.includes(q)) ? '' : 'none';
  });
}

/* ── 유틸 ──────────────────────────────────────────────────── */
function docsFmtSize(n) {
  if (n == null) return '';
  if (n < 1024) return n + ' B';
  if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
  return (n / 1024 / 1024).toFixed(1) + ' MB';
}

function docsFileIcon(ext) {
  ext = (ext || '').toLowerCase();
  if (DOCS_MD_EXT.includes(ext)) return '📝';
  if (ext === 'pdf') return '📕';
  if (DOCS_TEXT_EXT.includes(ext)) return '📄';
  if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'].includes(ext)) return '🖼';
  if (['xlsx', 'xls', 'csv'].includes(ext)) return '📊';
  if (['docx', 'doc'].includes(ext)) return '📘';
  if (['json', 'xml', 'yaml', 'yml'].includes(ext)) return '🔧';
  if (['dxf', 'dwg'].includes(ext)) return '📐';
  return '📄';
}

function docsDebounce(fn, ms) {
  let t;
  return function (...a) { clearTimeout(t); t = setTimeout(() => fn.apply(this, a), ms); };
}

let _docsToastTimer = null;
function docsToast(msg) {
  let el = document.getElementById('docs-toast');
  if (!el) {
    el = document.createElement('div');
    el.id = 'docs-toast';
    el.className = 'docs-toast';
    document.body.appendChild(el);
  }
  el.textContent = msg;
  el.classList.add('show');
  clearTimeout(_docsToastTimer);
  _docsToastTimer = setTimeout(() => el.classList.remove('show'), 2200);
}
