"""Web UI — Flask 单页应用

提供搜索、状态查看、chunk 详情、触发索引等功能。
"""

from __future__ import annotations

import asyncio
import logging
from pathlib import Path

from flask import Flask, jsonify, request

from .config import Config, load_config
from .db import Database
from .embed import OllamaEmbedder

logger = logging.getLogger(__name__)


def create_app(config: Config | None = None) -> Flask:
    """创建 Flask 应用"""
    if config is None:
        config = load_config()

    app = Flask(__name__)
    app.config["INDEX1_CONFIG"] = config

    # 单例：所有请求共享同一个 db 和 embedder，避免每次请求新建连接
    _db = Database(config.db_path_resolved, config.embedding_dim)
    _db.connect()
    _embedder = OllamaEmbedder(
        model=config.embedding_model,
        ollama_url=config.ollama_url,
        dim=config.embedding_dim,
    )

    # 持久 event loop：Flask 请求共享同一个 loop，防止 httpx client 跨 loop 失效
    import threading
    _loop = asyncio.new_event_loop()
    _loop_thread = threading.Thread(target=_loop.run_forever, daemon=True)
    _loop_thread.start()

    def _run_async(coro):
        """在共享 event loop 上执行协程，避免 asyncio.run() 每次创建/销毁 loop"""
        import concurrent.futures
        future = asyncio.run_coroutine_threadsafe(coro, _loop)
        try:
            return future.result(timeout=120)
        except concurrent.futures.TimeoutError:
            future.cancel()
            raise TimeoutError("操作超时（120s）")

    def _get_db() -> Database:
        return _db

    def _get_embedder() -> OllamaEmbedder:
        return _embedder

    # ── 页面路由 ──

    @app.route("/")
    def index():
        return _HTML_TEMPLATE

    @app.route("/favicon.ico")
    def favicon():
        svg = (
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">'
            '<ellipse cx="16" cy="16" rx="14" ry="5" stroke="#4A90D9" stroke-width="2.5" fill="none" opacity=".7"/>'
            '<ellipse cx="16" cy="16" rx="14" ry="5" stroke="#4A90D9" stroke-width="2.5" fill="none" opacity=".7" transform="rotate(60 16 16)"/>'
            '<ellipse cx="16" cy="16" rx="14" ry="5" stroke="#4A90D9" stroke-width="2.5" fill="none" opacity=".7" transform="rotate(120 16 16)"/>'
            '<circle cx="16" cy="16" r="4" fill="#4A90D9"/>'
            '</svg>'
        )
        return app.response_class(svg, mimetype="image/svg+xml")

    # ── API 路由 ──

    @app.route("/api/search")
    def api_search():
        from .search import search as do_search

        q = request.args.get("q", "").strip()
        if not q:
            return jsonify({"results": [], "total": 0, "query_ms": 0, "mode": ""})
        if len(q) > 500:
            return jsonify({"error": "query too long (max 500 chars)"}), 400

        collection = request.args.get("collection") or None
        try:
            top_k = min(int(request.args.get("top_k", 10)), 100)
        except (ValueError, TypeError):
            return jsonify({"error": "top_k must be an integer"}), 400

        db = _get_db()
        embedder = _get_embedder()
        try:
            resp = _run_async(
                do_search(
                    q, config, db, embedder,
                    collection=collection, top_k=top_k, verbose=True,
                )
            )
            results = []
            for r in resp.results:
                results.append({
                    "chunk_id": r.chunk_id,
                    "score": r.score,
                    "source": r.source,
                    "section": r.section,
                    "summary": r.summary,
                    "relevance": r.relevance,
                    "content": r.content,
                    "token_count": r.token_count,
                })
            return jsonify({
                "results": results,
                "total": resp.total,
                "query_ms": resp.query_ms,
                "mode": resp.mode,
                "hint": resp.hint,
            })
        except Exception as e:
            logger.warning("搜索失败: %s", e)
            return jsonify({"error": "search failed"}), 500

    @app.route("/api/status")
    def api_status():
        db = _get_db()
        try:
            stats = db.get_stats()
            return jsonify(stats)
        except Exception as e:
            logger.warning("获取状态失败: %s", e)
            return jsonify({"error": "failed to get status"}), 500

    @app.route("/api/doc/<int:doc_id>/chunks")
    def api_doc_chunks(doc_id):
        db = _get_db()
        try:
            chunks = db.get_doc_chunks(doc_id)
            if not chunks:
                return jsonify({"error": "document not found"}), 404
            return jsonify({
                "source": chunks[0].source if chunks else None,
                "chunks": [
                    {
                        "id": c.id,
                        "content": c.content,
                        "section": c.section,
                        "chunk_index": c.chunk_index,
                        "token_count": c.token_count,
                        "summary": c.summary,
                    }
                    for c in chunks
                ],
            })
        except Exception as e:
            logger.warning("获取文档 chunks 失败: %s", e)
            return jsonify({"error": "failed to get doc chunks"}), 500

    @app.route("/api/browse")
    def api_browse():
        collection = request.args.get("collection") or None
        if collection and ("\x00" in collection or len(collection) > 200):
            return jsonify({"error": "invalid collection"}), 400
        doc_type = request.args.get("doc_type") or None
        if doc_type and ("\x00" in doc_type or len(doc_type) > 50):
            return jsonify({"error": "invalid doc_type"}), 400
        try:
            limit = min(int(request.args.get("limit", 50)), 200)
        except (ValueError, TypeError):
            limit = 50
        try:
            offset = max(int(request.args.get("offset", 0)), 0)
        except (ValueError, TypeError):
            offset = 0
        db = _get_db()
        try:
            docs = db.browse_collection(collection, doc_type=doc_type, limit=limit, offset=offset)
            # 返回该集合实际存在的 doc_types
            doc_types = []
            if collection:
                rows = db.conn.execute(
                    "SELECT DISTINCT doc_type FROM documents WHERE collection = ? ORDER BY doc_type",
                    (collection,),
                ).fetchall()
                doc_types = [r[0] for r in rows]
            return jsonify({"docs": docs, "total": len(docs), "doc_types": doc_types})
        except Exception as e:
            logger.warning("浏览集合失败: %s", e)
            return jsonify({"error": "failed to browse"}), 500

    @app.route("/api/collections")
    def api_collections():
        db = _get_db()
        try:
            return jsonify(db.get_collection_stats())
        except Exception as e:
            logger.warning("获取集合统计失败: %s", e)
            return jsonify({"error": "failed to get collections"}), 500

    @app.route("/api/chunk/<int:chunk_id>")
    def api_chunk(chunk_id):
        db = _get_db()
        try:
            chunk = db.get_chunk(chunk_id)
            if not chunk:
                return jsonify({"error": "chunk not found"}), 404
            ctx = db.get_chunk_context(chunk_id)
            return jsonify({
                "id": chunk.id,
                "doc_id": chunk.doc_id,
                "content": chunk.content,
                "section": chunk.section,
                "parent_section": chunk.parent_section,
                "chunk_index": chunk.chunk_index,
                "tags": chunk.tags,
                "token_count": chunk.token_count,
                "summary": chunk.summary,
                "source": chunk.source,
                "context": ctx,
            })
        except Exception as e:
            logger.warning("获取 chunk 失败: %s", e)
            return jsonify({"error": "failed to get chunk"}), 500

    @app.route("/api/reindex", methods=["POST"])
    def api_reindex():
        from .indexer import index_files

        data = request.get_json(silent=True) or {}
        paths = data.get("paths", [])
        if not paths or not isinstance(paths, list):
            return jsonify({"error": "paths is required (list of strings)"}), 400
        if len(paths) > 10:
            return jsonify({"error": "too many paths (max 10)"}), 400

        # 验证路径存在且不含空字符，限制在用户目录内，阻止符号链接逃逸
        valid_paths = []
        home_dir = Path.home().resolve()
        for p in paths:
            if not isinstance(p, str) or "\x00" in p:
                return jsonify({"error": "invalid path"}), 400
            orig = Path(p).expanduser()
            if not orig.exists():
                return jsonify({"error": f"path not found: {p}"}), 400
            # 检查路径每一级是否含符号链接（防止中间路径逃逸）
            for part in [orig] + list(orig.parents):
                if part.is_symlink():
                    return jsonify({"error": f"symlink in path not allowed: {p}"}), 403
            resolved = orig.resolve()
            try:
                resolved.relative_to(home_dir)
            except ValueError:
                return jsonify({"error": f"path not allowed: {p}"}), 403
            valid_paths.append(str(resolved))

        force = bool(data.get("force", False))
        collection = data.get("collection") or None

        db = _get_db()
        embedder = _get_embedder()
        try:
            base_dir = None
            if len(valid_paths) == 1:
                p = Path(valid_paths[0]).resolve()
                base_dir = p if p.is_dir() else p.parent

            stats = _run_async(
                index_files(
                    valid_paths, config, db, embedder,
                    collection=collection, force=force, base_dir=base_dir,
                )
            )
            return jsonify(stats)
        except Exception as e:
            logger.warning("索引失败: %s", e)
            return jsonify({"error": "indexing failed"}), 500

    return app


