# -*- coding: utf-8 -*-
"""MCP tool definitions backed by Server Manager services."""

from __future__ import annotations

import contextlib
import io
import json
import os
import subprocess
import traceback
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from entrypoints.cli.manager import ServerManagerCLI
from services.commands import (
    check_nodes_status,
    flatten_server_dirs,
    generate_start_config,
    get_server_list,
    read_merged_config,
    read_text_file_best_effort,
    resolve_log_file_path,
    REBAR3_CMD,
)
from services.rg_search import rg_search_directory


JsonDict = Dict[str, Any]


@dataclass(frozen=True)
class ToolDefinition:
    name: str
    title: str
    description: str
    input_schema: JsonDict
    handler: Callable[[JsonDict], JsonDict]
    annotations: Optional[JsonDict] = None

    def to_mcp_tool(self) -> JsonDict:
        data: JsonDict = {
            "name": self.name,
            "title": self.title,
            "description": self.description,
            "inputSchema": self.input_schema,
        }
        if self.annotations:
            data["annotations"] = self.annotations
        return data


class ToolError(Exception):
    """Expected tool failure returned as an MCP tool error."""


def _schema(properties: JsonDict, required: Optional[List[str]] = None) -> JsonDict:
    return {
        "type": "object",
        "properties": properties,
        "required": required or [],
        "additionalProperties": False,
    }


def _text_result(data: Any, *, is_error: bool = False) -> JsonDict:
    text = data if isinstance(data, str) else json.dumps(data, ensure_ascii=False, indent=2)
    result: JsonDict = {
        "content": [{"type": "text", "text": text}],
        "isError": is_error,
    }
    if not is_error and not isinstance(data, str):
        result["structuredContent"] = data
    return result


def _mask_sensitive(config: JsonDict) -> JsonDict:
    masked = dict(config)
    for key in list(masked):
        lowered = key.lower()
        if "pass" in lowered or "password" in lowered or "secret" in lowered:
            masked[key] = "***"
    return masked


def _truncate(text: str, max_chars: int) -> str:
    if len(text) <= max_chars:
        return text
    return text[:max_chars] + f"\n... <truncated {len(text) - max_chars} chars>"


