"""Refactoring engine for safe code transformations.

This module provides refactoring operations that use AST analysis
to ensure safe and accurate code modifications.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Any

from ieum.ast.base import (
    Symbol,
    SymbolType,
    Reference,
    ParseResult,
    BaseASTParser,
    get_parser_for_file,
)


@dataclass
class FileEdit:
    """Represents an edit to be applied to a file."""

    file_path: str
    start_line: int
    start_column: int
    end_line: int
    end_column: int
    old_text: str
    new_text: str

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "file_path": self.file_path,
            "start_line": self.start_line,
            "start_column": self.start_column,
            "end_line": self.end_line,
            "end_column": self.end_column,
            "old_text": self.old_text,
            "new_text": self.new_text,
        }


@dataclass
class RefactorResult:
    """Result of a refactoring operation."""

    success: bool
    edits: list[FileEdit] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    affected_files: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "success": self.success,
            "edits": [e.to_dict() for e in self.edits],
            "errors": self.errors,
            "warnings": self.warnings,
            "affected_files": self.affected_files,
        }


class RefactorEngine:
    """Engine for safe code refactoring operations."""

    def __init__(self, project_path: str | Path | None = None):
        """Initialize the refactoring engine.

        Args:
            project_path: Root path of the project.
        """
        self.project_path = Path(project_path) if project_path else Path.cwd()
        self._parse_cache: dict[str, ParseResult] = {}

    def _get_parser(self, file_path: str | Path) -> Optional[BaseASTParser]:
        """Get appropriate parser for a file."""
        return get_parser_for_file(file_path)

    def _parse_file(self, file_path: str | Path) -> Optional[ParseResult]:
        """Parse a file with caching."""
        file_path = str(file_path)

        if file_path in self._parse_cache:
            return self._parse_cache[file_path]

        parser = self._get_parser(file_path)
        if not parser:
            return None

        result = parser.parse_file(file_path)
        if result.success:
            self._parse_cache[file_path] = result

        return result

    def clear_cache(self) -> None:
        """Clear the parse cache."""
        self._parse_cache.clear()

    def find_symbol(
        self,
        name: str,
        file_path: Optional[str] = None,
        symbol_type: Optional[SymbolType] = None,
    ) -> Optional[Symbol]:
        """Find a symbol by name.

        Args:
            name: Symbol name to find.
            file_path: Optional file to search in.
            symbol_type: Optional type filter.

        Returns:
            Found symbol or None.
        """
        files = [file_path] if file_path else self._get_project_files()

        for f in files:
            result = self._parse_file(f)
            if not result:
                continue

            for symbol in result.symbols:
                if symbol.name == name:
                    if symbol_type is None or symbol.type == symbol_type:
                        return symbol

        return None

    def find_references(
        self,
        symbol_name: str,
        file_path: Optional[str] = None,
    ) -> list[Reference]:
        """Find all references to a symbol.

        Args:
            symbol_name: Symbol name to find references for.
            file_path: Optional file to search in.

        Returns:
            List of references.
        """
        files = [file_path] if file_path else self._get_project_files()
        references = []

        for f in files:
            result = self._parse_file(f)
            if not result:
                continue

            for ref in result.references:
                if ref.symbol_name == symbol_name:
                    references.append(ref)

        return references

    def rename_symbol(
        self,
        old_name: str,
        new_name: str,
        file_path: Optional[str] = None,
        symbol_type: Optional[SymbolType] = None,
    ) -> RefactorResult:
        """Rename a symbol across files.

        Args:
            old_name: Current symbol name.
            new_name: New symbol name.
            file_path: Optional specific file.
            symbol_type: Optional type filter.

        Returns:
            RefactorResult with edits.
        """
        result = RefactorResult(success=False)

        # Validate new name
        if not self._is_valid_identifier(new_name):
            result.errors.append(f"Invalid identifier: {new_name}")
            return result

        # Find symbol definition
        symbol = self.find_symbol(old_name, file_path, symbol_type)
        if not symbol:
            result.errors.append(f"Symbol not found: {old_name}")
            return result

        # Find all references
        references = self.find_references(old_name, file_path)

        # Create edit for definition
        result.edits.append(FileEdit(
            file_path=symbol.file_path,
            start_line=symbol.range.start_line,
            start_column=symbol.range.start_column,
            end_line=symbol.range.start_line,
            end_column=symbol.range.start_column + len(old_name),
            old_text=old_name,
            new_text=new_name,
        ))
        result.affected_files.append(symbol.file_path)

        # Create edits for references
        seen_locations = {(symbol.file_path, symbol.range.start_line, symbol.range.start_column)}

        for ref in references:
            loc = (ref.file_path, ref.line, ref.column)
            if loc in seen_locations:
                continue
            seen_locations.add(loc)

            result.edits.append(FileEdit(
                file_path=ref.file_path,
                start_line=ref.line,
                start_column=ref.column,
                end_line=ref.line,
                end_column=ref.column + len(old_name),
                old_text=old_name,
                new_text=new_name,
            ))

            if ref.file_path not in result.affected_files:
                result.affected_files.append(ref.file_path)

        result.success = True
        return result

    def extract_function(
        self,
        file_path: str,
        start_line: int,
        end_line: int,
        function_name: str,
        parameters: Optional[list[str]] = None,
    ) -> RefactorResult:
        """Extract code into a new function.

        Args:
            file_path: Path to the file.
            start_line: Start line of code to extract.
            end_line: End line of code to extract.
            function_name: Name for the new function.
            parameters: Optional parameter names.

        Returns:
            RefactorResult with edits.
        """
        result = RefactorResult(success=False)

        if not self._is_valid_identifier(function_name):
            result.errors.append(f"Invalid function name: {function_name}")
            return result

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                lines = f.readlines()
        except Exception as e:
            result.errors.append(f"Failed to read file: {e}")
            return result

        if start_line < 1 or end_line > len(lines):
            result.errors.append("Invalid line range")
            return result

        # Extract the code
        extracted_lines = lines[start_line - 1:end_line]
        extracted_code = "".join(extracted_lines)

        # Detect indentation
        base_indent = len(extracted_lines[0]) - len(extracted_lines[0].lstrip())
        func_indent = " " * (base_indent // 2 if base_indent > 0 else 0)

        # Analyze variables (simplified)
        if parameters is None:
            parameters = self._detect_parameters(extracted_code)

        # Detect language and create function
        parser = self._get_parser(file_path)
        if parser and parser.LANGUAGE == "python":
            func_def = self._create_python_function(
                function_name, parameters, extracted_code, base_indent
            )
            call_code = f"{func_indent}{function_name}({', '.join(parameters)})\n"
        else:
            func_def = self._create_cpp_function(
                function_name, parameters, extracted_code, base_indent
            )
            call_code = f"{func_indent}{function_name}({', '.join(parameters)});\n"

        # Edit 1: Replace extracted code with function call
        result.edits.append(FileEdit(
            file_path=file_path,
            start_line=start_line,
            start_column=0,
            end_line=end_line,
            end_column=len(lines[end_line - 1]) if end_line <= len(lines) else 0,
            old_text=extracted_code,
            new_text=call_code,
        ))

        # Edit 2: Add function definition
        # Find where to insert (before the containing function or at module level)
        parse_result = self._parse_file(file_path)
        insert_line = start_line - 1  # Default to just before extraction

        if parse_result:
            for symbol in parse_result.symbols:
                if (symbol.type in (SymbolType.FUNCTION, SymbolType.METHOD)
                    and symbol.range.start_line <= start_line <= symbol.range.end_line):
                    insert_line = symbol.range.start_line - 1
                    break

        result.edits.append(FileEdit(
            file_path=file_path,
            start_line=insert_line,
            start_column=0,
            end_line=insert_line,
            end_column=0,
            old_text="",
            new_text=func_def + "\n\n",
        ))

        result.affected_files.append(file_path)
        result.success = True
        return result

    def _detect_parameters(self, code: str) -> list[str]:
        """Detect potential parameters in extracted code (simplified)."""
        # Find variable-like identifiers that might be parameters
        identifiers = set(re.findall(r"\b([a-z_][a-z0-9_]*)\b", code, re.IGNORECASE))

        # Filter out common keywords and built-ins
        keywords = {
            "if", "else", "for", "while", "return", "def", "class", "import",
            "from", "try", "except", "finally", "with", "as", "and", "or",
            "not", "in", "is", "True", "False", "None", "self", "cls",
            "int", "str", "float", "bool", "list", "dict", "set", "tuple",
            "print", "len", "range", "enumerate", "zip", "map", "filter",
        }

        return [p for p in sorted(identifiers) if p.lower() not in keywords][:5]

    def _create_python_function(
        self,
        name: str,
        params: list[str],
        body: str,
        base_indent: int,
    ) -> str:
        """Create Python function definition."""
        indent = " " * base_indent
        param_str = ", ".join(params) if params else ""

        # Adjust body indentation
        body_lines = body.split("\n")
        adjusted_body = "\n".join(
            f"{indent}    {line.strip()}" if line.strip() else ""
            for line in body_lines
        )

        return f"{indent}def {name}({param_str}):\n{adjusted_body}"

    def _create_cpp_function(
        self,
        name: str,
        params: list[str],
        body: str,
        base_indent: int,
    ) -> str:
        """Create C++ function definition."""
        indent = " " * base_indent
        param_str = ", ".join(f"auto {p}" for p in params) if params else ""

        # Adjust body indentation
        body_lines = body.split("\n")
        adjusted_body = "\n".join(
            f"{indent}    {line.strip()}" if line.strip() else ""
            for line in body_lines
        )

        return f"{indent}void {name}({param_str}) {{\n{adjusted_body}\n{indent}}}"

    def _is_valid_identifier(self, name: str) -> bool:
        """Check if a name is a valid identifier."""
        return bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name))

    def _get_project_files(self) -> list[str]:
        """Get all parseable files in the project."""
        files = []
        extensions = {".py", ".cpp", ".cxx", ".cc", ".c", ".h", ".hpp"}

        ignore_dirs = {
            ".git", ".svn", "__pycache__", "node_modules",
            ".venv", "venv", "build", "dist", ".tox",
        }

        for path in self.project_path.rglob("*"):
            if path.is_file() and path.suffix in extensions:
                if not any(ignored in path.parts for ignored in ignore_dirs):
                    files.append(str(path))

        return files

    def apply_edits(self, edits: list[FileEdit]) -> bool:
        """Apply edits to files.

        Args:
            edits: List of edits to apply.

        Returns:
            True if all edits were applied successfully.
        """
        # Group edits by file
        edits_by_file: dict[str, list[FileEdit]] = {}
        for edit in edits:
            if edit.file_path not in edits_by_file:
                edits_by_file[edit.file_path] = []
            edits_by_file[edit.file_path].append(edit)

        # Apply edits file by file
        for file_path, file_edits in edits_by_file.items():
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    content = f.read()

                # Sort edits in reverse order to apply from end to start
                file_edits.sort(
                    key=lambda e: (e.start_line, e.start_column),
                    reverse=True,
                )

                lines = content.split("\n")

                for edit in file_edits:
                    # Convert to 0-based indexing
                    start_line = edit.start_line - 1
                    end_line = edit.end_line - 1

                    if start_line == end_line:
                        # Single line edit
                        line = lines[start_line]
                        lines[start_line] = (
                            line[:edit.start_column]
                            + edit.new_text
                            + line[edit.end_column:]
                        )
                    else:
                        # Multi-line edit
                        first_line = lines[start_line][:edit.start_column]
                        last_line = lines[end_line][edit.end_column:]
                        lines[start_line:end_line + 1] = [
                            first_line + edit.new_text + last_line
                        ]

                with open(file_path, "w", encoding="utf-8") as f:
                    f.write("\n".join(lines))

                # Clear cache for modified file
                if file_path in self._parse_cache:
                    del self._parse_cache[file_path]

            except Exception:
                return False

        return True


# Convenience functions
def analyze_code(file_path: str) -> dict:
    """Analyze code structure using AST.

    Args:
        file_path: Path to the file.

    Returns:
        Dictionary with analysis results.
    """
    parser = get_parser_for_file(file_path)
    if not parser:
        return {"error": f"No parser available for {file_path}"}

    result = parser.parse_file(file_path)
    return result.to_dict()


def find_symbol(
    name: str,
    file_path: Optional[str] = None,
    project_path: Optional[str] = None,
) -> Optional[dict]:
    """Find a symbol by name.

    Args:
        name: Symbol name.
        file_path: Optional file to search.
        project_path: Optional project root.

    Returns:
        Symbol dict or None.
    """
    engine = RefactorEngine(project_path)
    symbol = engine.find_symbol(name, file_path)
    return symbol.to_dict() if symbol else None


def rename_symbol(
    old_name: str,
    new_name: str,
    file_path: Optional[str] = None,
    project_path: Optional[str] = None,
    apply: bool = False,
) -> dict:
    """Rename a symbol.

    Args:
        old_name: Current name.
        new_name: New name.
        file_path: Optional file scope.
        project_path: Optional project root.
        apply: Whether to apply changes.

    Returns:
        RefactorResult dict.
    """
    engine = RefactorEngine(project_path)
    result = engine.rename_symbol(old_name, new_name, file_path)

    if apply and result.success:
        engine.apply_edits(result.edits)

    return result.to_dict()