# ── 内联 HTML 模板 ──

_HTML_TEMPLATE = r"""<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>index1</title>
<link rel="icon" type="image/svg+xml" href="/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ── Design System: Fira Code + Fira Sans, 浅蓝清新 ── */
[data-theme="light"] {
  --bg: #FAFBFD;
  --bg-card: #FFFFFF;
  --bg-hover: #F4F6FA;
  --bg-input: #F1F4F9;
  --bg-accent: #EFF4FF;
  --border: #E4E9F1;
  --border-hover: #CDD4E0;
  --text: #1A2138;
  --text-2: #4A5468;
  --text-3: #8E99AE;
  --accent: #4A90D9;
  --accent-bg: rgba(74,144,217,0.07);
  --accent-text: #3A7BC8;
  --accent-glow: rgba(74,144,217,0.1);
  --green: #2D9E6F;
  --green-bg: rgba(45,158,111,0.07);
  --yellow: #D08B2D;
  --yellow-bg: rgba(208,139,45,0.07);
  --red: #CC4444;
  --mark-bg: rgba(74,144,217,0.1);
  --mark-text: #3A7BC8;
  --shadow-sm: 0 1px 2px rgba(26,33,56,0.03);
  --shadow: 0 1px 3px rgba(26,33,56,0.05), 0 1px 2px rgba(26,33,56,0.03);
  --shadow-lg: 0 8px 32px rgba(26,33,56,0.06);
  --modal-bg: rgba(26,33,56,0.12);
}
[data-theme="dark"] {
  --bg: #0E1525;
  --bg-card: #172033;
  --bg-hover: #1E2A42;
  --bg-input: #172033;
  --bg-accent: rgba(74,144,217,0.08);
  --border: #253350;
  --border-hover: #334768;
  --text: #E2E7F0;
  --text-2: #94A0B8;
  --text-3: #5E6E8A;
  --accent: #6BADE8;
  --accent-bg: rgba(107,173,232,0.1);
  --accent-text: #8EC3F0;
  --accent-glow: rgba(107,173,232,0.12);
  --green: #4CD9A0;
  --green-bg: rgba(76,217,160,0.1);
  --yellow: #F0C050;
  --yellow-bg: rgba(240,192,80,0.1);
  --red: #F07070;
  --mark-bg: rgba(107,173,232,0.14);
  --mark-text: #8EC3F0;
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
  --shadow: 0 1px 3px rgba(0,0,0,0.3);
  --shadow-lg: 0 8px 32px rgba(0,0,0,0.35);
  --modal-bg: rgba(0,0,0,0.4);
}
:root {
  --r: 10px;
  --font: 'Fira Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --mono: 'Fira Code', ui-monospace, monospace;
}

*,*::before,*::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  transition: background 0.3s ease, color 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
.app { max-width: 900px; margin: 0 auto; padding: 32px 24px; }

/* ── Header ── */
.header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 28px; padding-bottom: 20px;
  border-bottom: 1px solid var(--border);
}
.logo {
  font-family: var(--mono); font-size: 20px; font-weight: 700;
  color: var(--text); letter-spacing: -0.5px;
  display: flex; align-items: center; gap: 10px;
}
.logo span { color: var(--accent); }
.logo-icon { flex-shrink: 0; }
.logo-icon g { animation-play-state: paused !important; }
.logo-icon.active g { animation-play-state: running !important; }
.logo-icon .core-pulse { r: 4.5; }
.logo-icon.active .core-pulse { animation: corePulse 3s ease-in-out infinite; }
@keyframes spinCW { to { transform: rotate(360deg); } }
@keyframes spinCCW { to { transform: rotate(-360deg); } }
@keyframes corePulse { 0%,100% { r: 4; } 50% { r: 5.5; } }
.header-right { display: flex; align-items: center; gap: 10px; }
.nav-tabs { display: flex; gap: 2px; background: var(--bg-input); border-radius: 10px; padding: 3px; }
.nav-tab {
  padding: 6px 16px; border-radius: 8px; border: none; background: none;
  color: var(--text-3); cursor: pointer; font: 500 13px var(--font);
  transition: all 0.2s ease;
}
.nav-tab:hover { color: var(--text-2); }
.nav-tab.active { background: var(--bg-card); color: var(--text); box-shadow: var(--shadow); }
.theme-btn {
  width: 34px; height: 34px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--bg-card);
  color: var(--text-3); cursor: pointer; display: flex;
  align-items: center; justify-content: center;
  transition: all 0.2s ease; flex-shrink: 0;
}
.theme-btn:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); }
.theme-btn svg { width: 16px; height: 16px; }
[data-theme="dark"] .icon-sun { display: none; }
[data-theme="light"] .icon-moon { display: none; }

/* ── Search ── */
.search-row { display: flex; gap: 10px; margin-bottom: 16px; }
.search-box { position: relative; flex: 1; }
.search-box svg.s-icon {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  color: var(--text-3); width: 18px; height: 18px; pointer-events: none;
  transition: color 0.2s;
}
.search-box input:focus ~ svg.s-icon,
.search-box:focus-within svg.s-icon { color: var(--accent); }
.search-box input {
  width: 100%; padding: 12px 70px 12px 42px;
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--r); color: var(--text); font: 15px var(--font);
  outline: none; transition: all 0.2s ease;
}
.search-box input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
.search-box input::placeholder { color: var(--text-3); font-weight: 300; }
.s-actions {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; gap: 4px;
}
.s-clear {
  display: none; background: none; border: none;
  color: var(--text-3); cursor: pointer; padding: 3px 5px;
  border-radius: 4px; font-size: 16px; line-height: 1;
}
.s-clear:hover { color: var(--text); }
.s-clear.show { display: block; }
.kbd {
  padding: 2px 7px; border-radius: 5px;
  font: 11px var(--mono); color: var(--text-3);
  background: var(--bg-input); border: 1px solid var(--border);
}
.col-select {
  padding: 0 30px 0 12px; height: 44px;
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--r); color: var(--text-2); font: 13px var(--font);
  cursor: pointer; outline: none; appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2394A3B8' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
  transition: border-color 0.2s;
}
.col-select:focus { border-color: var(--accent); }

/* ── Meta ── */
.meta-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; font-size: 12px; color: var(--text-3); flex-wrap: wrap; }
.badge { padding: 3px 10px; border-radius: 99px; font: 600 10px var(--mono); letter-spacing: 0.4px; text-transform: uppercase; }
.badge-hybrid { background: var(--accent-bg); color: var(--accent); }
.badge-bm25 { background: var(--yellow-bg); color: var(--yellow); }

/* ── Results ── */
.results { display: flex; flex-direction: column; gap: 6px; }
.result-card {
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--r); padding: 14px 16px; cursor: pointer;
  transition: all 0.15s ease; animation: fadeIn 0.2s ease both;
}
.result-card:hover { background: var(--bg-hover); border-color: var(--border-hover); transform: translateY(-1px); box-shadow: var(--shadow); }
.result-card.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.r-top { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.r-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.r-dot.high { background: var(--green); box-shadow: 0 0 6px var(--green-bg); }
.r-dot.medium { background: var(--yellow); }
.r-dot.low { background: var(--text-3); }
.r-src { font: 500 12px var(--mono); color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.r-right { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
.r-tok { font: 11px var(--mono); color: var(--text-3); background: var(--bg-input); padding: 2px 7px; border-radius: 5px; }
.r-score { font: 11px var(--mono); color: var(--text-3); }
.r-sec { font: 11px var(--mono); color: var(--text-3); margin-bottom: 3px; padding-left: 15px; }
.r-sum { font-size: 13px; color: var(--text-2); line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; padding-left: 15px; }
.r-sum mark { background: var(--mark-bg); color: var(--mark-text); border-radius: 2px; padding: 0 2px; }

/* ── Modal ── */
.modal-overlay {
  display: none; position: fixed; inset: 0;
  background: var(--modal-bg); backdrop-filter: blur(8px);
  z-index: 100; justify-content: center; align-items: flex-start;
  padding: 60px 20px; overflow-y: auto;
}
.modal-overlay.open { display: flex; animation: mfade 0.15s ease; }
@keyframes mfade { from { opacity: 0; } to { opacity: 1; } }
.modal {
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 14px; width: 100%; max-width: 720px;
  padding: 28px; position: relative;
  box-shadow: var(--shadow-lg); animation: mslide 0.2s ease;
}
@keyframes mslide { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: none; } }
.m-bar { position: absolute; right: 14px; top: 14px; display: flex; gap: 4px; }
.m-btn {
  background: none; border: 1px solid var(--border);
  color: var(--text-3); cursor: pointer;
  padding: 6px 10px; border-radius: 7px;
  font: 12px var(--font); transition: all 0.15s;
  display: flex; align-items: center; gap: 4px;
}
.m-btn:hover { color: var(--text); background: var(--bg-hover); }
.m-btn.copied { color: var(--green); border-color: var(--green); }
.modal h2 { font: 600 15px var(--mono); margin-bottom: 16px; padding-right: 100px; color: var(--accent); word-break: break-all; }
.m-meta {
  display: flex; flex-wrap: wrap; gap: 16px;
  margin-bottom: 16px; padding: 12px 14px;
  background: var(--bg); border-radius: 8px; border: 1px solid var(--border);
}
.m-meta-item .label { font: 400 10px var(--font); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 2px; }
.m-meta-item .value { font: 13px var(--mono); color: var(--text); }
.m-content {
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 16px; font: 13px/1.7 var(--mono);
  white-space: pre-wrap; word-wrap: break-word;
  max-height: 450px; overflow-y: auto; tab-size: 4;
}
.m-ctx { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); font: 11px var(--mono); color: var(--text-3); display: flex; gap: 20px; }

/* ── Panels ── */
.panel { display: none; }
.panel.active { display: block; }

/* ── Status ── */
.status-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 18px; }
.stat-card {
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--r); padding: 18px 20px;
  transition: border-color 0.2s;
}
.stat-card:hover { border-color: var(--border-hover); }
.stat-card .label { font: 500 10px var(--font); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px; }
.stat-card .value { font: 700 28px var(--mono); letter-spacing: -1px; color: var(--accent); }
.stat-card .value.small { font-size: 13px; font-weight: 500; letter-spacing: 0; }
.stat-card .value.on { color: var(--green); }
.stat-card .value.off { color: var(--text-3); }
.stat-card .sub { font: 11px var(--mono); color: var(--text-3); margin-top: 4px; }
.s-refresh {
  display: inline-flex; align-items: center; gap: 6px;
  margin-bottom: 16px; padding: 7px 14px;
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 8px; color: var(--text-3); font: 12px var(--font);
  cursor: pointer; transition: all 0.15s;
}
.s-refresh:hover { color: var(--accent); border-color: var(--accent); }

/* ── Index ── */
.idx-form {
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 12px; padding: 24px;
}
.idx-form label { display: block; font: 500 10px var(--font); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px; }
.idx-form input[type="text"] {
  width: 100%; padding: 10px 14px;
  background: var(--bg-input); border: 1px solid var(--border);
  border-radius: 8px; color: var(--text); font: 13px var(--mono);
  outline: none; margin-bottom: 16px; transition: border-color 0.2s;
}
.idx-form input[type="text"]:focus { border-color: var(--accent); }
.idx-opts { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
.chk { display: flex; align-items: center; gap: 6px; font: 13px var(--font); color: var(--text-2); cursor: pointer; }
.chk input { accent-color: var(--accent); width: 15px; height: 15px; }
.btn {
  padding: 10px 24px; border: none; border-radius: 8px;
  font: 600 13px var(--font); cursor: pointer; transition: all 0.15s;
}
.btn-p { background: var(--accent); color: #fff; }
.btn-p:hover { filter: brightness(1.1); box-shadow: 0 2px 12px var(--accent-glow); }
.btn-p:disabled { opacity: 0.4; cursor: not-allowed; filter: none; box-shadow: none; }
.idx-log {
  margin-top: 16px; background: var(--bg); border: 1px solid var(--border);
  border-radius: 8px; padding: 12px 16px; font: 12px var(--mono);
  color: var(--text-3); max-height: 220px; overflow-y: auto; display: none;
}
.idx-log.show { display: block; }
.idx-log .ok { color: var(--green); }
.idx-log .err { color: var(--red); }
.idx-log .ts { opacity: 0.4; margin-right: 6px; }

/* ── Toast ── */
.toast {
  position: fixed; bottom: 24px; left: 50%;
  transform: translateX(-50%) translateY(60px);
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px 20px; font: 13px var(--font);
  color: var(--text); box-shadow: var(--shadow-lg);
  z-index: 200; opacity: 0; transition: all 0.3s ease; pointer-events: none;
}
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }

/* ── Spinner ── */
.spinner {
  display: inline-block; width: 14px; height: 14px;
  border: 2px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; animation: spin 0.6s linear infinite;
  vertical-align: middle; margin-right: 6px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Empty ── */
.empty { text-align: center; padding: 52px 20px; color: var(--text-3); font-size: 14px; }
.empty svg { margin-bottom: 12px; opacity: 0.4; }
.empty .hint { margin-top: 12px; font: 12px var(--mono); opacity: 0.35; }

/* ── View Modes ── */
.view-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.view-bar-left { font: 13px var(--font); color: var(--text-3); }
.view-bar-left b { color: var(--text-2); font-weight: 600; }
.view-modes { display: flex; gap: 2px; background: var(--bg-input); border-radius: 8px; padding: 3px; }
.view-mode {
  width: 30px; height: 28px; border: none; border-radius: 6px;
  background: none; color: var(--text-3); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: all 0.15s;
}
.view-mode:hover { color: var(--text-2); }
.view-mode.active { background: var(--bg-card); color: var(--accent); box-shadow: var(--shadow-sm); }
.view-mode svg { width: 15px; height: 15px; }

/* ── Table View ── */
.results-table { width: 100%; border-collapse: collapse; }
.results-table th {
  text-align: left; font: 500 10px var(--font); color: var(--text-3);
  text-transform: uppercase; letter-spacing: 0.6px;
  padding: 8px 12px; border-bottom: 1px solid var(--border);
}
.results-table td {
  padding: 10px 12px; border-bottom: 1px solid var(--border);
  font: 13px var(--mono); color: var(--text-2); cursor: pointer;
  transition: background 0.1s;
}
.results-table tr:hover td { background: var(--bg-hover); }
.results-table .t-path { color: var(--accent); font-weight: 500; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.results-table .t-num { text-align: right; color: var(--text-3); font-size: 12px; }
.results-table .t-type { font-size: 11px; }

/* ── Grid View ── */
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
.grid-card {
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--r); padding: 14px 16px; cursor: pointer;
  transition: all 0.15s; animation: fadeIn 0.2s ease both;
}
.grid-card:hover { background: var(--bg-hover); border-color: var(--accent); transform: translateY(-1px); box-shadow: var(--shadow); }
.grid-card .g-icon { font-size: 20px; margin-bottom: 6px; }
.grid-card .g-name { font: 500 12px var(--mono); color: var(--accent); margin-bottom: 6px; word-break: break-all; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.grid-card .g-meta { font: 11px var(--font); color: var(--text-3); }

/* ── Project Cards ── */
.proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
.proj-card {
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--r); padding: 18px 20px; cursor: pointer;
  transition: all 0.15s ease; animation: fadeIn 0.2s ease both;
}
.proj-card:hover { background: var(--bg-hover); border-color: var(--accent); transform: translateY(-2px); box-shadow: var(--shadow); }
.proj-name { font: 600 15px var(--mono); color: var(--accent); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.proj-name svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--accent); opacity: 0.7; }
.proj-stats { display: flex; gap: 16px; }
.proj-stat { font: 12px var(--font); color: var(--text-3); }
.proj-stat b { color: var(--text-2); font-weight: 600; }
.proj-overview { margin-bottom: 8px; }
.proj-overview-title { font: 500 12px var(--font); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 10px; }
.proj-total { text-align: center; padding: 16px; margin-bottom: 16px; font: 13px var(--font); color: var(--text-3); background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--r); }
.proj-total b { color: var(--accent); font: 700 20px var(--mono); }

/* ── Scrollbar ── */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-hover); }

/* ── Responsive ── */
@media (max-width: 640px) {
  .app { padding: 20px 16px; }
  .header { flex-wrap: wrap; gap: 12px; }
  .search-row { flex-direction: column; }
  .col-select { min-width: auto; height: 40px; }
  .stat-card .value { font-size: 22px; }
  .modal { padding: 20px; }
  .kbd { display: none; }
}
</style>
</head>
<body>
<div class="app">
  <div class="header">
    <div class="logo">
      <svg class="logo-icon" width="56" height="56" viewBox="0 0 56 56" fill="none">
        <g style="transform-origin:28px 28px;animation:spinCW 8s linear infinite">
          <ellipse cx="28" cy="28" rx="18" ry="6" stroke="var(--accent)" stroke-width="1.5" opacity=".25"/>
          <circle cx="46" cy="28" r="2.5" fill="var(--accent)" opacity=".5"/>
          <circle cx="10" cy="28" r="2" fill="var(--accent)" opacity=".3"/>
        </g>
        <g style="transform-origin:28px 28px;animation:spinCCW 6s linear infinite">
          <ellipse cx="28" cy="28" rx="18" ry="6" stroke="var(--accent)" stroke-width="1.5" opacity=".25" transform="rotate(60 28 28)"/>
          <circle cx="28" cy="10.2" r="2.5" fill="var(--accent)" opacity=".5" transform="rotate(60 28 28)"/>
        </g>
        <g style="transform-origin:28px 28px;animation:spinCW 10s linear infinite">
          <ellipse cx="28" cy="28" rx="18" ry="6" stroke="var(--accent)" stroke-width="1.5" opacity=".25" transform="rotate(120 28 28)"/>
          <circle cx="28" cy="45.8" r="2" fill="var(--accent)" opacity=".4" transform="rotate(120 28 28)"/>
        </g>
        <circle class="core-pulse" cx="28" cy="28" r="4.5" fill="var(--accent)"/>
      </svg>
      <div><span>index</span>1</div>
    </div>
    <div class="header-right">
      <div class="nav-tabs">
        <button class="nav-tab active" data-panel="search-panel" onclick="switchTab(this)">Search</button>
        <button class="nav-tab" data-panel="status-panel" onclick="switchTab(this)">Status</button>
        <button class="nav-tab" data-panel="index-panel" onclick="switchTab(this)">Index</button>
      </div>
      <button class="theme-btn" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
        <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72l1.42 1.42M1 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
        <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
      </button>
    </div>
  </div>

  <!-- Search -->
  <div id="search-panel" class="panel active">
    <div class="search-row">
      <div class="search-box">
        <svg class="s-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
        <input type="text" id="search-input" placeholder="Search knowledge base..." autofocus>
        <div class="s-actions">
          <button class="s-clear" id="s-clear" onclick="clearSearch()" aria-label="Clear search">&times;</button>
          <span class="kbd" id="kbd-slash">/</span>
        </div>
      </div>
      <select class="col-select" id="col-filter" onchange="doSearch()" aria-label="Filter collection">
        <option value="">All Projects</option>
      </select>
      <select class="col-select" id="type-filter" onchange="applyTypeFilter()" aria-label="Filter type">
        <option value="">All Types</option>
      </select>
    </div>
    <div id="meta" class="meta-bar" style="display:none"></div>
    <div id="proj-overview" class="proj-overview"></div>
    <div id="results" class="results"></div>
  </div>

  <!-- Status -->
  <div id="status-panel" class="panel"><div id="status-content"></div></div>

  <!-- Index -->
  <div id="index-panel" class="panel">
    <div class="idx-form">
      <label>Path</label>
      <input type="text" id="idx-path" placeholder="/path/to/project">
      <div class="idx-opts">
        <label class="chk"><input type="checkbox" id="idx-force"> Force re-index</label>
        <input type="text" id="idx-col" placeholder="collection" style="width:170px;margin-bottom:0;">
      </div>
      <button class="btn btn-p" id="idx-btn" onclick="runIndex()">Index</button>
      <div id="idx-log" class="idx-log"></div>
    </div>
  </div>
</div>

<!-- Modal -->
<div id="chunk-modal" class="modal-overlay" onclick="if(event.target===this)closeModal()">
  <div class="modal" role="dialog" aria-modal="true">
    <div class="m-bar">
      <button class="m-btn" id="copy-btn" onclick="copyContent()" aria-label="Copy content">
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
        <span>Copy</span>
      </button>
      <button class="m-btn" onclick="closeModal()" aria-label="Close">
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
      </button>
    </div>
    <h2 id="m-title"></h2>
    <div id="m-meta" class="m-meta"></div>
    <div id="m-content" class="m-content"></div>
  </div>
</div>

<div id="toast" class="toast" role="alert"></div>

<script>
// ── Theme ──
function toggleTheme() {
  const html = document.documentElement;
  const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  html.setAttribute('data-theme', next);
  try { localStorage.setItem('index1-theme', next); } catch(e) {}
}
(function() {
  try {
    const saved = localStorage.getItem('index1-theme');
    if (saved) document.documentElement.setAttribute('data-theme', saved);
  } catch(e) {}
})();

// ── State ──
let selIdx = -1, curResults = [], curContent = '', curViewMode = 'list', curBrowseDocs = [];

// ── Tabs ──
function switchTab(btn) {
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
  document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
  btn.classList.add('active');
  document.getElementById(btn.dataset.panel).classList.add('active');
  if (btn.dataset.panel === 'status-panel') loadStatus();
}

// ── Search ──
let timer = null;
const sInput = document.getElementById('search-input');
const sClear = document.getElementById('s-clear');
const sKbd = document.getElementById('kbd-slash');

sInput.addEventListener('input', () => {
  clearTimeout(timer);
  sClear.classList.toggle('show', sInput.value.length > 0);
  sKbd.style.display = sInput.value.length > 0 ? 'none' : '';
  timer = setTimeout(doSearch, 300);
});
sInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    if (selIdx >= 0 && curResults[selIdx]) showChunk(curResults[selIdx].chunk_id);
    else { clearTimeout(timer); doSearch(); }
    return;
  }
  if (e.key === 'ArrowDown') { e.preventDefault(); moveSel(1); }
  if (e.key === 'ArrowUp') { e.preventDefault(); moveSel(-1); }
});

function clearSearch() {
  sInput.value = ''; sInput.focus();
  sClear.classList.remove('show'); sKbd.style.display = '';
  selIdx = -1; curResults = []; curBrowseDocs = []; activateLogo(false);
  document.getElementById('results').innerHTML = '';
  document.getElementById('meta').style.display = 'none';
  document.getElementById('proj-overview').style.display = '';
  document.getElementById('col-filter').value = '';
  document.getElementById('type-filter').value = '';
  // 恢复全局 type filter
  fetch('/api/status').then(r => r.json()).then(d => { if (d.doc_types) updTypes(d.doc_types); }).catch(() => {});
  loadProjectOverview();
}

function moveSel(d) {
  const c = document.querySelectorAll('.result-card');
  if (!c.length) return;
  c.forEach(x => x.classList.remove('selected'));
  selIdx = Math.max(-1, Math.min(c.length - 1, selIdx + d));
  if (selIdx >= 0) { c[selIdx].classList.add('selected'); c[selIdx].scrollIntoView({ block: 'nearest' }); }
}

function hl(text, q) {
  if (!q || !text) return esc(text);
  const terms = q.split(/\s+/).filter(t => t.length > 1);
  if (!terms.length) return esc(text);
  const e = esc(text);
  const p = terms.map(t => esc(t).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
  try { return e.replace(new RegExp('(' + p + ')', 'gi'), '<mark>$1</mark>'); } catch(x) { return e; }
}

async function doSearch() {
  const q = sInput.value.trim();
  const rEl = document.getElementById('results');
  const mEl = document.getElementById('meta');
  const col = document.getElementById('col-filter').value;
  selIdx = -1; curResults = [];

  if (!q) {
    // 搜索框为空但选了集合 → 浏览该集合
    if (col) { browseCollection(col); return; }
    clearSearch(); return;
  }

  document.getElementById('proj-overview').style.display = 'none';
  rEl.innerHTML = '<div class="empty"><span class="spinner"></span> Searching...</div>';
  mEl.style.display = 'none';

  try {
    let url = '/api/search?q=' + encodeURIComponent(q) + '&top_k=10';
    if (col) url += '&collection=' + encodeURIComponent(col);
    const resp = await fetch(url);
    const data = await resp.json();

    if (data.error) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }
    if (!data.results || !data.results.length) { activateLogo(false); rEl.innerHTML = '<div class="empty">No results for "' + esc(q) + '"</div>'; return; }

    curResults = data.results;
    activateLogo(data.results.length > 0);
    const mc = data.mode === 'hybrid' ? 'badge-hybrid' : 'badge-bm25';
    mEl.innerHTML = '<span>' + data.total + ' results &middot; ' + data.query_ms + 'ms</span><span class="badge ' + mc + '">' + esc(data.mode) + '</span>' + (data.hint ? '<span>' + esc(data.hint) + '</span>' : '');
    mEl.style.display = 'flex';

    rEl.innerHTML = data.results.map((r, i) =>
      '<div class="result-card" style="animation-delay:' + (i*30) + 'ms" onclick="showChunk(' + r.chunk_id + ')" onmouseenter="selIdx=' + i + '">' +
        '<div class="r-top">' +
          '<span class="r-dot ' + r.relevance + '"></span>' +
          '<span class="r-src">' + esc(r.source || '?') + '</span>' +
          '<div class="r-right">' + (r.token_count ? '<span class="r-tok">' + r.token_count + ' tok</span>' : '') +
            '<span class="r-score">' + r.score.toFixed(3) + '</span></div>' +
        '</div>' +
        (r.section ? '<div class="r-sec">' + esc(r.section) + '</div>' : '') +
        '<div class="r-sum">' + hl(r.summary ? r.summary.substring(0,200) : (r.content ? r.content.substring(0,200) : ''), q) + '</div>' +
      '</div>'
    ).join('');
  } catch (e) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

// ── Chunk ──
async function showChunk(id) {
  const modal = document.getElementById('chunk-modal');
  const tEl = document.getElementById('m-title');
  const mEl = document.getElementById('m-meta');
  const cEl = document.getElementById('m-content');
  curContent = ''; tEl.textContent = 'Loading...'; mEl.innerHTML = ''; cEl.textContent = '';
  modal.classList.add('open');
  document.getElementById('copy-btn').classList.remove('copied');
  document.getElementById('copy-btn').querySelector('span').textContent = 'Copy';

  try {
    const data = await (await fetch('/api/chunk/' + id)).json();
    if (data.error) { tEl.textContent = 'Error'; cEl.textContent = data.error; return; }
    curContent = data.content || '';
    tEl.textContent = data.source || 'Chunk #' + data.id;
    mEl.innerHTML = [
      { l: 'Chunk', v: '#' + data.id }, { l: 'Section', v: data.section || '-' },
      { l: 'Index', v: data.chunk_index }, { l: 'Tokens', v: data.token_count || '-' },
      { l: 'Tags', v: data.tags || '-' },
    ].map(m => '<div class="m-meta-item"><div class="label">' + m.l + '</div><div class="value">' + esc(String(m.v)) + '</div></div>').join('');
    cEl.textContent = curContent;
    if (data.context && (data.context.prev_section || data.context.next_section)) {
      const d = document.createElement('div'); d.className = 'm-ctx';
      if (data.context.prev_section) { const s = document.createElement('span'); s.textContent = '\u2190 ' + data.context.prev_section; d.appendChild(s); }
      if (data.context.next_section) { const s = document.createElement('span'); s.textContent = data.context.next_section + ' \u2192'; d.appendChild(s); }
      cEl.appendChild(d);
    }
  } catch (e) { tEl.textContent = 'Error'; cEl.textContent = e.message; }
}

async function copyContent() {
  if (!curContent) return;
  try {
    await navigator.clipboard.writeText(curContent);
    const b = document.getElementById('copy-btn'); b.classList.add('copied');
    b.querySelector('span').textContent = 'Copied!';
    setTimeout(() => { b.classList.remove('copied'); b.querySelector('span').textContent = 'Copy'; }, 2000);
  } catch(e) { toast('Copy failed'); }
}

function closeModal() { document.getElementById('chunk-modal').classList.remove('open'); }

// ── Status ──
async function loadStatus() {
  const el = document.getElementById('status-content');
  el.innerHTML = '<div class="empty"><span class="spinner"></span></div>';
  try {
    const data = await (await fetch('/api/status')).json();
    if (data.error) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }
    const vc = data.vec_loaded ? 'on' : 'off';
    el.innerHTML =
      '<button class="s-refresh" onclick="loadStatus()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10"/></svg> Refresh</button>' +
      '<div class="status-grid">' +
        sc('Documents', data.doc_count, '') + sc('Chunks', data.chunk_count, '') +
        sc('Collections', data.collections ? data.collections.length : 0, data.collections ? data.collections.join(', ') : '-') +
        sc('Vector', data.vec_loaded ? 'ON' : 'OFF', data.vec_loaded ? 'sqlite-vec loaded' : 'BM25 only', vc) +
      '</div><div class="status-grid">' +
        sc('Last Indexed', data.last_indexed || 'Never', '', 'small') +
        sc('Database', '', data.db_path || '-', 'small') + '</div>';
    updCol(data.collections || []);
    updTypes(data.doc_types || []);
  } catch(e) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

function sc(l, v, s, c) {
  return '<div class="stat-card"><div class="label">' + l + '</div><div class="value ' + (c||'') + '">' + esc(String(v)) + '</div>' + (s ? '<div class="sub">' + esc(s) + '</div>' : '') + '</div>';
}

function updCol(cols) {
  const sel = document.getElementById('col-filter');
  const cur = sel.value;
  sel.innerHTML = '<option value="">All Projects</option>';
  cols.forEach(c => { const o = document.createElement('option'); o.value = c; o.textContent = c; if (c === cur) o.selected = true; sel.appendChild(o); });
}

const TYPE_LABELS = { markdown: 'Markdown', python: 'Python', rust: 'Rust', javascript: 'JavaScript', text: 'Text' };
function updTypes(types) {
  const sel = document.getElementById('type-filter');
  const cur = sel.value;
  sel.innerHTML = '<option value="">All Types</option>';
  (types || []).forEach(t => { const o = document.createElement('option'); o.value = t; o.textContent = TYPE_LABELS[t] || t; if (t === cur) o.selected = true; sel.appendChild(o); });
}

function applyTypeFilter() {
  const tf = document.getElementById('type-filter').value;
  const col = document.getElementById('col-filter').value;
  // 如果在浏览模式（没有搜索词），重新加载浏览结果
  if (!sInput.value.trim() && col) {
    browseCollection(col);
    return;
  }
  // 搜索模式下，客户端过滤
  if (!tf) { document.querySelectorAll('.result-card').forEach(c => c.style.display = ''); return; }
  document.querySelectorAll('.result-card').forEach((c, i) => {
    if (!curResults[i]) return;
    const src = curResults[i].source || '';
    const ext = src.split('.').pop().toLowerCase();
    const typeMap = { py: 'python', md: 'markdown', rs: 'rust', js: 'javascript', ts: 'javascript', txt: 'text', toml: 'text', cfg: 'text', ini: 'text', yml: 'text', yaml: 'text' };
    const docType = typeMap[ext] || 'text';
    c.style.display = (docType === tf) ? '' : 'none';
  });
}

// ── Index ──
async function runIndex() {
  const pi = document.getElementById('idx-path');
  const path = pi.value.trim();
  if (!path) { pi.focus(); return; }
  const force = document.getElementById('idx-force').checked;
  const col = document.getElementById('idx-col').value.trim() || undefined;
  const btn = document.getElementById('idx-btn');
  const log = document.getElementById('idx-log');
  btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Indexing...';
  log.classList.add('show');
  log.innerHTML = '<div><span class="ts">' + new Date().toLocaleTimeString() + '</span> ' + esc(path) + '</div>';
  try {
    const data = await (await fetch('/api/reindex', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths: [path], force, collection: col }) })).json();
    const t = new Date().toLocaleTimeString();
    if (data.error) log.innerHTML += '<div class="err"><span class="ts">' + t + '</span> ' + esc(data.error) + '</div>';
    else { log.innerHTML += '<div class="ok"><span class="ts">' + t + '</span> ' + data.indexed + ' indexed, ' + data.skipped + ' skipped, ' + data.failed + ' failed</div>'; toast(data.indexed + ' files indexed'); if (data.indexed > 0) activateLogo(true); }
  } catch(e) { log.innerHTML += '<div class="err">' + esc(e.message) + '</div>'; }
  finally { btn.disabled = false; btn.textContent = 'Index'; }
}

// ── Keys ──
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') { closeModal(); return; }
  if (['INPUT','TEXTAREA','SELECT'].includes(e.target.tagName)) return;
  if (e.key === '/') { e.preventDefault(); document.querySelectorAll('.nav-tab')[0].click(); sInput.focus(); }
  if (e.key >= '1' && e.key <= '3') { const t = document.querySelectorAll('.nav-tab'); if (t[e.key-1]) switchTab(t[e.key-1]); }
});

// ── Util ──
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function toast(m) { const t = document.getElementById('toast'); t.textContent = m; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2500); }

// ── Logo activation ──
function activateLogo(on) {
  const el = document.querySelector('.logo-icon');
  if (el) el.classList.toggle('active', !!on);
}

// ── Project Overview ──
async function loadProjectOverview() {
  const el = document.getElementById('proj-overview');
  try {
    const data = await (await fetch('/api/collections')).json();
    if (!data || !data.length) {
      el.innerHTML = '<div class="empty"><svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg><p>No indexed projects yet</p><div class="hint">Use the Index tab to add projects</div></div>';
      return;
    }
    const totalDocs = data.reduce((s, c) => s + c.doc_count, 0);
    const totalChunks = data.reduce((s, c) => s + c.chunk_count, 0);
    el.innerHTML =
      '<div class="proj-total"><b>' + data.length + '</b> projects &middot; <b>' + totalDocs + '</b> documents &middot; <b>' + totalChunks + '</b> chunks</div>' +
      '<div class="proj-grid">' + data.map((c, i) =>
        '<div class="proj-card" style="animation-delay:' + (i*50) + 'ms" onclick="browseCollection(\'' + esc(c.collection) + '\')">' +
          '<div class="proj-name"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>' + esc(c.collection) + '</div>' +
          '<div class="proj-stats"><span class="proj-stat"><b>' + c.doc_count + '</b> docs</span><span class="proj-stat"><b>' + c.chunk_count + '</b> chunks</span></div>' +
        '</div>'
      ).join('') + '</div>';
  } catch(e) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

async function browseCollection(col) {
  document.getElementById('col-filter').value = col;
  document.getElementById('proj-overview').style.display = 'none';
  const rEl = document.getElementById('results');
  const mEl = document.getElementById('meta');
  rEl.innerHTML = '<div class="empty"><span class="spinner"></span> Loading...</div>';

  const tf = document.getElementById('type-filter').value;
  let url = '/api/browse?collection=' + encodeURIComponent(col) + '&limit=100';
  if (tf) url += '&doc_type=' + encodeURIComponent(tf);

  try {
    const data = await (await fetch(url)).json();
    if (data.error) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }

    // 动态更新 type filter，只显示该集合存在的类型
    if (data.doc_types) updTypes(data.doc_types);

    if (!data.docs || !data.docs.length) { rEl.innerHTML = '<div class="empty">No documents in ' + esc(col) + (tf ? ' (' + tf + ')' : '') + '</div>'; mEl.innerHTML = '<span>0 documents</span><button class="m-btn" style="margin-left:auto" onclick="clearSearch()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>'; mEl.style.display = 'flex'; return; }

    curBrowseDocs = data.docs;

    mEl.innerHTML = '<span>' + data.docs.length + ' documents in <b>' + esc(col) + '</b></span>' +
      '<div class="view-modes">' +
        '<button class="view-mode' + (curViewMode==='list'?' active':'') + '" onclick="switchViewMode(\'list\')" title="List view"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="2" width="14" height="2" rx="0.5"/><rect x="1" y="7" width="14" height="2" rx="0.5"/><rect x="1" y="12" width="14" height="2" rx="0.5"/></svg></button>' +
        '<button class="view-mode' + (curViewMode==='table'?' active':'') + '" onclick="switchViewMode(\'table\')" title="Table view"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="0.5" opacity=".7"/><rect x="9" y="1" width="6" height="6" rx="0.5" opacity=".7"/><rect x="1" y="9" width="6" height="6" rx="0.5" opacity=".7"/><rect x="9" y="9" width="6" height="6" rx="0.5" opacity=".7"/></svg></button>' +
        '<button class="view-mode' + (curViewMode==='grid'?' active':'') + '" onclick="switchViewMode(\'grid\')" title="Grid view"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="4" height="4" rx="0.5"/><rect x="6" y="1" width="4" height="4" rx="0.5"/><rect x="11" y="1" width="4" height="4" rx="0.5"/><rect x="1" y="6" width="4" height="4" rx="0.5"/><rect x="6" y="6" width="4" height="4" rx="0.5"/><rect x="11" y="6" width="4" height="4" rx="0.5"/><rect x="1" y="11" width="4" height="4" rx="0.5"/><rect x="6" y="11" width="4" height="4" rx="0.5"/><rect x="11" y="11" width="4" height="4" rx="0.5"/></svg></button>' +
      '</div>' +
      '<button class="m-btn" onclick="clearSearch()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>';
    mEl.style.display = 'flex';

    renderBrowseView(data.docs);
  } catch(e) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

const TYPE_ICONS = {
  python: '\ud83d\udc0d', rust: '\u2699\ufe0f', javascript: '\ud83d\udfe1',
  markdown: '\ud83d\udcdd', text: '\ud83d\udcc4'
};

function switchViewMode(mode) {
  curViewMode = mode;
  document.querySelectorAll('.view-mode').forEach(b => b.classList.remove('active'));
  const btns = document.querySelectorAll('.view-mode');
  const idx = {list:0, table:1, grid:2}[mode] || 0;
  if (btns[idx]) btns[idx].classList.add('active');
  if (curBrowseDocs.length) renderBrowseView(curBrowseDocs);
}

function renderBrowseView(docs) {
  const rEl = document.getElementById('results');
  if (curViewMode === 'table') {
    rEl.innerHTML = '<table class="results-table"><thead><tr><th>File</th><th>Type</th><th style="text-align:right">Chunks</th><th style="text-align:right">Tokens</th></tr></thead><tbody>' +
      docs.map(d =>
        '<tr class="browse-row" data-doc-id="' + d.id + '">' +
          '<td class="t-path">' + esc(d.path) + '</td>' +
          '<td class="t-type">' + (TYPE_ICONS[d.doc_type]||'') + ' ' + (TYPE_LABELS[d.doc_type]||d.doc_type) + '</td>' +
          '<td class="t-num">' + d.chunk_count + '</td>' +
          '<td class="t-num">' + d.total_tokens + '</td>' +
        '</tr>'
      ).join('') + '</tbody></table>';
  } else if (curViewMode === 'grid') {
    rEl.innerHTML = '<div class="results-grid">' + docs.map((d, i) => {
      const fname = d.path.split('/').pop();
      return '<div class="grid-card browse-row" data-doc-id="' + d.id + '" style="animation-delay:' + (i*20) + 'ms">' +
        '<div class="g-icon">' + (TYPE_ICONS[d.doc_type]||'\ud83d\udcc4') + '</div>' +
        '<div class="g-name">' + esc(fname) + '</div>' +
        '<div class="g-meta">' + d.chunk_count + ' chunks &middot; ' + d.total_tokens + ' tok</div>' +
      '</div>';
    }).join('') + '</div>';
  } else {
    rEl.innerHTML = docs.map((d, i) =>
      '<div class="result-card browse-row" data-doc-id="' + d.id + '" style="animation-delay:' + (i*20) + 'ms">' +
        '<div class="r-top">' +
          '<span style="font-size:14px">' + (TYPE_ICONS[d.doc_type]||'\ud83d\udcc4') + '</span>' +
          '<span class="r-src">' + esc(d.path) + '</span>' +
          '<div class="r-right"><span class="r-tok">' + d.chunk_count + ' chunks</span><span class="r-tok">' + d.total_tokens + ' tok</span></div>' +
        '</div>' +
      '</div>'
    ).join('');
  }
  // 事件委托：安全绑定点击事件，避免 XSS
  rEl.querySelectorAll('.browse-row').forEach(el => {
    el.addEventListener('click', () => browseDoc(parseInt(el.dataset.docId)));
  });
}

async function browseDoc(docId) {
  const modal = document.getElementById('chunk-modal');
  const tEl = document.getElementById('m-title');
  const mEl = document.getElementById('m-meta');
  const cEl = document.getElementById('m-content');
  curContent = ''; tEl.textContent = 'Loading...'; mEl.innerHTML = ''; cEl.textContent = '';
  modal.classList.add('open');
  document.getElementById('copy-btn').classList.remove('copied');
  document.getElementById('copy-btn').querySelector('span').textContent = 'Copy';

  try {
    const data = await (await fetch('/api/doc/' + docId + '/chunks')).json();
    if (data.error) { tEl.textContent = 'Error'; cEl.textContent = data.error; return; }
    tEl.textContent = data.source || 'Document #' + docId;
    mEl.innerHTML = '<div class="m-meta-item"><div class="label">Chunks</div><div class="value">' + data.chunks.length + '</div></div>' +
      '<div class="m-meta-item"><div class="label">Tokens</div><div class="value">' + data.chunks.reduce((s,c) => s + (c.token_count||0), 0) + '</div></div>';
    curContent = data.chunks.map(c => (c.section ? '## ' + c.section + '\n\n' : '') + c.content).join('\n\n---\n\n');
    cEl.textContent = curContent;
  } catch(e) { tEl.textContent = 'Error'; cEl.textContent = e.message; }
}

// ── Init ──
fetch('/api/status').then(r => r.json()).then(d => {
  if (d.collections) updCol(d.collections);
  if (d.doc_types) updTypes(d.doc_types);
}).catch(() => {});
loadProjectOverview();
</script>
</body>
</html>
"""
