"""
Core file CRUD operations for FileManager V2.

Provides:
- read(): Load files into memory
- write(): Create/overwrite files
- edit(): Search and replace with diff generation
- close(): Free memory
- Path resolution with redundant directory detection
"""

import os
import difflib
from pathlib import Path
from datetime import datetime
from typing import Optional, List, TYPE_CHECKING

from egregore.core.context_scaffolds.data_types import StateOperatorResult
from .models import FileBlock

if TYPE_CHECKING:
    from .scaffold import FileManagerScaffold


class FileOperations:
    """Core file CRUD operations."""

    def __init__(self, scaffold: 'FileManagerScaffold'):
        """
        Initialize file operations with scaffold reference.

        Args:
            scaffold: Parent FileManagerScaffold instance
        """
        self.scaffold = scaffold
        self.state = scaffold.state

    def read(self, path: str, lines: Optional[str] = None) -> StateOperatorResult:
        """
        Read file into memory.

        Args:
            path: File path (absolute or relative)
            lines: Optional line range (e.g., "1-50", "10-20")

        Returns:
            StateOperatorResult with success status
        """
        # Validate path
        resolved_path = self._resolve_path(path)

        if not os.path.exists(resolved_path):
            similar = self._find_similar_files(path)
            message = f"File not found: {path}"
            if similar:
                message += f"\nSimilar files: {', '.join(similar)}"
            return StateOperatorResult(
                success=False,
                message=message
            )

        # Check file limit
        if len(self.state.files) >= self.state.max_files_open:
            return StateOperatorResult(
                success=False,
                message=f"Max files open ({self.state.max_files_open}). Close some files first."
            )

        # Read content
        try:
            with open(resolved_path, 'r', encoding='utf-8') as f:
                if lines:
                    content = self._read_line_range(f, lines)
                else:
                    content = f.read()
        except Exception as e:
            return StateOperatorResult(
                success=False,
                message=f"Failed to read file: {str(e)}"
            )

        # Create FileBlock
        file_block = FileBlock(
            path=str(resolved_path),
            content=content,
            lines=len(content.splitlines()),
            tokens=self.scaffold.count_tokens(content),
            modified=False
        )

        # Set scaffold reference for error formatting
        file_block._scaffold = self.scaffold

        # Add to state
        self.state.files[str(resolved_path)] = file_block

        return StateOperatorResult(
            success=True,
            message=f"Opened {path} ({file_block.tokens} tokens)"
        )

    def write(self, path: str, content: str) -> StateOperatorResult:
        """
        Write content to file.

        Args:
            path: File path
            content: File content

        Returns:
            StateOperatorResult with success status
        """
        if not content:
            return StateOperatorResult(
                success=False,
                message="Content is required for write operation"
            )

        # Resolve path with redundant directory detection
        resolved_path = self._resolve_path(path)

        # Check file limit
        if str(resolved_path) not in self.state.files and len(self.state.files) >= self.state.max_files_open:
            return StateOperatorResult(
                success=False,
                message=f"Max files open ({self.state.max_files_open}). Close some files first."
            )

        # Write to filesystem
        try:
            resolved_path.parent.mkdir(parents=True, exist_ok=True)
            with open(resolved_path, 'w', encoding='utf-8') as f:
                f.write(content)
        except Exception as e:
            return StateOperatorResult(
                success=False,
                message=f"Failed to write file: {str(e)}"
            )

        # Create FileBlock
        file_block = FileBlock(
            path=str(resolved_path),
            content=content,
            lines=len(content.splitlines()),
            tokens=self.scaffold.count_tokens(content),
            modified=False
        )

        # Set scaffold reference for error formatting
        file_block._scaffold = self.scaffold

        # Run linting if plugins are enabled
        if self.scaffold.plugin_manager and self.scaffold.plugin_manager.enabled_plugins:
            lint_results = self.scaffold.plugin_manager.lint_file(
                str(resolved_path),
                content
            )
            file_block = file_block.model_copy(update={
                'lint_results': lint_results,
                'last_linted': datetime.now()
            })
            file_block._scaffold = self.scaffold

        # Add to state
        self.state.files[str(resolved_path)] = file_block

        return StateOperatorResult(
            success=True,
            message=f"Written to {path}"
        )

    def edit(self, path: str, old_text: str, new_text: str) -> StateOperatorResult:
        """
        Search and replace in file.

        Args:
            path: File path
            old_text: Text to find
            new_text: Text to replace with

        Returns:
            StateOperatorResult with diff output
        """
        resolved_path = self._resolve_path(path)

        if str(resolved_path) not in self.state.files:
            return StateOperatorResult(
                success=False,
                message=f"File not open: {path}. Use read() first."
            )

        file_block = self.state.files[str(resolved_path)]
        old_content = file_block.content

        # Perform replacement
        if old_text not in old_content:
            return StateOperatorResult(
                success=False,
                message=f"Text not found in {path}: {old_text[:50]}{'...' if len(old_text) > 50 else ''}"
            )

        new_content = old_content.replace(old_text, new_text)

        # Generate diff
        diff_output = self._generate_diff(old_content, new_content, path)

        # Update FileBlock using model_copy for immutability
        updated_block = file_block.model_copy(update={
            'content': new_content,
            'lines': len(new_content.splitlines()),
            'tokens': self.scaffold.count_tokens(new_content),
            'modified': True,
            'previous_content': old_content
        })

        # Set scaffold reference for error formatting
        updated_block._scaffold = self.scaffold

        # Run linting if plugins are enabled
        if self.scaffold.plugin_manager and self.scaffold.plugin_manager.enabled_plugins:
            lint_results = self.scaffold.plugin_manager.lint_file(
                str(resolved_path),
                new_content
            )
            updated_block = updated_block.model_copy(update={
                'lint_results': lint_results,
                'last_linted': datetime.now()
            })
            updated_block._scaffold = self.scaffold

        # Replace in state
        self.state.files[str(resolved_path)] = updated_block

        # Write to disk
        try:
            with open(resolved_path, 'w', encoding='utf-8') as f:
                f.write(new_content)
        except Exception as e:
            return StateOperatorResult(
                success=False,
                message=f"Failed to write changes: {str(e)}"
            )

        return StateOperatorResult(
            success=True,
            message=f"Updated {path}\n\n{diff_output}"
        )

    def close(self, path: str) -> StateOperatorResult:
        """
        Close file and free memory.

        Args:
            path: File path

        Returns:
            StateOperatorResult with success status
        """
        resolved_path = self._resolve_path(path)

        if str(resolved_path) not in self.state.files:
            return StateOperatorResult(
                success=False,
                message=f"File not open: {path}"
            )

        del self.state.files[str(resolved_path)]

        return StateOperatorResult(
            success=True,
            message=f"Closed {path}"
        )

    # =========================================================================
    # HELPER METHODS
    # =========================================================================

    def _generate_diff(self, old_content: str, new_content: str, path: str) -> str:
        """Generate diff output with line numbers."""
        if old_content == new_content:
            return ""

        old_lines = old_content.splitlines()
        new_lines = new_content.splitlines()

        matcher = difflib.SequenceMatcher(None, old_lines, new_lines)

        diff_lines = []
        added_lines = 0
        removed_lines = 0

        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == 'equal':
                # Show 2 context lines
                for i in range(i1, min(i1 + 2, i2)):
                    diff_lines.append(f" {i+1:3d} | {old_lines[i]}")
            elif tag == 'delete':
                for i in range(i1, i2):
                    diff_lines.append(f"-{i+1:3d} | {old_lines[i]}")
                    removed_lines += 1
            elif tag == 'insert':
                for j in range(j1, j2):
                    diff_lines.append(f"+{j+1:3d} | {new_lines[j]}")
                    added_lines += 1
            elif tag == 'replace':
                for i in range(i1, i2):
                    diff_lines.append(f"-{i+1:3d} | {old_lines[i]}")
                    removed_lines += 1
                for j in range(j1, j2):
                    diff_lines.append(f"+{j+1:3d} | {new_lines[j]}")
                    added_lines += 1

        summary = f"(+{added_lines} -{removed_lines})"
        return f"```diff\n{chr(10).join(diff_lines)}\n```\n{summary}"

    def _resolve_path(self, path: str) -> Path:
        """
        Resolve path with redundant directory detection.

        Examples:
            scripts/scripts/file.py -> scripts/file.py
        """
        if Path(path).is_absolute():
            return Path(path).resolve()

        base_dir = self.state.active_directory or self.state.working_dir or os.getcwd()
        base_path = Path(base_dir)

        # Check for redundant directory nesting (scripts/scripts/scripts)
        if self.state.working_dir:
            working_dir_name = Path(self.state.working_dir).name
            path_parts = Path(path).parts

            if path_parts and path_parts[0] == working_dir_name:
                # Strip redundant prefix
                remaining_parts = path_parts[1:] if len(path_parts) > 1 else (".",)
                normalized_path = Path(*remaining_parts)
            else:
                normalized_path = Path(path)
        else:
            normalized_path = Path(path)

        return (base_path / normalized_path).resolve()

    def _find_similar_files(self, path: str) -> List[str]:
        """Find similar files using fuzzy matching."""
        try:
            base_dir = self.state.active_directory or self.state.working_dir or os.getcwd()
            target_name = Path(path).name

            similar = []
            for root, dirs, files in os.walk(base_dir):
                for file in files:
                    # Simple similarity check (starts with same letters)
                    if file.lower().startswith(target_name[:3].lower()):
                        rel_path = Path(root) / file
                        similar.append(str(rel_path.relative_to(base_dir)))

                # Limit search depth
                if len(similar) >= 5:
                    break

            return similar[:5]
        except Exception:
            return []

    def _read_line_range(self, file_handle, line_range: str) -> str:
        """Read specific line range from file."""
        try:
            # Parse range "1-50" or "10-20"
            parts = line_range.split('-')
            if len(parts) != 2:
                # Invalid range, read all
                file_handle.seek(0)
                return file_handle.read()

            start = int(parts[0])
            end = int(parts[1])

            # Read all lines
            file_handle.seek(0)
            all_lines = file_handle.readlines()

            # Extract range (1-indexed)
            selected_lines = all_lines[max(0, start-1):end]

            return ''.join(selected_lines)
        except Exception:
            # On error, read all
            file_handle.seek(0)
            return file_handle.read()
