"""
GIA File Tools — MCP Server
Exposes secure, sandboxed file operations over the Model Context Protocol.

Tools provided:
  - read_file        : Read a file's content
  - write_file       : Create / overwrite a file (atomic writes)
  - replace_text     : Search-and-replace inside a file (fuzzy whitespace matching)
  - list_directory   : List files & folders in a directory
  - search_files     : Grep-like search across files
  - get_file_structure : Recursive tree view of a directory

All paths are sandboxed to a configurable root directory.
Set GIA_FILE_ROOT env var to control it (default: cwd).
"""

from __future__ import annotations

import fnmatch
import os
import re
import shutil
import tempfile
from typing import Any

from mcp.server import Server
import mcp.types as types
from mcp.server.stdio import stdio_server

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

ROOT_DIR = os.environ.get("GIA_FILE_ROOT", os.getcwd())

# Directories to skip when walking / listing
SKIP_DIRS = {
    ".git", "__pycache__", "node_modules", ".venv", "venv",
    ".idea", ".vscode", "dist", "build", ".next",
}

# ---------------------------------------------------------------------------
# Path safety
# ---------------------------------------------------------------------------

def _safe_path(relative: str) -> str:
    """Resolve *relative* inside ROOT_DIR; raise if it escapes the sandbox."""
    relative = relative.lstrip("/")
    full = os.path.abspath(os.path.join(ROOT_DIR, relative))
    if not (full.startswith(ROOT_DIR + os.sep) or full == ROOT_DIR):
        raise ValueError(f"Access denied — path '{relative}' is outside the sandbox.")
    return full


def _format_size(size: int) -> str:
    for unit in ("B", "KB", "MB", "GB"):
        if size < 1024.0:
            return f"{size:.1f} {unit}" if unit != "B" else f"{size} {unit}"
        size /= 1024.0
    return f"{size:.1f} TB"


# ---------------------------------------------------------------------------
# Fuzzy matching helpers (ported from FileToolkit)
# ---------------------------------------------------------------------------

def _normalize_ws(text: str) -> str:
    text = text.replace("\r\n", "\n").replace("\r", "\n").replace("\t", "    ")
    return "\n".join(line.rstrip() for line in text.split("\n"))


def _find_fuzzy(content: str, target: str):
    """Return (start, end, matched_original_text) or (None, None, None)."""
    if target in content:
        idx = content.find(target)
        return idx, idx + len(target), target

    nc, nt = _normalize_ws(content), _normalize_ws(target)
    if nt in nc:
        norm_lines = nc.split("\n")
        target_lines = nt.split("\n")
        orig_lines = content.split("\n")
        for i in range(len(norm_lines) - len(target_lines) + 1):
            if norm_lines[i : i + len(target_lines)] == target_lines:
                matched = "\n".join(orig_lines[i : i + len(target_lines)])
                start = sum(len(l) + 1 for l in orig_lines[:i])
                return start, start + len(matched), matched
    return None, None, None


# ---------------------------------------------------------------------------
# Tree builder
# ---------------------------------------------------------------------------

def _build_tree(path: str, prefix: str, depth: int, max_depth: int, sizes: bool) -> list[str]:
    if depth >= max_depth:
        return []
    try:
        items = sorted(
            os.listdir(path),
            key=lambda x: (not os.path.isdir(os.path.join(path, x)), x.lower()),
        )
    except PermissionError:
        return [f"{prefix}[Permission Denied]"]

    items = [i for i in items if i not in SKIP_DIRS]
    lines: list[str] = []
    for idx, item in enumerate(items):
        is_last = idx == len(items) - 1
        connector = "└── " if is_last else "├── "
        item_path = os.path.join(path, item)
        if os.path.isdir(item_path):
            lines.append(f"{prefix}{connector}📁 {item}/")
            ext = "    " if is_last else "│   "
            lines.extend(_build_tree(item_path, prefix + ext, depth + 1, max_depth, sizes))
        else:
            sz = ""
            if sizes:
                try:
                    sz = f"  ({_format_size(os.path.getsize(item_path))})"
                except OSError:
                    pass
            lines.append(f"{prefix}{connector}📄 {item}{sz}")
    return lines


# ---------------------------------------------------------------------------
# MCP Server definition
# ---------------------------------------------------------------------------

server = Server("gia-file-tools")


