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

from __future__ import annotations

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

import click

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


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_deps(config: Config) -> tuple[Database, OllamaEmbedder]:
    """创建数据库和 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,
    )
    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=True, type=click.Path(exists=True))
@click.option("-c", "--collection", default=None, help="集合名称")
@click.option("--force", is_flag=True, help="强制全量重索引")
@click.pass_context
def index(ctx, paths, collection, force):
    """索引文件或目录"""
    from .indexer import index_files

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

    # 推断 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']} 文件)"
            )
        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 = Database(config.db_path_resolved, config.embedding_dim)
    try:
        db.connect()
        stats = db.get_stats()
    except Exception as e:
        click.secho(f"数据库连接失败: {e}", fg="red", err=True)
        return
    finally:
        db.close()

    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 '不可用'}")


@main.command()
@click.argument("key", required=False)
@click.argument("value", required=False)
def config(key, value):
    """查看或修改配置"""
    cfg = load_config()

    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()
@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()
@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,
    )

    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:
        pass
    db.close()
    click.echo("已停止监听")


@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.command()
def serve():
    """启动 MCP Server（stdio 模式）"""
    from .mcp_server import run_server

    run_server()
