"""Edit tool — find-and-replace text in files."""

from __future__ import annotations

import difflib
import os
from typing import Any

from pi_agents.agent.types import AgentTool, AgentToolResult
from pi_agents.ai.types import TextContent


EDIT_TOOL_NAME = "edit"
EDIT_TOOL_DESCRIPTION = "Edit a file by replacing exact text. The old_text must match exactly."

EDIT_TOOL_PARAMETERS = {
    "type": "object",
    "properties": {
        "path": {"type": "string", "description": "File path to edit"},
        "oldText": {"type": "string", "description": "Exact text to find"},
        "newText": {"type": "string", "description": "Replacement text"},
    },
    "required": ["path", "oldText", "newText"],
}


def _normalize_line_endings(text: str) -> tuple[str, str]:
    """Normalize line endings and return (normalized, original_ending)."""
    if "\r\n" in text:
        return text.replace("\r\n", "\n"), "\r\n"
    if "\r" in text:
        return text.replace("\r", "\n"), "\r"
    return text, "\n"


def _make_diff(old_text: str, new_text: str, path: str) -> str:
    """Generate unified diff."""
    old_lines = old_text.splitlines(keepends=True)
    new_lines = new_text.splitlines(keepends=True)
    diff = difflib.unified_diff(old_lines, new_lines, fromfile=f"a/{path}", tofile=f"b/{path}")
    return "".join(diff)


def create_edit_tool(cwd: str | None = None) -> AgentTool:
    """Create an edit tool for find-and-replace operations."""

    class EditTool(AgentTool):
        def __init__(self) -> None:
            super().__init__(
                name=EDIT_TOOL_NAME,
                description=EDIT_TOOL_DESCRIPTION,
                parameters=EDIT_TOOL_PARAMETERS,
            )
            self.label = "Edit"

        async def execute(
            self,
            tool_call_id: str,
            params: dict[str, Any],
            signal: Any = None,
            on_update: Any = None,
        ) -> AgentToolResult:
            path = params.get("path", "")
            old_text = params.get("oldText", "")
            new_text = params.get("newText", "")

            if cwd and not os.path.isabs(path):
                path = os.path.join(cwd, path)

            if not os.path.exists(path):
                return AgentToolResult(
                    content=[TextContent(type="text", text=f"File not found: {path}")]
                )

            try:
                with open(path, "r", encoding="utf-8", errors="replace") as f:
                    original = f.read()
            except Exception as e:
                return AgentToolResult(
                    content=[TextContent(type="text", text=f"Error reading file: {e}")]
                )

            # Strip BOM
            content = original.lstrip("\ufeff")
            normalized, line_ending = _normalize_line_endings(content)
            norm_old, _ = _normalize_line_endings(old_text)

            # Count occurrences
            count = normalized.count(norm_old)
            if count == 0:
                return AgentToolResult(
                    content=[TextContent(type="text", text=f"Text not found in {path}. Ensure old_text matches exactly.")]
                )
            if count > 1:
                return AgentToolResult(
                    content=[TextContent(type="text", text=f"Found {count} occurrences of old_text in {path}. Provide more context to make the match unique.")]
                )

            # Replace
            norm_new, _ = _normalize_line_endings(new_text)
            new_content = normalized.replace(norm_old, norm_new, 1)

            # Restore original line endings
            if line_ending != "\n":
                new_content = new_content.replace("\n", line_ending)

            # Write
            try:
                with open(path, "w", encoding="utf-8") as f:
                    f.write(new_content)
            except Exception as e:
                return AgentToolResult(
                    content=[TextContent(type="text", text=f"Error writing file: {e}")]
                )

            # Generate diff
            diff = _make_diff(content, new_content.replace(line_ending, "\n") if line_ending != "\n" else new_content, path)

            # Find first changed line
            first_changed = None
            pos = normalized.find(norm_old)
            if pos >= 0:
                first_changed = normalized[:pos].count("\n") + 1

            return AgentToolResult(
                content=[TextContent(type="text", text=diff or f"Applied edit to {path}")],
                details={"diff": diff, "firstChangedLine": first_changed},
            )

    return EditTool()
