from fastmcp import FastMCP
from pathlib import Path
import shutil
from datetime import datetime

# Directorio base seguro
SANDBOX_DIR = Path("./sandbox").resolve()
SANDBOX_DIR.mkdir(parents=True, exist_ok=True)

def safe_path(requested_path: str) -> Path:
    """Siempre interpreta la ruta como relativa al SANDBOX_DIR, incluso si empieza con '/'."""
    # Normaliza la ruta quitando el primer '/'
    normalized = requested_path.strip().lstrip("/")
    full_path = (SANDBOX_DIR / normalized).resolve()

    if not full_path.is_relative_to(SANDBOX_DIR):
        raise ValueError("Ruta fuera del directorio permitido.")
    return full_path

mcp = FastMCP()

@mcp.tool(description="Lee y devuelve el contenido de un archivo de texto ubicado en el sistema de archivos seguro. El archivo debe estar dentro del sandbox.")
def read_file(path: str) -> str:
    try:
        file_path = safe_path(path)
        if not file_path.is_file():
            raise FileNotFoundError(f"Archivo '{path}' no encontrado.")
        return file_path.read_text(encoding="utf-8")
    except Exception as e:
        return f"⚠️ Error al leer archivo '{path}': {str(e)}"
    

@mcp.tool(description="Escribe contenido de texto en un archivo dentro del sandbox. Si el archivo ya existe, será sobrescrito.")
def write_file(path: str, content: str) -> str:
    file_path = safe_path(path)
    file_path.parent.mkdir(parents=True, exist_ok=True)
    file_path.write_text(content, encoding="utf-8")
    return "Archivo guardado correctamente."

@mcp.tool(description="Elimina de forma segura un archivo ubicado dentro del sandbox.")
def delete_file(path: str) -> str:
    file_path = safe_path(path)
    if not file_path.is_file():
        raise FileNotFoundError("Archivo no encontrado.")
    file_path.unlink()
    return "Archivo eliminado."

@mcp.tool(description="Crea una carpeta (y sus carpetas padre si es necesario) dentro del sandbox.")
def create_folder(path: str) -> str:
    folder_path = safe_path(path)
    folder_path.mkdir(parents=True, exist_ok=True)
    return "Carpeta creada."



@mcp.tool(description="Lista archivos y carpetas dentro de una ruta del sandbox.")
def list_directory(path: str = ".") -> list[str]:
    folder = safe_path(path)
    if not folder.is_dir():
        raise NotADirectoryError("Ruta no corresponde a una carpeta.")
    return sorted(str(p.relative_to(SANDBOX_DIR)) for p in folder.iterdir())

@mcp.tool(description="Muestra la estructura de carpetas y archivos como un árbol, desde una ruta dentro del sandbox.")
def tree(path: str = ".", depth: int = 3) -> str:
    base = safe_path(path)
    if not base.is_dir():
        raise NotADirectoryError("Ruta no corresponde a una carpeta.")

    tree_output = []

    def walk(dir_path: Path, prefix: str = "", level: int = 0):
        if level > depth:
            return
        entries = sorted(dir_path.iterdir())
        for i, entry in enumerate(entries):
            connector = "└── " if i == len(entries) - 1 else "├── "
            tree_output.append(f"{prefix}{connector}{entry.name}")
            if entry.is_dir():
                extension = "    " if i == len(entries) - 1 else "│   "
                walk(entry, prefix + extension, level + 1)

    tree_output.append(f"{base.name}/")
    walk(base)
    return "\n".join(tree_output)

@mcp.tool(description="Devuelve información detallada sobre un archivo: tamaño en bytes, fecha de modificación y tipo.")
def file_info(path: str) -> dict:
    fpath = safe_path(path)
    if not fpath.exists():
        raise FileNotFoundError("Archivo no encontrado.")
    return {
        "nombre": fpath.name,
        "tipo": "carpeta" if fpath.is_dir() else "archivo",
        "tamaño_bytes": fpath.stat().st_size,
        "última_modificación": datetime.fromtimestamp(fpath.stat().st_mtime).isoformat(),
        "relativo": str(fpath.relative_to(SANDBOX_DIR))
    }

@mcp.tool(description="Copia un archivo o carpeta dentro del sandbox a otra ruta.")
def copy_file(src: str, dest: str) -> str:
    src_path = safe_path(src)
    dest_path = safe_path(dest)
    if src_path.is_dir():
        shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
    else:
        dest_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src_path, dest_path)
    return "Copia completada."

@mcp.tool(description="Mueve o renombra un archivo o carpeta dentro del sandbox.")
def move_file(src: str, dest: str) -> str:
    src_path = safe_path(src)
    dest_path = safe_path(dest)
    dest_path.parent.mkdir(parents=True, exist_ok=True)
    src_path.rename(dest_path)
    return "Movimiento completado."

@mcp.tool(description="Elimina todos los archivos y subcarpetas dentro de una carpeta del sandbox.")
def clear_folder(path: str) -> str:
    folder_path = safe_path(path)
    if not folder_path.is_dir():
        raise NotADirectoryError("La ruta no es una carpeta.")
    for item in folder_path.iterdir():
        if item.is_file() or item.is_symlink():
            item.unlink()
        elif item.is_dir():
            shutil.rmtree(item)
    return "Carpeta vaciada."


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/fs")

