"""
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, List, Optional, Tuple, Dict
import math
import json
from difflib import SequenceMatcher

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",
}

CHARS_PER_TOKEN = 2.5
DEFAULT_MAX_TOKENS = 8000

# ---------------------------------------------------------------------------
# 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"


# ---------------------------------------------------------------------------
# Summarization Helpers (ported from StorageToolkit)
# ---------------------------------------------------------------------------

def _summarize_python_file(content: str, max_chars: int, focus_keywords: list,
                           file_path: str, line_count: int, estimated_tokens: int) -> str:
    lines = content.split('\n')
    parts: List[str] = []
    parts.append(f"# === FILE SUMMARY: {file_path} ===")
    parts.append(f"# Total lines: {line_count} | Est. tokens: {estimated_tokens} | Pruned")
    parts.append("")

    imports = [l for l in lines[:100] if l.strip().startswith(('import ', 'from '))]
    if imports:
        parts.append("# --- IMPORTS ---")
        parts.extend(imports[:30])
        if len(imports) > 30:
            parts.append(f"# ... and {len(imports) - 30} more imports")
        parts.append("")

    class_pat = re.compile(r'^class\s+(\w+)(?:\([^)]*\))?:', re.MULTILINE)
    func_pat = re.compile(r'^(\s*)(?:async\s+)?def\s+(\w+)\s*\([^)]*\)(?:\s*->\s*[^:]+)?:', re.MULTILINE)

    classes = []
    for m in class_pat.finditer(content):
        name = m.group(1)
        ln = content[:m.start()].count('\n') + 1
        doc = ""
        after = content[m.end():m.end()+500]
        dm = re.search(r'(?:"""|\'\'\')(.*?)(?:"""|\'\'\')' , after, re.DOTALL)
        if dm:
            doc = dm.group(1).strip()[:200]
        classes.append((name, ln, doc))

    if classes:
        parts.append("# --- CLASSES ---")
        for name, ln, doc in classes:
            parts.append(f"class {name}:  # Line {ln}")
            if doc:
                parts.append(f'    """{doc}..."""')
        parts.append("")

    functions = []
    for m in func_pat.finditer(content):
        indent = m.group(1)
        fname = m.group(2)
        ln = content[:m.start()].count('\n') + 1
        sig_end = content.find(':', m.start()) + 1
        full_sig = content[m.start():sig_end].strip()
        focused = any(kw in fname.lower() for kw in focus_keywords)
        functions.append((indent, fname, ln, full_sig, focused))

    if functions:
        parts.append("# --- FUNCTIONS/METHODS ---")
        sorted_f = sorted(functions, key=lambda x: (not x[4], x[2]))
        shown = 0
        for indent, fname, ln, sig, focused in sorted_f:
            if shown >= 50 and not focused:
                continue
            pfx = "[FOCUS] " if focused else ""
            parts.append(f"{pfx}{sig}  # Line {ln}")
            shown += 1
        if len(functions) > 50:
            parts.append(f"# ... and {len(functions) - 50} more functions")
        parts.append("")

    if focus_keywords:
        parts.append("# --- FOCUSED CONTENT ---")
        for kw in focus_keywords:
            pat = re.compile(rf'^.*{re.escape(kw)}.*$', re.MULTILINE | re.IGNORECASE)
            matches = pat.findall(content)[:10]
            if matches:
                parts.append(f"# Matches for '{kw}':")
                for mt in matches:
                    parts.append(f"  {mt.strip()[:150]}")
        parts.append("")

    summary = '\n'.join(parts)
    if len(summary) > max_chars:
        summary = summary[:max_chars - 100] + "\n\n# ... [TRUNCATED - use read_file for specific sections]"
    return summary

