<script>
  // 문서 /documents — 확정 시안 'DEVONthink 컬럼 브라우저' 전면 재작성.
  // 레이아웃: [리스트 컬럼 ~340px] [리더(DocumentViewer) 1fr] [인스펙터 ~300px, ⓘ 토글].
  // 데스크탑=3-pane / 모바일=흐름형(리스트 → 풀스크린 리더 + 정보 하단시트 Drawer).
  // 기존 기능(검색·모드·AI답변·필터칩·일괄작업·키보드내비·업로드·페이지네이션)은 전부 흡수.
  // See Also(관련 문서)는 벡터 유사도 엔드포인트 부재(코드 TODO)라 degrade — eid 세션 후 백엔드.
  import { page } from '$app/stores';
  import { goto } from '$app/navigation';
  import { api } from '$lib/api';
  import { addToast } from '$lib/stores/toast';
  import { Info, X, Plus, Trash2, Tag, FolderTree, Sparkles, ChevronLeft, ArrowUpDown } from 'lucide-svelte';
  import DocumentViewer from '$lib/components/DocumentViewer.svelte';
  import MarkdownStatusBadge from '$lib/components/MarkdownStatusBadge.svelte';
  import { isMdStatusVisible } from '$lib/utils/mdStatus';
  import UploadDropzone from '$lib/components/UploadDropzone.svelte';
  import Drawer from '$lib/components/ui/Drawer.svelte';
  import Modal from '$lib/components/ui/Modal.svelte';
  import ConfirmDialog from '$lib/components/ui/ConfirmDialog.svelte';
  import Button from '$lib/components/ui/Button.svelte';
  import Select from '$lib/components/ui/Select.svelte';
  import TextInput from '$lib/components/ui/TextInput.svelte';
  import { ui } from '$lib/stores/uiState.svelte';
  import { useIsXl } from '$lib/composables/useMedia.svelte';
  import { useListKeyboardNav } from '$lib/composables/useListKeyboardNav.svelte';
  import { pLimit } from '$lib/utils/pLimit';
  import { isQuestion } from '$lib/utils/isQuestion';
  import { domainBgClass, domainLabel } from '$lib/utils/domainSlug';
  import AskAnswerCard from '$lib/components/AskAnswerCard.svelte';

  const FORMATS = ['pdf', 'hwp', 'hwpx', 'md', 'docx', 'xlsx', 'png', 'jpg'];

  const DOMAIN_OPTIONS = [
    { value: 'Knowledge/Philosophy', label: 'Knowledge / Philosophy' },
    { value: 'Knowledge/Language', label: 'Knowledge / Language' },
    { value: 'Knowledge/Engineering', label: 'Knowledge / Engineering' },
    { value: 'Knowledge/Industrial_Safety', label: 'Knowledge / Industrial Safety' },
    { value: 'Knowledge/Programming', label: 'Knowledge / Programming' },
    { value: 'Knowledge/General', label: 'Knowledge / General' },
    { value: 'Reference', label: 'Reference' },
  ];

  const MAX_SELECTION = 50;

  let documents = $state([]);
  let total = $state(0);
  let loading = $state(true);
  let searchQuery = $state('');
  let searchMode = $state('hybrid');
  let searchResults = $state(null);
  let selectedDoc = $state(null);

  // 이드 답변 상태 (질문형 검색)
  let askData = $state(null);
  let askLoading = $state(false);
  let askError = $state(false);
  let askDismissed = $state(false);

  async function askSearch(q) {
    askLoading = true; askError = false; askData = null;
    try {
      askData = await api(`/search/ask?q=${encodeURIComponent(q)}&limit=10`);
    } catch {
      askError = true; askData = null;
    } finally {
      askLoading = false;
    }
  }

  let showAskCard = $derived(
    !askDismissed && (
      askLoading ||
      (askData && !askData.refused && askData.ai_answer && askData.synthesis_status === 'completed')
    )
  );

  // 인스펙터(우측) 토글 — xl+ inline, < xl Drawer.
  const isXl = useIsXl();
  let inspectorOpen = $state(
    typeof localStorage !== 'undefined' ? localStorage.getItem('inspectorOpen') !== 'false' : true
  );
  function setInspectorOpen(v) {
    inspectorOpen = v;
    if (typeof localStorage !== 'undefined') localStorage.setItem('inspectorOpen', String(v));
  }
  function toggleInfoPanel() {
    if (!selectedDoc) return;
    if (isXl.current) setInspectorOpen(!inspectorOpen);
    else { if (ui.isDrawerOpen('meta')) ui.closeDrawer(); else ui.openDrawer('meta'); }
  }
  let isPanelActive = $derived(
    selectedDoc ? (isXl.current ? inspectorOpen : ui.isDrawerOpen('meta')) : false
  );
  $effect(() => {
    if (isXl.current && ui.isDrawerOpen('meta')) ui.closeDrawer();
  });

  // URL params → filter
  let currentPage = $derived(parseInt($page.url.searchParams.get('page') || '1'));
  let filterDomain = $derived($page.url.searchParams.get('domain') || '');
  let filterSubGroup = $derived($page.url.searchParams.get('sub_group') || '');
  let filterTag = $derived($page.url.searchParams.get('tag') || '');
  let filterSource = $derived($page.url.searchParams.get('source') || '');
  let filterFormat = $derived($page.url.searchParams.get('format') || '');
  let filterCategory = $derived($page.url.searchParams.get('category') || '');

  // 필터 칩 popover
  let tagPopoverOpen = $state(false);
  let formatPopoverOpen = $state(false);

  // 다중 선택 + bulk
  let selectedIds = $state(new Set());
  let bulkDomainValue = $state('');
  let bulkTagValue = $state('');
  let bulkBusy = $state(false);

  // 정렬 (클라이언트, 현재 페이지 한정) — 시안 제목/수정 sortable
  let sortKey = $state(null); // 'title' | 'updated' | null(서버순서)
  let sortDir = $state('asc');
  function toggleSort(key) {
    if (sortKey === key) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
    else { sortKey = key; sortDir = key === 'updated' ? 'desc' : 'asc'; }
  }

  // 키보드 네비게이션
  let kbIndex = $state(0);
  function scrollSelectedIntoView() {
    queueMicrotask(() => {
      const el = document.querySelector('[data-kb-selected="true"]');
      if (el instanceof HTMLElement) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
    });
  }
  useListKeyboardNav({
    get items() { return sortedItems; },
    get index() { return kbIndex; },
    setIndex(i) { kbIndex = i; scrollSelectedIntoView(); },
    onenter(doc) { if (doc) selectDoc(doc); },
  });

  $effect(() => {
    const _p = currentPage, _d = filterDomain, _s = filterSubGroup, _t = filterTag,
      _src = filterSource, _f = filterFormat, _c = filterCategory;
    const urlQ = $page.url.searchParams.get('q') || '';
    const urlMode = $page.url.searchParams.get('mode') || 'hybrid';
    searchQuery = urlQ;
    searchMode = urlMode;
    selectedDoc = null;
    selectedIds = new Set();
    kbIndex = 0;
    askData = null; askLoading = false; askError = false; askDismissed = false;
    if (ui.isDrawerOpen('meta')) ui.closeDrawer();
    if (urlQ) doSearch(urlQ, urlMode);
    else loadDocuments();
  });

  async function loadDocuments() {
    loading = true;
    searchResults = null;
    try {
      const params = new URLSearchParams();
      params.set('page', String(currentPage));
      params.set('page_size', '50');
      if (filterDomain) params.set('domain', filterDomain);
      if (filterSubGroup) params.set('sub_group', filterSubGroup);
      if (filterTag) params.set('tag', filterTag);
      if (filterSource) params.set('source', filterSource);
      if (filterFormat) params.set('format', filterFormat);
      if (filterCategory) params.set('category', filterCategory);
      const data = await api(`/documents/?${params}`);
      documents = data.items;
      total = data.total;
    } catch {
      addToast('error', '문서 목록 로딩 실패');
    } finally {
      loading = false;
    }
  }

  function submitSearch() {
    const params = new URLSearchParams($page.url.searchParams);
    params.delete('page');
    if (searchQuery.trim()) params.set('q', searchQuery.trim()); else params.delete('q');
    if (searchMode !== 'hybrid') params.set('mode', searchMode); else params.delete('mode');
    for (const [key, val] of [...params.entries()]) if (!val) params.delete(key);
    const qs = params.toString();
    goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
  }
  function handleSearchKeydown(e) {
    if (e.key === 'Enter' && !e.isComposing) {
      e.preventDefault(); e.stopPropagation(); submitSearch();
    }
  }

  async function doSearch(q, mode) {
    loading = true;
    if (isQuestion(q)) askSearch(q);
    try {
      const data = await api(`/search/?q=${encodeURIComponent(q)}&mode=${mode}&limit=50`);
      searchResults = data.results;
      total = data.total;
    } catch {
      addToast('error', '검색 실패');
      searchResults = [];
    } finally {
      loading = false;
    }
  }

  function changePage(p) {
    const params = new URLSearchParams($page.url.searchParams);
    if (p > 1) params.set('page', String(p)); else params.delete('page');
    const qs = params.toString();
    goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
  }

  function clearAllFilters() {
    goto('/documents', { noScroll: true });
    searchQuery = '';
  }

  function addFilter(key, value) {
    const params = new URLSearchParams($page.url.searchParams);
    params.set(key, value);
    params.delete('page');
    const qs = params.toString();
    goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
    tagPopoverOpen = false; formatPopoverOpen = false;
  }
  function removeFilter(key) {
    const params = new URLSearchParams($page.url.searchParams);
    params.delete(key);
    if (key === 'domain') params.delete('sub_group');
    const qs = params.toString();
    goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
  }

  async function selectDoc(doc) {
    if (selectedDoc?.id === doc.id) { selectedDoc = null; return; }
    selectedDoc = doc; // 즉시 표시(리더 + 기본 인스펙터)
    // 인스펙터 풀 메타 하이드레이션 — 검색 결과(SearchResult)는 메타가 빈약(태그/크기/하위/md상태/읽음 없음).
    // 풀 문서를 조회해 채운다(기존 GET /documents/{id}, 백엔드 무변). 리스트 모드도 md상태 등 보강.
    try {
      const full = await api(`/documents/${doc.id}`);
      if (selectedDoc?.id === doc.id) selectedDoc = { ...doc, ...full };
    } catch { /* 실패 시 기본 정보 유지 */ }
  }

  // bulk 선택
  function toggleRowSelect(id, checked) {
    const next = new Set(selectedIds);
    if (checked) next.add(id); else next.delete(id);
    selectedIds = next;
  }
  function selectAll() { selectedIds = new Set(sortedItems.map((d) => d.id)); }
  function clearSelection() { selectedIds = new Set(); }

  async function runBulk(label, taskFn) {
    const ids = [...selectedIds];
    if (ids.length === 0 || ids.length > MAX_SELECTION) return;
    bulkBusy = true;
    const limit = pLimit(5);
    const results = await Promise.allSettled(ids.map((id) => limit(() => taskFn(id))));
    bulkBusy = false;
    const success = results.filter((r) => r.status === 'fulfilled').length;
    const failed = results.length - success;
    if (failed > 0) addToast('error', `${label}: ${failed}건 실패`);
    if (success > 0) addToast('success', `${label}: ${success}건 완료`);
    clearSelection();
    loadDocuments();
  }
  async function bulkApplyDomain() {
    if (!bulkDomainValue) return;
    const domain = bulkDomainValue;
    await runBulk('도메인 변경', (id) =>
      api(`/documents/${id}`, { method: 'PATCH', body: JSON.stringify({ ai_domain: domain }) }));
    bulkDomainValue = '';
    ui.closeModal('bulk-domain');
  }
  async function bulkAddTag() {
    const tag = bulkTagValue.trim();
    if (!tag) return;
    await runBulk('태그 추가', async (id) => {
      const doc = sortedItems.find((d) => d.id === id);
      // ★ 검색 결과(SearchResult)는 ai_tags 가 없음 → 빈 배열로 PATCH 하면 기존 태그 전체 유실(덮어쓰기).
      //   미확인이면 풀 문서를 조회해 실제 태그를 머지(데이터 손실 방지).
      let existing = Array.isArray(doc?.ai_tags) ? doc.ai_tags : null;
      if (existing == null) {
        try { const full = await api(`/documents/${id}`); existing = Array.isArray(full?.ai_tags) ? full.ai_tags : []; }
        catch { return; } // 현재 태그를 못 읽으면 덮어쓰지 않고 건너뜀
      }
      if (existing.includes(tag)) return;
      return api(`/documents/${id}`, { method: 'PATCH', body: JSON.stringify({ ai_tags: [...existing, tag] }) });
    });
    bulkTagValue = '';
    ui.closeModal('bulk-tag');
  }
  async function bulkDelete() {
    await runBulk('삭제', (id) => api(`/documents/${id}?delete_file=true`, { method: 'DELETE' }));
  }

  function handleKeydown(e) {
    if (e.key === 'Escape') {
      if (selectedDoc && !ui.isDrawerOpen('meta') && !isXl.current) { selectedDoc = null; return; }
      ui.handleEscape();
    }
  }

  let totalPages = $derived(Math.ceil(total / 50));
  let items = $derived(searchResults || documents);
  let sortedItems = $derived.by(() => {
    // 검색 모드는 서버 관련도순 유지 + SearchResult 에 updated_at 부재(NaN 비교=무의미) → 클라 정렬 비활성
    if (!sortKey || searchResults) return items;
    const arr = [...items];
    const dir = sortDir === 'asc' ? 1 : -1;
    arr.sort((a, b) => {
      if (sortKey === 'title') return dir * String(a.title || '').localeCompare(String(b.title || ''), 'ko');
      const ad = new Date(a.updated_at || a.created_at).getTime();
      const bd = new Date(b.updated_at || b.created_at).getTime();
      return dir * (ad - bd);
    });
    return arr;
  });
  let hasActiveFilters = $derived(
    !!filterDomain || !!filterSubGroup || !!filterTag || !!filterSource || !!filterFormat || !!searchQuery
  );
  let selectionCount = $derived(selectedIds.size);
  let selectionOverLimit = $derived(selectionCount > MAX_SELECTION);
  let allVisibleSelected = $derived(
    sortedItems.length > 0 && sortedItems.every((d) => selectedIds.has(d.id))
  );

  $effect(() => {
    if (kbIndex >= sortedItems.length && sortedItems.length > 0) kbIndex = sortedItems.length - 1;
    else if (sortedItems.length === 0) kbIndex = 0;
  });
  let kbSelectedId = $derived(sortedItems[kbIndex]?.id ?? null);

  // 현재 결과 상위 태그 (클라 집계)
  let topTags = $derived.by(() => {
    const counts = new Map();
    for (const d of items) for (const t of d.ai_tags || []) counts.set(t, (counts.get(t) || 0) + 1);
    return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20);
  });

  // 바깥 클릭으로 popover 닫기
  $effect(() => {
    if (!tagPopoverOpen && !formatPopoverOpen) return;
    function onDocClick(e) {
      const target = e.target;
      if (!(target instanceof Element)) return;
      if (!target.closest('[data-popover-root]')) { tagPopoverOpen = false; formatPopoverOpen = false; }
    }
    document.addEventListener('click', onDocClick);
    return () => document.removeEventListener('click', onDocClick);
  });

  // ─── 표시 헬퍼 ───
  function formatBadge(fmt) {
    const f = (fmt || '').toLowerCase();
    const label = f === 'md' ? 'MD' : `${(f || '?').toUpperCase()}→MD`;
    let cls = 'bg-surface-active text-dim';
    if (f === 'md') cls = 'bg-success/15 text-success';
    else if (f === 'pdf') cls = 'bg-error/10 text-error';
    else if (f === 'hwp' || f === 'hwpx') cls = 'bg-warning/15 text-warning';
    else if (f === 'xlsx' || f === 'docx' || f === 'pptx') cls = 'bg-domain-engineering/15 text-domain-engineering';
    return { label, cls };
  }
  function shortDate(iso) {
    if (!iso) return '';
    const d = new Date(iso);
    const now = new Date();
    const mm = String(d.getMonth() + 1).padStart(2, '0');
    const dd = String(d.getDate()).padStart(2, '0');
    return d.getFullYear() === now.getFullYear()
      ? `${mm}-${dd}`
      : `${String(d.getFullYear()).slice(2)}-${mm}-${dd}`;
  }
  function fileSizeLabel(bytes) {
    if (bytes == null) return null;
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
    return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  }
