"""CLI 命令行入口"""

from __future__ import annotations

import asyncio
import concurrent.futures
import json
import logging
import sys
from pathlib import Path

import click

from .config import BACKEND_PRESETS, Config, load_config, save_config
from .db import Database
from .embed import Embedder, create_embedder


def _setup_logging(verbose: bool = False) -> None:
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(message)s",
        stream=sys.stderr,
    )


def _build_db(config: Config) -> Database:
    """创建 corpus 数据库实例"""
    db = Database(config.db_path_resolved, config.effective_embedding_dim)
    try:
        db.connect()
    except Exception:
        db.close()
        raise
    return db


def _build_cog_db(config: Config) -> "CogDatabase":
    """创建 cognition 数据库实例"""
    from .cognitive.db_cog import CogDatabase
    db_cog = CogDatabase(config.cognitive_db_path_resolved, config.effective_embedding_dim)
    try:
        db_cog.connect()
    except Exception:
        db_cog.close()
        raise
    return db_cog


def _build_deps(config: Config) -> tuple[Database, Embedder]:
    """创建数据库和 embedder 实例"""
    db = _build_db(config)
    embedder = create_embedder(config)
    return db, embedder


@click.group()
@click.version_option(package_name="index1")
@click.option("-v", "--verbose", is_flag=True, help="详细输出")
@click.pass_context
def main(ctx, verbose):
    """index1 - AI-native project knowledge base"""
    _setup_logging(verbose)
    ctx.ensure_object(dict)
    ctx.obj["verbose"] = verbose


@main.command()
@click.argument("paths", nargs=-1, required=False, type=click.Path(exists=True))
@click.option("-c", "--collection", default=None, help="集合名称")
@click.option("--force", is_flag=True, help="强制全量重索引")
@click.option("--resume", is_flag=True, help="续传上次中断的索引任务")
@click.pass_context
def index(ctx, paths, collection, force, resume):
    """索引文件或目录"""
    from .indexer import index_files

    config = load_config()
    db, embedder = _build_deps(config)

    # --resume 模式：从上次中断处继续
    if resume:
        run = db.get_last_interrupted_run(collection)
        if not run or not run["failed_paths"]:
            click.secho("没有可续传的中断任务", fg="yellow")
            db.close()
            return
        resume_paths = [p for p in run["failed_paths"] if Path(p).exists()]
        if not resume_paths:
            click.secho("上次失败的文件已不存在，无需续传", fg="yellow")
            db.close()
            return
        click.echo(
            f"续传上次中断任务 (run #{run['id']}): "
            f"{len(resume_paths)} 个失败文件待重试"
        )
        paths = tuple(resume_paths)
        force = True  # 强制重索引失败文件
        if collection and collection != run["collection"]:
            click.secho(
                f"警告: 上次中断任务的 collection 是 '{run['collection']}'，"
                f"将忽略 --collection '{collection}'",
                fg="yellow",
            )
        collection = run["collection"]

    if not paths:
        click.secho("错误: 请提供路径或使用 --resume 续传", fg="red", err=True)
        db.close()
        return

    # 推断 base_dir：如果只有一个目录参数，用它作 base_dir
    base_dir = None
    if len(paths) == 1:
        p = Path(paths[0]).resolve()
        base_dir = p if p.is_dir() else p.parent

    async def _run():
        try:
            stats = await index_files(
                list(paths), config, db, embedder,
                collection=collection, force=force, base_dir=base_dir,
            )
            click.echo(
                f"\n索引完成: {stats['indexed']} 已索引, "
                f"{stats['skipped']} 已跳过, "
                f"{stats['failed']} 失败 "
                f"(共 {stats['total']} 文件)"
            )
            if stats.get("interrupted") or stats.get("failed_files"):
                click.secho(
                    "提示: 运行 'index1 index --resume' 可续传失败文件",
                    fg="yellow",
                )
        finally:
            await embedder.close()
            db.close()

    asyncio.run(_run())


