#!/usr/bin/env python3
"""
로컬 검색 MCP 서버를 위한 진단 도구 (Doctor).
시스템 상태, 설정, DB 무결성 등을 검사하고 구조화된 진단 결과(JSON)를 반환합니다.
ANSI 코드나 print 문을 사용하지 않고 순수 데이터 형태로 결과를 제공합니다.
"""
import json
import os
import sys
import importlib
from typing import Mapping, Optional, TypeAlias
from sari.core.cjk import lindera_available, lindera_dict_uri, lindera_error
from sari.core.config import Config
from sari.core.settings import settings
from sari.core.workspace import WorkspaceManager
from sari.mcp.server_registry import ServerRegistry, get_registry_path
from sari.core.policy_engine import load_daemon_policy, load_daemon_runtime_status
from sari.mcp.cli.mcp_client import probe_sari_daemon, is_http_running as _is_http_running
from sari.core.daemon_resolver import resolve_daemon_address as get_daemon_address
from sari.core.daemon_runtime_state import RUNTIME_HOST, RUNTIME_PORT
from sari.mcp.tools.doctor_common import (
    result as _result,
    safe_float as _safe_float,
    safe_int as _safe_int,
    safe_pragma_table_name as _safe_pragma_table_name,
)
from sari.mcp.tools.doctor_daemon_endpoint import (
    identify as _identify_sari_daemon,
    read_pid as _read_pid,
    resolve_http_endpoint_for_daemon as _resolve_http_endpoint_for_daemon,
)
from sari.mcp.tools.doctor_actions import (
    auto_fixable as _auto_fixable,
    check_workspace_overlaps as _check_workspace_overlaps,
    recommendations as _recommendations,
    run_auto_fixes as _run_auto_fixes,
    run_rescan as _run_rescan,
)
from sari.mcp.tools.doctor_checks_db import (
    check_db as _check_db,
    check_db_integrity as _check_db_integrity,
    check_db_migration_safety as _check_db_migration_safety,
    check_engine_runtime as _check_engine_runtime,
    check_engine_sync_dlq as _check_engine_sync_dlq,
    check_fts_rebuild_policy as _check_fts_rebuild_policy,
    check_storage_switch_guard as _check_storage_switch_guard,
    check_writer_health as _check_writer_health,
)
from sari.mcp.tools.doctor_checks_system import (
    check_disk_space as _check_disk_space,
    check_log_errors as _check_log_errors,
    check_network as _check_network,
    check_port as _check_port,
    check_process_resources as _check_process_resources,
    check_system_env as _check_system_env,
)
from sari.mcp.tools.doctor_checks_daemon import (
    check_daemon as _check_daemon_impl,
    check_daemon_policy as _check_daemon_policy_impl,
    check_daemon_runtime_markers as _check_daemon_runtime_markers_impl,
    check_http_service as _check_http_service_impl,
)

DoctorResult: TypeAlias = dict[str, object]
DoctorResults: TypeAlias = list[DoctorResult]

_cli_identify = _identify_sari_daemon


def _platform_tokenizer_tag() -> str:
    """현재 플랫폼에 맞는 토크나이저 태그를 반환합니다."""
    import platform
    plat = sys.platform
    arch = platform.machine().lower()
    if plat.startswith("darwin"):
        if arch in {"arm64", "aarch64"}:
            return "macosx_11_0_arm64"
        if arch in {"x86_64", "amd64"}:
            return "macosx_10_9_x86_64"
        return "macosx"
    if plat.startswith("win"):
        return "win_amd64"
    if plat.startswith("linux"):
        if arch in {"aarch64", "arm64"}:
            return "manylinux_2_17_aarch64"
        return "manylinux_2_17_x86_64"
    return "unknown"


def _check_engine_tokenizer_data() -> DoctorResult:
    """토크나이저 데이터(lindera 등)가 설치되어 있는지 확인합니다."""
    if lindera_available():
        uri = lindera_dict_uri() or "embedded://ipadic"
        return _result("CJK Tokenizer", True, f"active ({uri})")
    err = lindera_error() or "not available"
    return _result(
        "CJK Tokenizer",
        False,
        f"{err} (install/upgrade package 'lindera-python-ipadic')")