class ServerManagerToolbox:
    """Server Manager operations exposed to MCP tools."""

    def __init__(self, server_root: Optional[str] = None, config_path: Optional[str] = None,
                 cookie: Optional[str] = None):
        self.server_root = server_root
        self.config_path = config_path
        self.cookie = cookie

    def list_tools(self) -> List[ToolDefinition]:
        return [
            ToolDefinition(
                name="server_manager_list_servers",
                title="List Servers",
                description="List server directories grouped by type. Optionally include online status.",
                input_schema=_schema({
                    "server_root": {"type": "string", "description": "Optional server project root override."},
                    "config_path": {"type": "string", "description": "Optional config file override."},
                    "include_status": {"type": "boolean", "description": "Whether to check Erlang node status."},
                    "target_ip": {"type": "string", "description": "Optional target host/IP for status checks."},
                }),
                handler=self.list_servers,
                annotations={"readOnlyHint": True, "destructiveHint": False},
            ),
            ToolDefinition(
                name="server_manager_check_status",
                title="Check Node Status",
                description="Check whether one or more Erlang server nodes are online.",
                input_schema=_schema({
                    "servers": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Server directory or node names. If omitted, all discovered servers are checked.",
                    },
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                    "target_ip": {"type": "string"},
                    "cookie": {"type": "string"},
                }),
                handler=self.check_status,
                annotations={"readOnlyHint": True, "destructiveHint": False},
            ),
            ToolDefinition(
                name="server_manager_read_config",
                title="Read Config",
                description="Read merged Server Manager config, optionally including a specific server kv.config.",
                input_schema=_schema({
                    "server": {"type": "string", "description": "Optional server directory name."},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                    "include_sensitive": {
                        "type": "boolean",
                        "description": "Return passwords/secrets instead of masking them. Defaults to false.",
                    },
                }),
                handler=self.read_config,
                annotations={"readOnlyHint": True, "destructiveHint": False},
            ),
            ToolDefinition(
                name="server_manager_create_server",
                title="Create Server",
                description="Create a game/login/center/cross/client server from project templates.",
                input_schema=_schema({
                    "server_type": {
                        "type": "string",
                        "enum": ["game", "login", "center", "cross", "client"],
                    },
                    "server_id": {"type": "integer"},
                    "prefix": {"type": "string"},
                    "db_host": {"type": "string"},
                    "db_user": {"type": "string"},
                    "db_pass": {"type": "string"},
                    "db_port": {"type": "integer"},
                    "login_node": {"type": "string"},
                    "center_node": {"type": "string"},
                    "server_name": {"type": "string"},
                    "tcp_port": {"type": "integer"},
                    "http_port": {"type": "integer"},
                    "open_time": {"type": "string"},
                    "overwrite": {"type": "boolean"},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                }, required=["server_type", "server_id"]),
                handler=self.create_server,
                annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False},
            ),
            ToolDefinition(
                name="server_manager_regenerate_config",
                title="Regenerate Sys Config",
                description="Regenerate sys.config for a server from default/tool/kv config.",
                input_schema=_schema({
                    "server": {"type": "string"},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                }, required=["server"]),
                handler=self.regenerate_config,
                annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": True},
            ),
            ToolDefinition(
                name="server_manager_start_server",
                title="Start Server",
                description="Start a server in a background terminal/window. Foreground mode is not exposed over MCP.",
                input_schema=_schema({
                    "server": {"type": "string"},
                    "quick": {"type": "boolean", "description": "Use direct erl quick start instead of rebar3 shell."},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                }, required=["server"]),
                handler=self.start_server,
                annotations={"readOnlyHint": False, "destructiveHint": False, "openWorldHint": True},
            ),
            ToolDefinition(
                name="server_manager_stop_server",
                title="Stop Server",
                description="Stop a running Erlang server node.",
                input_schema=_schema({
                    "server": {"type": "string"},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                }, required=["server"]),
                handler=self.stop_server,
                annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True},
            ),
            ToolDefinition(
                name="server_manager_compile",
                title="Compile Server",
                description="Run a rebar3 compile command and return captured output.",
                input_schema=_schema({
                    "target": {
                        "type": "string",
                        "enum": ["all", "code", "proto", "table", "tbllog"],
                        "description": "Compile target. Defaults to all.",
                    },
                    "server_type": {
                        "type": "string",
                        "enum": ["game", "login"],
                        "description": "Compile profile family. Defaults to game.",
                    },
                    "timeout_seconds": {"type": "integer", "description": "Timeout, max 3600. Defaults to 1200."},
                    "max_output_chars": {"type": "integer", "description": "Output truncation size. Defaults to 20000."},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                }),
                handler=self.compile,
                annotations={"readOnlyHint": False, "destructiveHint": False, "openWorldHint": True},
            ),
            ToolDefinition(
                name="server_manager_search_logs",
                title="Search Logs",
                description="Search a server log directory using ripgrep when available.",
                input_schema=_schema({
                    "server": {"type": "string"},
                    "pattern": {"type": "string"},
                    "case_sensitive": {"type": "boolean"},
                    "use_regex": {"type": "boolean"},
                    "whole_word": {"type": "boolean"},
                    "glob": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Optional rg glob filters, e.g. ['*.log*'].",
                    },
                    "max_output_chars": {"type": "integer"},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                }, required=["server", "pattern"]),
                handler=self.search_logs,
                annotations={"readOnlyHint": True, "destructiveHint": False},
            ),
            ToolDefinition(
                name="server_manager_read_log",
                title="Read Log",
                description="Read a specific log file from a server log directory with a byte limit.",
                input_schema=_schema({
                    "server": {"type": "string"},
                    "base_filename": {
                        "type": "string",
                        "description": "Base log file name, e.g. error.log or info.log.",
                    },
                    "date_yyyymmdd": {"type": "string", "description": "Optional date suffix, e.g. 20260516."},
                    "subdir": {"type": "string", "description": "Optional subdir under log, e.g. tag_log."},
                    "max_bytes": {"type": "integer", "description": "Read limit. Defaults to 1048576."},
                    "server_root": {"type": "string"},
                    "config_path": {"type": "string"},
                }, required=["server", "base_filename"]),
                handler=self.read_log,
                annotations={"readOnlyHint": True, "destructiveHint": False},
            ),
        ]

    def tool_map(self) -> Dict[str, ToolDefinition]:
        return {tool.name: tool for tool in self.list_tools()}

    def call_tool(self, name: str, arguments: Optional[JsonDict]) -> JsonDict:
        tool = self.tool_map().get(name)
        if not tool:
            return _text_result(f"未知工具: {name}", is_error=True)
        try:
            payload = tool.handler(dict(arguments or {}))
            return _text_result(payload)
        except ToolError as e:
            return _text_result(str(e), is_error=True)
        except Exception:
            return _text_result(traceback.format_exc(), is_error=True)

    def _arg(self, arguments: JsonDict, key: str, default: Any = None) -> Any:
        value = arguments.get(key, default)
        if value is None:
            return default
        return value

    def _make_cli(self, arguments: JsonDict) -> tuple[ServerManagerCLI, str]:
        server_root = self._arg(arguments, "server_root", self.server_root)
        config_path = self._arg(arguments, "config_path", self.config_path)
        cookie = self._arg(arguments, "cookie", self.cookie)
        buf = io.StringIO()
        with contextlib.redirect_stdout(buf):
            cli = ServerManagerCLI(server_root=server_root, config_path=config_path, quiet=True)
            if cookie:
                cli.cookie = str(cookie)
        return cli, buf.getvalue()

    def _capture_cli_call(self, cli: ServerManagerCLI, func: Callable[[], Any]) -> tuple[Any, str]:
        buf = io.StringIO()
        with contextlib.redirect_stdout(buf):
            result = func()
        return result, buf.getvalue()

    def list_servers(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        servers = get_server_list(cli.run_dir)
        status = None
        if bool(arguments.get("include_status", False)):
            names = flatten_server_dirs(cli.run_dir)
            status = check_nodes_status(
                names,
                cli.cookie,
                target_ip=arguments.get("target_ip"),
                erl_path=cli.config.get("erl_path"),
            )
        return {
            "server_root": cli.server_root,
            "run_dir": cli.run_dir,
            "servers": servers,
            "status": status,
            "output": init_output.strip(),
        }

    def check_status(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        servers = arguments.get("servers") or flatten_server_dirs(cli.run_dir)
        if not isinstance(servers, list):
            raise ToolError("servers 必须是字符串数组")
        results = check_nodes_status(
            [str(item) for item in servers],
            str(arguments.get("cookie") or cli.cookie),
            target_ip=arguments.get("target_ip"),
            erl_path=cli.config.get("erl_path"),
        )
        return {
            "server_root": cli.server_root,
            "run_dir": cli.run_dir,
            "status": results,
            "output": init_output.strip(),
        }

    def read_config(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        server = arguments.get("server")
        config = read_merged_config(cli.server_root, server_dir=server, run_dir=cli.run_dir)
        if not bool(arguments.get("include_sensitive", False)):
            config = _mask_sensitive(config)
        return {
            "server_root": cli.server_root,
            "run_dir": cli.run_dir,
            "server": server,
            "config": config,
            "output": init_output.strip(),
        }

    def create_server(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        success, output = self._capture_cli_call(
            cli,
            lambda: cli.create_server_cmd(
                server_type=str(arguments["server_type"]),
                server_id=int(arguments["server_id"]),
                prefix=arguments.get("prefix"),
                db_host=arguments.get("db_host"),
                db_user=arguments.get("db_user"),
                db_pass=arguments.get("db_pass"),
                db_port=arguments.get("db_port"),
                login_node=arguments.get("login_node"),
                center_node=arguments.get("center_node"),
                server_name=arguments.get("server_name"),
                tcp_port=arguments.get("tcp_port"),
                http_port=arguments.get("http_port"),
                open_time=arguments.get("open_time"),
                overwrite=bool(arguments.get("overwrite", False)),
            ),
        )
        return {"success": bool(success), "output": (init_output + output).strip()}

    def regenerate_config(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        server = str(arguments["server"])
        success, output = self._capture_cli_call(cli, lambda: cli.regenerate_config(server))
        return {"success": bool(success), "server": server, "output": (init_output + output).strip()}

    def start_server(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        server = str(arguments["server"])
        quick = bool(arguments.get("quick", False))
        success, output = self._capture_cli_call(
            cli,
            lambda: cli.start_server(server, use_rebar=not quick, foreground=False),
        )
        return {
            "success": bool(success),
            "server": server,
            "quick": quick,
            "output": (init_output + output).strip(),
        }

    def stop_server(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        server = str(arguments["server"])
        success, output = self._capture_cli_call(cli, lambda: cli.stop_server(server))
        return {"success": bool(success), "server": server, "output": (init_output + output).strip()}

    def compile(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        target = str(arguments.get("target") or "all")
        server_type = str(arguments.get("server_type") or "game")
        timeout = min(max(int(arguments.get("timeout_seconds") or 1200), 1), 3600)
        max_chars = min(max(int(arguments.get("max_output_chars") or 20000), 1000), 200000)
        profile = "login_server_dev" if server_type == "login" else "game_server_dev"
        cmd_map = {
            "all": f"{REBAR3_CMD} as {profile} compile",
            "code": f"{REBAR3_CMD} as {profile} compile",
            "proto": f"{REBAR3_CMD} protobuf compile",
            "table": f"{REBAR3_CMD} cache compile",
            "tbllog": f"{REBAR3_CMD} tbllog compile",
        }
        cmd = cmd_map.get(target)
        if not cmd:
            raise ToolError(f"未知编译目标: {target}")
        env = os.environ.copy()
        env["ESCRIPT_EMULATOR"] = "erl"
        proc = subprocess.run(
            cmd,
            shell=True,
            cwd=cli.server_root,
            env=env,
            text=True,
            encoding="utf-8",
            errors="replace",
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            timeout=timeout,
        )
        return {
            "success": proc.returncode == 0,
            "return_code": proc.returncode,
            "command": cmd,
            "server_root": cli.server_root,
            "output": _truncate((init_output + "\n" + (proc.stdout or "")).strip(), max_chars),
        }

    def search_logs(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        server = str(arguments["server"])
        search_root = Path(cli.run_dir) / server / "log"
        max_chars = min(max(int(arguments.get("max_output_chars") or 20000), 1000), 200000)
        out, err = rg_search_directory(
            search_root=search_root,
            pattern=str(arguments["pattern"]),
            case_sensitive=bool(arguments.get("case_sensitive", False)),
            use_regex=bool(arguments.get("use_regex", False)),
            whole_word=bool(arguments.get("whole_word", False)),
            glob_globs=arguments.get("glob"),
        )
        if err:
            raise ToolError(err)
        return {
            "server": server,
            "search_root": str(search_root),
            "matches": _truncate(out, max_chars),
            "output": init_output.strip(),
        }

    def read_log(self, arguments: JsonDict) -> JsonDict:
        cli, init_output = self._make_cli(arguments)
        server = str(arguments["server"])
        max_bytes = min(max(int(arguments.get("max_bytes") or 1048576), 1024), 10 * 1024 * 1024)
        path = resolve_log_file_path(
            cli.run_dir,
            server,
            str(arguments["base_filename"]),
            date_yyyymmdd=arguments.get("date_yyyymmdd"),
            subdir=arguments.get("subdir") or "",
        )
        content, err = read_text_file_best_effort(path, max_bytes=max_bytes)
        if err:
            raise ToolError(err)
        return {
            "server": server,
            "path": str(path),
            "content": content,
            "output": init_output.strip(),
        }