@main.command()
@click.option("-c", "--collection", default=None, help="集合名称")
@click.pass_context
def backfill(ctx, collection):
    """回填缺失的向量（不重新分块，只补 embedding）"""
    from .indexer import backfill_embeddings

    config = load_config()
    db, embedder = _build_deps(config)

    async def _run():
        try:
            stats = await backfill_embeddings(
                config, db, embedder, collection=collection,
            )
            if stats["total"] == 0:
                click.echo("所有 chunk 都有向量，无需回填")
            else:
                click.echo(
                    f"\n回填完成: {stats['filled']} 成功, "
                    f"{stats['failed']} 失败 "
                    f"(共 {stats['total']} 缺失)"
                )
        finally:
            await embedder.close()
            db.close()

    asyncio.run(_run())


@main.command()
@click.argument("query")
@click.option("-c", "--collection", default=None, help="限定搜索范围")
@click.option("-k", "--top-k", default=5, help="返回结果数量")
@click.option("--verbose", is_flag=True, help="显示完整内容")
@click.pass_context
def search(ctx, query, collection, top_k, verbose):
    """搜索知识库"""
    from .search import search as do_search

    config = load_config()
    db, embedder = _build_deps(config)

    async def _run():
        try:
            resp = await do_search(
                query, config, db, embedder,
                collection=collection, top_k=top_k, verbose=verbose,
            )

            if resp.hint:
                click.secho(f"[{resp.mode}] {resp.hint}", fg="yellow", err=True)

            if not resp.results:
                click.echo("未找到结果")
                return

            click.echo(f"\n找到 {resp.total} 条结果 ({resp.query_ms}ms, {resp.mode}):\n")

            for i, r in enumerate(resp.results, 1):
                # 相关度颜色
                color = {"high": "green", "medium": "yellow", "low": "white"}.get(
                    r.relevance, "white"
                )
                click.secho(f"  [{i}] ", nl=False)
                click.secho(f"[{r.relevance}] ", fg=color, nl=False)
                click.echo(f"{r.source or '?'}")

                if r.section:
                    click.echo(f"      {r.section}")

                if r.summary:
                    summary = r.summary[:120] + "..." if len(r.summary) > 120 else r.summary
                    click.echo(f"      {summary}")

                if verbose and r.content:
                    click.echo(f"      ---")
                    for line in r.content.split("\n")[:10]:
                        click.echo(f"      {line}")

                click.echo()
        finally:
            await embedder.close()
            db.close()

    asyncio.run(_run())


@main.command()
def status():
    """查看索引状态"""
    config = load_config()
    db = _build_db(config)
    try:
        stats = db.get_stats()
    except Exception as e:
        click.secho(f"数据库连接失败: {e}", fg="red", err=True)
        return
    finally:
        db.close()

    # 当前 collection（显式配置 vs 推断）
    coll = config.collection
    is_inferred = coll == "default"
    coll_label = f"{coll} (默认)" if is_inferred else coll
    click.echo(f"当前集合: {coll_label}")

    click.echo(f"数据库: {stats['db_path']}")
    click.echo(f"文档数: {stats['doc_count']}")
    click.echo(f"片段数: {stats['chunk_count']}")
    click.echo(f"全部集合: {', '.join(stats['collections']) or '(空)'}")
    click.echo(f"最近索引: {stats['last_indexed'] or '从未'}")
    click.echo(f"向量搜索: {'可用' if stats['vec_loaded'] else '不可用'}")

    # 最近索引运行记录
    runs = db.get_recent_runs(limit=3)
    if runs:
        click.echo("\n最近索引任务:")
        for r in runs:
            icon = {"completed": "✓", "interrupted": "⚠", "running": "…"}.get(r["status"], "?")
            click.echo(
                f"  [{icon}] {r['started_at']} — "
                f"{r['indexed']}/{r['total_files']} 已索引, "
                f"{r['failed']} 失败 ({r['status']})"
            )
        interrupted = [r for r in runs if r["status"] == "interrupted" and r["failed"] > 0]
        if interrupted:
            click.secho("  → 运行 'index1 index --resume' 续传失败文件", fg="yellow")


@main.command()
@click.argument("key", required=False)
@click.argument("value", required=False)
@click.option("--backend", type=click.Choice(list(BACKEND_PRESETS.keys())),
              help="快捷切换后端预设（等价于 index1 init --backend）")