@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    return [
        # 1 — read_file
        types.Tool(
            name="read_file",
            description="Read the content of a text/code file. Path is relative to the sandbox root.",
            inputSchema={
                "type": "object",
                "properties": {
                    "file_path": {
                        "type": "string",
                        "description": "Relative path to the file.",
                    },
                },
                "required": ["file_path"],
            },
        ),
        # 2 — write_file
        types.Tool(
            name="write_file",
            description="Create a new file or completely overwrite an existing one (atomic write).",
            inputSchema={
                "type": "object",
                "properties": {
                    "file_path": {
                        "type": "string",
                        "description": "Relative path to the file.",
                    },
                    "content": {
                        "type": "string",
                        "description": "Full content to write.",
                    },
                },
                "required": ["file_path", "content"],
            },
        ),
        # 3 — replace_text
        types.Tool(
            name="replace_text",
            description=(
                "Replace a specific block of text inside a file. "
                "Supports fuzzy whitespace matching so minor indentation differences are tolerated."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "file_path": {
                        "type": "string",
                        "description": "Relative path to the file.",
                    },
                    "target_snippet": {
                        "type": "string",
                        "description": "The exact text block to find.",
                    },
                    "replacement_snippet": {
                        "type": "string",
                        "description": "The new text to replace it with.",
                    },
                },
                "required": ["file_path", "target_snippet", "replacement_snippet"],
            },
        ),
        # 4 — list_directory
        types.Tool(
            name="list_directory",
            description="List files and folders in a directory.",
            inputSchema={
                "type": "object",
                "properties": {
                    "directory_path": {
                        "type": "string",
                        "description": "Relative path to the directory (default '.').",
                        "default": ".",
                    },
                },
                "required": [],
            },
        ),
        # 5 — search_files
        types.Tool(
            name="search_files",
            description="Search for a pattern across files in a directory (grep-like).",
            inputSchema={
                "type": "object",
                "properties": {
                    "pattern": {
                        "type": "string",
                        "description": "String or regex pattern to search for.",
                    },
                    "directory": {
                        "type": "string",
                        "description": "Relative directory to search in (default '.').",
                        "default": ".",
                    },
                    "file_pattern": {
                        "type": "string",
                        "description": "Glob for filenames, e.g. '*.py' (default '*').",
                        "default": "*",
                    },
                    "max_results": {
                        "type": "integer",
                        "description": "Maximum matches to return (default 50).",
                        "default": 50,
                    },
                },
                "required": ["pattern"],
            },
        ),
        # 6 — get_file_structure
        types.Tool(
            name="get_file_structure",
            description="Get a recursive tree view of a directory structure.",
            inputSchema={
                "type": "object",
                "properties": {
                    "directory_path": {
                        "type": "string",
                        "description": "Relative path (default '.').",
                        "default": ".",
                    },
                    "max_depth": {
                        "type": "integer",
                        "description": "Max recursion depth (default 3).",
                        "default": 3,
                    },
                    "include_sizes": {
                        "type": "boolean",
                        "description": "Show file sizes (default false).",
                        "default": False,
                    },
                },
                "required": [],
            },
        ),
    ]


# ---------------------------------------------------------------------------
# Tool dispatch
# ---------------------------------------------------------------------------

