"""MCP Server — 5 tools

tools: docs_search, docs_get, docs_status, docs_reindex, docs_config
"""

from __future__ import annotations

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass

from mcp.server.fastmcp import Context, FastMCP

from .config import Config, load_config, save_config
from .db import Database
from .embed import OllamaEmbedder
from .indexer import index_files
from .search import search as do_search


@dataclass
class AppContext:
    """MCP Server 共享上下文"""

    config: Config
    db: Database
    embedder: OllamaEmbedder


@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
    """管理服务器生命周期：启动时连接数据库 + 检查 Ollama，关闭时清理"""
    import logging

    logger = logging.getLogger(__name__)

    config = load_config()
    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,
    )

    # 启动时检查 Ollama — 提前发现问题，不要等到第一次搜索才爆
    ollama_ok = await embedder.check_availability()
    if ollama_ok:
        logger.info("index1 启动: Ollama 可用，模型 %s 已就绪", config.embedding_model)
    else:
        logger.warning(
            "index1 启动: Ollama 不可用 — %s。向量搜索已禁用，仅 BM25 关键词搜索可用",
            embedder.unavailable_reason,
        )

    try:
        yield AppContext(config=config, db=db, embedder=embedder)
    finally:
        await embedder.close()
        db.close()


mcp = FastMCP("index1", lifespan=app_lifespan)


def _get_ctx(ctx: Context) -> AppContext:
    """从 MCP Context 获取应用上下文"""
    return ctx.request_context.lifespan_context


@mcp.tool()
async def docs_search(
    query: str,
    ctx: Context,
    collection: str | None = None,
    top_k: int = 5,
) -> dict:
    """搜索项目知识库

    使用 BM25 + 向量混合搜索，返回最相关的文档片段。
    支持中英文查询，自动调整搜索策略。

    Args:
        query: 搜索查询文本
        collection: 限定搜索范围（可选）
        top_k: 返回结果数量，默认 5
    """
    app = _get_ctx(ctx)
    resp = await do_search(
        query, app.config, app.db, app.embedder,
        collection=collection, top_k=top_k,
    )
    result = {
        "results": [
            {
                "id": f"chunk_{r.chunk_id}",
                "score": r.score,
                "source": r.source,
                "section": r.section,
                "summary": r.summary,
                "relevance": r.relevance,
            }
            for r in resp.results
        ],
        "total": resp.total,
        "query_ms": resp.query_ms,
        "mode": resp.mode,
        "hint": resp.hint,
    }
    # Ollama 不可用时，在搜索结果中提示用户
    if not app.embedder.available:
        result["ollama_status"] = "unavailable"
        result["ollama_error"] = app.embedder.unavailable_reason
        result["ollama_hint"] = (
            "当前仅 BM25 关键词搜索可用。"
            "可使用 docs_config(key='ollama_reconnect', value='1') 尝试重连，"
            "或忽略此提示继续使用降级搜索。"
        )
    return result


@mcp.tool()
async def docs_get(chunk_id: int, ctx: Context) -> dict:
    """获取文档片段的完整内容

    根据 chunk_id 返回完整文本、上下文和元数据。
    配合 docs_search 使用：先搜索获取摘要，再按需获取完整内容。

    Args:
        chunk_id: 片段 ID（从 docs_search 结果中获取）
    """
    app = _get_ctx(ctx)
    chunk = app.db.get_chunk(chunk_id)
    if not chunk:
        return {"error": f"chunk {chunk_id} 不存在"}

    context = app.db.get_chunk_context(chunk_id)

    return {
        "id": chunk.id,
        "content": chunk.content,
        "source": chunk.source,
        "section": chunk.section,
        "parent_section": chunk.parent_section,
        "tags": chunk.tags,
        "token_count": chunk.token_count,
        "context": context,
    }


@mcp.tool()
async def docs_status(ctx: Context) -> dict:
    """查看知识库索引状态

    返回文档数、片段数、集合列表、最近索引时间等统计信息。
    """
    app = _get_ctx(ctx)
    stats = app.db.get_stats()
    stats["ollama"] = {
        "available": app.embedder.available,
        "model": app.embedder.model,
        "reason": app.embedder.unavailable_reason or None,
    }
    return stats


@mcp.tool()
async def docs_reindex(
    ctx: Context,
    collection: str | None = None,
    path: str | None = None,
) -> dict:
    """重建索引

    重新扫描并索引指定路径或集合。如果不指定参数则使用配置中的 watch_paths。

    Args:
        collection: 集合名称（可选）
        path: 要索引的文件或目录路径（可选）
    """
    import logging
    from pathlib import Path

    logger = logging.getLogger(__name__)
    app = _get_ctx(ctx)

    paths = [path] if path else app.config.watch_paths
    if not paths:
        return {"error": "未指定索引路径，请先配置 watch_paths 或传入 path 参数"}

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

    logger.info("开始重建索引: %s", ", ".join(str(p) for p in paths))
    stats = await index_files(
        paths, app.config, app.db, app.embedder,
        collection=collection, force=True, base_dir=base_dir,
    )
    logger.info(
        "索引完成: %d 已索引, %d 跳过, %d 失败 (共 %d 文件)",
        stats["indexed"], stats["skipped"], stats["failed"], stats["total"],
    )
    return stats


@mcp.tool()
async def docs_config(
    ctx: Context,
    key: str | None = None,
    value: str | None = None,
) -> dict:
    """查看或修改配置

    不传参数时返回当前配置。传入 key 和 value 时修改配置。

    Args:
        key: 配置键名（可选），如 embedding_model、top_k
        value: 配置值（可选）
    """
    app = _get_ctx(ctx)

    # 特殊命令：重连 Ollama
    if key == "ollama_reconnect":
        app.embedder.reset_cooldown()
        ok = await app.embedder.check_availability()
        if ok:
            return {
                "message": "Ollama 重连成功，向量搜索已恢复",
                "ollama_available": True,
                "model": app.embedder.model,
            }
        return {
            "message": "Ollama 重连失败，仍使用 BM25 降级搜索",
            "ollama_available": False,
            "reason": app.embedder.unavailable_reason,
            "hint": "请检查: 1) ollama serve 是否运行  2) ollama pull " + app.embedder.model,
        }

    if key and value:
        from dataclasses import fields

        valid_keys = {f.name for f in fields(Config)}
        if key not in valid_keys:
            return {"error": f"未知配置项: {key}"}

        converted = _convert_value(value, key)
        setattr(app.config, key, converted)
        save_config(app.config)
        return {"message": f"已更新 {key} = {converted}"}

    # 返回当前配置
    from dataclasses import asdict

    return asdict(app.config)


_TYPE_CONVERTERS = {
    "db_path": str,
    "embedding_model": str,
    "embedding_dim": int,
    "ollama_url": str,
    "top_k": int,
    "rrf_k": int,
    "collection": str,
}


def _convert_value(value: str, key: str) -> object:
    """根据配置键名安全转换值"""
    converter = _TYPE_CONVERTERS.get(key, str)
    if converter is int:
        return int(value)
    if converter is float:
        return float(value)
    if key == "watch_paths":
        return [v.strip() for v in value.split(",") if v.strip()]
    return value


def run_server() -> None:
    """启动 MCP Server（stdio 模式）"""
    mcp.run()
