from __future__ import annotations

from pathlib import Path
from typing import ClassVar, final

import aiofiles
from pydantic import BaseModel, Field

from revibe.core.tools.base import (
    BaseTool,
    BaseToolConfig,
    BaseToolState,
    ToolError,
    ToolPermission,
)
from revibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
from revibe.core.types import ToolCallEvent, ToolResultEvent


class WriteFileArgs(BaseModel):
    path: str = Field(
        description=(
            "REQUIRED. File path to create or overwrite (relative to project root or absolute). "
            "Parent directories created automatically. Examples: 'src/utils.py', 'config.json', './new_file.txt'. "
            "Path must be within project directory. For partial edits, use search_replace instead."
        )
    )
    content: str = Field(
        description=(
            "REQUIRED. Complete UTF-8 file content. This replaces the entire file. "
            "For editing parts of a file, use search_replace instead. "
            "Content size limited by max_write_bytes (default ~64KB)."
        )
    )
    overwrite: bool = Field(
        default=False,
        description=(
            "Optional. Default: False. Must be True to overwrite existing files. "
            "If file exists and overwrite=False, operation fails to prevent accidental data loss. "
            "Always read existing files first with read_file before overwriting."
        ),
    )


class WriteFileResult(BaseModel):
    path: str
    bytes_written: int
    file_existed: bool
    content: str


class WriteFileConfig(BaseToolConfig):
    permission: ToolPermission = ToolPermission.ASK
    max_write_bytes: int = 64_000
    create_parent_dirs: bool = True


class WriteFileState(BaseToolState):
    recently_written_files: list[str] = Field(default_factory=list)


class WriteFile(
    BaseTool[WriteFileArgs, WriteFileResult, WriteFileConfig, WriteFileState],
    ToolUIData[WriteFileArgs, WriteFileResult],
):
    description: ClassVar[str] = (
        "Create new files or completely overwrite existing files with UTF-8 content. "
        "REQUIRED: 'path' (file path) and 'content' (full file content). "
        "OPTIONAL: 'overwrite' (default=False, must be True to replace existing files). "
        "Use for: new files, complete rewrites. For partial edits, use search_replace instead. "
        "Examples: write_file(path='new.py', content='...'), write_file(path='config.json', content='{}', overwrite=True)."
    )

    @classmethod
    def get_call_display(cls, event: ToolCallEvent) -> ToolCallDisplay:
        if not isinstance(event.args, WriteFileArgs):
            return ToolCallDisplay(summary="Write")

        path = Path(event.args.path).name
        summary = f"Write ({path})"
        return ToolCallDisplay(summary=summary, content=event.args.content)

    @classmethod
    def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
        if isinstance(event.result, WriteFileResult):
            action = "Overwritten" if event.result.file_existed else "Created"
            return ToolResultDisplay(
                success=True, message=f"{action} {Path(event.result.path).name}"
            )

        return ToolResultDisplay(success=True, message="File written")

    @classmethod
    def get_status_text(cls) -> str:
        return "Writing file"

    def check_allowlist_denylist(self, args: WriteFileArgs) -> ToolPermission | None:
        import fnmatch

        file_path = Path(args.path).expanduser()
        if not file_path.is_absolute():
            file_path = self.config.effective_workdir / file_path
        file_str = str(file_path)

        for pattern in self.config.denylist:
            if fnmatch.fnmatch(file_str, pattern):
                return ToolPermission.NEVER

        for pattern in self.config.allowlist:
            if fnmatch.fnmatch(file_str, pattern):
                return ToolPermission.ALWAYS

        return None

    @final
    async def run(self, args: WriteFileArgs) -> WriteFileResult:
        file_path, file_existed, content_bytes = self._prepare_and_validate_path(args)

        await self._write_file(args, file_path)

        BUFFER_SIZE = 10
        self.state.recently_written_files.append(str(file_path))
        if len(self.state.recently_written_files) > BUFFER_SIZE:
            self.state.recently_written_files.pop(0)

        return WriteFileResult(
            path=str(file_path),
            bytes_written=content_bytes,
            file_existed=file_existed,
            content=args.content,
        )

    def _prepare_and_validate_path(self, args: WriteFileArgs) -> tuple[Path, bool, int]:
        if not args.path.strip():
            raise ToolError("Path cannot be empty")

        content_bytes = len(args.content.encode("utf-8"))
        if content_bytes > self.config.max_write_bytes:
            raise ToolError(
                f"Content exceeds {self.config.max_write_bytes} bytes limit"
            )

        file_path = Path(args.path).expanduser()
        if not file_path.is_absolute():
            file_path = self.config.effective_workdir / file_path
        file_path = file_path.resolve()

        try:
            file_path.relative_to(self.config.effective_workdir.resolve())
        except ValueError:
            raise ToolError(f"Cannot write outside project directory: {file_path}")

        file_existed = file_path.exists()

        if file_existed and not args.overwrite:
            raise ToolError(
                f"File '{file_path}' exists. Set overwrite=True to replace."
            )

        if self.config.create_parent_dirs:
            file_path.parent.mkdir(parents=True, exist_ok=True)
        elif not file_path.parent.exists():
            raise ToolError(f"Parent directory does not exist: {file_path.parent}")

        return file_path, file_existed, content_bytes

    async def _write_file(self, args: WriteFileArgs, file_path: Path) -> None:
        try:
            async with aiofiles.open(file_path, mode="w", encoding="utf-8") as f:
                await f.write(args.content)
        except Exception as e:
            raise ToolError(f"Error writing {file_path}: {e}") from e