def config(key, value, backend):
    """查看或修改配置"""
    cfg = load_config()

    # --backend 快捷切换
    if backend:
        preset = BACKEND_PRESETS[backend]
        cfg.embed_api = preset["embed_api"]
        cfg.ollama_url = preset["ollama_url"]
        save_config(cfg)
        click.secho(f"已切换到 {preset['desc']}", fg="green")
        click.echo(f"  embed_api:  {cfg.embed_api}")
        click.echo(f"  ollama_url: {cfg.ollama_url}")
        if preset["embed_api"] == "openai_compat":
            click.echo(f"\n提示: 可能需要调整模型和维度:")
            click.echo(f"  index1 config embedding_model <模型名>")
            click.echo(f"  index1 config embedding_dim <维度>")
        return

    if key and value:
        from dataclasses import fields

        valid = {f.name for f in fields(Config)}
        if key not in valid:
            click.secho(f"未知配置项: {key}", fg="red", err=True)
            click.echo(f"可用配置: {', '.join(sorted(valid))}")
            return

        # 根据默认值的实际类型转换，避免字符串匹配误判
        default_val = getattr(Config(), key)
        if isinstance(default_val, bool):
            value = value.lower() in ("true", "1", "yes")
        elif isinstance(default_val, int):
            value = int(value)
        elif isinstance(default_val, float):
            value = float(value)
        elif isinstance(default_val, list):
            value = [v.strip() for v in value.split(",") if v.strip()]

        setattr(cfg, key, value)
        save_config(cfg)
        click.echo(f"已设置 {key} = {value}")
        if key == "embedding_model":
            click.secho("注意: 模型已更改，请运行 index1 index --force 重建索引", fg="yellow")
        return

    if key:
        # 只查看单个配置项
        from dataclasses import fields

        valid = {f.name for f in fields(Config)}
        if key not in valid:
            click.secho(f"未知配置项: {key}", fg="red", err=True)
            return
        click.echo(f"{key} = {getattr(cfg, key)}")
        return

    # 显示全部配置
    from dataclasses import fields

    click.echo("当前配置:")
    for f in fields(Config):
        click.echo(f"  {f.name}: {getattr(cfg, f.name)}")


@main.command("repo-map")
@click.option("-c", "--collection", default=None, help="集合名称")
@click.option("--budget", default=1024, help="Token 预算")
@click.option("--focus", multiple=True, help="聚焦文件路径")
def repo_map(collection, budget, focus):
    """生成项目代码结构地图"""
    from .graph import generate_repo_map

    config = load_config()
    db = _build_db(config)
    try:
        col = collection or config.collection
        focus_files = list(focus) if focus else None
        result = generate_repo_map(db, col, budget=budget, focus_files=focus_files)
        if result.strip():
            click.echo(result)
        else:
            click.secho("无代码图数据。请先安装 codegraph 依赖并重新索引。", fg="yellow")
    except Exception as e:
        click.secho(f"生成 repo-map 失败: {e}", fg="red", err=True)
    finally:
        db.close()


@main.command()
@click.argument("file_path", required=False)
@click.option("-c", "--collection", default=None, help="集合名称")
@click.option("--rank", is_flag=True, help="按 PageRank 排序")
def symbols(file_path, collection, rank):
    """查看符号统计或文件的符号列表"""
    config = load_config()
    db = _build_db(config)
    try:
        if file_path:
            # 查看特定文件的符号
            doc = db.conn.execute(
                "SELECT id FROM documents WHERE path = ?", (file_path,)
            ).fetchone()
            if not doc:
                click.secho(f"文件未索引: {file_path}", fg="red")
                return
            syms = db.get_symbols_by_doc(doc[0])
            if not syms:
                click.echo(f"{file_path}: 无符号")
                return
            click.echo(f"\n{file_path} ({len(syms)} 个符号):\n")
            for s in sorted(syms, key=lambda x: x.get("line_start", 0)):
                pr = f" (rank={s['pagerank']:.4f})" if s.get("pagerank") else ""
                click.echo(f"  {s.get('line_start', '?'):>4}│{s['kind']} {s['name']}{pr}")
        else:
            # 全局统计
            col = collection or config.collection
            stats = db.get_symbol_stats(col)
            click.echo(f"符号统计 (collection={col}):")
            click.echo(f"  符号数: {stats.get('symbol_count', 0)}")
            click.echo(f"  引用数: {stats.get('ref_count', 0)}")

            if rank:
                # 按 PageRank 排序 top 20
                rows = db.conn.execute(
                    """SELECT s.name, s.kind, s.pagerank, d.path
                       FROM symbols s
                       JOIN documents d ON s.doc_id = d.id
                       WHERE d.collection = ? AND s.pagerank > 0
                       ORDER BY s.pagerank DESC LIMIT 20""",
                    (col,),
                ).fetchall()
                if rows:
                    click.echo(f"\nTop 20 (PageRank):\n")
                    for name, kind, pr, path in rows:
                        click.echo(f"  {pr:.6f}  {kind:>8} {name}  ({path})")
    except Exception as e:
        click.secho(f"查询失败: {e}", fg="red", err=True)
    finally:
        db.close()


