from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any

from ..util import ensure_dir, truncate_text, utcnow_iso


class FileAccessError(RuntimeError):
    pass


def resolve_path(context, raw_path: str) -> Path:
    project_root = Path(context.runtime.config_manager.get("project_root")).resolve()
    workspace_dir = Path(context.runtime.config_manager.get("workspace_dir")).resolve()
    data_dir = Path(context.runtime.config_manager.get("data_dir")).resolve()
    logs_dir = Path(context.runtime.config_manager.get("logs_dir")).resolve()
    backups_dir = Path(context.runtime.config_manager.get("backups_dir")).resolve()
    allowed_roots = [project_root, workspace_dir, data_dir, logs_dir, backups_dir]
    candidate = Path(raw_path).expanduser()
    if not candidate.is_absolute():
        candidate = (project_root / candidate).resolve()
    else:
        candidate = candidate.resolve()
    if context.session.security_mode == "god":
        return candidate
    if any(candidate == root or candidate.is_relative_to(root) for root in allowed_roots):
        return candidate
    raise FileAccessError(f"Path outside allowed roots for mode={context.session.security_mode}: {candidate}")


def backup_if_exists(context, path: Path, reason: str) -> Path | None:
    if not path.exists():
        return None
    backups_root = ensure_dir(context.runtime.config_manager.get("backups_dir"))
    timestamp = utcnow_iso().replace(":", "-")
    target = backups_root / f"{reason}-{path.name}-{timestamp}"
    if path.is_file():
        shutil.copy2(path, target)
    else:
        shutil.copytree(path, target)
    return target


async def read_file_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    path = resolve_path(context, arguments["path"])
    max_chars = int(arguments.get("max_chars", 12000))
    content = path.read_text(encoding="utf-8")
    return {"path": str(path), "content": truncate_text(content, max_chars)}


async def write_file_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    path = resolve_path(context, arguments["path"])
    overwrite = bool(arguments.get("overwrite", True))
    if path.exists() and not overwrite:
        raise FileExistsError(f"File already exists: {path}")
    backup = backup_if_exists(context, path, "write-file")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(str(arguments.get("content", "")), encoding="utf-8")
    return {"path": str(path), "backup": str(backup) if backup else None, "bytes": path.stat().st_size}


async def append_file_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    path = resolve_path(context, arguments["path"])
    path.parent.mkdir(parents=True, exist_ok=True)
    path.open("a", encoding="utf-8").write(str(arguments.get("content", "")))
    return {"path": str(path), "bytes": path.stat().st_size}


async def replace_in_file_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    path = resolve_path(context, arguments["path"])
    old = str(arguments["old"])
    new = str(arguments.get("new", ""))
    count = int(arguments.get("count", 0))
    content = path.read_text(encoding="utf-8")
    if old not in content:
        raise ValueError("Target text not found")
    backup = backup_if_exists(context, path, "replace-in-file")
    updated = content.replace(old, new, count) if count > 0 else content.replace(old, new)
    path.write_text(updated, encoding="utf-8")
    replacements = content.count(old) if count <= 0 else min(content.count(old), count)
    return {"path": str(path), "backup": str(backup) if backup else None, "replacements": replacements}


async def list_dir_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    path = resolve_path(context, arguments.get("path", "."))
    entries = []
    for entry in sorted(path.iterdir(), key=lambda item: item.name):
        entries.append(
            {
                "name": entry.name,
                "path": str(entry),
                "type": "dir" if entry.is_dir() else "file",
                "size": entry.stat().st_size if entry.is_file() else None,
            }
        )
    return {"path": str(path), "entries": entries}


async def make_dir_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    path = resolve_path(context, arguments["path"])
    path.mkdir(parents=True, exist_ok=True)
    return {"path": str(path), "created": True}


async def delete_path_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    path = resolve_path(context, arguments["path"])
    recursive = bool(arguments.get("recursive", False))
    backup = backup_if_exists(context, path, "delete-path")
    if path.is_dir():
        if not recursive:
            path.rmdir()
        else:
            shutil.rmtree(path)
    elif path.exists():
        path.unlink()
    return {"path": str(path), "backup": str(backup) if backup else None, "deleted": True}
