"""Ollama 环境诊断与自动修复

index1 doctor — 检测 Ollama 安装、服务、模型状态，可自动修复。
"""

from __future__ import annotations

import platform
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass

import logging

import click
import httpx

from .config import Config, MODEL_PROFILES, auto_select_model, _get_memory_gb

# 抑制 httpx 请求日志
logging.getLogger("httpx").setLevel(logging.WARNING)


@dataclass
class CheckResult:
    """单项检查结果"""

    name: str
    status: str  # "pass" / "warn" / "fail" / "skip"
    message: str
    fix_hint: str | None = None


# 状态符号
_ICONS = {
    "pass": click.style("✓", fg="green"),
    "warn": click.style("!", fg="yellow"),
    "fail": click.style("✗", fg="red"),
    "skip": click.style("-", fg="white", dim=True),
}


async def run_doctor(config: Config, *, fix: bool = False) -> list[CheckResult]:
    """运行全部诊断检查

    Args:
        config: index1 配置
        fix: 是否尝试自动修复

    Returns:
        检查结果列表
    """
    results: list[CheckResult] = []
    ollama_url = config.ollama_url.rstrip("/")
    model = config.embedding_model

    click.echo()

    # 系统信息
    sys_name = platform.system()
    mem_gb = _get_memory_gb()
    arch = platform.machine()
    click.echo(f"  系统: {sys_name} {platform.release()} ({arch}), 内存 {mem_gb:.0f} GB")
    click.echo()

    # Windows 版本检查
    if sys_name == "Windows":
        ver = platform.version() or ""
        try:
            parts = ver.split(".")
            if parts and int(parts[0]) < 10:
                click.secho(
                    f"  ⚠ Windows {ver} 不受 Ollama 支持，需要 Windows 10 或更高",
                    fg="yellow",
                )
                click.echo()
        except (ValueError, IndexError, AttributeError):
            pass

    # OpenAI 兼容模式：跳过 Ollama 特定检查
    if config.embed_api == "openai_compat":
        return await _run_openai_compat_checks(config)

    # ── Check 1: Ollama 二进制 ──
    ollama_bin = shutil.which("ollama")
    if ollama_bin:
        version = _get_ollama_version(ollama_bin)
        ver_str = f" (v{version})" if version else ""
        r1 = CheckResult("Ollama 已安装", "pass", f"{ollama_bin}{ver_str}")
    else:
        install_hint = _get_install_hint(sys_name)
        r1 = CheckResult(
            "Ollama 已安装", "fail", "未找到 ollama 命令",
            fix_hint=install_hint,
        )
    results.append(r1)
    _print_check(1, 6, r1)

    if r1.status == "fail":
        # 无法继续后续检查
        for i, name in enumerate(["服务运行中", f"模型 {model}", "Embedding 测试", "模型推荐"], 2):
            r = CheckResult(name, "skip", "需要先安装 Ollama")
            results.append(r)
            _print_check(i, 6, r)
        _print_summary(results, fix)
        return results

    # ── Check 2: 服务运行中 ──
    service_ok = await _check_service(ollama_url)
    if service_ok:
        r2 = CheckResult("服务运行中", "pass", ollama_url)
    elif fix:
        click.echo(click.style("        → 正在启动 Ollama 服务...", fg="yellow"))
        started = _start_ollama_service(ollama_url)
        if started:
            r2 = CheckResult("服务运行中", "pass", f"{ollama_url} (刚启动)")
        else:
            r2 = CheckResult(
                "服务运行中", "fail", "启动失败",
                fix_hint="手动运行: ollama serve",
            )
    else:
        r2 = CheckResult(
            "服务运行中", "fail", "服务未运行",
            fix_hint="运行 index1 doctor --fix 自动启动",
        )
    results.append(r2)
    _print_check(2, 6, r2)

    if r2.status == "fail":
        for i, name in enumerate([f"模型 {model}", "Embedding 测试", "模型推荐"], 3):
            r = CheckResult(name, "skip", "需要先启动 Ollama 服务")
            results.append(r)
            _print_check(i, 6, r)
        _print_summary(results, fix)
        return results

    # ── Check 3: 模型已下载 ──
    model_ok = await _check_model(ollama_url, model)
    if model_ok:
        r3 = CheckResult(f"模型 {model}", "pass", "已下载")
    elif fix:
        click.echo(click.style(f"        → 正在下载模型 {model}...", fg="yellow"))
        pulled = _pull_model(model)
        if pulled:
            r3 = CheckResult(f"模型 {model}", "pass", "下载完成")
        else:
            r3 = CheckResult(
                f"模型 {model}", "fail", "下载失败",
                fix_hint=f"手动运行: ollama pull {model}",
            )
    else:
        r3 = CheckResult(
            f"模型 {model}", "fail", "未下载",
            fix_hint="运行 index1 doctor --fix 自动下载",
        )
    results.append(r3)
    _print_check(3, 6, r3)

    # ── Check 4: Embedding 测试 ──
    if r3.status != "pass":
        r4 = CheckResult("Embedding 测试", "skip", "模型未就绪")
        results.append(r4)
        _print_check(4, 6, r4)
    else:
        embed_ok, embed_dim = await _test_embedding(ollama_url, model)
        if embed_ok:
            dim_match = embed_dim == config.effective_embedding_dim
            if dim_match:
                r4 = CheckResult("Embedding 测试", "pass", f"维度 {embed_dim}")
            else:
                dim_key = "onnx_dim" if config.embed_backend == "onnx" else "embedding_dim"
                r4 = CheckResult(
                    "Embedding 测试", "warn",
                    f"维度 {embed_dim}，配置期望 {config.effective_embedding_dim}",
                    fix_hint=f"运行: index1 config {dim_key} {embed_dim}",
                )
        else:
            r4 = CheckResult("Embedding 测试", "fail", "生成失败")
        results.append(r4)
        _print_check(4, 6, r4)

    # ── Check 5: 模型推荐 ──
    recommended = auto_select_model()
    rec_profile = MODEL_PROFILES[recommended]
    current_profile = _find_current_profile(model)
    mem_hint = f"内存 {mem_gb:.0f}GB → {rec_profile['desc']}"

    if current_profile == recommended:
        r5 = CheckResult(
            "模型推荐", "pass",
            f"当前: {recommended} ({model})，{mem_hint}",
        )
    elif current_profile:
        r5 = CheckResult(
            "模型推荐", "warn",
            f"当前: {current_profile} ({model})，推荐: {recommended} ({rec_profile['model']})",
            fix_hint=f"{mem_hint}，可选: index1 config embedding_model {rec_profile['model']}",
        )
    else:
        r5 = CheckResult(
            "模型推荐", "warn",
            f"当前: {model} (自定义)，推荐: {recommended} ({rec_profile['model']})",
            fix_hint=mem_hint,
        )
    results.append(r5)
    _print_check(5, 6, r5)

    # ── Check 6: 中文/多语言支持提示 ──
    has_jieba = False
    try:
        import jieba  # noqa: F401
        has_jieba = True
    except ImportError:
        pass

    is_chinese_model = model in ("bge-m3",)
    if has_jieba and is_chinese_model:
        r6 = CheckResult(
            "中文支持", "pass",
            f"jieba 分词已安装，模型 {model} 支持中文语义搜索",
        )
    elif has_jieba and not is_chinese_model:
        r6 = CheckResult(
            "中文支持", "warn",
            f"jieba 分词已安装，但当前模型 {model} 中文效果一般",
            fix_hint="中文优化: ollama pull bge-m3 && index1 config embedding_model bge-m3 && index1 config embedding_dim 1024",
        )
    elif not has_jieba and is_chinese_model:
        r6 = CheckResult(
            "中文支持", "warn",
            f"模型 {model} 支持中文语义搜索，但 jieba 分词未安装（BM25 中文效果差）",
            fix_hint="安装 jieba: pip install jieba  或  pip install index1[chinese]",
        )
    else:
        r6 = CheckResult(
            "中文支持", "warn",
            "中文搜索未优化（jieba 未安装 + 非中文模型）",
            fix_hint="中文优化: pip install jieba && ollama pull bge-m3 && index1 config embedding_model bge-m3 && index1 config embedding_dim 1024",
        )
    results.append(r6)
    _print_check(6, 6, r6)

    _print_summary(results, fix)
    return results


