import asyncio
import json
import os
import platform
import re
import sys
import tempfile
from contextlib import contextmanager

from fastmcp import Context, FastMCP
from fastmcp.server.lifespan import lifespan

from my_mcp import filesystem_server
from my_mcp.path_validator import resolve_path, validate_path
from my_mcp.server_config import ServerConfig
from my_mcp.umlaut_fixer import fix_document

BACKEND_TIMEOUT = float(os.environ.get("MCP_BACKEND_TIMEOUT", "60"))


def _parse_allowed_dirs(raw: str) -> list[str]:
    """Parse allowed directories from env var, supporting multiple delimiters.
    On Windows, ':' is reserved for drive letters (C:\\), so only ',' and ';' split paths.
    On other platforms, ':' is also accepted (Unix PATH-style)."""
    pattern = r"[,;]" if platform.system() == "Windows" else r"[,:;]"
    return [d.strip() for d in re.split(pattern, raw) if d.strip()]


def _get_system_temp_dirs() -> list[str]:
    """Get system temp directories, handling 8.3 short paths on Windows."""
    temps = set()
    for var in ("TEMP", "TMP"):
        val = os.environ.get(var)
        if val:
            temps.add(str(resolve_path(val)))
    temps.add(str(resolve_path(tempfile.gettempdir())))
    return list(temps)


_cli_dirs = sys.argv[1:]
ALLOWED_DIRS: list[str] = (
    _cli_dirs
    if _cli_dirs
    else _parse_allowed_dirs(os.environ.get("ALLOWED_DIRS", os.getcwd()))
)
ALLOWED_DIRS.extend(d for d in _get_system_temp_dirs() if d not in ALLOWED_DIRS)
SERVER_CONFIG_PATH = os.environ.get("SERVER_CONFIG", "server_config.json")


@contextmanager
def utf8_temp(content: str):
    """Write content to a UTF-8 temp file, yield its path, then clean up."""
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".txt", encoding="utf-8", delete=False
    ) as tmp:
        tmp.write(content)
        tmp_path = tmp.name
    try:
        yield tmp_path
    finally:
        os.unlink(tmp_path)


@lifespan
async def manage_backend(_server):
    config = ServerConfig.from_path(SERVER_CONFIG_PATH)
    client = filesystem_server.create_client(ALLOWED_DIRS)
    try:
        await asyncio.wait_for(client.__aenter__(), timeout=20)
    except TimeoutError as e:
        raise RuntimeError("Backend server init timed out") from e
    try:
        # Verify the backend is actually running before accepting tool calls
        try:
            tools = await asyncio.wait_for(client.list_tools(), timeout=10)
        except TimeoutError as e:
            raise RuntimeError(
                "Backend server (@modelcontextprotocol/server-filesystem) did not "
                "respond within 10 seconds. Make sure Node.js and npx are installed "
                "and working correctly."
            ) from e
        tool_names = [t.name for t in tools]
        if filesystem_server.TOOL_NAME not in tool_names:
            raise RuntimeError(
                f"Backend server started but '{filesystem_server.TOOL_NAME}' tool "
                f"not found. Available tools: {tool_names}"
            )
        backend_lock = asyncio.Lock()
        yield {"backend": client, "config": config, "backend_lock": backend_lock}
    finally:
        await client.__aexit__(None, None, None)


mcp = FastMCP("filesystem-edit-mcp", lifespan=manage_backend)


@mcp.tool
async def read_text_file(
    path: str,
    head: int | None = None,
    tail: int | None = None,
    ctx: Context | None = None,
) -> str:
    """Read complete contents of a file as text.
    Reads the source file in the configured encoding.
    Cannot specify both head and tail simultaneously."""
    assert ctx is not None
    validate_path(path, ALLOWED_DIRS)
    backend = ctx.lifespan_context["backend"]
    backend_lock = ctx.lifespan_context["backend_lock"]
    encoding = ctx.lifespan_context["config"].encoding_for(path)

    with open(path, encoding=encoding) as f:
        content = f.read()

    with utf8_temp(content) as tmp_path:
        args: dict[str, str | int] = {"path": tmp_path}
        if head is not None:
            args["head"] = head
        if tail is not None:
            args["tail"] = tail
        try:
            async with backend_lock:
                result = await asyncio.wait_for(
                    backend.call_tool("read_text_file", args),
                    timeout=BACKEND_TIMEOUT,
                )
        except TimeoutError as e:
            raise RuntimeError(
                f"Backend call timed out after {BACKEND_TIMEOUT}s while reading '{path}'. "
                "The file may be too large or the backend server is unresponsive. "
                "Set MCP_BACKEND_TIMEOUT env var to increase timeout."
            ) from e

    return result.content[0].text if result.content else ""


