from __future__ import annotations

import os
import shutil
import uuid
from pathlib import Path

from pydantic import Field

from agency_swarm.tools.base_tool import BaseTool
from agency_swarm.tools.utils import _resolve_mime_type


def _path_entry_exists(path_value: Path) -> bool:
    return os.path.lexists(path_value)


def _get_mnt_dir() -> Path:
    return Path(os.getenv("MNT_DIR", "/app/mnt")).expanduser().resolve()


def _get_max_bytes() -> int:
    raw_value = os.getenv("FILE_PREVIEW_MAX_BYTES", "104857600")
    try:
        return max(int(raw_value), 0)
    except ValueError:
        return 104857600


def _resolve_path(path_value: str) -> Path:
    candidate = Path(path_value).expanduser()
    if not candidate.is_absolute():
        candidate = Path.cwd() / candidate
    # Keep symlink paths intact; abspath normalizes to absolute without dereferencing links.
    return Path(os.path.abspath(candidate))


def _sanitize_anchor(anchor: str) -> str:
    cleaned = anchor.strip("\\/").replace(":", "")
    if not cleaned:
        return "abs"
    return cleaned.replace("\\", "_").replace("/", "_")


def _build_mnt_destination(resolved_path: Path, mnt_dir: Path) -> Path:
    cwd = Path.cwd().resolve()
    try:
        relative_path = resolved_path.relative_to(cwd)
        return mnt_dir / relative_path
    except ValueError:
        anchor_label = _sanitize_anchor(resolved_path.anchor)
        return mnt_dir / anchor_label / Path(*resolved_path.parts[1:])


def _mime_type_for_path(path_value: Path) -> str:
    try:
        return _resolve_mime_type(path_value)
    except ValueError:
        return "application/octet-stream"


def _move_with_overwrite_safety(source_path: Path, destination: Path) -> Path:
    if source_path.is_symlink():
        return _move_symlink_with_overwrite_safety(source_path, destination)

    if not _path_entry_exists(destination):
        return Path(shutil.move(str(source_path), str(destination)))

    staging_name = f".{destination.name}.incoming-{uuid.uuid4().hex}"
    staging_path = destination.with_name(staging_name)
    moved_to_staging = Path(shutil.move(str(source_path), str(staging_path)))
    try:
        moved_to_staging.replace(destination)
    except Exception:
        # Best-effort rollback keeps destination data safe and restores source when possible.
        if moved_to_staging.exists() and not source_path.exists():
            try:
                shutil.move(str(moved_to_staging), str(source_path))
            except Exception:
                pass
        raise

    return destination


def _resolve_relocated_symlink_target(source_path: Path, destination: Path) -> str:
    link_target = os.readlink(source_path)
    if os.path.isabs(link_target):
        return link_target

    absolute_target = (source_path.parent / link_target).resolve()
    return os.path.relpath(str(absolute_target), start=str(destination.parent))


def _move_symlink_with_overwrite_safety(source_path: Path, destination: Path) -> Path:
    link_target = _resolve_relocated_symlink_target(source_path, destination)

    if not _path_entry_exists(destination):
        destination.symlink_to(link_target)
        source_path.unlink()
        return destination

    staging_name = f".{destination.name}.incoming-{uuid.uuid4().hex}"
    staging_path = destination.with_name(staging_name)
    try:
        staging_path.symlink_to(link_target)
        staging_path.replace(destination)
    except Exception:
        if staging_path.exists():
            staging_path.unlink()
        raise

    source_path.unlink()
    return destination


class PresentFiles(BaseTool):  # type: ignore[metaclass]
    """
    Returns metadata for local files and ensures they persist by moving them into the MNT directory.
    Use this tool when you want to show a local file to the user in the chat UI.
    If multiple inputs resolve to one MNT destination path, `overwrite=True` keeps the latest file.
    """

    files: list[str] = Field(..., description="List of file paths to present as previews.")
    overwrite: bool | None = Field(
        True,
        description="Whether to overwrite an existing file in the MNT destination when path collisions occur.",
    )

    def run(self):
        """
        Returns a dict containing file metadata and any errors encountered.
        """
        if not self.files:
            return {"files": [], "errors": ["No files provided."]}

        mnt_dir = _get_mnt_dir()
        max_bytes = _get_max_bytes()
        results: list[dict[str, object]] = []
        errors: list[str] = []

        for file_path in self.files:
            try:
                resolved_path = _resolve_path(file_path)
            except Exception as exc:
                errors.append(f"Unable to resolve path '{file_path}': {exc}")
                continue

            if not resolved_path.exists():
                errors.append(f"File not found: {resolved_path}")
                continue

            if resolved_path.is_dir():
                errors.append(f"Path is a directory, not a file: {resolved_path}")
                continue

            try:
                file_size = resolved_path.stat().st_size
            except Exception as exc:
                errors.append(f"Unable to read file size for {resolved_path}: {exc}")
                continue

            too_large = bool(max_bytes and file_size > max_bytes)
            try:
                if resolved_path.is_relative_to(mnt_dir):
                    final_path = resolved_path
                else:
                    mnt_dir.mkdir(parents=True, exist_ok=True)
                    destination = _build_mnt_destination(resolved_path, mnt_dir)
                    destination.parent.mkdir(parents=True, exist_ok=True)
                    should_overwrite = True if self.overwrite is None else self.overwrite
                    if _path_entry_exists(destination):
                        if destination.is_dir():
                            errors.append(f"Unable to overwrite directory in MNT: {destination}. Remove it and retry.")
                            continue
                        if not should_overwrite:
                            errors.append(f"Destination already exists and overwrite is disabled: {destination}.")
                            continue
                    final_path = _move_with_overwrite_safety(resolved_path, destination)
            except Exception as exc:
                errors.append(f"Unable to move file to MNT directory for {resolved_path}: {exc}")
                continue

            if too_large:
                errors.append(f"File size exceeds limit ({file_size} bytes > {max_bytes} bytes): {final_path}")
                continue

            results.append(
                {
                    "name": final_path.name,
                    "mime_type": _mime_type_for_path(final_path),
                    "path": final_path.as_posix(),
                    "size_bytes": file_size,
                }
            )

        return {"files": results, "errors": errors}