def _summarize_js_file(content: str, max_chars: int, focus_keywords: list,
                       file_path: str, line_count: int, estimated_tokens: int) -> str:
    lines = content.split('\n')
    parts: List[str] = []
    parts.append(f"// === FILE SUMMARY: {file_path} ===")
    parts.append(f"// Total lines: {line_count} | Est. tokens: {estimated_tokens} | Pruned")
    parts.append("")

    imports = [l for l in lines[:100]
               if l.strip().startswith('import ') or ('require(' in l and l.strip().startswith(('const ', 'let ', 'var ')))]
    if imports:
        parts.append("// --- IMPORTS ---")
        parts.extend(imports[:30])
        if len(imports) > 30:
            parts.append(f"// ... and {len(imports) - 30} more imports")
        parts.append("")

    patterns = [
        (r'(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>', 'Arrow Function'),
        (r'(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)', 'Function'),
        (r'(?:export\s+)?class\s+(\w+)', 'Class'),
        (r'(?:export\s+)?const\s+(\w+)\s*:\s*React\.FC', 'React Component'),
    ]
    defs = []
    for pat, dtype in patterns:
        for m in re.finditer(pat, content, re.MULTILINE):
            defs.append((m.group(1), dtype, content[:m.start()].count('\n') + 1))
    if defs:
        parts.append("// --- DEFINITIONS ---")
        for name, dtype, ln in sorted(set(defs), key=lambda x: x[2]):
            parts.append(f"// [{dtype}] {name} - Line {ln}")
        parts.append("")

    export_pat = re.compile(r'export\s+(?:default\s+)?(\w+)', re.MULTILINE)
    exports = [m.group(1) for m in export_pat.finditer(content)]
    if exports:
        parts.append(f"// --- EXPORTS: {', '.join(set(exports)[:20])} ---")
        parts.append("")

    summary = '\n'.join(parts)
    if len(summary) > max_chars:
        summary = summary[:max_chars - 100] + "\n\n// ... [TRUNCATED]"
    return summary

def _summarize_xml_file(content: str, max_chars: int, focus_keywords: list,
                        file_path: str, line_count: int, estimated_tokens: int) -> str:
    parts: List[str] = []
    parts.append(f"<!-- === FILE SUMMARY: {file_path} === -->")
    parts.append(f"<!-- Total lines: {line_count} | Est. tokens: {estimated_tokens} | Pruned -->")
    parts.append("")

    ns_pat = re.compile(r'xmlns(?::\w+)?=["\']([^"\']+)["\']')
    namespaces = ns_pat.findall(content[:2000])
    if namespaces:
        parts.append("<!-- NAMESPACES -->")
        for ns in namespaces[:10]:
            parts.append(f"<!-- {ns} -->")
        parts.append("")

    bpmn_elements = [
        (r'<(?:\w+:)?process\s+id=["\']([^"\']+)["\']', 'Process'),
        (r'<(?:\w+:)?startEvent\s+id=["\']([^"\']+)["\']', 'StartEvent'),
        (r'<(?:\w+:)?endEvent\s+id=["\']([^"\']+)["\']', 'EndEvent'),
        (r'<(?:\w+:)?userTask\s+id=["\']([^"\']+)["\'](?:\s+name=["\']([^"\']+)["\'])?', 'UserTask'),
        (r'<(?:\w+:)?scriptTask\s+id=["\']([^"\']+)["\'](?:\s+name=["\']([^"\']+)["\'])?', 'ScriptTask'),
        (r'<(?:\w+:)?serviceTask\s+id=["\']([^"\']+)["\'](?:\s+name=["\']([^"\']+)["\'])?', 'ServiceTask'),
        (r'<(?:\w+:)?exclusiveGateway\s+id=["\']([^"\']+)["\']', 'ExclusiveGateway'),
        (r'<(?:\w+:)?parallelGateway\s+id=["\']([^"\']+)["\']', 'ParallelGateway'),
    ]
    elems = []
    for pat, etype in bpmn_elements:
        for m in re.finditer(pat, content, re.IGNORECASE):
            eid = m.group(1)
            ename = m.group(2) if len(m.groups()) > 1 and m.group(2) else ""
            ln = content[:m.start()].count('\n') + 1
            elems.append((etype, eid, ename, ln))
    if elems:
        parts.append("<!-- BPMN ELEMENTS -->")
        for etype, eid, ename, ln in sorted(elems, key=lambda x: x[3]):
            ns = f' name="{ename}"' if ename else ""
            parts.append(f'<!-- [{etype}] id="{eid}"{ns} (Line {ln}) -->')
        parts.append("")

    seq_pat = re.compile(r'<(?:\w+:)?sequenceFlow[^>]*sourceRef=["\']([^"\']+)["\'][^>]*targetRef=["\']([^"\']+)["\']', re.IGNORECASE)
    flows = [(m.group(1), m.group(2)) for m in seq_pat.finditer(content)]
    if flows:
        parts.append("<!-- SEQUENCE FLOWS -->")
        for src, tgt in flows[:30]:
            parts.append(f"<!-- {src} --> {tgt} -->")
        if len(flows) > 30:
            parts.append(f"<!-- ... and {len(flows) - 30} more flows -->")
        parts.append("")

    summary = '\n'.join(parts)
    if len(summary) > max_chars:
        summary = summary[:max_chars - 100] + "\n\n<!-- ... [TRUNCATED] -->"
    return summary