@main.command()
@click.option("-p", "--port", default=6888, help="监听端口")
@click.option("--host", default="127.0.0.1", help="监听地址")
def web(port, host):
    """启动 Web UI"""
    from .web import create_app

    config = load_config()
    app = create_app(config)
    click.echo(f"index1 web UI: http://{host}:{port}")
    app.run(host=host, port=port, debug=False)


@main.command("observer-worker")
@click.option("-p", "--port", default=None, type=int, help="监听端口（默认 6889）")
def observer_worker(port):
    """启动 Observer Worker 持久 HTTP 服务（#247）"""
    from .cognitive.observer_worker import run_worker

    config = load_config()
    if port is not None:
        config.observer_worker_port = port
    run_worker(config)


@main.command()
@click.argument("paths", nargs=-1, required=True, type=click.Path(exists=True))
@click.option("-c", "--collection", default=None, help="集合名称")
def watch(paths, collection):
    """监听文件变更，自动重索引"""
    import signal
    import threading

    from .indexer import reindex_file
    from .search import get_cache
    from .watcher import FileWatcher

    config = load_config()
    db, embedder = _build_deps(config)
    col = collection or config.collection

    base_dir = None
    if len(paths) == 1:
        p = Path(paths[0]).resolve()
        base_dir = p if p.is_dir() else p.parent

    # 后台 event loop：所有 reindex 任务提交到同一个 loop，避免每次创建/销毁
    _loop = asyncio.new_event_loop()
    _loop_thread = threading.Thread(target=_loop.run_forever, daemon=True)
    _loop_thread.start()

    def on_reindex(file_path: str) -> None:
        future = asyncio.run_coroutine_threadsafe(
            reindex_file(
                file_path, config, db, embedder,
                collection=col, base_dir=base_dir,
            ),
            _loop,
        )
        try:
            future.result(timeout=60)
        except concurrent.futures.TimeoutError:
            future.cancel()  # 超时后取消协程，防止孤儿累积
            logging.getLogger(__name__).warning("重索引超时 %s（60s），已取消", file_path)
        except Exception as e:
            logging.getLogger(__name__).warning("重索引失败 %s: %s", file_path, e)
        get_cache().invalidate_collection(col)

    def on_delete(rel_path: str) -> None:
        db.delete_document(rel_path)
        get_cache().invalidate_collection(col)

    watcher = FileWatcher(
        paths=list(paths),
        reindex_callback=on_reindex,
        delete_callback=on_delete,
        base_dir=base_dir,
        db=db,
    )

    click.echo(f"监听中... (Ctrl+C 停止)")
    watcher.start()

    stop_event = threading.Event()

    def _on_signal(sig, frame):
        stop_event.set()

    signal.signal(signal.SIGINT, _on_signal)
    signal.signal(signal.SIGTERM, _on_signal)

    stop_event.wait()

    watcher.stop()
    # 关闭后台 event loop
    _loop.call_soon_threadsafe(_loop.stop)
    _loop_thread.join(timeout=5)
    # 关闭 embedder 和 db
    try:
        asyncio.run(embedder.close())
    except Exception as e:
        logger.debug("关闭 embedder 失败: %s", e)
    db.close()
    click.echo("已停止监听")


@main.command()
@click.option("--backend", type=click.Choice(list(BACKEND_PRESETS.keys()) + ["custom"]),
              help="快捷切换后端预设")
