"""
Interface consistency validator for checking code modifications
"""

import ast
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Set, Optional, Any, Tuple


@dataclass
class FunctionSignature:
    """
    Function signature information

    Attributes:
        name: Function name
        parameters: List of parameter names
        defaults: Dictionary of parameter defaults
        return_annotation: Return type annotation (if present)
        is_method: Whether this is a class method
        class_name: Class name if this is a method
    """

    name: str
    parameters: List[str]
    defaults: Dict[str, Any] = field(default_factory=dict)
    return_annotation: Optional[str] = None
    is_method: bool = False
    class_name: Optional[str] = None


@dataclass
class ClassSignature:
    """
    Class signature information

    Attributes:
        name: Class name
        methods: Dictionary of method signatures
        base_classes: List of base class names
        attributes: List of class attributes
    """

    name: str
    methods: Dict[str, FunctionSignature] = field(default_factory=dict)
    base_classes: List[str] = field(default_factory=list)
    attributes: List[str] = field(default_factory=list)


@dataclass
class SignatureChange:
    """
    Detected signature change

    Attributes:
        component: Component name (function/class/method)
        change_type: Type of change
        description: Detailed description
        severity: Change severity ('breaking' | 'compatible' | 'minor')
    """

    component: str
    change_type: str
    description: str
    severity: str  # 'breaking' | 'compatible' | 'minor'


@dataclass
class InterfaceValidation:
    """
    Interface validation result

    Attributes:
        is_compatible: Whether changes are backward compatible
        breaking_changes: List of breaking changes
        signature_changes: List of all signature changes
        affected_files: List of files that may be affected
        required_updates: List of required updates to dependent files
        warnings: List of warnings
        recommendations: List of recommendations
    """

    is_compatible: bool
    breaking_changes: List[str] = field(default_factory=list)
    signature_changes: List[SignatureChange] = field(default_factory=list)
    affected_files: List[str] = field(default_factory=list)
    required_updates: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    recommendations: List[str] = field(default_factory=list)