def _summarize_text_file(content: str, max_chars: int, focus_keywords: list,
                         file_path: str, line_count: int, estimated_tokens: int) -> str:
    parts: List[str] = []
    parts.append(f"# === FILE SUMMARY: {file_path} ===")
    parts.append(f"# Total lines: {line_count} | Est. tokens: {estimated_tokens} | Pruned")
    parts.append("")

    heading_pat = re.compile(r'^(#{1,6})\s+(.+)$', re.MULTILINE)
    headings = []
    for m in heading_pat.finditer(content):
        level = len(m.group(1))
        title = m.group(2).strip()
        ln = content[:m.start()].count('\n') + 1
        headings.append((level, title, ln))

    if headings:
        parts.append("# --- TABLE OF CONTENTS ---")
        for level, title, ln in headings:
            indent = "  " * (level - 1)
            parts.append(f"{indent}- {title} (Line {ln})")
        parts.append("")

    first_end = content.find('\n## ', 500)
    if first_end == -1:
        first_end = min(len(content), max_chars // 2)
    parts.append("# --- BEGINNING OF CONTENT ---")
    parts.append(content[:first_end].strip())
    parts.append("")

    if focus_keywords:
        parts.append("# --- FOCUSED SECTIONS ---")
        for kw in focus_keywords:
            for level, title, ln in headings:
                if kw.lower() in title.lower():
                    start = content.find(f"{'#' * level} {title}")
                    if start != -1:
                        nh = re.search(rf'^#{{1,{level}}}\s', content[start+1:], re.MULTILINE)
                        end = start + nh.start() + 1 if nh else start + 2000
                        parts.append(content[start:end][:1500])
                        parts.append("")

    summary = '\n'.join(parts)
    if len(summary) > max_chars:
        summary = summary[:max_chars - 100] + "\n\n# ... [TRUNCATED - use focus_sections for specific content]"
    return summary

def _summarize_generic_file(content: str, max_chars: int,
                            file_path: str, line_count: int, estimated_tokens: int) -> str:
    parts: List[str] = []
    parts.append(f"# === FILE SUMMARY: {file_path} ===")
    parts.append(f"# Total lines: {line_count} | Est. tokens: {estimated_tokens} | Pruned")
    parts.append(f"# Showing first ~{int(max_chars / CHARS_PER_TOKEN)} tokens")
    parts.append("")
    parts.append("# --- FILE CONTENT (BEGINNING) ---")
    parts.append(content[:max_chars - 500])
    if len(content) > max_chars - 500:
        parts.append("")
        parts.append("# ... [FILE TRUNCATED - use read_file for specific sections]")
    return '\n'.join(parts)

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

def _normalize_whitespace(text: str) -> str:
    text = text.replace('\r\n', '\n').replace('\r', '\n')
    text = text.replace('\t', '    ')
    lines = [line.rstrip() for line in text.split('\n')]
    return '\n'.join(lines)

def _strategy_exact(content: str, target: str) -> Optional[Tuple[int, int, str, str]]:
    idx = content.find(target)
    if idx != -1:
        return (idx, idx + len(target), target, 'exactMatch')
    return None

def _strategy_trimmed(content: str, target: str) -> Optional[Tuple[int, int, str, str]]:
    trimmed = target.strip()
    if not trimmed:
        return None
    idx = content.find(trimmed)
    if idx != -1:
        return (idx, idx + len(trimmed), content[idx:idx + len(trimmed)], 'trimmedMatch')
    return None

def _strategy_normalized(content: str, target: str) -> Optional[Tuple[int, int, str, str]]:
    norm_content = _normalize_whitespace(content)
    norm_target = _normalize_whitespace(target)
    if not norm_target.strip():
        return None
    if norm_target not in norm_content:
        return None
    content_lines = content.split('\n')
    norm_content_lines = norm_content.split('\n')
    norm_target_lines = norm_target.split('\n')
    n_target = len(norm_target_lines)
    for i in range(len(norm_content_lines) - n_target + 1):
        if norm_content_lines[i:i + n_target] == norm_target_lines:
            matched_lines = content_lines[i:i + n_target]
            matched_text = '\n'.join(matched_lines)
            start_pos = sum(len(l) + 1 for l in content_lines[:i])
            end_pos = start_pos + len(matched_text)
            return (start_pos, end_pos, matched_text, 'normalizedMatch')
    return None

def _strategy_case_insensitive(content: str, target: str) -> Optional[Tuple[int, int, str, str]]:
    lower_content = content.lower()
    lower_target = target.strip().lower()
    if not lower_target:
        return None
    idx = lower_content.find(lower_target)
    if idx != -1:
        end = idx + len(lower_target)
        return (idx, end, content[idx:end], 'caseInsensitiveMatch')
    return None

def _strategy_line_by_line_flexible(content: str, target: str) -> Optional[Tuple[int, int, str, str]]:
    content_lines = content.split('\n')
    target_lines = target.split('\n')
    while target_lines and not target_lines[0].strip():
        target_lines = target_lines[1:]
    while target_lines and not target_lines[-1].strip():
        target_lines = target_lines[:-1]
    if not target_lines:
        return None
    n = len(target_lines)
    stripped_target = [l.strip() for l in target_lines]
    for i in range(len(content_lines) - n + 1):
        chunk = content_lines[i:i + n]
        if [l.strip() for l in chunk] == stripped_target:
            matched_text = '\n'.join(chunk)
            start_pos = sum(len(l) + 1 for l in content_lines[:i])
            end_pos = start_pos + len(matched_text)
            return (start_pos, end_pos, matched_text, 'lineByLineFlexible')
    return None

def _strategy_leading_whitespace_flex(content: str, target: str) -> Optional[Tuple[int, int, str, str]]:
    content_lines = content.split('\n')
    target_lines = target.split('\n')
    while target_lines and not target_lines[0].strip():
        target_lines = target_lines[1:]
    while target_lines and not target_lines[-1].strip():
        target_lines = target_lines[:-1]
    if not target_lines:
        return None
    n = len(target_lines)
    for i in range(len(content_lines) - n + 1):
        chunk = content_lines[i:i + n]
        first_content_indent = len(chunk[0]) - len(chunk[0].lstrip())
        first_target_indent = len(target_lines[0]) - len(target_lines[0].lstrip())
        offset = first_content_indent - first_target_indent
        match = True
        for cl, tl in zip(chunk, target_lines):
            cl_stripped = cl.rstrip()
            tl_stripped = tl.rstrip()
            if offset >= 0:
                expected = ' ' * offset + tl_stripped
            else:
                if tl_stripped.startswith(' ' * (-offset)):
                    expected = tl_stripped[(-offset):]
                else:
                    match = False
                    break
            if cl_stripped != expected:
                match = False
                break
        if match:
            matched_text = '\n'.join(chunk)
            start_pos = sum(len(l) + 1 for l in content_lines[:i])
            end_pos = start_pos + len(matched_text)
            return (start_pos, end_pos, matched_text, 'leadingWhitespaceFlex')
    return None

def _strategy_whitespace_ignored(content: str, target: str) -> Optional[Tuple[int, int, str, str]]:
    stripped_content = re.sub(r'\s', '', content)
    stripped_target = re.sub(r'\s', '', target)
    if not stripped_target:
        return None
    stripped_idx = stripped_content.find(stripped_target)
    if stripped_idx == -1:
        return None
    orig_start = None
    orig_end = None
    stripped_count = 0
    for i, ch in enumerate(content):
        if not ch.isspace():
            if stripped_count == stripped_idx and orig_start is None:
                orig_start = i
            if stripped_count == stripped_idx + len(stripped_target) - 1:
                orig_end = i + 1
                break
            stripped_count += 1
        else:
            if orig_start is not None:
                orig_end = i + 1
    if orig_start is not None and orig_end is not None:
        matched_text = content[orig_start:orig_end]
        return (orig_start, orig_end, matched_text, 'whitespaceIgnoredMatch')
    return None

def _strategy_sliding_window_similarity(content: str, target: str,
                                         threshold: float = 0.82) -> Optional[Tuple[int, int, str, str]]:
    content_lines = content.split('\n')
    target_lines = target.split('\n')
    while target_lines and not target_lines[0].strip():
        target_lines = target_lines[1:]
    while target_lines and not target_lines[-1].strip():
        target_lines = target_lines[:-1]
    if not target_lines:
        return None
    target_text = '\n'.join(target_lines)
    n = len(target_lines)
    scale = 0.15
    min_len = max(1, math.floor(n * (1 - scale)))
    max_len = math.ceil(n * (1 + scale))
    best_ratio = 0.0
    best_start = -1
    best_length = 0
    for length in range(min_len, min(max_len + 1, len(content_lines) + 1)):
        for i in range(len(content_lines) - length + 1):
            chunk_text = '\n'.join(content_lines[i:i + length])
            ratio = SequenceMatcher(None, chunk_text, target_text).ratio()
            if ratio > best_ratio:
                best_ratio = ratio
                best_start = i
                best_length = length
    if best_ratio >= threshold and best_start >= 0:
        chunk = content_lines[best_start:best_start + best_length]
        matched_text = '\n'.join(chunk)
        start_pos = sum(len(l) + 1 for l in content_lines[:best_start])
        end_pos = start_pos + len(matched_text)
        return (start_pos, end_pos, matched_text, f'slidingWindowSimilarity({best_ratio:.2f})')
    return None

def _find_best_match(content: str, target: str) -> Tuple[Optional[int], Optional[int], Optional[str], Optional[str]]:
    """Run all matching strategies in cascade. Returns first hit."""
    strategies = [
        ('exactMatch',            _strategy_exact),
        ('trimmedMatch',          _strategy_trimmed),
        ('normalizedMatch',       _strategy_normalized),
        ('caseInsensitiveMatch',  _strategy_case_insensitive),
        ('lineByLineFlexible',    _strategy_line_by_line_flexible),
        ('leadingWhitespaceFlex', _strategy_leading_whitespace_flex),
        ('whitespaceIgnored',     _strategy_whitespace_ignored),
        ('slidingWindowSimilarity', _strategy_sliding_window_similarity),
    ]
    for name, fn in strategies:
        try:
            result = fn(content, target)
            if result is not None:
                return result
        except Exception:
            continue
    return (None, None, None, None)

def _find_similar_lines(content: str, target: str, threshold: float = 0.6) -> str:
    """Find the most similar chunk in the file for diagnostics."""
    content_lines = content.splitlines()
    target_lines = target.splitlines()
    if not target_lines:
        return ''
    best_ratio = 0.0
    best_idx = 0
    for i in range(len(content_lines) - len(target_lines) + 1):
        chunk = content_lines[i:i + len(target_lines)]
        ratio = SequenceMatcher(None, target_lines, chunk).ratio()
        if ratio > best_ratio:
            best_ratio = ratio
            best_idx = i
    if best_ratio < threshold:
        return ''
    ctx = 5
    start = max(0, best_idx - ctx)
    end = min(len(content_lines), best_idx + len(target_lines) + ctx)
    preview = content_lines[start:end]
    header = f"# Most similar block (similarity={best_ratio:.0%}, lines {start+1}-{end}):"
    return header + '\n' + '\n'.join(preview)


# ---------------------------------------------------------------------------
# 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 — read_file_summarized
        types.Tool(
            name="read_file_summarized",
            description="Read a large file from storage with context pruning to fit within token limits. Useful for large code files.",
            inputSchema={
                "type": "object",
                "properties": {
                    "file_path": {"type": "string", "description": "Relative path to the file."},
                    "max_tokens": {"type": "integer", "description": "Maximum tokens (default 8000).", "default": 8000},
                    "focus_sections": {"type": "string", "description": "Keywords to prioritize (comma-separated).", "default": ""},
                },
                "required": ["file_path"],
            },
        ),
        # 3 — 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"],
            },
        ),
        # 4 — 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"],
            },
        ),
        # 5 — batch_replace
        types.Tool(
            name="batch_replace",
            description="Replace multiple blocks of text in a file in one operation.",
            inputSchema={
                "type": "object",
                "properties": {
                    "file_path": {"type": "string", "description": "Relative path to the file."},
                    "replacements_json": {
                        "type": "string", 
                        "description": "JSON array of objects with 'target' and 'replacement' keys (e.g. [{\"target\": \"old\", \"replacement\": \"new\"}])."
                    },
                },
                "required": ["file_path", "replacements_json"],
            },
        ),
        # 6 — 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": [],
            },
        ),
        # 7 — 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"],
            },
        ),
        # 8 — 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": [],
            },
        ),
        # 9 — delete_file
        types.Tool(
            name="delete_file",
            description="Delete a single file.",
            inputSchema={
                "type": "object",
                "properties": {"file_path": {"type": "string"}},
                "required": ["file_path"],
            },
        ),
        # 10 — delete_directory
        types.Tool(
            name="delete_directory",
            description="Delete a directory and all files within it.",
            inputSchema={
                "type": "object",
                "properties": {"directory_path": {"type": "string"}},
                "required": ["directory_path"],
            },
        ),
    ]