def init(backend):
    """初始化 embedding 后端配置

    交互式选择 embedding 后端，或通过 --backend 快捷切换。

    \b
    示例:
      index1 init                    # 交互式选择
      index1 init --backend ollama   # 切回 Ollama
      index1 init --backend llama-cpp
      index1 init --backend lm-studio
      index1 init --backend custom   # 自定义 URL/模型/维度
    """
    cfg = load_config()
    interactive = backend is None  # 无 --backend 参数 = 交互模式

    if interactive:
        # 交互式选择后端
        click.echo("\n选择 embedding 后端:\n")
        choices = list(BACKEND_PRESETS.keys()) + ["custom", "skip"]
        labels = {
            "ollama": "Ollama（推荐，开箱即用）",
            "llama-cpp": "llama.cpp server",
            "lm-studio": "LM Studio",
            "custom": "其他 OpenAI 兼容服务",
            "skip": "跳过（纯 BM25 文本搜索，不需要 embedding）",
        }
        for i, key in enumerate(choices):
            marker = " (当前)" if _is_current_backend(cfg, key) else ""
            click.echo(f"  [{i}] {labels[key]}{marker}")
        click.echo()

        idx = click.prompt("请选择", type=click.IntRange(0, len(choices) - 1))
        backend = choices[idx]

    if backend == "skip":
        click.secho("已跳过 embedding 配置，将使用纯 BM25 文本搜索", fg="yellow")
        return

    if backend == "custom":
        _setup_custom_backend(cfg)
    elif backend in BACKEND_PRESETS:
        preset = BACKEND_PRESETS[backend]
        cfg.embed_api = preset["embed_api"]
        cfg.ollama_url = preset["ollama_url"]
        click.echo(f"\n已选择: {preset['desc']}")

        if interactive:
            # 交互模式：允许修改 URL 和模型
            if preset["embed_api"] == "openai_compat":
                custom_url = click.prompt(
                    "服务地址", default=preset["ollama_url"], show_default=True,
                )
                cfg.ollama_url = custom_url
            _setup_model(cfg)

    save_config(cfg)
    click.echo()
    click.secho("✅ 配置已保存到 ~/.claude-index1/config.yaml", fg="green")
    click.echo()
    click.echo(f"  embed_api:       {cfg.embed_api}")
    click.echo(f"  ollama_url:      {cfg.ollama_url}")
    click.echo(f"  embedding_model: {cfg.embedding_model}")
    click.echo(f"  embedding_dim:   {cfg.embedding_dim}")
    click.echo()
    click.echo("运行 index1 doctor 验证连接")


def _is_current_backend(cfg: Config, backend: str) -> bool:
    """判断当前配置是否匹配指定后端"""
    if backend == "skip" or backend == "custom":
        return False
    preset = BACKEND_PRESETS.get(backend)
    if not preset:
        return False
    return (
        cfg.embed_api == preset["embed_api"]
        and cfg.ollama_url == preset["ollama_url"]
    )


def _setup_model(cfg: Config) -> None:
    """交互式选择 embedding 模型"""
    from .config import MODEL_PROFILES, auto_select_model

    auto_key = auto_select_model()
    click.echo(f"\n选择 embedding 模型（系统推荐: {auto_key}）:\n")

    keys = list(MODEL_PROFILES.keys())
    for i, key in enumerate(keys):
        profile = MODEL_PROFILES[key]
        marker = " ← 推荐" if key == auto_key else ""
        current = " (当前)" if cfg.embedding_model == profile["model"] else ""
        click.echo(f"  [{i}] {key}: {profile['model']} ({profile['dim']}维) — {profile['desc']}{marker}{current}")

    click.echo()
    idx = click.prompt(
        "请选择",
        type=click.IntRange(0, len(keys) - 1),
        default=keys.index(auto_key),
        show_default=True,
    )
    selected = MODEL_PROFILES[keys[idx]]
    cfg.embedding_model = selected["model"]
    cfg.embedding_dim = selected["dim"]


def _setup_custom_backend(cfg: Config) -> None:
    """自定义 OpenAI 兼容后端"""
    cfg.embed_api = "openai_compat"
    click.echo("\n配置自定义 OpenAI 兼容服务:\n")
    cfg.ollama_url = click.prompt("服务地址", default="http://localhost:8080")
    _setup_model(cfg)


@main.command()
@click.option("--fix", is_flag=True, help="自动修复发现的问题")
def doctor(fix):
    """检查 Ollama 环境配置"""
    from .doctor import run_doctor

    config = load_config()
    asyncio.run(run_doctor(config, fix=fix))


@main.group()
def cog():
    """认知记忆管理"""
    pass