</script>

<svelte:window on:keydown={handleKeydown} />

<div class="flex h-full min-h-0">

  <!-- ═══ 좌: 리스트 컬럼 ═══ -->
  <div class="{selectedDoc ? 'hidden lg:flex' : 'flex'} flex-col w-full lg:w-[340px] lg:shrink-0 lg:border-r border-default min-h-0">
    <UploadDropzone onupload={loadDocuments} />

    <!-- 검색바 -->
    <div class="flex gap-2 px-3 py-2 shrink-0">
      <input
        data-search-input
        type="text"
        bind:value={searchQuery}
        onkeydown={handleSearchKeydown}
        placeholder="검색 후 Enter (/ 포커스)"
        class="flex-1 min-w-0 px-3 py-1.5 bg-surface border border-default rounded-lg text-text text-sm focus:border-accent outline-none"
      />
      <select
        bind:value={searchMode}
        onchange={() => { if (searchQuery) submitSearch(); }}
        class="hidden sm:block px-2 py-1.5 bg-surface border border-default rounded-lg text-text text-xs"
      >
        <option value="hybrid">하이브리드</option>
        <option value="fts">전문검색</option>
        <option value="trgm">부분매칭</option>
        <option value="vector">의미검색</option>
      </select>
      {#if searchQuery.trim()}
        <a
          href={`/ask?q=${encodeURIComponent(searchQuery.trim())}`}
          class="flex items-center px-2 py-1.5 rounded-lg border border-default text-dim hover:text-accent hover:border-accent transition-colors"
          title="이 쿼리로 AI 답변"
        ><Sparkles size={14} /></a>
      {/if}
    </div>

    <!-- 필터 칩 row -->
    <div class="flex flex-wrap items-center gap-1.5 px-3 pb-2 shrink-0">
      {#if filterDomain}
        <span class="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded bg-accent/15 text-accent border border-accent/30">
          {filterDomain.replace('Knowledge/', '')}{filterSubGroup ? ` / ${filterSubGroup}` : ''}
          <button type="button" onclick={() => removeFilter('domain')} class="hover:text-error -mr-0.5 ml-0.5" aria-label="도메인 필터 제거"><X size={10} /></button>
        </span>
      {/if}
      {#if filterTag}
        <span class="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded bg-surface text-text border border-default">
          #{filterTag}
          <button type="button" onclick={() => removeFilter('tag')} class="hover:text-error -mr-0.5 ml-0.5" aria-label="태그 필터 제거"><X size={10} /></button>
        </span>
      {/if}
      {#if filterFormat}
        <span class="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded bg-surface text-text border border-default uppercase">
          {filterFormat}
          <button type="button" onclick={() => removeFilter('format')} class="hover:text-error -mr-0.5 ml-0.5" aria-label="형식 필터 제거"><X size={10} /></button>
        </span>
      {/if}
      {#if filterSource}
        <span class="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded bg-surface text-text border border-default">
          {filterSource}
          <button type="button" onclick={() => removeFilter('source')} class="hover:text-error -mr-0.5 ml-0.5" aria-label="소스 필터 제거"><X size={10} /></button>
        </span>
      {/if}

      <!-- + 태그 -->
      <div class="relative" data-popover-root>
        <button type="button" onclick={() => { tagPopoverOpen = !tagPopoverOpen; formatPopoverOpen = false; }}
          class="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded bg-surface text-dim border border-default border-dashed hover:text-text hover:border-accent transition-colors"
          aria-haspopup="listbox" aria-expanded={tagPopoverOpen}><Plus size={10} /> 태그</button>
        {#if tagPopoverOpen}
          <div class="absolute top-full left-0 mt-1 z-dropdown bg-surface border border-default rounded-md shadow-lg min-w-[180px] max-h-[280px] overflow-y-auto py-1" role="listbox" aria-label="태그 선택">
            {#if topTags.length === 0}
              <p class="text-[10px] text-dim px-3 py-2">현재 결과에 태그 없음</p>
            {:else}
              {#each topTags as [tag, count]}
                <button type="button" role="option" aria-selected={filterTag === tag} onclick={() => addFilter('tag', tag)}
                  class="flex items-center justify-between w-full text-left text-[11px] px-3 py-1 text-text hover:bg-surface-hover">
                  <span>#{tag}</span><span class="text-dim ml-2 tabular-nums">{count}</span>
                </button>
              {/each}
            {/if}
          </div>
        {/if}
      </div>

      <!-- + 형식 -->
      <div class="relative" data-popover-root>
        <button type="button" onclick={() => { formatPopoverOpen = !formatPopoverOpen; tagPopoverOpen = false; }}
          class="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded bg-surface text-dim border border-default border-dashed hover:text-text hover:border-accent transition-colors"
          aria-haspopup="listbox" aria-expanded={formatPopoverOpen}><Plus size={10} /> 형식</button>
        {#if formatPopoverOpen}
          <div class="absolute top-full left-0 mt-1 z-dropdown bg-surface border border-default rounded-md shadow-lg min-w-[140px] py-1" role="listbox" aria-label="형식 선택">
            {#each FORMATS as fmt}
              <button type="button" role="option" aria-selected={filterFormat === fmt} onclick={() => addFilter('format', fmt)}
                class="flex items-center w-full text-left text-[11px] px-3 py-1 text-text hover:bg-surface-hover uppercase">{fmt}</button>
            {/each}
          </div>
        {/if}
      </div>

      {#if hasActiveFilters}
        <button type="button" onclick={clearAllFilters} class="text-[10px] text-dim hover:text-text px-2 py-0.5 ml-auto">초기화</button>
      {/if}
    </div>

    <!-- 선택 toolbar -->
    {#if selectionCount > 0}
      <div class="flex flex-wrap items-center gap-2 px-3 py-2 shrink-0 bg-accent/10 border-y border-accent/30">
        <span class="text-xs font-medium {selectionOverLimit ? 'text-error' : 'text-accent'}">
          {selectionCount}건 선택{#if selectionOverLimit}<span class="ml-1">(최대 {MAX_SELECTION} 초과)</span>{/if}
        </span>
        <button type="button" onclick={selectAll} disabled={allVisibleSelected} class="text-[11px] text-dim hover:text-text disabled:opacity-40">전체</button>
        <button type="button" onclick={clearSelection} class="text-[11px] text-dim hover:text-text">해제</button>
        <div class="flex-1"></div>
        <button type="button" title="일괄 도메인" disabled={selectionOverLimit || bulkBusy} onclick={() => ui.openModal('bulk-domain')}
          class="p-1.5 rounded-lg border border-default text-dim hover:text-accent hover:border-accent disabled:opacity-40"><FolderTree size={15} /></button>
        <button type="button" title="일괄 태그" disabled={selectionOverLimit || bulkBusy} onclick={() => ui.openModal('bulk-tag')}
          class="p-1.5 rounded-lg border border-default text-dim hover:text-accent hover:border-accent disabled:opacity-40"><Tag size={15} /></button>
        <button type="button" title="일괄 삭제" disabled={selectionOverLimit || bulkBusy} onclick={() => ui.openModal('bulk-delete')}
          class="p-1.5 rounded-lg border border-default text-dim hover:text-error hover:border-error disabled:opacity-40"><Trash2 size={15} /></button>
      </div>
    {/if}

    <!-- 컬럼 헤더 + 결과 카운트 -->
    <div class="flex items-center gap-2 px-3 py-1.5 shrink-0 border-b border-default bg-surface text-[10px] uppercase tracking-wide text-dim font-bold">
      {#if searchResults}
        <!-- 검색 모드: 서버 관련도순(클라 정렬 비활성, SearchResult 메타 부재) -->
        <span>관련도순</span>
        <span class="flex-1"></span>
        {#if !loading}<span class="text-faint normal-case font-normal tracking-normal">{total}건</span>{/if}
      {:else}
        <button type="button" onclick={() => toggleSort('title')} class="flex items-center gap-1 hover:text-accent {sortKey === 'title' ? 'text-accent' : ''}">
          제목 {#if sortKey === 'title'}<span>{sortDir === 'asc' ? '↑' : '↓'}</span>{:else}<ArrowUpDown size={10} class="opacity-50" />{/if}
        </button>
        <span class="flex-1"></span>
        {#if !loading}<span class="text-faint normal-case font-normal tracking-normal">{total}건</span>{/if}
        <button type="button" onclick={() => toggleSort('updated')} class="flex items-center gap-1 hover:text-accent {sortKey === 'updated' ? 'text-accent' : ''}">
          수정 {#if sortKey === 'updated'}<span>{sortDir === 'asc' ? '↑' : '↓'}</span>{/if}
        </button>
      {/if}
    </div>

    <!-- 리스트 본문 -->
    <div class="flex-1 overflow-y-auto overflow-x-hidden min-h-0">
      {#if loading}
        <div class="p-3 space-y-1.5">
          {#each Array(8) as _}<div class="bg-surface rounded-lg border border-default animate-pulse h-12"></div>{/each}
        </div>
      {:else if sortedItems.length === 0}
        <div class="text-center py-12 px-4 text-dim">
          {#if searchQuery}
            <p class="text-sm mb-2">'{searchQuery}' 결과 없음</p>
            <button onclick={clearAllFilters} class="text-xs text-accent hover:underline">필터 초기화</button>
          {:else if hasActiveFilters}
            <p class="text-sm mb-2">이 분류에 문서가 없습니다</p>
            <button onclick={clearAllFilters} class="text-xs text-accent hover:underline">필터 초기화</button>
          {:else}
            <p class="text-sm">등록된 문서가 없습니다</p>
          {/if}
        </div>
      {:else}
        {#each sortedItems as doc (doc.id)}
          {@const badge = formatBadge(doc.file_format)}
          {@const isSel = selectedDoc?.id === doc.id}
          <div
            data-kb-selected={doc.id === kbSelectedId}
            class="group flex items-start gap-2 px-3 py-2.5 border-b border-default cursor-pointer transition-colors
              {isSel ? 'bg-accent/12' : 'odd:bg-surface/40 hover:bg-surface-hover'}
              {doc.id === kbSelectedId && !isSel ? 'ring-1 ring-inset ring-accent/40' : ''}"
            onclick={() => selectDoc(doc)}
            onkeydown={(e) => { if (e.key === 'Enter') selectDoc(doc); }}
            role="button"
            tabindex="0"
          >
            <input
              type="checkbox"
              checked={selectedIds.has(doc.id)}
              onclick={(e) => { e.stopPropagation(); toggleRowSelect(doc.id, e.currentTarget.checked); }}
              class="mt-1 shrink-0 accent-accent {selectedIds.size > 0 ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity"
              aria-label="선택"
            />
            <span class="w-2 h-2 rounded-sm mt-1.5 shrink-0 {domainBgClass(doc.ai_domain)}"></span>
            <div class="flex-1 min-w-0">
              <div class="text-[13px] font-medium text-text leading-snug truncate {isSel ? 'text-accent' : 'group-hover:text-accent'}">{doc.title || '제목 없음'}</div>
              <div class="flex items-center gap-2 mt-1">
                <span class="text-[10px] text-dim truncate">{domainLabel(doc.ai_domain)}</span>
                <span class="text-[10px] text-faint shrink-0 tabular-nums ml-auto">{shortDate(doc.updated_at || doc.created_at)}</span>
              </div>
            </div>
            <span class="text-[8.5px] font-extrabold rounded px-1.5 py-0.5 tracking-wide shrink-0 mt-0.5 {badge.cls}">{badge.label}</span>
          </div>
        {/each}

        {#if !searchResults && totalPages > 1}
          <div class="flex flex-wrap justify-center gap-1 p-3">
            {#each Array(totalPages) as _, i}
              <button onclick={() => changePage(i + 1)}
                class="px-2.5 py-0.5 rounded text-xs transition-colors {currentPage === i + 1 ? 'bg-accent text-white' : 'bg-surface text-dim hover:text-text'}">{i + 1}</button>
            {/each}
          </div>
        {/if}
      {/if}
    </div>
  </div>

  <!-- ═══ 중앙: 리더 ═══ -->
  <div class="{selectedDoc ? 'flex' : 'hidden lg:flex'} flex-1 min-w-0 flex-col min-h-0">
    {#if selectedDoc}
      <!-- 리더 상단 바: (모바일) 뒤로 / (lg) 인스펙터 토글 -->
      <div class="flex items-center gap-2 px-3 py-1.5 shrink-0 border-b border-default bg-sidebar">
        <button type="button" onclick={() => { selectedDoc = null; if (ui.isDrawerOpen('meta')) ui.closeDrawer(); }}
          class="lg:hidden flex items-center gap-1 text-xs text-accent-hover font-medium" aria-label="목록으로">
          <ChevronLeft size={15} /> 문서
        </button>
        <div class="flex-1"></div>
        <button type="button" onclick={toggleInfoPanel} aria-pressed={isPanelActive} title="문서 정보"
          class="p-1.5 rounded-lg border transition-colors {isPanelActive ? 'border-accent text-accent bg-accent/10' : 'border-default text-dim hover:text-accent hover:border-accent'}">
          <Info size={16} />
        </button>
      </div>
      <div class="flex-1 min-h-0">
        <DocumentViewer doc={selectedDoc} />
      </div>
    {:else if showAskCard}
      <div class="p-4 lg:p-6 overflow-y-auto">
        <AskAnswerCard
          data={askData}
          loading={askLoading}
          error={askError}
          onCitationClick={(docId) => goto(`/documents/${docId}`)}
          onDismiss={() => { askDismissed = true; }}
        />
      </div>
    {:else}
      <div class="hidden lg:flex flex-1 items-center justify-center text-dim text-sm">
        왼쪽에서 문서를 선택하세요
      </div>
    {/if}
  </div>

  <!-- ═══ 우: 인스펙터 (xl+ inline) ═══ -->
  {#if selectedDoc && inspectorOpen}
    <aside class="hidden xl:flex flex-col w-[300px] shrink-0 border-l border-default bg-sidebar overflow-y-auto" aria-label="문서 정보">
      {@render inspector(selectedDoc)}
    </aside>
  {/if}
</div>

<!-- < xl 폴백: Drawer (정보 하단/측면 시트) -->
<Drawer id="meta" side="right" width="rail" aria-label="문서 정보">
  {#if selectedDoc}
    <div class="overflow-y-auto h-full bg-sidebar">{@render inspector(selectedDoc)}</div>
  {/if}
</Drawer>

<!-- Bulk modals -->
<Modal id="bulk-domain" title="일괄 도메인 변경" size="sm">
  <p class="text-xs text-dim mb-3">{selectionCount}건의 문서에 새 도메인을 지정합니다.</p>
  <Select bind:value={bulkDomainValue} options={DOMAIN_OPTIONS} placeholder="도메인 선택" label="도메인" />
  {#snippet footer()}
    <Button variant="ghost" size="sm" onclick={() => ui.closeModal('bulk-domain')}>취소</Button>
    <Button variant="primary" size="sm" disabled={!bulkDomainValue} loading={bulkBusy} onclick={bulkApplyDomain}>적용</Button>
  {/snippet}
</Modal>

<Modal id="bulk-tag" title="일괄 태그 추가" size="sm">
  <p class="text-xs text-dim mb-3">{selectionCount}건의 문서에 태그를 추가합니다. 이미 같은 태그가 있으면 건너뜁니다.</p>
  <TextInput bind:value={bulkTagValue} placeholder="태그 입력" label="태그" />
  {#snippet footer()}
    <Button variant="ghost" size="sm" onclick={() => ui.closeModal('bulk-tag')}>취소</Button>
    <Button variant="primary" size="sm" disabled={!bulkTagValue.trim()} loading={bulkBusy} onclick={bulkAddTag}>추가</Button>
  {/snippet}
</Modal>

<ConfirmDialog
  id="bulk-delete"
  title="선택한 문서 삭제"
  message="{selectionCount}건의 문서를 원본 파일과 함께 삭제합니다. 되돌릴 수 없습니다."
  confirmLabel="삭제"
  tone="danger"
  loading={bulkBusy}
  onconfirm={bulkDelete}
/>

<!-- 인스펙터 콘텐츠 (데스크탑 aside + 모바일 drawer 공용) -->
{#snippet inspector(doc)}
  {@const fmt = formatBadge(doc.file_format)}
  {@const size = fileSizeLabel(doc.file_size)}
  <div class="flex flex-col gap-5 p-4">
    <!-- 정보 -->
    <div>
      <div class="text-[10px] font-extrabold uppercase tracking-wide text-faint mb-2">정보</div>
      <div class="flex flex-col">
        <div class="flex justify-between gap-2 text-xs py-1"><span class="text-dim">종류</span><span class="text-text font-medium text-right">{fmt.label}</span></div>
        {#if doc.ai_domain}<div class="flex justify-between gap-2 text-xs py-1"><span class="text-dim">도메인</span><span class="text-text font-medium text-right truncate">{domainLabel(doc.ai_domain)}</span></div>{/if}
        {#if doc.ai_sub_group}<div class="flex justify-between gap-2 text-xs py-1"><span class="text-dim">하위</span><span class="text-text font-medium text-right truncate">{doc.ai_sub_group}</span></div>{/if}
        <div class="flex justify-between gap-2 text-xs py-1"><span class="text-dim">수정</span><span class="text-text font-medium text-right">{shortDate(doc.updated_at || doc.created_at)}</span></div>
        {#if size}<div class="flex justify-between gap-2 text-xs py-1"><span class="text-dim">원본</span><span class="text-text font-medium text-right">{size}</span></div>{/if}
        {#if isMdStatusVisible(doc.md_status)}<div class="flex items-center justify-between gap-2 text-xs py-1"><span class="text-dim">md 상태</span><MarkdownStatusBadge mdStatus={doc.md_status} mdExtractionError={doc.md_extraction_error} mdExtractionQuality={doc.md_extraction_quality} /></div>{/if}
        {#if doc.read_count}<div class="flex justify-between gap-2 text-xs py-1"><span class="text-dim">읽음</span><span class="text-text font-medium text-right">{doc.read_count}회</span></div>{/if}
      </div>
    </div>

    <!-- 태그 -->
    {#if doc.ai_tags && doc.ai_tags.length > 0}
      <div>
        <div class="text-[10px] font-extrabold uppercase tracking-wide text-faint mb-2">태그</div>
        <div class="flex flex-wrap gap-1.5">
          {#each doc.ai_tags as t}
            <button type="button" onclick={() => addFilter('tag', t)} class="text-[10.5px] text-accent-hover bg-accent/12 rounded-full px-2.5 py-0.5 hover:bg-accent/20 transition-colors">{t}</button>
          {/each}
        </div>
      </div>
    {/if}

    <!-- See Also (벡터 유사도 엔드포인트 부재 — 준비 중) -->
    <div>
      <div class="text-[10px] font-extrabold uppercase tracking-wide text-faint mb-2">See Also (관련 문서)</div>
      <p class="text-[11px] text-faint leading-relaxed">벡터 유사도 기반 관련 문서 — 준비 중</p>
    </div>

    <!-- AI 분류 -->
    {#if doc.ai_domain}
      <div>
        <div class="text-[10px] font-extrabold uppercase tracking-wide text-faint mb-2">AI 분류</div>
        <div class="bg-surface border border-default rounded-lg p-3 text-xs leading-relaxed text-text">
          도메인 <strong>{domainLabel(doc.ai_domain)}</strong>{#if doc.ai_sub_group} · 하위 <strong>{doc.ai_sub_group}</strong>{/if}
          {#if doc.ai_confidence != null}<br /><span class="text-success font-bold mt-1 inline-block">신뢰도 {doc.ai_confidence.toFixed(2)}</span>{/if}
          {#if doc.ai_analysis_tier}<span class="text-faint ml-1">· {doc.ai_analysis_tier}</span>{/if}
        </div>
      </div>
    {/if}
  </div>
{/snippet}
