"""Auto-fixing for common code issues.

Uses autopep8 and other tools to automatically fix formatting,
indentation, and other common issues in generated code.
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, List, Optional

if TYPE_CHECKING:
    from autodebug.validation.fix_validator import ValidationError


class AutoFixer:
    """Auto-fix common code issues.

    Attempts to fix issues detected during validation, such as:
    - Indentation problems
    - Formatting issues
    - Missing imports
    """

    def __init__(self, max_line_length: int = 100, aggressive: int = 1):
        """Initialize the auto-fixer.

        Args:
            max_line_length: Maximum line length for formatting
            aggressive: Aggression level for autopep8 (0-2)
        """
        self.max_line_length = max_line_length
        self.aggressive = aggressive
        self._autopep8_available: Optional[bool] = None

    @property
    def autopep8_available(self) -> bool:
        """Check if autopep8 is available (cached)."""
        if self._autopep8_available is None:
            try:
                import autopep8
                self._autopep8_available = True
            except ImportError:
                self._autopep8_available = False
        return self._autopep8_available

    def fix_formatting(self, code: str) -> str:
        """Fix formatting issues using autopep8.

        Args:
            code: Python source code to fix

        Returns:
            Formatted code
        """
        if not self.autopep8_available:
            return code  # Return unchanged if not available

        import autopep8

        return autopep8.fix_code(
            code,
            options={
                "max_line_length": self.max_line_length,
                "aggressive": self.aggressive,
            },
        )

    def fix_indentation(self, code: str, target_indent: int = 4) -> str:
        """Normalize indentation to consistent spacing.

        Converts tabs to spaces and normalizes indent levels.

        Args:
            code: Python source code to fix
            target_indent: Number of spaces per indent level

        Returns:
            Code with normalized indentation
        """
        lines = code.split("\n")
        fixed = []

        for line in lines:
            if line.strip():
                # Detect current indent (handle both tabs and spaces)
                leading_whitespace = ""
                for char in line:
                    if char in " \t":
                        leading_whitespace += char
                    else:
                        break

                # Convert tabs to spaces
                expanded = leading_whitespace.replace("\t", "    ")
                current_spaces = len(expanded)

                # Normalize to multiples of target_indent
                # If there's any indentation, ensure at least one level
                if current_spaces > 0:
                    # Round up to ensure indented code stays indented
                    indent_levels = max(1, (current_spaces + target_indent - 1) // target_indent)
                    normalized = indent_levels * target_indent
                else:
                    normalized = 0

                stripped = line.lstrip()
                fixed.append(" " * normalized + stripped)
            else:
                # Preserve empty lines
                fixed.append(line)

        return "\n".join(fixed)

    def fix_trailing_whitespace(self, code: str) -> str:
        """Remove trailing whitespace from all lines.

        Args:
            code: Python source code to fix

        Returns:
            Code without trailing whitespace
        """
        lines = code.split("\n")
        fixed = [line.rstrip() for line in lines]
        return "\n".join(fixed)

    def ensure_final_newline(self, code: str) -> str:
        """Ensure code ends with a single newline.

        Args:
            code: Python source code to fix

        Returns:
            Code ending with single newline
        """
        return code.rstrip() + "\n"

    def add_missing_imports(self, code: str, missing: List[str]) -> str:
        """Add missing imports at the top of the file.

        Inserts imports after existing imports or docstrings.

        Args:
            code: Python source code
            missing: List of module names to import

        Returns:
            Code with imports added
        """
        if not missing:
            return code

        import_lines = []
        for module in missing:
            if "." in module:
                # from package import module
                parts = module.rsplit(".", 1)
                import_lines.append(f"from {parts[0]} import {parts[1]}")
            else:
                import_lines.append(f"import {module}")

        import_block = "\n".join(import_lines)

        # Find insertion point (after docstring and existing imports)
        lines = code.split("\n")
        insert_idx = 0

        # Skip module docstring
        if lines and lines[0].strip().startswith(('"""', "'''")):
            # Find closing quote
            if lines[0].strip().endswith(('"""', "'''")):
                insert_idx = 1
            else:
                for i, line in enumerate(lines[1:], 1):
                    if line.strip().endswith(('"""', "'''")):
                        insert_idx = i + 1
                        break

        # Skip existing imports
        for i, line in enumerate(lines[insert_idx:], insert_idx):
            stripped = line.strip()
            if stripped.startswith(("import ", "from ")):
                insert_idx = i + 1
            elif stripped.startswith("#"):
                continue  # Skip comments
            elif stripped == "":
                continue  # Skip blank lines
            else:
                break

        # Insert imports
        lines.insert(insert_idx, import_block)
        if insert_idx > 0 and lines[insert_idx - 1].strip():
            # Add blank line before if needed
            lines.insert(insert_idx, "")
        if insert_idx < len(lines) - 1 and lines[insert_idx + 1].strip():
            # Add blank line after if needed
            lines.insert(insert_idx + 1, "")

        return "\n".join(lines)

    def fix_common_syntax_errors(self, code: str) -> str:
        """Attempt to fix common syntax errors.

        Handles simple cases like:
        - Missing colons after def/if/for/while
        - Unclosed parentheses on same line
        - Common typos

        Args:
            code: Python source code to fix

        Returns:
            Code with simple fixes applied
        """
        lines = code.split("\n")
        fixed = []

        for i, line in enumerate(lines):
            stripped = line.rstrip()

            # Fix missing colon after def/class/if/for/while/try/except/with
            keywords = ("def ", "class ", "if ", "elif ", "else", "for ", "while ",
                        "try", "except", "finally", "with ", "async def ", "async for ",
                        "async with ")

            for kw in keywords:
                if stripped.lstrip().startswith(kw):
                    # Check if line ends properly
                    if stripped.endswith(")") or stripped.endswith(")"):
                        # Missing colon after closing paren
                        stripped += ":"
                    elif kw in ("else", "try", "finally") and not stripped.endswith(":"):
                        stripped += ":"
                    break

            fixed.append(stripped)

        return "\n".join(fixed)

    def try_auto_fix(
        self, code: str, errors: List[ValidationError]
    ) -> Optional[str]:
        """Attempt to auto-fix based on validation errors.

        Analyzes the errors and applies appropriate fixes.

        Args:
            code: Original code that failed validation
            errors: List of validation errors

        Returns:
            Fixed code if any fixes were applied, None otherwise
        """
        fixed = code
        changes_made = False

        for error in errors:
            if error.stage == "syntax":
                if "indent" in error.message.lower():
                    fixed = self.fix_indentation(fixed)
                    changes_made = True
                elif "expected ':'" in error.message.lower():
                    fixed = self.fix_common_syntax_errors(fixed)
                    changes_made = True

            elif error.stage == "import":
                # Extract module name from error message
                # Error format: "Cannot resolve import: module_name"
                match = re.search(r"Cannot resolve import:\s*(\S+)", error.message)
                if match:
                    module = match.group(1)
                    # Don't add if it's already being imported
                    if module not in fixed:
                        fixed = self.add_missing_imports(fixed, [module])
                        changes_made = True

            elif error.stage == "lint":
                # Fix whitespace issues
                if "whitespace" in error.message.lower():
                    fixed = self.fix_trailing_whitespace(fixed)
                    changes_made = True

        # Final formatting pass
        if changes_made or self.autopep8_available:
            formatted = self.fix_formatting(fixed)
            if formatted != code:
                fixed = formatted
                changes_made = True

        # Ensure final newline
        final = self.ensure_final_newline(fixed)
        if final != code:
            fixed = final
            changes_made = True

        return fixed if changes_made else None