class InterfaceValidator:
    """
    Interface consistency validator

    This class validates that code modifications maintain interface
    compatibility with dependent code.
    """

    def __init__(self, workspace_path: str):
        """
        Initialize interface validator

        Args:
            workspace_path: Path to workspace root
        """
        self.workspace = Path(workspace_path)
        self.dependency_graph: Dict[str, Set[str]] = {}

    def build_dependency_graph(self) -> None:
        """
        Build dependency graph for the workspace

        Scans all Python files and builds a graph of import dependencies.
        """
        self.dependency_graph.clear()

        # Find all Python files
        python_files = list(self.workspace.rglob("*.py"))

        for py_file in python_files:
            try:
                with open(py_file, "r", encoding="utf-8") as f:
                    content = f.read()

                # Parse imports
                tree = ast.parse(content)
                imports = self._extract_imports(tree)

                # Store dependencies
                file_key = str(py_file.relative_to(self.workspace))
                self.dependency_graph[file_key] = set()

                for imp in imports:
                    # Try to resolve import to file path
                    resolved = self._resolve_import(imp, py_file)
                    if resolved:
                        self.dependency_graph[file_key].add(resolved)

            except Exception:
                # Skip files that can't be parsed
                continue

    def find_dependents(self, file_path: str) -> List[str]:
        """
        Find files that depend on the given file

        Args:
            file_path: Path to file (relative to workspace)

        Returns:
            List of dependent file paths
        """
        if not self.dependency_graph:
            self.build_dependency_graph()

        dependents = []
        target_path = str(Path(file_path).relative_to(self.workspace))

        for file, deps in self.dependency_graph.items():
            if target_path in deps:
                dependents.append(file)

        return dependents

    def validate_interface_changes(
        self, original_code: str, modified_code: str, file_path: str
    ) -> InterfaceValidation:
        """
        Validate interface changes between original and modified code

        Args:
            original_code: Original code
            modified_code: Modified code
            file_path: File path for context

        Returns:
            Interface validation result
        """
        validation = InterfaceValidation(is_compatible=True)

        try:
            # Parse both versions
            original_tree = ast.parse(original_code)
            modified_tree = ast.parse(modified_code)

            # Extract signatures
            original_sigs = self._extract_signatures(original_tree)
            modified_sigs = self._extract_signatures(modified_tree)

            # Compare signatures
            changes = self._compare_signatures(original_sigs, modified_sigs)
            validation.signature_changes = changes

            # Identify breaking changes
            breaking = [c for c in changes if c.severity == "breaking"]
            validation.breaking_changes = [f"{c.component}: {c.description}" for c in breaking]

            # Check compatibility
            validation.is_compatible = len(breaking) == 0

            # Find affected files
            if not validation.is_compatible:
                dependents = self.find_dependents(file_path)
                validation.affected_files = dependents

                # Generate required updates
                for dep in dependents:
                    validation.required_updates.append(f"Update imports and usage in {dep}")

            # Generate recommendations
            if changes:
                validation.recommendations.append("Review all signature changes carefully")
                if breaking:
                    validation.recommendations.append(
                        "Consider deprecation warnings before removing functionality"
                    )
                    validation.recommendations.append(
                        "Update documentation to reflect interface changes"
                    )

        except SyntaxError as e:
            validation.is_compatible = False
            validation.warnings.append(f"Syntax error in code: {e}")

        return validation

    def _extract_imports(self, tree: ast.AST) -> List[str]:
        """
        Extract import statements from AST

        Args:
            tree: AST tree

        Returns:
            List of imported module names
        """
        imports = []

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    imports.append(alias.name)
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    imports.append(node.module)

        return imports

    def _resolve_import(self, import_name: str, from_file: Path) -> Optional[str]:
        """
        Resolve import to file path

        Args:
            import_name: Import module name
            from_file: File containing the import

        Returns:
            Resolved file path relative to workspace, or None
        """
        # Handle relative imports
        if import_name.startswith("."):
            # Relative import
            parts = import_name.split(".")
            level = len(parts) - len([p for p in parts if p])
            module_parts = [p for p in parts if p]

            current_dir = from_file.parent
            for _ in range(level - 1):
                current_dir = current_dir.parent

            if module_parts:
                target = current_dir / "/".join(module_parts)
            else:
                target = current_dir

            # Try as package or module
            if (target / "__init__.py").exists():
                return str((target / "__init__.py").relative_to(self.workspace))
            elif target.with_suffix(".py").exists():
                return str(target.with_suffix(".py").relative_to(self.workspace))

        else:
            # Absolute import within workspace
            parts = import_name.split(".")
            target = self.workspace / "/".join(parts)

            if (target / "__init__.py").exists():
                return str((target / "__init__.py").relative_to(self.workspace))
            elif target.with_suffix(".py").exists():
                return str(target.with_suffix(".py").relative_to(self.workspace))

        return None

    def _extract_signatures(self, tree: ast.AST) -> Dict[str, Any]:
        """
        Extract function and class signatures from AST

        Args:
            tree: AST tree

        Returns:
            Dictionary of signatures
        """
        signatures = {"functions": {}, "classes": {}}

        for node in ast.iter_child_nodes(tree):
            if isinstance(node, ast.FunctionDef):
                sig = self._extract_function_signature(node)
                signatures["functions"][node.name] = sig

            elif isinstance(node, ast.ClassDef):
                sig = self._extract_class_signature(node)
                signatures["classes"][node.name] = sig

        return signatures

    def _extract_function_signature(
        self, node: ast.FunctionDef, class_name: Optional[str] = None
    ) -> FunctionSignature:
        """
        Extract function signature from AST node

        Args:
            node: FunctionDef AST node
            class_name: Class name if this is a method

        Returns:
            Function signature
        """
        # Extract parameters
        params = []
        defaults = {}

        for arg in node.args.args:
            params.append(arg.arg)

        # Extract defaults
        num_defaults = len(node.args.defaults)
        if num_defaults > 0:
            default_params = params[-num_defaults:]
            for param, default in zip(default_params, node.args.defaults):
                defaults[param] = ast.unparse(default)

        # Extract return annotation
        return_annotation = None
        if node.returns:
            return_annotation = ast.unparse(node.returns)

        return FunctionSignature(
            name=node.name,
            parameters=params,
            defaults=defaults,
            return_annotation=return_annotation,
            is_method=class_name is not None,
            class_name=class_name,
        )

    def _extract_class_signature(self, node: ast.ClassDef) -> ClassSignature:
        """
        Extract class signature from AST node

        Args:
            node: ClassDef AST node

        Returns:
            Class signature
        """
        # Extract base classes
        bases = []
        for base in node.bases:
            if isinstance(base, ast.Name):
                bases.append(base.id)
            else:
                bases.append(ast.unparse(base))

        # Extract methods
        methods = {}
        for item in node.body:
            if isinstance(item, ast.FunctionDef):
                sig = self._extract_function_signature(item, node.name)
                methods[item.name] = sig

        # Extract class attributes
        attributes = []
        for item in node.body:
            if isinstance(item, ast.AnnAssign):
                if isinstance(item.target, ast.Name):
                    attributes.append(item.target.id)

        return ClassSignature(
            name=node.name, methods=methods, base_classes=bases, attributes=attributes
        )

    def _compare_signatures(
        self, original: Dict[str, Any], modified: Dict[str, Any]
    ) -> List[SignatureChange]:
        """
        Compare signatures and identify changes

        Args:
            original: Original signatures
            modified: Modified signatures

        Returns:
            List of signature changes
        """
        changes = []

        # Compare functions
        changes.extend(
            self._compare_functions(original.get("functions", {}), modified.get("functions", {}))
        )

        # Compare classes
        changes.extend(
            self._compare_classes(original.get("classes", {}), modified.get("classes", {}))
        )

        return changes

    def _compare_functions(
        self, original: Dict[str, FunctionSignature], modified: Dict[str, FunctionSignature]
    ) -> List[SignatureChange]:
        """Compare function signatures"""
        changes = []

        # Check for removed functions
        for name in original:
            if name not in modified:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="function_removed",
                        description=f"Function '{name}' was removed",
                        severity="breaking",
                    )
                )

        # Check for added functions
        for name in modified:
            if name not in original:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="function_added",
                        description=f"Function '{name}' was added",
                        severity="compatible",
                    )
                )

        # Check for modified functions
        for name in set(original.keys()) & set(modified.keys()):
            orig_sig = original[name]
            mod_sig = modified[name]

            # Check parameters
            if orig_sig.parameters != mod_sig.parameters:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="parameters_changed",
                        description=f"Function '{name}' parameters changed from {orig_sig.parameters} to {mod_sig.parameters}",
                        severity="breaking",
                    )
                )

            # Check defaults
            if orig_sig.defaults != mod_sig.defaults:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="defaults_changed",
                        description=f"Function '{name}' parameter defaults changed",
                        severity="minor",
                    )
                )

            # Check return type
            if orig_sig.return_annotation != mod_sig.return_annotation:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="return_type_changed",
                        description=f"Function '{name}' return type changed",
                        severity="breaking",
                    )
                )

        return changes

    def _compare_classes(
        self, original: Dict[str, ClassSignature], modified: Dict[str, ClassSignature]
    ) -> List[SignatureChange]:
        """Compare class signatures"""
        changes = []

        # Check for removed classes
        for name in original:
            if name not in modified:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="class_removed",
                        description=f"Class '{name}' was removed",
                        severity="breaking",
                    )
                )

        # Check for added classes
        for name in modified:
            if name not in original:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="class_added",
                        description=f"Class '{name}' was added",
                        severity="compatible",
                    )
                )

        # Check for modified classes
        for name in set(original.keys()) & set(modified.keys()):
            orig_cls = original[name]
            mod_cls = modified[name]

            # Check base classes
            if orig_cls.base_classes != mod_cls.base_classes:
                changes.append(
                    SignatureChange(
                        component=name,
                        change_type="base_classes_changed",
                        description=f"Class '{name}' base classes changed",
                        severity="breaking",
                    )
                )

            # Check methods
            method_changes = self._compare_functions(orig_cls.methods, mod_cls.methods)
            for change in method_changes:
                change.component = f"{name}.{change.component}"
                changes.append(change)

        return changes
