"""
File Operations Module

Provides file read, write, edit, and delete operations with safety checks.
"""

import difflib
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Optional


@dataclass
class FileEdit:
    """Represents a file edit operation"""

    old_text: str
    new_text: str
    start_line: Optional[int] = None  # 1-indexed
    end_line: Optional[int] = None  # 1-indexed


@dataclass
class FileOperationResult:
    """Result of a file operation"""

    success: bool
    message: str
    path: Optional[Path] = None
    backup_path: Optional[Path] = None
    diff: Optional[str] = None


class FileOperations:
    """
    File operations for groknroll agent

    Features:
    - Read files with encoding detection
    - Write new files with parent directory creation
    - Edit files with backup and diff
    - Delete files with backup option
    - Safety checks and validation
    """

    def __init__(self, project_path: Path, create_backups: bool = True):
        """
        Initialize file operations

        Args:
            project_path: Root project path
            create_backups: Whether to create backups before modifications
        """
        self.project_path = project_path.resolve()
        self.create_backups = create_backups
        self.backup_dir = self.project_path / ".groknroll" / "backups"

        if create_backups:
            self.backup_dir.mkdir(parents=True, exist_ok=True)

    def read_file(self, path: Path, encoding: str = "utf-8") -> FileOperationResult:
        """
        Read file contents

        Args:
            path: File path (relative to project or absolute)
            encoding: File encoding

        Returns:
            FileOperationResult with file contents in message
        """
        try:
            file_path = self._resolve_path(path)

            if not file_path.exists():
                return FileOperationResult(
                    success=False, message=f"File not found: {path}", path=file_path
                )

            if not file_path.is_file():
                return FileOperationResult(
                    success=False, message=f"Not a file: {path}", path=file_path
                )

            content = file_path.read_text(encoding=encoding)

            return FileOperationResult(success=True, message=content, path=file_path)

        except UnicodeDecodeError as e:
            return FileOperationResult(
                success=False,
                message=f"Encoding error: {e}. Try different encoding.",
                path=file_path if "file_path" in locals() else None,
            )
        except Exception as e:
            return FileOperationResult(
                success=False,
                message=f"Error reading file: {e}",
                path=file_path if "file_path" in locals() else None,
            )

    def write_file(
        self, path: Path, content: str, encoding: str = "utf-8", overwrite: bool = False
    ) -> FileOperationResult:
        """
        Write content to file

        Args:
            path: File path
            content: File content
            encoding: File encoding
            overwrite: Allow overwriting existing files

        Returns:
            FileOperationResult
        """
        try:
            file_path = self._resolve_path(path)

            # Check if file exists
            if file_path.exists() and not overwrite:
                return FileOperationResult(
                    success=False,
                    message=f"File already exists: {path}. Use overwrite=True to replace.",
                    path=file_path,
                )

            # Create backup if file exists
            backup_path = None
            if file_path.exists() and self.create_backups:
                backup_path = self._create_backup(file_path)

            # Create parent directories
            file_path.parent.mkdir(parents=True, exist_ok=True)

            # Write file
            file_path.write_text(content, encoding=encoding)

            return FileOperationResult(
                success=True,
                message=f"File written: {file_path.relative_to(self.project_path)}",
                path=file_path,
                backup_path=backup_path,
            )

        except Exception as e:
            return FileOperationResult(
                success=False,
                message=f"Error writing file: {e}",
                path=file_path if "file_path" in locals() else None,
            )

    def edit_file(
        self, path: Path, edits: list[FileEdit], encoding: str = "utf-8"
    ) -> FileOperationResult:
        """
        Edit file with multiple edits

        Args:
            path: File path
            edits: List of FileEdit operations
            encoding: File encoding

        Returns:
            FileOperationResult with diff
        """
        try:
            file_path = self._resolve_path(path)

            if not file_path.exists():
                return FileOperationResult(
                    success=False, message=f"File not found: {path}", path=file_path
                )

            # Read original content
            original_content = file_path.read_text(encoding=encoding)
            lines = original_content.splitlines(keepends=True)

            # Apply edits
            modified_content = original_content
            for edit in edits:
                if edit.start_line is not None and edit.end_line is not None:
                    # Line-based edit
                    modified_content = self._apply_line_edit(
                        modified_content,
                        edit.old_text,
                        edit.new_text,
                        edit.start_line,
                        edit.end_line,
                    )
                else:
                    # String replacement edit
                    if edit.old_text not in modified_content:
                        return FileOperationResult(
                            success=False,
                            message=f"Text not found in file: {edit.old_text[:50]}...",
                            path=file_path,
                        )
                    modified_content = modified_content.replace(edit.old_text, edit.new_text, 1)

            # Create diff
            diff = self._create_diff(original_content, modified_content, str(path))

            # Create backup
            backup_path = None
            if self.create_backups:
                backup_path = self._create_backup(file_path)

            # Write modified content
            file_path.write_text(modified_content, encoding=encoding)

            return FileOperationResult(
                success=True,
                message=f"File edited: {file_path.relative_to(self.project_path)}",
                path=file_path,
                backup_path=backup_path,
                diff=diff,
            )

        except Exception as e:
            return FileOperationResult(
                success=False,
                message=f"Error editing file: {e}",
                path=file_path if "file_path" in locals() else None,
            )

    def delete_file(self, path: Path, backup: bool = True) -> FileOperationResult:
        """
        Delete file

        Args:
            path: File path
            backup: Create backup before deleting

        Returns:
            FileOperationResult
        """
        try:
            file_path = self._resolve_path(path)

            if not file_path.exists():
                return FileOperationResult(
                    success=False, message=f"File not found: {path}", path=file_path
                )

            if not file_path.is_file():
                return FileOperationResult(
                    success=False, message=f"Not a file: {path}", path=file_path
                )

            # Create backup
            backup_path = None
            if backup and self.create_backups:
                backup_path = self._create_backup(file_path)

            # Delete file
            file_path.unlink()

            return FileOperationResult(
                success=True,
                message=f"File deleted: {file_path.relative_to(self.project_path)}",
                path=file_path,
                backup_path=backup_path,
            )

        except Exception as e:
            return FileOperationResult(
                success=False,
                message=f"Error deleting file: {e}",
                path=file_path if "file_path" in locals() else None,
            )

    def list_files(
        self, directory: Optional[Path] = None, pattern: str = "*", recursive: bool = False
    ) -> FileOperationResult:
        """
        List files in directory

        Args:
            directory: Directory path (defaults to project root)
            pattern: Glob pattern
            recursive: Recursive search

        Returns:
            FileOperationResult with list of files in message
        """
        try:
            dir_path = self._resolve_path(directory) if directory else self.project_path

            if not dir_path.exists():
                return FileOperationResult(
                    success=False, message=f"Directory not found: {directory}", path=dir_path
                )

            if not dir_path.is_dir():
                return FileOperationResult(
                    success=False, message=f"Not a directory: {directory}", path=dir_path
                )

            # List files
            if recursive:
                files = list(dir_path.rglob(pattern))
            else:
                files = list(dir_path.glob(pattern))

            # Filter to files only
            files = [f for f in files if f.is_file()]

            # Create relative paths
            relative_files = [str(f.relative_to(self.project_path)) for f in files]

            file_list = "\n".join(sorted(relative_files))

            return FileOperationResult(success=True, message=file_list, path=dir_path)

        except Exception as e:
            return FileOperationResult(
                success=False,
                message=f"Error listing files: {e}",
                path=dir_path if "dir_path" in locals() else None,
            )

    # =========================================================================
    # Helper Methods
    # =========================================================================

    def _resolve_path(self, path: Path) -> Path:
        """Resolve path relative to project root"""
        if path.is_absolute():
            return path.resolve()
        return (self.project_path / path).resolve()

    def _create_backup(self, file_path: Path) -> Path:
        """Create backup of file"""
        from datetime import datetime

        # Create backup filename with timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_name = f"{file_path.name}.{timestamp}.bak"
        backup_path = self.backup_dir / backup_name

        # Copy file
        shutil.copy2(file_path, backup_path)

        return backup_path

    def _create_diff(self, original: str, modified: str, filename: str) -> str:
        """Create unified diff between original and modified content"""
        original_lines = original.splitlines(keepends=True)
        modified_lines = modified.splitlines(keepends=True)

        diff = difflib.unified_diff(
            original_lines,
            modified_lines,
            fromfile=f"a/{filename}",
            tofile=f"b/{filename}",
            lineterm="",
        )

        return "".join(diff)

    def _apply_line_edit(
        self, content: str, old_text: str, new_text: str, start_line: int, end_line: int
    ) -> str:
        """Apply line-based edit to content"""
        lines = content.splitlines(keepends=True)

        # Validate line numbers (1-indexed)
        if start_line < 1 or end_line > len(lines):
            raise ValueError(f"Invalid line range: {start_line}-{end_line}")

        # Extract range (convert to 0-indexed)
        range_text = "".join(lines[start_line - 1 : end_line])

        # Verify old_text matches
        if old_text not in range_text:
            raise ValueError(f"Text not found in specified line range: {old_text[:50]}...")

        # Replace in range
        modified_range = range_text.replace(old_text, new_text, 1)

        # Reconstruct content
        result_lines = lines[: start_line - 1] + [modified_range] + lines[end_line:]

        return "".join(result_lines)

    def restore_backup(self, backup_path: Path) -> FileOperationResult:
        """Restore file from backup"""
        try:
            if not backup_path.exists():
                return FileOperationResult(
                    success=False, message=f"Backup not found: {backup_path}"
                )

            # Extract original filename from backup
            # Format: filename.ext.YYYYMMDD_HHMMSS.bak
            parts = backup_path.name.split(".")
            if len(parts) < 3:
                return FileOperationResult(
                    success=False, message=f"Invalid backup filename: {backup_path.name}"
                )

            original_name = ".".join(parts[:-2])  # Remove timestamp and .bak
            original_path = self.project_path / original_name

            # Restore file
            shutil.copy2(backup_path, original_path)

            return FileOperationResult(
                success=True,
                message=f"File restored from backup: {original_path.relative_to(self.project_path)}",
                path=original_path,
                backup_path=backup_path,
            )

        except Exception as e:
            return FileOperationResult(success=False, message=f"Error restoring backup: {e}")