@server.call_tool()
async def handle_call_tool(
    name: str,
    arguments: dict | None,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
    args = arguments or {}

    try:
        if name == "read_file":
            return _tool_read_file(args)
        elif name == "write_file":
            return _tool_write_file(args)
        elif name == "replace_text":
            return _tool_replace_text(args)
        elif name == "list_directory":
            return _tool_list_directory(args)
        elif name == "search_files":
            return _tool_search_files(args)
        elif name == "get_file_structure":
            return _tool_get_file_structure(args)
        else:
            raise ValueError(f"Unknown tool: {name}")
    except Exception as exc:
        return [types.TextContent(type="text", text=f"Error: {exc}")]


# ---------------------------------------------------------------------------
# Tool implementations
# ---------------------------------------------------------------------------

def _text(msg: str):
    return [types.TextContent(type="text", text=msg)]


def _tool_read_file(args: dict):
    fp = _safe_path(args["file_path"])
    if not os.path.exists(fp):
        return _text(f"Error: File '{args['file_path']}' does not exist.")
    if os.path.isdir(fp):
        return _text(f"Error: '{args['file_path']}' is a directory. Use list_directory.")
    try:
        with open(fp, "r", encoding="utf-8") as f:
            return _text(f.read())
    except UnicodeDecodeError:
        return _text(f"Error: '{args['file_path']}' is binary or unsupported encoding.")


def _tool_write_file(args: dict):
    fp = _safe_path(args["file_path"])
    content = args["content"]
    directory = os.path.dirname(fp)
    if directory and not os.path.exists(directory):
        os.makedirs(directory, exist_ok=True)

    # Atomic write: write to tmp then rename
    with tempfile.NamedTemporaryFile("w", dir=directory or ".", delete=False, encoding="utf-8") as tmp:
        tmp.write(content)
        tmp_path = tmp.name
    shutil.move(tmp_path, fp)
    return _text(f"Successfully wrote {len(content)} chars to {args['file_path']}")


def _tool_replace_text(args: dict):
    fp = _safe_path(args["file_path"])
    if not os.path.exists(fp):
        return _text(f"Error: File '{args['file_path']}' does not exist.")

    target = args["target_snippet"]
    replacement = args["replacement_snippet"]

    if target == replacement:
        return _text("Error: target and replacement are identical — no change made.")

    with open(fp, "r", encoding="utf-8") as f:
        content = f.read()

    start, end, matched = _find_fuzzy(content, target)
    if start is None:
        return _text(
            f"Error: Could not find target_snippet in '{args['file_path']}'.\n"
            f"First 80 chars of target: {target[:80]}"
        )

    new_content = content[:start] + replacement + content[end:]
    if new_content == content:
        return _text("Warning: replacement produced identical content — no change.")

    directory = os.path.dirname(fp)
    with tempfile.NamedTemporaryFile("w", dir=directory or ".", delete=False, encoding="utf-8") as tmp:
        tmp.write(new_content)
        tmp_path = tmp.name
    shutil.move(tmp_path, fp)

    return _text(
        f"Successfully updated {args['file_path']}\n"
        f"- {matched[:60]}…\n+ {replacement[:60]}…"
    )


def _tool_list_directory(args: dict):
    dp = _safe_path(args.get("directory_path", "."))
    if not os.path.exists(dp):
        return _text(f"Error: Directory does not exist.")
    if not os.path.isdir(dp):
        return _text(f"Error: Not a directory.")

    items = sorted(
        os.listdir(dp),
        key=lambda x: (not os.path.isdir(os.path.join(dp, x)), x.lower()),
    )
    lines = []
    for item in items:
        item_path = os.path.join(dp, item)
        if os.path.isdir(item_path):
            lines.append(f"📁 {item}/")
        else:
            sz = _format_size(os.path.getsize(item_path))
            lines.append(f"📄 {item}  ({sz})")
    return _text("\n".join(lines) if lines else "(Empty Directory)")


def _tool_search_files(args: dict):
    pattern_str = args["pattern"]
    directory = args.get("directory", ".")
    file_glob = args.get("file_pattern", "*")
    max_results = int(args.get("max_results", 50))
    dp = _safe_path(directory)

    if not os.path.isdir(dp):
        return _text(f"Error: '{directory}' is not a directory.")

    try:
        regex = re.compile(pattern_str, re.IGNORECASE)
    except re.error:
        regex = re.compile(re.escape(pattern_str), re.IGNORECASE)

    results: list[str] = []
    files_searched = 0

    for root, dirs, files in os.walk(dp):
        dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
        for fname in files:
            if not fnmatch.fnmatch(fname, file_glob):
                continue
            fpath = os.path.join(root, fname)
            files_searched += 1
            try:
                with open(fpath, "r", encoding="utf-8", errors="ignore") as fh:
                    for lineno, line in enumerate(fh, 1):
                        if regex.search(line):
                            rel = os.path.relpath(fpath, ROOT_DIR)
                            results.append(f"{rel}:{lineno}: {line.rstrip()}")
                            if len(results) >= max_results:
                                break
            except (OSError, UnicodeDecodeError):
                continue
            if len(results) >= max_results:
                break
        if len(results) >= max_results:
            break

    if not results:
        return _text(f"No matches for '{pattern_str}' in {files_searched} files.")

    header = f"Found {len(results)} matches in {files_searched} files:\n\n"
    return _text(header + "\n".join(results))


def _tool_get_file_structure(args: dict):
    dp = _safe_path(args.get("directory_path", "."))
    max_depth = int(args.get("max_depth", 3))
    sizes = bool(args.get("include_sizes", False))

    if not os.path.isdir(dp):
        return _text(f"Error: Not a directory.")

    lines = [f"📁 {args.get('directory_path', '.')}/"]
    lines.extend(_build_tree(dp, "", 0, max_depth, sizes))
    return _text("\n".join(lines))


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

async def run():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options(),
        )
