<script>
  /**
   * /study/topics/[id] — 학습 워크스페이스 통합 뷰.
   *
   * 한 주제(예: 가스기사) 아래에 묶인 필기 세션 + 자료 문서를 한 화면에서.
   * 응답은 sections + stats dict 구조 — 후속 PR 에서 audio_assets/vocab_decks/question_sets 키 추가 가능.
   */
  import { onMount, untrack } from 'svelte';
  import { page } from '$app/stores';
  import { goto } from '$app/navigation';
  import { api } from '$lib/api';
  import { addToast } from '$lib/stores/toast';
  import {
    ArrowLeft, FolderKanban, PenLine, BookOpen, Plus, Trash2, ArrowRight, Languages,
    ChevronRight, ChevronDown, FolderOpen, FolderPlus, HelpCircle, Edit, Play, CheckCircle2, XCircle,
    ListChecks,
  } from 'lucide-svelte';
  import Button from '$lib/components/ui/Button.svelte';
  import EmptyState from '$lib/components/ui/EmptyState.svelte';
  import Skeleton from '$lib/components/ui/Skeleton.svelte';
  import TextInput from '$lib/components/ui/TextInput.svelte';

  let topicId = $derived(Number($page.params.id));
  let detail = $state(null); // { topic, sections, stats }
  let loading = $state(true);

  // PR-10: 문제풀이 세션 (진행 중 + 최근 완료)
  let quizSessions = $state({ active: null, recent_done: [] });

  // PR-11: 문제 섹션 회차별 그룹 expand/collapse 상태. key = exam_round 표시 문자열.
  let roundsExpanded = $state({});

  // PR-12-A: 회차별 첫 expand 시 한 번만 bulk fetch — qid → {repeat_round_count, similar_round_count, ...}
  let relatedCounts = $state({});
  let roundsRelatedFetched = $state({}); // exam_round 표시 키 단위 fetch 여부

  // 자료 추가 모달
  let docModalOpen = $state(false);
  let docSearch = $state('');
  let docCandidates = $state([]); // {id, title, file_format, ...}
  let docSearching = $state(false);
  let docSelected = $state(new Set()); // Set<number>
  let docLinking = $state(false);

  // 카테고리 트리 (자료실 /api/library/tree)
  let tree = $state([]);
  let treeLoading = $state(false);
  let treeExpanded = $state({}); // {path: bool}
  let activeTreePath = $state(null); // 현재 선택된 카테고리 path (null = 전체)
  let bulkAddingPath = $state(null); // 진행 중 path (스피너용)

  // 자료 페이지네이션
  const DOC_PAGE_SIZE = 100; // 백엔드 max
  let docPage = $state(1);
  let docTotal = $state(0);

  // 통합뷰 자료 섹션 트리 그룹핑 (자료실 @library/<path> 기준)
  let docTreeExpanded = $state({}); // {fullPath: bool}

  /**
   * documents 를 첫 library_path 기준으로 트리 빌드.
   * 한 자료가 여러 카테고리에 속해도 그룹핑은 첫 path 만 사용 (단순화).
   */
  function buildDocTree(documents) {
    const root = { name: '', path: '', children: new Map(), documents: [] };
    const unclassified = [];
    let pathSet = new Set();

    for (const d of documents) {
      const paths = d.library_paths ?? [];
      if (paths.length === 0) {
        unclassified.push(d);
        continue;
      }
      const path = paths[0];
      pathSet.add(path);
      const segments = path.split('/');
      let node = root;
      for (let i = 0; i < segments.length; i++) {
        const seg = segments[i];
        const childPath = segments.slice(0, i + 1).join('/');
        if (!node.children.has(seg)) {
          node.children.set(seg, {
            name: seg,
            path: childPath,
            children: new Map(),
            documents: [],
          });
        }
        node = node.children.get(seg);
      }
      node.documents.push(d);
    }
    return { root, unclassified, leafPathCount: pathSet.size + (unclassified.length > 0 ? 1 : 0) };
  }

  /** 노드 + 하위 모든 자료 수 합산 */
  function nodeTotalCount(node) {
    let n = node.documents.length;
    for (const child of node.children.values()) n += nodeTotalCount(child);
    return n;
  }

  /** 트리 내 모든 fullPath 수집 (expand all 용) */
  function collectAllPaths(node, out) {
    for (const child of node.children.values()) {
      out.push(child.path);
      collectAllPaths(child, out);
    }
  }

  let docTree = $derived(buildDocTree(detail?.sections?.documents ?? []));

  function expandAllDocs() {
    const paths = [];
    collectAllPaths(docTree.root, paths);
    if (docTree.unclassified.length > 0) paths.push('__unclassified__');
    const next = {};
    for (const p of paths) next[p] = true;
    docTreeExpanded = next;
  }

  function collapseAllDocs() {
    docTreeExpanded = {};
  }

  function toggleDocPath(path) {
    docTreeExpanded = { ...docTreeExpanded, [path]: !docTreeExpanded[path] };
  }

  async function load() {
    loading = true;
    try {
      detail = await api(`/study-topics/${topicId}`);
      // PR-10: 세션 목록 함께 로드. 실패해도 detail 은 유지.
      try {
        quizSessions = await api(`/study-topics/${topicId}/quiz-sessions?limit=10`);
      } catch {
        quizSessions = { active: null, recent_done: [] };
      }
    } catch (err) {
      addToast('error', err.detail || '학습 주제 로딩 실패');
      detail = null;
    } finally {
      loading = false;
    }
  }

  /** 새 문제풀이 시작 — 기존 in_progress 있으면 confirm 후 abandon. */
  async function startNewQuiz() {
    const hasActive = !!quizSessions.active;
    if (hasActive) {
      const ok = confirm(
        `진행 중인 문제풀이 세션이 있습니다 (${quizSessions.active.cursor}/${quizSessions.active.total} 풀음). 새로 시작하면 그 세션은 포기됩니다. 진행할까요?`,
      );
      if (!ok) return;
    }
    try {
      const res = await api(`/study-topics/${topicId}/quiz-sessions`, {
        method: 'POST',
        body: JSON.stringify({
          target_per_subject: 20,
          abandon_existing: hasActive,
          quiz_mode: 'random',  // PR-12-B: 향후 셀렉터 대비 명시.
        }),
      });
      goto(`/study/topics/${topicId}/review?session=${res.id}`);
    } catch (err) {
      addToast('error', err?.detail || '문제풀이 시작 실패');
    }
  }

  /** 진행 중 세션 이어풀기. */
  function resumeActiveQuiz() {
    if (!quizSessions.active) return;
    goto(`/study/topics/${topicId}/review?session=${quizSessions.active.id}`);
  }

  function fmtDateTime(s) {
    if (!s) return '';
    return new Date(s).toLocaleString('ko-KR', { dateStyle: 'short', timeStyle: 'short' });
  }

  $effect(() => {
    void topicId;
    if (Number.isFinite(topicId)) {
      untrack(() => load());
    }
  });

  async function unlinkDocument(docId) {
    if (!confirm('이 자료를 주제에서 분리합니다. 자료 본체는 유지됩니다.')) return;
    try {
      await api(`/study-topics/${topicId}/documents/${docId}`, { method: 'DELETE' });
      addToast('success', '자료 분리됨');
      await load();
    } catch (err) {
      addToast('error', err.detail || '분리 실패');
    }
  }

  async function detachSession(sessId) {
    if (!confirm('이 필기 세션을 주제에서 분리합니다. 세션은 미분류로 남고 데이터는 유지됩니다.')) return;
    try {
      await api(`/study-topics/${topicId}/sessions/${sessId}`, { method: 'DELETE' });
      addToast('success', '세션 분리됨');
      await load();
    } catch (err) {
      addToast('error', err.detail || '분리 실패');
    }
  }

  async function searchDocs(resetPage = true) {
    if (resetPage) docPage = 1;
    docSearching = true;
    try {
      const params = new URLSearchParams();
      params.set('page', String(docPage));
      params.set('page_size', String(DOC_PAGE_SIZE));
      if (activeTreePath) params.set('path', activeTreePath);
      if (docSearch.trim()) params.set('q', docSearch.trim());
      const res = await api(`/documents/library?${params}`);
      docTotal = res.total ?? 0;
      // 이미 매핑된 자료는 candidates 에서 빼기 (detail.sections.documents 의 id set 비교)
      const existing = new Set((detail?.sections?.documents ?? []).map((d) => d.id));
      docCandidates = (res.items ?? []).filter((d) => !existing.has(d.id));
    } catch (err) {
      addToast('error', err.detail || '자료 검색 실패');
      docCandidates = [];
      docTotal = 0;
    } finally {
      docSearching = false;
    }
  }

  function gotoDocPage(p) {
    docPage = p;
    searchDocs(false);
  }

  async function loadTree() {
    treeLoading = true;
    try {
      tree = await api('/library/tree');
    } catch {
      addToast('error', '자료 트리 로딩 실패');
      tree = [];
    } finally {
      treeLoading = false;
    }
  }

  function openDocModal() {
    docModalOpen = true;
    docSearch = '';
    docCandidates = [];
    docSelected = new Set();
    activeTreePath = null;
    treeExpanded = {};
    if (tree.length === 0) loadTree();
    searchDocs();
  }

  function toggleDocSelect(id) {
    const next = new Set(docSelected);
    if (next.has(id)) next.delete(id); else next.add(id);
    docSelected = next;
  }

  function selectAllVisibleDocs() {
    const next = new Set(docSelected);
    for (const d of docCandidates) next.add(d.id);
    docSelected = next;
  }

  function clearDocSelection() {
    docSelected = new Set();
  }

  function selectTreePath(path) {
    activeTreePath = path;
    docSearch = '';
    docSelected = new Set();
    searchDocs();
  }

  function toggleTreeExpand(path) {
    treeExpanded = { ...treeExpanded, [path]: !treeExpanded[path] };
  }

  async function bulkAddByPath(path) {
    bulkAddingPath = path;
    try {
      const res = await api(`/study-topics/${topicId}/documents/by-path`, {
        method: 'POST',
        body: JSON.stringify({ path, include_subtree: true }),
      });
      const msgs = [`${res.linked_count}건 연결됨`];
      if (res.skipped_existing_count) msgs.push(`${res.skipped_existing_count}건 이미 연결`);
      if (res.total_in_path === 0) msgs[0] = '카테고리에 자료가 없습니다';
      addToast(res.linked_count > 0 ? 'success' : 'info', msgs.join(' · '));
      await load();
      // 자료 트리 자체에는 영향 없지만 candidates 갱신
      if (activeTreePath === path) await searchDocs();
    } catch (err) {
      addToast('error', err.detail || '카테고리 일괄 추가 실패');
    } finally {
      bulkAddingPath = null;
    }
  }

  async function linkSelectedDocs() {
    if (docSelected.size === 0) {
      addToast('error', '추가할 자료를 선택하세요');
      return;
    }
    docLinking = true;
    try {
      // 백엔드 max_length=100 우회를 위해 100개씩 chunk 분할 POST.
      const ids = Array.from(docSelected);
      const CHUNK = 100;
      let linkedTotal = 0;
      let skippedTotal = 0;
      for (let i = 0; i < ids.length; i += CHUNK) {
        const chunk = ids.slice(i, i + CHUNK);
        const result = await api(`/study-topics/${topicId}/documents`, {
          method: 'POST',
          body: JSON.stringify({ document_ids: chunk }),
        });
        linkedTotal += result.linked?.length ?? 0;
        skippedTotal += result.skipped_existing?.length ?? 0;
      }
      addToast(
        'success',
        `${linkedTotal}건 연결됨` + (skippedTotal ? ` · ${skippedTotal}건 이미 연결` : '')
      );
      docModalOpen = false;
      await load();
    } catch (err) {
      addToast('error', err.detail || '자료 연결 실패');
    } finally {
      docLinking = false;
    }
  }

  function fmtDate(s) {
    return new Date(s).toLocaleDateString('ko-KR', { dateStyle: 'medium' });
  }

  // PR-11: 문제 회차별 그룹.
  // - "(회차 미지정)"은 UI group key 전용 — q.exam_round 원본 값(NULL/문자열)과 절대 섞지 않음.
  // - 정렬은 "YYYY년 N회" 파싱해서 year desc → round desc. localeCompare 단독 사용 금지 (2024년 10회 vs 2024년 2회 회귀).
  const UNCAT_ROUND_KEY = '(회차 미지정)';

  function parseRoundKey(round) {
    if (typeof round !== 'string') return { year: -1, round: -1 };
    const m = round.match(/(\d{4}).*?(\d+)/);
    if (!m) return { year: -1, round: -1 };
    return { year: Number(m[1]), round: Number(m[2]) };
  }

  /** [['2024년 1회', [q,...]], ..., ['(회차 미지정)', [...]]] desc 정렬, 미지정은 맨 아래. */
  let questionsByRound = $derived.by(() => {
    const groups = new Map();
    for (const q of detail?.sections?.questions ?? []) {
      const key = q.exam_round || UNCAT_ROUND_KEY;
      if (!groups.has(key)) groups.set(key, []);
      groups.get(key).push(q);
    }
    // 그룹 내부 정렬: exam_question_number asc, 미설정이면 created_at desc fallback.
    for (const arr of groups.values()) {
      arr.sort((a, b) => {
        const an = a.exam_question_number;
        const bn = b.exam_question_number;
        if (an != null && bn != null) return an - bn;
        if (an != null) return -1;
        if (bn != null) return 1;
        return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
      });
    }
    return [...groups.entries()].sort(([a], [b]) => {
      if (a === UNCAT_ROUND_KEY) return 1;
      if (b === UNCAT_ROUND_KEY) return -1;
      const pa = parseRoundKey(a);
      const pb = parseRoundKey(b);
      if (pb.year !== pa.year) return pb.year - pa.year;
      if (pb.round !== pa.round) return pb.round - pa.round;
      return b.localeCompare(a);
    });
  });

  /** 회차 행 라벨용 합산 — 누적 시도 / "마지막 결과: 정답/오답" (last_correct 기반). */
  function roundAggregate(items) {
    let attempts = 0;
    let lastOk = 0;
    let lastNg = 0;
    for (const q of items) {
      attempts += q.attempt_count ?? 0;
      if (q.last_correct === true) lastOk += 1;
      else if (q.last_correct === false) lastNg += 1;
    }
    return { attempts, lastOk, lastNg };
  }

  function toggleRound(key) {
    const willExpand = !roundsExpanded[key];
    roundsExpanded = { ...roundsExpanded, [key]: willExpand };
    if (willExpand) {
      void fetchRoundRelatedCounts(key);
    }
  }

  function expandAllRounds() {
    const next = {};
    for (const [k] of questionsByRound) {
      next[k] = true;
      void fetchRoundRelatedCounts(k);
    }
    roundsExpanded = next;
  }

  function collapseAllRounds() {
    roundsExpanded = {};
  }

  // PR-12-A 후속: 반복 출제 등급 → 라벨 매핑.
  function repeatBadge(grade, roundCount) {
    if (grade === '단골') return { emoji: '🌟', text: '단골' };
    if (grade === '잘 나오는 반복 출제') return { emoji: '🔥', text: '잘 나오는 반복 출제' };
    if (grade === '반복 출제') return { emoji: '🔥', text: '반복 출제' };
    if (grade === '신출') return { emoji: '🆕', text: '신출' };
    if (grade === '빈출') return { emoji: '📜', text: '빈출' };
    if (roundCount >= 2) return { emoji: '🔥', text: '반복 출제' };
    return null;
  }

  /** PR-12-A: 회차 expand 첫 진입 시 그 회차 안 카드 qid 들의 round_count 배지 batch fetch.
   *  백엔드는 입력 qid 끼리만 비교하지 않고 topic 전체 ready pool 과 비교 — 다른 회차 출제 여부 잡힘.
   *  같은 회차 다시 접고 펴도 추가 호출 안 함 (roundsRelatedFetched 캐시). */
  async function fetchRoundRelatedCounts(roundKey) {
    if (roundsRelatedFetched[roundKey]) return;
    const group = (questionsByRound.find(([k]) => k === roundKey) ?? [, []])[1];
    const ids = group.map((q) => q.id);
    if (ids.length === 0) return;
    roundsRelatedFetched = { ...roundsRelatedFetched, [roundKey]: true };
    try {
      const res = await api(`/study-topics/${topicId}/related-types-bulk`, {
        method: 'POST',
        body: JSON.stringify({ question_ids: ids }),
      });
      relatedCounts = { ...relatedCounts, ...(res.items ?? {}) };
    } catch {
      // 실패 시 다음 expand 에서 재시도 가능하게 캐시 플래그 해제.
      const { [roundKey]: _, ...rest } = roundsRelatedFetched;
      roundsRelatedFetched = rest;
    }
  }