# ---------------------------------------------------------------------------
# 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 == "read_file_summarized":
            return _tool_read_file_summarized(args)
        elif name == "write_file":
            return _tool_write_file(args)
        elif name == "replace_text":
            return _tool_replace_text(args)
        elif name == "batch_replace":
            return _tool_batch_replace(args)
        elif name == "delete_file":
            return _tool_delete_file(args)
        elif name == "delete_directory":
            return _tool_delete_directory(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_read_file_summarized(args: dict):
    fp = _safe_path(args["file_path"])
    max_tokens = int(args.get("max_tokens", DEFAULT_MAX_TOKENS))
    focus_sections = args.get("focus_sections", "")
    
    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.")
    
    try:
        with open(fp, "r", encoding="utf-8") as f:
            content = f.read()
    except UnicodeDecodeError:
        return _text(f"Error: '{args['file_path']}' is binary or unsupported.")

    file_size = len(content)
    line_count = content.count('\n') + 1
    estimated_tokens = int(file_size / CHARS_PER_TOKEN)
    max_chars = int(max_tokens * CHARS_PER_TOKEN)

    if file_size <= max_chars:
        return _text(content)

    focus_keywords = [s.strip().lower() for s in focus_sections.split(',') if s.strip()]
    file_ext = os.path.splitext(fp)[1].lower()

    if file_ext in ('.py',):
        summary = _summarize_python_file(content, max_chars, focus_keywords, args['file_path'], line_count, estimated_tokens)
    elif file_ext in ('.js', '.jsx', '.ts', '.tsx'):
        summary = _summarize_js_file(content, max_chars, focus_keywords, args['file_path'], line_count, estimated_tokens)
    elif file_ext in ('.bpmn', '.xml'):
        summary = _summarize_xml_file(content, max_chars, focus_keywords, args['file_path'], line_count, estimated_tokens)
    elif file_ext in ('.md', '.txt'):
        summary = _summarize_text_file(content, max_chars, focus_keywords, args['file_path'], line_count, estimated_tokens)
    else:
        summary = _summarize_generic_file(content, max_chars, args['file_path'], line_count, estimated_tokens)
        
    return _text(summary)


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, strategy = _find_best_match(content, target)
    if start is None:
        similar = _find_similar_lines(content, target)
        hint = f"\n\nDid you mean this block?\n{similar}" if similar else ""
        return _text(
            f"Error: Could not find target_snippet in '{args['file_path']}'.\n"
            f"All 8 matching strategies failed.{hint}"
        )

    remaining = content[end:]
    s2, _, _, _ = _find_best_match(remaining, target)
    if s2 is not None:
        return _text("Error: Target snippet matches multiple locations. Provide a more unique snippet.")

    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']} (strategy: {strategy})")


def _tool_batch_replace(args: dict):
    fp = _safe_path(args["file_path"])
    replacements_json = args["replacements_json"]
    
    if not os.path.exists(fp):
        return _text(f"Error: File '{args['file_path']}' does not exist.")
        
    try:
        replacements = json.loads(replacements_json)
        if not isinstance(replacements, list):
            return _text("Error: replacements_json must be a JSON array.")
    except json.JSONDecodeError as e:
        return _text(f"Error: Invalid JSON: {e}")
        
    with open(fp, "r", encoding="utf-8") as f:
        content = f.read()
        
    new_content = content
    report = []
    success_count = 0
    
    for i, rep in enumerate(replacements):
        target = rep.get('target')
        replacement = rep.get('replacement')
        if not target or replacement is None:
            report.append(f"Skipped #{i+1}: Missing target or replacement.")
            continue
            
        start_idx, end_idx, matched_text, strategy = _find_best_match(new_content, target)
        if start_idx is not None:
            remaining = new_content[end_idx:]
            s2, _, _, _ = _find_best_match(remaining, target)
            if s2 is not None:
                report.append(f"Failed #{i+1}: Target matches multiple locations.")
                continue
            
            new_content = new_content[:start_idx] + replacement + new_content[end_idx:]
            success_count += 1
            report.append(f"Replacement #{i+1}: Success ({strategy}).")
        else:
            similar = _find_similar_lines(new_content, target)
            hint = f" Did you mean:\n{similar}" if similar else ""
            report.append(f"Failed #{i+1}: Target not found.{hint}")
            
    if new_content == content:
        return _text("File is already up to date. No changes needed.\n" + "\n".join(report))
        
    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"Applied {success_count}/{len(replacements)} replacements to '{args['file_path']}'.\n" + "\n".join(report))

def _tool_delete_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 delete_directory.")
        
    try:
        os.remove(fp)
        return _text(f"Successfully deleted file '{args['file_path']}'.")
    except Exception as e:
        return _text(f"Error deleting file: {e}")

def _tool_delete_directory(args: dict):
    dp = _safe_path(args["directory_path"])
    if not os.path.exists(dp):
        return _text(f"Error: Directory '{args['directory_path']}' does not exist.")
    if not os.path.isdir(dp):
        return _text(f"Error: '{args['directory_path']}' is a file. Use delete_file.")
        
    try:
        shutil.rmtree(dp)
        return _text(f"Successfully deleted directory '{args['directory_path']}'.")
    except Exception as e:
        return _text(f"Error deleting directory: {e}")

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(),
        )