def _check_tree_sitter() -> DoctorResult:
    """Tree-sitter 패키지와 언어 파서가 설치되어 있는지 확인합니다."""
    try:
        from sari.core.parsers.ast_engine import ASTEngine
        engine = ASTEngine()
        if not engine.enabled:
            return _result(
                "Tree-sitter Support",
                False,
                "core 'tree-sitter' package not installed (optional)")

        # 주요 언어 파서 확인
        langs = [
            "python",
            "javascript",
            "typescript",
            "java",
            "go",
            "rust",
            "cpp"]
        installed = []
        for lang in langs:
            if engine._get_language(lang):
                installed.append(lang)

        if installed:
            return _result(
                "Tree-sitter Support",
                True,
                f"enabled for: {', '.join(installed)}")
        return _result(
            "Tree-sitter Support",
            True,
            "core enabled but no language parsers loaded yet")
    except Exception as e:
        return _result("Tree-sitter Support", False, str(e))


def _check_tree_sitter_language_runtime() -> DoctorResult:
    """파서 팩토리 기준 언어별 tree-sitter 런타임 지원 상태를 보고합니다."""
    try:
        from sari.core.parsers.ast_engine import ASTEngine
        from sari.core.parsers.factory import ParserFactory
    except Exception as e:
        return _result("Tree-sitter Language Runtime", False, str(e))

    engine = ASTEngine()
    if not engine.enabled:
        return _result(
            "Tree-sitter Language Runtime",
            False,
            "core 'tree-sitter' package not installed (optional)",
        )

    lang_map = getattr(ParserFactory, "_lang_map", {}) or {}
    configured = sorted({str(v).strip() for v in lang_map.values() if str(v).strip()})
    if not configured:
        return _result("Tree-sitter Language Runtime", True, "no configured parser languages")

    available: list[str] = []
    missing: list[str] = []
    for lang in configured:
        if engine._get_language(lang):
            available.append(lang)
        else:
            missing.append(lang)

    if not missing:
        return _result(
            "Tree-sitter Language Runtime",
            True,
            "all configured parser languages have runtime support",
        )
    detail = (
        f"available={', '.join(available) if available else '-'}; "
        f"missing={', '.join(missing)}"
    )
    return _result("Tree-sitter Language Runtime", False, detail)


def _check_embedded_engine_module() -> DoctorResult:
    """내장 엔진 모듈(tantivy)이 임포트 가능한지 확인합니다."""
    try:
        import tantivy  # type: ignore
        ver = getattr(tantivy, "__version__", "unknown")
        return _result("Embedded Engine Module", True, str(ver))
    except Exception as e:
        return _result(
            "Embedded Engine Module",
            False,
            f"{type(e).__name__}: {e}")


def _check_windows_write_lock_support() -> DoctorResult:
    """Windows 환경에서 파일 쓰기 잠금(locking)이 지원되는지 확인합니다."""
    if os.name != "nt":
        return _result("Windows Write Lock", True, "non-windows platform")
    try:
        import msvcrt  # type: ignore
        ok = hasattr(msvcrt, "locking") and hasattr(msvcrt, "LK_LOCK")
        if ok:
            return _result(
                "Windows Write Lock",
                True,
                "msvcrt.locking available")
        return _result(
            "Windows Write Lock",
            False,
            "msvcrt.locking is unavailable")
    except Exception as e:
        return _result("Windows Write Lock", False, f"{type(e).__name__}: {e}")


def _check_lindera_dictionary() -> DoctorResult:
    # Merged into CJK Tokenizer check above
    return _check_engine_tokenizer_data()


def _check_daemon() -> DoctorResult:
    return _check_daemon_impl(
        result_fn=_result,
        get_daemon_address=get_daemon_address,
        identify_daemon=_identify_sari_daemon,
        read_pid=_read_pid,
        process_resources=_check_process_resources,
        local_version=str(settings.VERSION),
        server_registry_cls=ServerRegistry,
    )