def _get_install_hint(sys_name: str) -> str:
    """根据平台返回 Ollama 安装命令"""
    if sys_name == "Darwin":
        return "运行: brew install ollama  或访问 https://ollama.com/download/mac"
    elif sys_name == "Linux":
        return "运行: curl -fsSL https://ollama.com/install.sh | sh"
    elif sys_name == "Windows":
        return "下载安装: https://ollama.com/download/windows"
    return "访问 https://ollama.com 安装 Ollama"


def _get_ollama_version(ollama_bin: str) -> str | None:
    """获取 Ollama 版本号"""
    import re

    try:
        result = subprocess.run(
            [ollama_bin, "--version"],
            capture_output=True, text=True, timeout=5,
        )
        # 输出格式: "ollama version is 0.15.4" 或 "Warning: ...\n... 0.15.4"
        for line in result.stdout.splitlines() + result.stderr.splitlines():
            match = re.search(r"\b(\d+\.\d+(?:\.\d+)?)\b", line)
            if match:
                return match.group(1)
    except (OSError, subprocess.TimeoutExpired):
        pass
    return None


async def _check_service(ollama_url: str, *, _client: httpx.AsyncClient | None = None) -> bool:
    """检测 Ollama 服务是否运行"""
    try:
        if _client:
            resp = await _client.get(f"{ollama_url}/api/version")
            return resp.status_code == 200
        async with httpx.AsyncClient(
            timeout=3.0,
            transport=httpx.AsyncHTTPTransport(),
        ) as client:
            resp = await client.get(f"{ollama_url}/api/version")
            return resp.status_code == 200
    except (httpx.ConnectError, httpx.TimeoutException, OSError):
        return False