class LibCSTFixer:
    """Format-preserving code transformations using LibCST.

    LibCST maintains the original formatting while making targeted
    changes, making it ideal for generating clean patches.
    """

    def __init__(self):
        self._libcst_available: Optional[bool] = None

    @property
    def libcst_available(self) -> bool:
        """Check if LibCST is available (cached)."""
        if self._libcst_available is None:
            try:
                import libcst
                self._libcst_available = True
            except ImportError:
                self._libcst_available = False
        return self._libcst_available

    def add_null_check(
        self, code: str, variable: str, line: int
    ) -> Optional[str]:
        """Add a null check for a variable using LibCST.

        Args:
            code: Python source code
            variable: Variable name to check
            line: Line number where check should be added

        Returns:
            Modified code with null check, or None if not possible
        """
        if not self.libcst_available:
            return None

        import libcst as cst

        try:
            tree = cst.parse_module(code)

            class NullCheckAdder(cst.CSTTransformer):
                def __init__(self, var_name: str, target_line: int):
                    super().__init__()
                    self.var_name = var_name
                    self.target_line = target_line
                    self.current_line = 1

                def visit_SimpleStatementLine(
                    self, node: cst.SimpleStatementLine
                ) -> bool:
                    # Track line numbers (simplified)
                    self.current_line += 1
                    return True

                def leave_If(
                    self, original_node: cst.If, updated_node: cst.If
                ) -> cst.If:
                    # Add null check logic here
                    # This is a simplified implementation
                    return updated_node

            transformer = NullCheckAdder(variable, line)
            modified_tree = tree.visit(transformer)
            return modified_tree.code

        except Exception:
            return None

    def rename_variable(
        self, code: str, old_name: str, new_name: str
    ) -> Optional[str]:
        """Rename a variable throughout the code using LibCST.

        Args:
            code: Python source code
            old_name: Current variable name
            new_name: New variable name

        Returns:
            Modified code with variable renamed, or None if not possible
        """
        if not self.libcst_available:
            return None

        import libcst as cst

        try:
            tree = cst.parse_module(code)

            class VariableRenamer(cst.CSTTransformer):
                def __init__(self, old: str, new: str):
                    super().__init__()
                    self.old = old
                    self.new = new

                def leave_Name(
                    self, original_node: cst.Name, updated_node: cst.Name
                ) -> cst.Name:
                    if updated_node.value == self.old:
                        return updated_node.with_changes(value=self.new)
                    return updated_node

            transformer = VariableRenamer(old_name, new_name)
            modified_tree = tree.visit(transformer)
            return modified_tree.code

        except Exception:
            return None
