from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any

from ..schemas import ToolDefinition
from ..util import ensure_dir, utcnow_iso
from .base import object_schema


class ProjectUpgradeError(RuntimeError):
    pass


def _project_root(context) -> Path:
    runtime = getattr(context, "runtime", None)
    if runtime is not None:
        manager = getattr(runtime, "config_manager", None)
        if manager is not None and hasattr(manager, "get"):
            root = manager.get("project_root")
            if root:
                return Path(root).expanduser().resolve()
    config = getattr(context, "config", {}) or {}
    root = config.get("project_root", ".")
    return Path(root).expanduser().resolve()


def _backups_root(context) -> Path:
    runtime = getattr(context, "runtime", None)
    if runtime is not None:
        manager = getattr(runtime, "config_manager", None)
        if manager is not None and hasattr(manager, "get"):
            target = manager.get("backups_dir")
            if target:
                return ensure_dir(target)
    return ensure_dir(_project_root(context) / "backups")


def resolve_project_path(context, raw_path: str) -> Path:
    root = _project_root(context)
    candidate = Path(raw_path).expanduser()
    if not candidate.is_absolute():
        candidate = (root / candidate).resolve()
    else:
        candidate = candidate.resolve()
    if candidate == root or candidate.is_relative_to(root):
        return candidate
    raise ProjectUpgradeError(f"Path escapes project root: {candidate}")


def backup_project_path(context, path: Path, reason: str) -> Path | None:
    if not path.exists():
        return None
    backups_root = _backups_root(context)
    timestamp = utcnow_iso().replace(":", "-")
    target = backups_root / f"{reason}-{path.name}-{timestamp}"
    if path.is_dir():
        shutil.copytree(path, target)
    else:
        shutil.copy2(path, target)
    return target


def _normalise_file_specs(arguments: dict[str, Any]) -> list[dict[str, Any]]:
    items = arguments.get("files") or []
    if not isinstance(items, list) or not items:
        raise ProjectUpgradeError("files must be a non-empty list")
    normalised: list[dict[str, Any]] = []
    for item in items:
        if not isinstance(item, dict):
            raise ProjectUpgradeError("each file entry must be an object")
        path = item.get("path")
        if not path:
            raise ProjectUpgradeError("each file entry requires path")
        normalised.append(
            {
                "path": str(path),
                "content": str(item.get("content", "")),
                "overwrite": bool(item.get("overwrite", True)),
            }
        )
    return normalised


async def read_project_bundle_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    paths = arguments.get("paths") or []
    if not isinstance(paths, list) or not paths:
        raise ProjectUpgradeError("paths must be a non-empty list")
    max_chars_each = int(arguments.get("max_chars_each", 200000))
    files = []
    for raw_path in paths:
        path = resolve_project_path(context, str(raw_path))
        content = path.read_text(encoding="utf-8")
        if len(content) > max_chars_each:
            content = content[:max_chars_each]
        files.append({"path": str(path), "content": content, "bytes": path.stat().st_size})
    return {"files": files}


async def write_project_bundle_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    entries = _normalise_file_specs(arguments)
    written = []
    for entry in entries:
        path = resolve_project_path(context, entry["path"])
        if path.exists() and not entry["overwrite"]:
            raise FileExistsError(f"File already exists: {path}")
        backup = backup_project_path(context, path, "project-bundle-write")
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(entry["content"], encoding="utf-8")
        written.append(
            {
                "path": str(path),
                "backup": str(backup) if backup else None,
                "bytes": path.stat().st_size,
            }
        )
    return {"written": written}


async def append_project_bundle_handler(context, arguments: dict[str, Any]) -> dict[str, Any]:
    entries = _normalise_file_specs(arguments)
    appended = []
    for entry in entries:
        path = resolve_project_path(context, entry["path"])
        backup = backup_project_path(context, path, "project-bundle-append")
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open("a", encoding="utf-8") as handle:
            handle.write(entry["content"])
        appended.append(
            {
                "path": str(path),
                "backup": str(backup) if backup else None,
                "bytes": path.stat().st_size,
            }
        )
    return {"appended": appended}


def build_tools() -> list[ToolDefinition]:
    return [
        ToolDefinition(
            name="project_read_bundle",
            description="Read multiple project files in one call. Paths must stay inside project root.",
            parameters=object_schema(
                {
                    "paths": {"type": "array", "items": {"type": "string"}},
                    "max_chars_each": {"type": "integer", "minimum": 1, "maximum": 500000},
                },
                required=["paths"],
            ),
            handler=read_project_bundle_handler,
        ),
        ToolDefinition(
            name="project_write_bundle",
            description="Create or overwrite multiple project files in one call. Paths must stay inside project root.",
            parameters=object_schema(
                {
                    "files": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "path": {"type": "string"},
                                "content": {"type": "string"},
                                "overwrite": {"type": "boolean"},
                            },
                            "required": ["path", "content"],
                            "additionalProperties": False,
                        },
                    }
                },
                required=["files"],
            ),
            handler=write_project_bundle_handler,
            destructive=True,
        ),
        ToolDefinition(
            name="project_append_bundle",
            description="Append content to multiple project files in one call. Paths must stay inside project root.",
            parameters=object_schema(
                {
                    "files": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "path": {"type": "string"},
                                "content": {"type": "string"},
                            },
                            "required": ["path", "content"],
                            "additionalProperties": False,
                        },
                    }
                },
                required=["files"],
            ),
            handler=append_project_bundle_handler,
            destructive=True,
        ),
    ]