def _start_ollama_service(ollama_url: str = "http://localhost:11434") -> bool:
    """后台启动 ollama serve"""
    try:
        popen_kwargs: dict = {
            "stdout": subprocess.DEVNULL,
            "stderr": subprocess.DEVNULL,
        }
        if platform.system() == "Windows":
            popen_kwargs["creationflags"] = getattr(
                subprocess, "CREATE_NEW_PROCESS_GROUP", 0x00000200
            )
        else:
            popen_kwargs["start_new_session"] = True

        subprocess.Popen(["ollama", "serve"], **popen_kwargs)
        # 等待服务就绪
        for _ in range(10):
            time.sleep(0.5)
            try:
                resp = httpx.get(
                    f"{ollama_url}/api/version",
                    timeout=2.0,
                    transport=httpx.HTTPTransport(),
                )
                if resp.status_code == 200:
                    return True
            except (httpx.ConnectError, httpx.TimeoutException, OSError):
                continue
        return False
    except OSError:
        return False


async def _check_model(ollama_url: str, model: str, *, _client: httpx.AsyncClient | None = None) -> bool:
    """检测模型是否已下载"""
    try:
        if _client:
            resp = await _client.get(f"{ollama_url}/api/tags")
        else:
            async with httpx.AsyncClient(
                timeout=5.0,
                transport=httpx.AsyncHTTPTransport(),
            ) as client:
                resp = await client.get(f"{ollama_url}/api/tags")
        resp.raise_for_status()
        models = resp.json().get("models", [])
        model_names = [m.get("name", "") for m in models]
        return any(name == model or name.startswith(f"{model}:") for name in model_names)
    except (httpx.HTTPError, httpx.ConnectError, httpx.TimeoutException, OSError):
        return False


def _pull_model(model: str) -> bool:
    """拉取 Ollama 模型（实时显示进度）"""
    try:
        result = subprocess.run(
            ["ollama", "pull", model],
            timeout=600,
        )
        return result.returncode == 0
    except (OSError, subprocess.TimeoutExpired):
        return False


async def _test_embedding(ollama_url: str, model: str, *, _client: httpx.AsyncClient | None = None) -> tuple[bool, int]:
    """测试 embedding 生成，返回 (成功, 维度)"""
    try:
        if _client:
            resp = await _client.post(
                f"{ollama_url}/api/embed",
                json={"model": model, "input": "hello world"},
            )
        else:
            async with httpx.AsyncClient(
                timeout=30.0,
                transport=httpx.AsyncHTTPTransport(),
            ) as client:
                resp = await client.post(
                    f"{ollama_url}/api/embed",
                    json={"model": model, "input": "hello world"},
                )
        resp.raise_for_status()
        embeddings = resp.json()["embeddings"]
        dim = len(embeddings[0])
        return True, dim
    except (httpx.HTTPError, httpx.ConnectError, httpx.TimeoutException,
            KeyError, IndexError, OSError):
        return False, 0


def _find_current_profile(model: str) -> str | None:
    """查找当前模型对应的 profile 名称"""
    for name, profile in MODEL_PROFILES.items():
        if profile["model"] == model:
            return name
    return None