@cog.command("doctor")
@click.option("-c", "--collection", default=None, help="限定项目范围")
def cog_doctor(collection):
    """诊断认知数据库 — 量化字段填充率"""
    config = load_config()
    if not config.cognitive_enabled:
        click.secho("认知模块已禁用 (cognitive_enabled=false)", fg="yellow")
        return

    from .cognitive.diagnosis import run_diagnosis, format_report

    db_cog = _build_cog_db(config)
    try:
        report = run_diagnosis(db_cog, collection=collection)
        click.echo(format_report(report))
    except Exception as e:
        click.secho(f"诊断失败: {e}", fg="red", err=True)
    finally:
        db_cog.close()


async def _run_backfill(db_cog, embedder, collection: str, batch_size: int) -> int:
    """异步回填所有缺失 embedding 的 facts"""
    total_filled = 0
    batch_num = 0
    while True:
        missing = db_cog.get_facts_missing_embedding(collection, limit=batch_size)
        if not missing:
            break
        batch_num += 1

        texts = []
        for _, assertion, context_json in missing:
            extra = ""
            if context_json:
                try:
                    ctx = json.loads(context_json) if isinstance(context_json, str) else context_json
                    parts = []
                    for k in ("changes", "reasoning", "error_message", "fix"):
                        v = ctx.get(k)
                        if v:
                            parts.append(str(v)[:100])
                    extra = " ".join(parts)
                except (json.JSONDecodeError, TypeError):
                    pass
            texts.append(f"{assertion} {extra}".strip()[:500])

        try:
            embeddings = await embedder.embed_batch(texts)
        except Exception as e:
            click.secho(f"Embedding 批次 {batch_num} 失败: {e}", fg="red", err=True)
            break

        filled = 0
        for (fact_id, _, _), emb in zip(missing, embeddings):
            if emb is not None:
                try:
                    db_cog.update_fact_embedding(fact_id, emb)
                    filled += 1
                except Exception as e:
                    logger.warning("Backfill 写入失败 (fact_id=%d): %s", fact_id, e)
        total_filled += filled
        click.echo(f"  批次 {batch_num}: {filled}/{len(missing)} 条已回填")

        if len(missing) < batch_size:
            break  # 最后一批

    return total_filled


@cog.command("backfill")
@click.option("-c", "--collection", default=None, help="限定项目范围")
@click.option("--batch-size", default=500, help="每批处理数量")
def cog_backfill(collection, batch_size):
    """一次性回填所有缺失 embedding 的 facts"""
    config = load_config()
    if not config.cognitive_enabled:
        click.secho("认知模块已禁用 (cognitive_enabled=false)", fg="yellow")
        return

    db_cog = _build_cog_db(config)
    embedder = create_embedder(config)
    try:
        asyncio.run(embedder.check_availability())
    except Exception:
        pass

    if not embedder.available:
        click.secho("Embedder 不可用，无法回填", fg="red", err=True)
        db_cog.close()
        return

    if not collection:
        collection = Path.cwd().name

    try:
        total_filled = asyncio.run(_run_backfill(db_cog, embedder, collection, batch_size))
        if total_filled > 0:
            click.secho(f"\n回填完成: {total_filled} 条 facts 已生成 embedding", fg="green")
        else:
            click.echo("无需回填 — 所有 facts 已有 embedding")
    except Exception as e:
        click.secho(f"回填失败: {e}", fg="red", err=True)
    finally:
        db_cog.close()


@cog.command()
def cog_status():
    """查看认知统计"""
    config = load_config()
    if not config.cognitive_enabled:
        click.secho("认知模块已禁用 (cognitive_enabled=false)", fg="yellow")
        return

    db_cog = _build_cog_db(config)
    try:
        stats = db_cog.get_stats()
        click.echo(f"数据库: {config.cognitive_db_path_resolved}")
        click.echo(f"事实总数: {stats['facts_total']}")
        click.echo(f"活跃事实: {stats['facts_active']}")
        click.echo(f"心智模型: {stats['mental_models']}")
        click.echo(f"向量搜索: {'可用' if stats['vec_loaded'] else '不可用'}")
        if stats['facts_by_source']:
            click.echo(f"来源分布: {stats['facts_by_source']}")
    except Exception as e:
        click.secho(f"认知数据库连接失败: {e}", fg="red", err=True)
    finally:
        db_cog.close()