</script>

<svelte:head>
  <title>{detail?.topic?.name ?? '주제'} — 공부</title>
</svelte:head>

<div class="p-4 md:p-6 max-w-5xl mx-auto">
  <!-- Breadcrumb -->
  <div class="flex items-center gap-2 text-xs md:text-sm mb-3">
    <a href="/study" class="text-dim hover:text-text">공부</a>
    <span class="text-faint">/</span>
    <a href="/study/topics" class="text-dim hover:text-text">주제로 보기</a>
    <span class="text-faint">/</span>
    <span class="text-text font-medium truncate">{detail?.topic?.name ?? '...'}</span>
  </div>

  {#if loading}
    <div class="space-y-3">
      <Skeleton h="h-12" rounded="lg" />
      <Skeleton h="h-32" rounded="lg" />
    </div>
  {:else if !detail}
    <EmptyState
      icon={FolderKanban}
      title="주제를 찾을 수 없습니다"
      description="삭제되었거나 권한이 없는 주제입니다."
    />
    <div class="mt-3">
      <Button href="/study/topics" icon={ArrowLeft}>주제 목록으로</Button>
    </div>
  {:else}
    <!-- 주제 헤더 -->
    <div class="mb-4 p-4 rounded-lg border border-default bg-surface">
      <div class="flex items-center gap-2 mb-1">
        {#if detail.topic.color}
          <span class="inline-block w-3 h-3 rounded-full shrink-0" style="background-color: {detail.topic.color}"></span>
        {:else}
          <FolderKanban size={16} class="text-accent shrink-0" />
        {/if}
        <h1 class="text-lg font-semibold text-text truncate">{detail.topic.name}</h1>
        {#if detail.topic.study_type}
          <span class="text-[10px] text-dim border border-default rounded px-1.5 py-0.5 shrink-0">{detail.topic.study_type}</span>
        {/if}
      </div>
      {#if detail.topic.description}
        <p class="text-xs text-dim mt-1 whitespace-pre-line">{detail.topic.description}</p>
      {/if}
      <div class="text-[11px] text-dim mt-3 flex items-center gap-3 flex-wrap">
        <span>필기 <span class="text-text">{detail.stats.session_count}</span></span>
        <span>자료 <span class="text-text">{detail.stats.document_count}</span></span>
        <!-- 후속 PR 자산 카운트 placeholder -->
        {#if detail.stats.audio_count > 0}<span>오디오 <span class="text-text">{detail.stats.audio_count}</span></span>{/if}
        {#if detail.stats.vocab_count > 0}<span>단어장 <span class="text-text">{detail.stats.vocab_count}</span></span>{/if}
        {#if detail.stats.question_set_count > 0}<span>문제 <span class="text-text">{detail.stats.question_set_count}</span></span>{/if}
      </div>
    </div>

    <!-- 필기 세션 -->
    <section class="mb-5">
      <div class="flex items-center justify-between mb-2">
        <h2 class="text-sm font-semibold text-text flex items-center gap-2">
          <PenLine size={14} class="text-accent" /> 필기 세션
          <span class="text-[10px] text-dim">{detail.sections.sessions.length}</span>
        </h2>
        <Button href="/study/write" size="sm" variant="ghost" icon={Plus}>새 세션</Button>
      </div>
      {#if detail.sections.sessions.length === 0}
        <div class="text-xs text-dim p-3 border border-dashed border-default/60 rounded-lg">
          이 주제에 연결된 필기 세션이 없습니다. /study/write 에서 새 세션을 만들 때 주제를 선택하거나, 기존 세션 편집에서 본 주제로 이동시키세요.
        </div>
      {:else}
        <div class="flex flex-col gap-2">
          {#each detail.sections.sessions as s (s.id)}
            <div class="flex items-center gap-3 p-3 rounded-lg border border-default bg-surface hover:bg-surface/80">
              <div class="flex-1 min-w-0">
                <div class="flex items-center gap-2 text-xs text-dim">
                  {#if s.study_type === 'language'}
                    <Languages size={12} />
                    <span>{s.language_code || '?'} · {s.learning_level || ''}</span>
                  {:else}
                    <BookOpen size={12} />
                    <span>{s.certification || '미지정'}</span>
                  {/if}
                  <span>·</span>
                  <span>{s.mode}</span>
                </div>
                <div class="text-sm text-text mt-1 truncate">
                  {s.subject || '(과목 미지정)'} — {s.topic || '(주제 미지정)'}
                </div>
                <div class="text-[10px] text-dim mt-1">{fmtDate(s.created_at)}</div>
              </div>
              <Button href={`/study/write/${s.id}`} size="sm" variant="ghost" icon={ArrowRight} iconPosition="right">열기</Button>
              <button
                type="button"
                onclick={() => detachSession(s.id)}
                class="p-1.5 rounded hover:bg-error/10 text-dim hover:text-error"
                aria-label="주제에서 분리"
              ><Trash2 size={12} /></button>
            </div>
          {/each}
        </div>
      {/if}
    </section>

    <!-- 자료 -->
    <section class="mb-5">
      <div class="flex items-center justify-between mb-2 flex-wrap gap-2">
        <h2 class="text-sm font-semibold text-text flex items-center gap-2">
          <BookOpen size={14} class="text-accent" /> 자료
          <span class="text-[10px] text-dim">{detail.sections.documents.length}</span>
        </h2>
        <Button onclick={openDocModal} size="sm" variant="ghost" icon={Plus}>자료 추가</Button>
      </div>
      {#if detail.sections.documents.length === 0}
        <div class="text-xs text-dim p-3 border border-dashed border-default/60 rounded-lg">
          이 주제에 연결된 자료가 없습니다. "자료 추가" 로 자료실 자료를 묶어보세요.
        </div>
      {:else}
        <!-- 트리 헤더: 총 N개 · 카테고리 K개 + 펼치기/접기 -->
        <div class="flex items-center justify-between mb-2 px-1 text-[11px] text-dim flex-wrap gap-2">
          <span>
            자료 <span class="text-text">{detail.sections.documents.length}</span>개
            · 카테고리 <span class="text-text">{docTree.leafPathCount}</span>개
          </span>
          <div class="flex items-center gap-2">
            <button type="button" onclick={expandAllDocs} class="text-accent hover:underline">모두 펼치기</button>
            <span class="text-faint">·</span>
            <button type="button" onclick={collapseAllDocs} class="text-dim hover:text-text">모두 접기</button>
          </div>
        </div>

        {#snippet docCard(d)}
          <div class="flex items-center gap-3 p-3 rounded-lg border border-default bg-surface hover:bg-surface/80">
            <div class="flex-1 min-w-0">
              <div class="text-sm text-text truncate">{d.title || `(제목 없음 · #${d.id})`}</div>
              <div class="text-[11px] text-dim mt-1 flex items-center gap-2 flex-wrap">
                <span>{d.file_format}</span>
                {#if d.ai_domain}<span>· {d.ai_domain}</span>{/if}
                {#if d.importance && d.importance !== 'medium'}<span>· {d.importance}</span>{/if}
                <span class="ml-auto">{fmtDate(d.linked_at)}</span>
              </div>
            </div>
            <Button href={`/documents/${d.id}`} size="sm" variant="ghost" icon={ArrowRight} iconPosition="right">열기</Button>
            <button
              type="button"
              onclick={() => unlinkDocument(d.id)}
              class="p-1.5 rounded hover:bg-error/10 text-dim hover:text-error"
              aria-label="주제에서 분리"
            ><Trash2 size={12} /></button>
          </div>
        {/snippet}

        {#snippet treeNode(node, depth)}
          {@const isExpanded = docTreeExpanded[node.path] === true}
          {@const totalUnder = nodeTotalCount(node)}
          <div class="flex flex-col gap-1">
            <button
              type="button"
              onclick={() => toggleDocPath(node.path)}
              style="padding-left: {depth * 12}px"
              class="w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm text-text hover:bg-bg transition-colors"
            >
              {#if isExpanded}<ChevronDown size={12} class="text-dim shrink-0" />{:else}<ChevronRight size={12} class="text-dim shrink-0" />{/if}
              <FolderOpen size={12} class="text-dim shrink-0" />
              <span class="truncate">{node.name}</span>
              <span class="ml-auto text-[10px] text-dim shrink-0">{totalUnder}</span>
            </button>
            {#if isExpanded}
              <div class="flex flex-col gap-1.5" style="padding-left: {(depth + 1) * 12}px">
                {#each node.documents as d (d.id)}
                  {@render docCard(d)}
                {/each}
              </div>
              {#each [...node.children.values()] as child (child.path)}
                {@render treeNode(child, depth + 1)}
              {/each}
            {/if}
          </div>
        {/snippet}

        <div class="flex flex-col gap-1">
          {#each [...docTree.root.children.values()] as child (child.path)}
            {@render treeNode(child, 0)}
          {/each}

          {#if docTree.unclassified.length > 0}
            {@const isExp = docTreeExpanded['__unclassified__'] === true}
            <button
              type="button"
              onclick={() => toggleDocPath('__unclassified__')}
              class="w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm text-text hover:bg-bg transition-colors"
            >
              {#if isExp}<ChevronDown size={12} class="text-dim shrink-0" />{:else}<ChevronRight size={12} class="text-dim shrink-0" />{/if}
              <FolderOpen size={12} class="text-dim shrink-0" />
              <span class="truncate text-dim italic">분류 없음</span>
              <span class="ml-auto text-[10px] text-dim shrink-0">{docTree.unclassified.length}</span>
            </button>
            {#if isExp}
              <div class="flex flex-col gap-1.5" style="padding-left: 12px">
                {#each docTree.unclassified as d (d.id)}
                  {@render docCard(d)}
                {/each}
              </div>
            {/if}
          {/if}
        </div>
      {/if}
    </section>

    <!-- 문제 (PR-2 → PR-11: 회차별 그룹 + 접기) -->
    <section class="mb-5">
      <div class="flex items-center justify-between mb-2 flex-wrap gap-2">
        <h2 class="text-sm font-semibold text-text flex items-center gap-2">
          <HelpCircle size={14} class="text-accent" /> 문제
          <span class="text-[10px] text-dim">
            {detail.sections.questions?.length ?? 0}개
            {#if questionsByRound.length > 0}· 회차 {questionsByRound.length}개{/if}
          </span>
        </h2>
        <div class="flex items-center gap-1 flex-wrap">
          {#if questionsByRound.length > 0}
            <button type="button" onclick={expandAllRounds} class="text-[11px] text-accent hover:underline px-1">모두 펼치기</button>
            <span class="text-faint text-[11px]">·</span>
            <button type="button" onclick={collapseAllRounds} class="text-[11px] text-dim hover:text-text px-1">모두 접기</button>
            <span class="w-1"></span>
          {/if}
          <Button href={`/study/topics/${topicId}/exam-rounds`} size="sm" variant="ghost" icon={ListChecks}>회차 보기</Button>
          <Button href={`/study/topics/${topicId}/review-queue`} size="sm" variant="ghost">복습함</Button>
          <Button href={`/study/topics/${topicId}/stats`} size="sm" variant="ghost">통계</Button>
          <Button href={`/study/topics/${topicId}/questions/new`} size="sm" variant="ghost" icon={Plus}>새 문제</Button>
          {#if (detail.sections.questions?.length ?? 0) > 0}
            <Button onclick={startNewQuiz} size="sm" icon={Play}>새 문제풀이</Button>
          {/if}
        </div>
      </div>
      {#if (detail.sections.questions?.length ?? 0) === 0}
        <div class="text-xs text-dim p-3 border border-dashed border-default/60 rounded-lg">
          이 주제에 입력된 문제가 없습니다. "새 문제" 로 4지선다 객관식을 추가하면 문제풀이 모드에서 무작위 출제됩니다.
        </div>
      {:else}
        <!-- PR-10: 진행 중 세션 카드 -->
        {#if quizSessions.active}
          {@const a = quizSessions.active}
          {@const pct = a.total > 0 ? Math.round((a.cursor / a.total) * 100) : 0}
          <div class="mb-2 p-3 rounded-lg border border-accent/40 bg-accent/5 flex items-center gap-3">
            <Play size={16} class="text-accent shrink-0" />
            <div class="flex-1 min-w-0">
              <div class="text-sm text-text">
                진행 중인 문제풀이 — <span class="font-medium">{a.cursor} / {a.total}</span> 풀음
                <span class="text-dim text-[11px]">({pct}%)</span>
              </div>
              <div class="w-full h-1.5 bg-default/30 rounded-full overflow-hidden mt-1.5">
                <div class="h-full bg-accent transition-all" style="width: {pct}%"></div>
              </div>
              <div class="text-[11px] text-dim mt-1">시작 {fmtDateTime(a.created_at)}</div>
            </div>
            <Button onclick={resumeActiveQuiz} size="sm" icon={ArrowRight} iconPosition="right">이어풀기</Button>
          </div>
        {/if}

        <!-- PR-10: 최근 완료 세션 (결과 카드) -->
        {#if quizSessions.recent_done?.length > 0}
          <div class="mb-2 flex flex-col gap-1.5">
            <div class="text-[11px] text-dim px-1">최근 결과</div>
            {#each quizSessions.recent_done as s (s.id)}
              {@const accuracy = s.total > 0 ? Math.round((s.correct_count / s.total) * 100) : 0}
              <a
                href={`/study/topics/${topicId}/quiz-sessions/${s.id}`}
                class="flex items-center gap-3 p-2.5 rounded border border-default bg-surface hover:border-accent/40 hover:bg-bg/30 transition-colors"
              >
                <div class="flex-1 min-w-0">
                  <div class="text-sm text-text flex items-center gap-2 flex-wrap">
                    <span class="text-success">정답 {s.correct_count}</span>
                    <span class="text-dim">·</span>
                    <span class="text-error">오답 {s.wrong_count}</span>
                    <span class="text-dim">·</span>
                    <span class="text-warning">모르겠음 {s.unsure_count}</span>
                    <span class="text-dim">·</span>
                    <span class="text-text font-medium">정답률 {accuracy}%</span>
                    {#if s.unreviewed_wrong_unsure_count > 0}
                      <span class="ml-auto text-[10px] text-warning bg-warning/10 rounded px-1.5 py-0.5">미확인 {s.unreviewed_wrong_unsure_count}</span>
                    {:else}
                      <span class="ml-auto text-[10px] text-success bg-success/10 rounded px-1.5 py-0.5">✓ 모두 확인</span>
                    {/if}
                  </div>
                  <div class="text-[11px] text-dim mt-0.5">
                    총 {s.total}문제 · 완료 {fmtDateTime(s.finished_at)}
                  </div>
                </div>
                <ArrowRight size={12} class="text-dim shrink-0" />
              </a>
            {/each}
          </div>
        {/if}

        <!-- PR-11: 회차별 아코디언 (디폴트 모두 접힘) -->
        <div class="flex flex-col gap-1.5">
          {#each questionsByRound as [roundKey, items] (roundKey)}
            {@const isExpanded = roundsExpanded[roundKey] === true}
            {@const isUncategorized = roundKey === UNCAT_ROUND_KEY}
            {@const agg = roundAggregate(items)}
            <div class="rounded-lg border {isUncategorized ? 'border-warning/30 bg-warning/5' : 'border-default bg-surface'}">
              <button
                type="button"
                onclick={() => toggleRound(roundKey)}
                class="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-bg/30 transition-colors rounded-lg"
              >
                {#if isExpanded}
                  <ChevronDown size={14} class="text-dim shrink-0" />
                {:else}
                  <ChevronRight size={14} class="text-dim shrink-0" />
                {/if}
                <span class="text-sm text-text font-medium truncate {isUncategorized ? 'italic' : ''}">{roundKey}</span>
                <span class="ml-auto text-[11px] text-dim flex items-center gap-2 flex-wrap">
                  <span>{items.length}문제</span>
                  {#if agg.attempts > 0}
                    <span>· 총 시도 {agg.attempts}건</span>
                    {#if agg.lastOk + agg.lastNg > 0}
                      <span>· 마지막 결과:</span>
                      <span class="text-success">정답 {agg.lastOk}</span>
                      <span class="text-dim">/</span>
                      <span class="text-error">오답 {agg.lastNg}</span>
                    {/if}
                  {/if}
                </span>
              </button>

              {#if isExpanded}
                {#if isUncategorized}
                  <div class="px-3 pb-2 text-[11px] text-warning/80 italic">
                    회차를 지정하면 회차 카드 페이지에서 진행률 추적이 가능합니다.
                  </div>
                {/if}
                <div class="px-2 pb-2 flex flex-col gap-1.5">
                  {#each items as q (q.id)}
                    {@const rc = relatedCounts[q.id] ?? null}
                    {@const repeatBdg = repeatBadge(rc?.repeat_grade, rc?.repeat_round_count ?? 0)}
                    <!-- 본문 링크와 [편집] 링크는 형제 구조로 분리해 이벤트 버블링 충돌 회피. -->
                    <div class="flex items-stretch gap-1 rounded border border-default bg-surface hover:border-accent/40 transition-colors overflow-hidden">
                      <a
                        href={`/study/topics/${topicId}/questions/${q.id}`}
                        data-sveltekit-preload-code="viewport"
                        data-sveltekit-preload-data="hover"
                        title="상세 보기"
                        class="flex-1 min-w-0 p-3 hover:bg-accent/5 transition-colors"
                      >
                        <div class="text-sm text-text line-clamp-2">
                          {#if q.exam_question_number}<span class="text-dim mr-1.5">{q.exam_question_number}.</span>{/if}{q.question_text}
                        </div>
                        <div class="text-[11px] text-dim mt-1 flex items-center gap-2 flex-wrap">
                          {#if q.subject}<span>{q.subject}</span>{/if}
                          {#if q.scope}<span>· {q.scope}</span>{/if}
                          <!-- PR-12-A: 반복 출제 / 유사 유형 배지 — round_count >= 2 일 때만 -->
                          {#if repeatBdg && rc?.repeat_round_count >= 2}
                            <span class="text-error/90">{repeatBdg.emoji} {repeatBdg.text} · {rc.repeat_round_count}개 회차</span>
                          {/if}
                          {#if rc?.similar_round_count >= 2}
                            <span class="text-accent/80">🧩 {rc.similar_round_count}개 회차</span>
                          {/if}
                          {#if q.attempt_count > 0}
                            <span class="ml-auto flex items-center gap-1">
                              {#if q.last_correct === true}
                                <CheckCircle2 size={11} class="text-success" />
                              {:else if q.last_correct === false}
                                <XCircle size={11} class="text-error" />
                              {/if}
                              <span>{q.attempt_count}회</span>
                            </span>
                          {/if}
                          {#if !q.is_active}<span class="text-warning">· 비활성</span>{/if}
                        </div>
                      </a>
                      <a
                        href={`/study/topics/${topicId}/questions/${q.id}/edit`}
                        title="편집"
                        class="flex items-center px-3 text-dim hover:text-accent hover:bg-accent/10 transition-colors border-l border-default"
                        aria-label="편집"
                      >
                        <Edit size={14} />
                      </a>
                    </div>
                  {/each}
                </div>
              {/if}
            </div>
          {/each}
        </div>
      {/if}
    </section>
  {/if}
</div>

<!-- 자료 추가 모달 -->
{#if docModalOpen}
  <button
    type="button"
    aria-label="모달 닫기"
    onclick={() => (docModalOpen = false)}
    class="fixed inset-0 z-40 bg-black/40"
  ></button>
  <div class="fixed inset-0 z-50 flex items-center justify-center p-2 md:p-4 pointer-events-none">
    <div class="w-full max-w-4xl bg-surface rounded-lg border border-default shadow-xl pointer-events-auto flex flex-col max-h-[90vh] md:max-h-[85vh]">
      <header class="flex items-center justify-between px-4 py-3 border-b border-default shrink-0">
        <div>
          <div class="text-sm font-semibold text-text">자료 추가</div>
          <div class="text-[11px] text-dim mt-0.5">카테고리를 클릭하면 그 하위 자료를 한 번에 추가할 수 있습니다.</div>
        </div>
        <button
          type="button"
          onclick={() => (docModalOpen = false)}
          class="p-1 rounded hover:bg-bg text-dim"
          aria-label="닫기"
        >×</button>
      </header>

      <div class="flex-1 min-h-0 grid grid-cols-1 md:grid-cols-[240px_1fr]">
        <!-- 좌측: 카테고리 트리 -->
        <aside class="border-b md:border-b-0 md:border-r border-default overflow-y-auto p-2 max-h-[40vh] md:max-h-none">
          <button
            type="button"
            onclick={() => selectTreePath(null)}
            class="w-full flex items-center justify-between gap-2 px-2 py-1.5 rounded text-sm transition-colors
              {activeTreePath === null ? 'bg-accent/15 text-accent' : 'text-text hover:bg-bg'}"
          >
            <span class="flex items-center gap-2"><FolderOpen size={14} /> 전체</span>
          </button>

          {#if treeLoading}
            <div class="space-y-1 mt-2">
              {#each Array(4) as _}<Skeleton h="h-7" rounded="md" />{/each}
            </div>
          {:else if tree.length === 0}
            <div class="text-xs text-dim p-3">자료실이 비어있습니다.</div>
          {:else}
            {#snippet treeNode(n, depth)}
              {@const isActive = activeTreePath === n.path}
              {@const hasChildren = (n.children?.length ?? 0) > 0}
              {@const isExpanded = treeExpanded[n.path]}
              {@const busy = bulkAddingPath === n.path}

              <div class="flex items-center" style="padding-left: {depth * 10}px">
                {#if hasChildren}
                  <button
                    type="button"
                    onclick={() => toggleTreeExpand(n.path)}
                    class="p-0.5 rounded hover:bg-bg text-dim shrink-0"
                    aria-label={isExpanded ? '접기' : '펼치기'}
                  >
                    {#if isExpanded}<ChevronDown size={11} />{:else}<ChevronRight size={11} />{/if}
                  </button>
                {:else}
                  <span class="w-4 shrink-0"></span>
                {/if}

                <button
                  type="button"
                  onclick={() => selectTreePath(n.path)}
                  class="flex-1 flex items-center justify-between gap-2 px-2 py-1 rounded text-xs transition-colors text-left
                    {isActive ? 'bg-accent/15 text-accent' : 'text-dim hover:bg-bg hover:text-text'}"
                  aria-current={isActive ? 'page' : undefined}
                >
                  <span class="truncate">{n.name}</span>
                  <span class="text-[10px] text-dim shrink-0">{n.count}</span>
                </button>

                <button
                  type="button"
                  onclick={() => bulkAddByPath(n.path)}
                  disabled={busy || n.count === 0}
                  class="p-1 rounded hover:bg-accent/10 text-dim hover:text-accent disabled:opacity-40 shrink-0"
                  aria-label="이 카테고리 하위 모두 추가"
                  title="이 카테고리 하위 모두 추가 ({n.count}건)"
                >
                  {#if busy}<span class="text-[10px]">...</span>{:else}<FolderPlus size={12} />{/if}
                </button>
              </div>

              {#if hasChildren && isExpanded}
                {#each n.children as c}
                  {@render treeNode(c, depth + 1)}
                {/each}
              {/if}
            {/snippet}

            {#each tree as n}
              {@render treeNode(n, 0)}
            {/each}
          {/if}
        </aside>

        <!-- 우측: 자료 카드 + 검색 + 체크박스 -->
        <section class="flex-1 min-h-0 flex flex-col overflow-hidden">
          <div class="px-4 py-3 border-b border-default shrink-0 flex flex-col gap-2">
            <div class="flex items-center gap-2 text-[11px] text-dim flex-wrap">
              {#if activeTreePath}
                <span>현재 카테고리: <span class="text-text">{activeTreePath}</span></span>
              {:else}
                <span>전체 자료실</span>
              {/if}
              <span class="ml-auto">
                총 <span class="text-text">{docTotal}</span>건
                {#if docTotal > DOC_PAGE_SIZE}· {docPage}/{Math.ceil(docTotal / DOC_PAGE_SIZE)}페이지{/if}
              </span>
            </div>
            {#if docTotal > DOC_PAGE_SIZE}
              <div class="text-[11px] text-accent/80 bg-accent/5 border border-accent/20 rounded px-2 py-1">
                💡 카테고리 전체를 묶으려면 좌측 트리 옆 폴더+ 아이콘으로 한 번에 추가할 수 있습니다.
              </div>
            {/if}
            <div class="flex gap-2">
              <input
                type="text"
                bind:value={docSearch}
                placeholder={activeTreePath ? '이 카테고리 안에서 제목 검색' : '자료 제목 검색'}
                class="flex-1 px-3 py-1.5 bg-surface border border-default rounded text-sm text-text outline-none focus:border-accent"
                onkeydown={(e) => { if (e.key === 'Enter') searchDocs(); }}
              />
              <Button size="sm" onclick={searchDocs} loading={docSearching}>검색</Button>
            </div>
            {#if docCandidates.length > 0}
              <div class="flex items-center gap-3 text-[11px]">
                <button type="button" onclick={selectAllVisibleDocs} class="text-accent hover:underline">전체 선택</button>
                <button type="button" onclick={clearDocSelection} class="text-dim hover:text-text">선택 해제</button>
              </div>
            {/if}
          </div>

          <div class="flex-1 min-h-0 overflow-y-auto px-4 py-3">
            {#if docSearching}
              <div class="space-y-2">
                {#each Array(5) as _}<Skeleton h="h-12" rounded="md" />{/each}
              </div>
            {:else if docCandidates.length === 0}
              <p class="text-xs text-dim text-center py-6">
                {activeTreePath ? '이 카테고리에 추가 가능한 자료가 없습니다.' : '검색 결과가 없습니다.'}
              </p>
            {:else}
              <ul class="flex flex-col gap-1">
                {#each docCandidates as d (d.id)}
                  <li>
                    <label class="flex items-center gap-3 p-2 rounded hover:bg-bg cursor-pointer">
                      <input
                        type="checkbox"
                        checked={docSelected.has(d.id)}
                        onchange={() => toggleDocSelect(d.id)}
                        class="shrink-0"
                      />
                      <div class="flex-1 min-w-0">
                        <div class="text-sm text-text truncate">{d.title || `#${d.id}`}</div>
                        <div class="text-[11px] text-dim flex items-center gap-2 flex-wrap">
                          <span>{d.file_format}</span>
                          {#if d.ai_domain}<span>· {d.ai_domain}</span>{/if}
                          {#if d.user_tags?.length}
                            {#each d.user_tags.filter((t) => !t.startsWith('@library/')).slice(0, 3) as t}
                              <span>· {t}</span>
                            {/each}
                          {/if}
                        </div>
                      </div>
                    </label>
                  </li>
                {/each}
              </ul>

              {#if docTotal > DOC_PAGE_SIZE}
                <div class="flex items-center justify-center gap-2 mt-3 text-xs">
                  <button
                    type="button"
                    disabled={docPage <= 1}
                    onclick={() => gotoDocPage(docPage - 1)}
                    class="px-3 py-1.5 rounded border border-default text-dim hover:text-text disabled:opacity-40"
                  >이전</button>
                  <span class="text-dim">{docPage} / {Math.ceil(docTotal / DOC_PAGE_SIZE)}</span>
                  <button
                    type="button"
                    disabled={docPage >= Math.ceil(docTotal / DOC_PAGE_SIZE)}
                    onclick={() => gotoDocPage(docPage + 1)}
                    class="px-3 py-1.5 rounded border border-default text-dim hover:text-text disabled:opacity-40"
                  >다음</button>
                </div>
              {/if}
            {/if}
          </div>
        </section>
      </div>

      <footer class="flex items-center justify-between px-4 py-3 border-t border-default shrink-0 flex-wrap gap-2">
        <div class="text-xs text-dim">{docSelected.size}건 선택됨</div>
        <div class="flex gap-2">
          <Button variant="ghost" onclick={() => (docModalOpen = false)} disabled={docLinking}>닫기</Button>
          <Button onclick={linkSelectedDocs} loading={docLinking} disabled={docSelected.size === 0}>
            {docSelected.size}건 추가
          </Button>
        </div>
      </footer>
    </div>
  </div>
{/if}