def _print_check(num: int, total: int, result: CheckResult) -> None:
    """打印单项检查结果"""
    icon = _ICONS[result.status]
    click.echo(f"  [{num}/{total}] {result.name:<24s} {icon} {result.message}")
    if result.fix_hint:
        click.echo(f"        {click.style('→', fg='yellow')} {result.fix_hint}")


async def _run_openai_compat_checks(config: Config) -> list[CheckResult]:
    """OpenAI 兼容模式的诊断检查"""
    results: list[CheckResult] = []
    url = config.ollama_url.rstrip("/")
    model = config.embedding_model

    click.echo(f"  模式: OpenAI 兼容 API ({url})")
    click.echo()

    # Check 1: 服务连通性
    try:
        async with httpx.AsyncClient(
            timeout=5.0,
            transport=httpx.AsyncHTTPTransport(),
        ) as client:
            resp = await client.post(
                f"{url}/v1/embeddings",
                json={"model": model, "input": "ping"},
                timeout=15.0,
            )
            resp.raise_for_status()
            data = resp.json().get("data", [])
            if data and data[0].get("embedding"):
                dim = len(data[0]["embedding"])
                r1 = CheckResult("Embedding 服务", "pass", f"{url} 可用")
                results.append(r1)
                _print_check(1, 3, r1)

                dim_match = dim == config.effective_embedding_dim
                if dim_match:
                    r2 = CheckResult("Embedding 测试", "pass", f"模型 {model}，维度 {dim}")
                else:
                    dim_key = "onnx_dim" if config.embed_backend == "onnx" else "embedding_dim"
                    r2 = CheckResult(
                        "Embedding 测试", "warn",
                        f"维度 {dim}，配置期望 {config.effective_embedding_dim}",
                        fix_hint=f"运行: index1 config {dim_key} {dim}",
                    )
                results.append(r2)
                _print_check(2, 3, r2)
            else:
                r1 = CheckResult("Embedding 服务", "fail", "返回空向量")
                results.append(r1)
                _print_check(1, 3, r1)
                r2 = CheckResult("Embedding 测试", "skip", "服务异常")
                results.append(r2)
                _print_check(2, 3, r2)
    except (httpx.ConnectError, httpx.TimeoutException) as e:
        r1 = CheckResult(
            "Embedding 服务", "fail", f"无法连接 {url}",
            fix_hint="确认 embedding 服务已启动",
        )
        results.append(r1)
        _print_check(1, 3, r1)
        r2 = CheckResult("Embedding 测试", "skip", "服务不可用")
        results.append(r2)
        _print_check(2, 3, r2)
    except httpx.HTTPError as e:
        r1 = CheckResult(
            "Embedding 服务", "fail", f"HTTP 错误: {e}",
            fix_hint="检查服务是否支持 /v1/embeddings 接口",
        )
        results.append(r1)
        _print_check(1, 3, r1)
        r2 = CheckResult("Embedding 测试", "skip", "服务异常")
        results.append(r2)
        _print_check(2, 3, r2)

    # Check 3: 配置信息
    r3 = CheckResult(
        "配置", "pass",
        f"api=openai_compat, model={model}, dim={config.effective_embedding_dim}",
    )
    results.append(r3)
    _print_check(3, 3, r3)

    _print_summary(results, fix=False)
    return results


def _print_summary(results: list[CheckResult], fix: bool) -> None:
    """打印汇总"""
    passed = sum(1 for r in results if r.status == "pass")
    failed = sum(1 for r in results if r.status == "fail")
    warned = sum(1 for r in results if r.status == "warn")
    skipped = sum(1 for r in results if r.status == "skip")

    click.echo()
    parts = []
    if passed:
        parts.append(click.style(f"{passed} 通过", fg="green"))
    if failed:
        parts.append(click.style(f"{failed} 失败", fg="red"))
    if warned:
        parts.append(click.style(f"{warned} 警告", fg="yellow"))
    if skipped:
        parts.append(f"{skipped} 跳过")

    click.echo(f"  结果: {', '.join(parts)}")

    if failed and not fix:
        click.echo(f"  运行 {click.style('index1 doctor --fix', bold=True)} 自动修复")
    elif failed == 0 and warned == 0:
        click.echo(click.style("  Ollama 环境就绪，向量搜索可用", fg="green"))
    click.echo()