@mcp.tool
async def edit_file(
    path: str,
    edits: list[dict],
    dryRun: bool = False,
    ctx: Context | None = None,
) -> str:
    """Edit a file using advanced pattern matching and formatting.
    Reads the file in the configured encoding, converts to UTF-8 for editing,
    then converts back. Applies umlaut fixing at the end.
    Use dryRun=True to preview changes before applying."""
    assert ctx is not None
    validate_path(path, ALLOWED_DIRS)
    backend = ctx.lifespan_context["backend"]
    backend_lock = ctx.lifespan_context["backend_lock"]
    config = ctx.lifespan_context["config"]
    encoding = config.encoding_for(path)

    with open(path, encoding=encoding) as f:
        content = f.read()

    with utf8_temp(content) as tmp_path:
        try:
            async with backend_lock:
                result = await asyncio.wait_for(
                    backend.call_tool(
                        filesystem_server.TOOL_NAME,
                        {
                            "path": tmp_path,
                            "edits": edits,
                            "dryRun": dryRun,
                        },
                    ),
                    timeout=BACKEND_TIMEOUT,
                )
        except TimeoutError as e:
            raise RuntimeError(
                f"Backend call timed out after {BACKEND_TIMEOUT}s while editing '{path}'. "
                "The file may be too large or the backend server is unresponsive. "
                "Set MCP_BACKEND_TIMEOUT env var to increase timeout."
            ) from e

        if not dryRun:
            with open(tmp_path, encoding="utf-8") as f:
                edited = f.read()
            if config.enable_umlaut_fixing:
                edited = fix_document(edited, config.umlaut_config)
            with open(path, "w", encoding=encoding) as f:
                f.write(edited)

    return result.content[0].text if result.content else ""


DEFAULT_CONFIG = {
    "enable_umlaut_fixing": False,
    "umlaut_config": {
        "string_mapping": {
            "ä": "AE_UMLT",
            "Ä": "CAE_UMLT",
            "ö": "OE_UMLT",
            "Ö": "COE_UMLT",
            "ü": "UE_UMLT",
            "Ü": "CUE_UMLT",
            "ß": "SS_UMLT",
        },
        "comment_mapping": {
            "ä": "ae",
            "Ä": "Ae",
            "ö": "oe",
            "Ö": "Oe",
            "ü": "ue",
            "Ü": "Ue",
            "ß": "ss",
        },
        "char_mapping": {
            "ä": "AE_UMLT_CHAR",
            "Ä": "CAE_UMLT_CHAR",
            "ö": "OE_UMLT_CHAR",
            "Ö": "COE_UMLT_CHAR",
            "ü": "UE_UMLT_CHAR",
            "Ü": "CUE_UMLT_CHAR",
            "ß": "SS_UMLT_CHAR",
        },
    },
    "encoding_config": {
        "default": "utf-8",
        "mapping": {
            "*.cpp": "latin-1",
            "*.h": "latin-1",
        },
    },
}


@mcp.tool
async def initialize_config(overwrite: bool = False) -> str:
    """Create a default server_config.json in the current working directory.
    Safe to call at any time — will not overwrite an existing config unless
    overwrite=True. After creating the file, edit it to match your project's
    umlaut mappings, file encodings, and whether umlaut fixing is enabled,
    then restart the server."""
    path = os.path.abspath(SERVER_CONFIG_PATH)
    if os.path.exists(path) and not overwrite:
        return f"Config already exists at {path}. Pass overwrite=True to replace it."
    with open(path, "w", encoding="utf-8") as f:
        json.dump(DEFAULT_CONFIG, f, indent=2, ensure_ascii=False)
    return (
        f"Created default config at {path}. "
        "Umlaut fixing is disabled by default — set enable_umlaut_fixing to true to activate it. "
        "Edit the config to suit your project, then restart the server."
    )


def main() -> None:
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