def _check_daemon_policy() -> DoctorResult:
    return _check_daemon_policy_impl(
        result_fn=_result,
        load_policy=load_daemon_policy,
        settings_obj=settings,
        get_daemon_address=get_daemon_address,
        resolve_http_endpoint_for_daemon=_resolve_http_endpoint_for_daemon,
        runtime_host_env=RUNTIME_HOST,
        runtime_port_env=RUNTIME_PORT,
    )


def _check_http_service(host: str, port: int) -> DoctorResult:
    return _check_http_service_impl(
        host,
        port,
        result_fn=_result,
        is_http_running=_is_http_running,
    )


def _check_daemon_runtime_markers() -> DoctorResult:
    return _check_daemon_runtime_markers_impl(
        result_fn=_result,
        load_runtime_status=load_daemon_runtime_status,
    )


def _check_search_first_usage(
        usage: Mapping[str, object], mode: str) -> DoctorResult:
    """검색 우선(Search-First) 정책 준수 여부를 확인합니다."""
    violations = int(usage.get("read_without_search", 0))
    searches = int(usage.get("search", 0))
    symbol_searches = int(usage.get("search_symbols", 0))
    if violations == 0:
        return _result("Search-First Usage", True, "")
    policy = mode if mode in {"off", "warn", "enforce"} else "unknown"
    error = (
        f"Search-first policy {policy}: {violations} read call(s) without prior search "
        f"(search={searches}, search_symbols={symbol_searches}).")
    return _result("Search-First Usage", False, error)


def _check_callgraph_plugin() -> DoctorResult:
    """콜 그래프 플러그인 로드 상태를 확인합니다."""
    mod_path = os.environ.get("SARI_CALLGRAPH_PLUGIN", "").strip()
    if not mod_path:
        return _result("CallGraph Plugin", True, "not configured")
    mods = [m.strip() for m in mod_path.split(",") if m.strip()]
    failed = []
    meta = []
    for m in mods:
        try:
            mod = importlib.import_module(m)
            version = getattr(mod, "__version__", "")
            api = getattr(mod, "__callgraph_plugin_api__", None)
            if api is not None:
                meta.append(f"{m}@{version} api={api}")
            else:
                meta.append(f"{m}{'@' + str(version) if version else ''}")
        except Exception:
            failed.append(m)
    if failed:
        return _result(
            "CallGraph Plugin",
            False,
            f"failed to load: {', '.join(failed)}")
    detail = "loaded"
    if meta:
        detail = "loaded: " + ", ".join(meta)
    return _result("CallGraph Plugin", True, detail)