@cog.command()
@click.argument("query")
@click.option("-c", "--collection", default=None, help="限定项目范围")
@click.option("-k", "--top-k", default=5, help="返回数量")
def recall(query, collection, top_k):
    """搜索认知记忆"""
    from .cognitive.search_cog import recall as do_recall

    config = load_config()
    if not config.cognitive_enabled:
        click.secho("认知模块已禁用", fg="yellow")
        return

    db_cog = _build_cog_db(config)
    try:
        result = asyncio.run(do_recall(query, db_cog, collection=collection, top_k=top_k))
        if not result.facts:
            click.echo("未找到相关认知记忆")
            return
        click.echo(f"\n找到 {result.total} 条结果 ({result.query_ms}ms, {result.mode}):\n")
        for i, f in enumerate(result.facts, 1):
            conf = f.get("confidence", 0)
            click.echo(f"  [{i}] {f['assertion'][:80]} ({conf:.0%})")
    except Exception as e:
        click.secho(f"搜索失败: {e}", fg="red", err=True)
    finally:
        db_cog.close()


@cog.command("list")
@click.option("-c", "--collection", default=None, help="限定项目范围")
@click.option("-n", "--limit", default=10, help="显示数量")
def cog_list(collection, limit):
    """列出最近的认知事实"""
    config = load_config()
    if not config.cognitive_enabled:
        click.secho("认知模块已禁用", fg="yellow")
        return

    db_cog = _build_cog_db(config)
    try:
        where = "WHERE collection=?" if collection else ""
        params = [collection] if collection else []
        rows = db_cog.conn.execute(
            f"SELECT id, assertion, confidence, source, created_at FROM facts "
            f"{where} ORDER BY created_at DESC LIMIT ?",
            params + [limit],
        ).fetchall()
        if not rows:
            click.echo("暂无认知事实")
            return
        for r in rows:
            conf = f"{r['confidence']:.0%}" if r['confidence'] else "?"
            click.echo(f"  #{r['id']} [{r['source']}] {r['assertion'][:60]} ({conf})")
    except Exception as e:
        click.secho(f"查询失败: {e}", fg="red", err=True)
    finally:
        db_cog.close()


@cog.command("rebuild")
@click.option("-c", "--collection", default=None, help="集合名称（默认从当前目录推断）")
@click.option("--since", default=None, help="起始日期（如 '2025-01-01' 或 '3 months ago'）")
@click.option("-n", "--limit", default=500, help="最大 commit 数")
@click.option("--dry-run", is_flag=True, help="仅统计，不写入数据库")
@click.option("--repo", default=".", type=click.Path(exists=True), help="Git 仓库路径")
def cog_rebuild(collection, since, limit, dry_run, repo):
    """从 git history 重建认知事实"""
    config = load_config()
    if not config.cognitive_enabled:
        click.secho("认知模块已禁用 (cognitive_enabled=false)", fg="yellow")
        return

    from .cognitive.rebuild import rebuild_from_git

    repo_path = Path(repo).resolve()

    if not collection:
        collection = repo_path.name

    db_cog = _build_cog_db(config)
    try:
        mode = "[DRY RUN] " if dry_run else ""
        click.echo(f"{mode}从 {repo_path} 重建认知事实 (collection={collection})")
        if since:
            click.echo(f"  起始: {since}")
        click.echo(f"  最大 commits: {limit}")

        stats = rebuild_from_git(
            db_cog=db_cog,
            repo_path=repo_path,
            collection=collection,
            since=since,
            limit=limit,
            dry_run=dry_run,
        )

        click.echo(f"\n重建完成:")
        click.echo(f"  处理 commits: {stats.commits_processed}")
        click.echo(f"  跳过 (merge): {stats.commits_skipped}")
        click.echo(f"  创建 facts:   {stats.facts_created}")
        click.echo(f"  重复跳过:     {stats.facts_duplicate}")
        if stats.errors:
            click.secho(f"  错误:         {stats.errors}", fg="yellow")
    except Exception as e:
        click.secho(f"重建失败: {e}", fg="red", err=True)
    finally:
        db_cog.close()


# Register cog as a subgroup of main
main.add_command(cog)


@main.command()
@click.argument("event_name", metavar="EVENT", required=False, default=None)
@click.option("--event", "event_alias", default=None, hidden=True,
              help="事件名（--event 别名，向后兼容）")