def execute_doctor(
    args: object,
    db: object = None,
    logger: object = None,
    roots: Optional[list[str]] = None,
) -> dict[str, object]:
    """Doctor 도구 실행 핸들러."""
    if not isinstance(args, Mapping):
        try:
            from sari.mcp.tools._util import mcp_response, pack_error, ErrorCode
        except Exception:
            from _util import mcp_response, pack_error, ErrorCode
        msg = "'args' must be an object"
        return mcp_response(
            "doctor",
            lambda: pack_error("doctor", ErrorCode.INVALID_ARGS, msg),
            lambda: {
                "error": {"code": ErrorCode.INVALID_ARGS.value, "message": msg},
                "isError": True,
            },
        )

    args_map: Mapping[str, object] = args
    ws_root = roots[0] if roots else WorkspaceManager.resolve_workspace_root()

    include_network = bool(args_map.get("include_network", True))
    include_port = bool(args_map.get("include_port", True))
    include_db = bool(args_map.get("include_db", True))
    include_disk = bool(args_map.get("include_disk", True))
    include_daemon = bool(args_map.get("include_daemon", True))
    include_venv = bool(args_map.get("include_venv", True))
    include_marker = bool(args_map.get("include_marker", False))
    port = _safe_int(args_map.get("port", 0), 0)
    min_disk_gb = _safe_float(args_map.get("min_disk_gb", 1.0), 1.0)

    auto_fix = bool(args_map.get("auto_fix", False))
    auto_fix_rescan = bool(args_map.get("auto_fix_rescan", False))

    results: DoctorResults = []

    results.extend(_check_system_env())

    if include_venv:
        in_venv = sys.prefix != sys.base_prefix
        results.append(
            _result(
                "Virtualenv",
                True,
                "" if in_venv else "Not running in venv (ok)"))

    if include_daemon:
        results.append(_check_daemon())
        results.append(_check_daemon_policy())
        results.append(_check_log_errors())

    if include_port:
        daemon_host, daemon_port = get_daemon_address()
        daemon_running = probe_sari_daemon(daemon_host, daemon_port)
        if daemon_running:
            results.append(
                _result(
                    "Daemon Port",
                    True,
                    f"In use by running daemon {daemon_host}:{daemon_port}"))
        else:
            results.append(_check_port(daemon_port, "Daemon"))

        http_host, http_port = _resolve_http_endpoint_for_daemon(
            daemon_host, daemon_port, port_override=port if port else None)
        results.append(_check_http_service(http_host, http_port))
        if not _is_http_running(http_host, http_port):
            results.append(_check_port(http_port, "HTTP"))

    if include_network:
        results.append(_check_network())

    if include_db:
        results.append(_check_db_integrity(ws_root))
        results.extend(_check_db(ws_root, allow_config_autofix=auto_fix))
        results.append(_check_db_migration_safety())
        results.append(_check_windows_write_lock_support())
        results.append(_check_engine_sync_dlq(ws_root))
        results.append(_check_embedded_engine_module())
        results.append(_check_engine_runtime(ws_root))
        results.append(_check_engine_tokenizer_data())
        results.append(_check_tree_sitter())
        results.append(_check_tree_sitter_language_runtime())
        results.append(_check_fts_rebuild_policy())
        results.append(_check_writer_health(db))
        results.append(_check_storage_switch_guard())

    if include_disk:
        results.append(_check_disk_space(ws_root, min_disk_gb))

    if include_marker:
        results.append(_check_daemon_runtime_markers())

    results.extend(_check_workspace_overlaps(ws_root))

    usage = args_map.get("search_usage")
    if isinstance(usage, dict):
        mode = str(args_map.get("search_first_mode", "unknown"))
        results.append(_check_search_first_usage(usage, mode))

    results.append(_check_callgraph_plugin())

    auto_fix_results: DoctorResults = []
    if auto_fix:
        actions = _auto_fixable(results)
        auto_fix_results = _run_auto_fixes(ws_root, actions)
        results.extend(auto_fix_results)
        if auto_fix_rescan:
            if any(not r.get("passed") for r in auto_fix_results):
                res = [
                    _result(
                        "Auto Fix Rescan Skipped",
                        False,
                        "auto-fix failed; rescan skipped")]
                auto_fix_results.extend(res)
                results.extend(res)
            else:
                res = _run_rescan(ws_root)
                auto_fix_results.extend(res)
                results.extend(res)

    output = {
        "workspace_root": ws_root,
        "results": results,
        "recommendations": _recommendations(results),
        "auto_fix": auto_fix_results,
    }

    try:
        from sari.mcp.tools._util import mcp_response, pack_header, pack_line
    except Exception:
        from _util import mcp_response, pack_header, pack_line

    def build_pack() -> str:
        compact = str(os.environ.get("SARI_RESPONSE_COMPACT")
                      or "1").strip().lower() not in {"0", "false", "no", "off"}
        payload = json.dumps(output, ensure_ascii=False, separators=(
            ",", ":") if compact else None, indent=None if compact else 2)
        lines = [pack_header("doctor", {}, returned=1)]
        lines.append(pack_line("t", single_value=payload))
        return "\n".join(lines)

    return mcp_response(
        "doctor",
        build_pack,
        lambda: output,  # Return raw dict, let mcp_response handle formatting
    )


if __name__ == "__main__":
    result = execute_doctor({})
    # result is the dict returned by mcp_response
    content = result["content"][0]["text"]
    print(content)