def hook(event_name, event_alias):
    """处理 Claude Code Hook 事件

    读取 stdin JSON，处理事件，输出 JSON 到 stdout。
    EVENT 参数可选 — 省略时自动从 stdin 的 hook_event_name 字段检测。

    \b
    支持的事件:
      SessionStart       苏醒 — 注入上下文 + 兜底维护检查
      UserPromptSubmit   定向 — 搜索相关 facts
      PostToolUse        观察 — 提取认知事实
      SessionEnd         反思 — 启动维护进程

    \b
    用法:
      index1 hook PostToolUse     # 显式指定事件
      index1 hook                 # 自动从 stdin 检测（插件模式）
    """
    from .cognitive.hooks import hook_main

    event = event_alias or event_name
    hook_main(event)


@main.command()
@click.option("--remove", is_flag=True, help="卸载插件")
@click.option("--status", "show_status", is_flag=True, help="查看安装状态")
def setup(remove, show_status):
    """安装/卸载 Claude Code 插件（hooks + MCP）

    \b
    用法:
      index1 setup           # 安装插件 — hooks 自动注册
      index1 setup --status  # 查看安装状态
      index1 setup --remove  # 卸载插件

    安装后重启 Claude Code 即生效，所有项目自动记录认知事实。
    """
    from .plugin import check_status, install_plugin, uninstall_plugin

    if show_status:
        status = check_status()
        click.echo("index1 Claude Code 插件状态:")
        click.echo(f"  插件目录:    {status['plugin_dir']}")
        click.echo(f"  hooks.json:  {'✅' if status['hooks_json'] else '❌'}")
        click.echo(f"  plugin.json: {'✅' if status['plugin_json'] else '❌'}")
        click.echo(f"  已启用:      {'✅' if status['enabled'] else '❌'}")
        if status["manual_hooks"]:
            click.echo("  ⚠️  settings.json 中有手动 hooks（运行 index1 setup 清理）")
        return

    if remove:
        uninstall_plugin()
        click.echo("✅ index1 插件已卸载")
        click.echo("   重启 Claude Code 生效")
        return

    try:
        plugin_dir = install_plugin()
    except OSError as e:
        click.echo(f"❌ 安装失败: {e}", err=True)
        raise SystemExit(1)
    click.echo("✅ index1 插件已安装")
    click.echo(f"   目录: {plugin_dir}")
    click.echo("   重启 Claude Code 生效")
    click.echo()
    click.echo("   已注册 hooks:")
    click.echo("     SessionStart      → 苏醒简报")
    click.echo("     UserPromptSubmit  → 认知上下文搜索")
    click.echo("     PostToolUse       → 事实提取 (Bash/Edit/Write)")
    click.echo("     SessionEnd        → 维护 + 会话摘要")

    # #255: 自动初始索引（新用户零配置可用）
    import os
    from pathlib import Path as _Path
    _raw_dir = os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
    if _raw_dir:
        _project_dir = str(_Path(_raw_dir).expanduser().resolve())
        if os.path.isdir(_project_dir):
            click.echo()
            click.echo(f"   正在索引项目: {_project_dir} ...")
            try:
                import subprocess, shutil
                index1_bin = shutil.which("index1")
                if not index1_bin:
                    click.echo("   ⚠️  index1 不在 PATH 中，跳过索引")
                elif not isinstance(index1_bin, str) or not isinstance(_project_dir, str):
                    click.echo("   ⚠️  参数类型异常，跳过索引")
                else:
                    result = subprocess.run(
                        [index1_bin, "index", _project_dir],
                        capture_output=True, text=True, timeout=300,
                    )
                    if result.returncode == 0:
                        click.echo("   ✅ 初始索引完成")
                    else:
                        _err = (result.stderr or "").strip()[:200]
                        click.echo(f"   ⚠️  索引失败: {_err}" if _err else "   ⚠️  索引失败")
                        click.echo(f"   稍后可手动运行: index1 index {_project_dir}")
            except Exception as e:
                click.echo(f"   ⚠️  索引跳过: {e}")
                click.echo(f"   稍后可手动运行: index1 index {_project_dir}")


@main.command()
def serve():
    """启动 MCP Server（stdio 模式）"""
    from .mcp_server import run_server

    run_server()
