"""Type inference combining static and dynamic analysis.

Infers types from runtime values and static code analysis,
providing the most accurate type information possible.
"""

from __future__ import annotations

import ast
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Dict, List, Optional

if TYPE_CHECKING:
    from autodebug.analysis.dynamic_inspector import DynamicContext


@dataclass
class TypeInfo:
    """Information about an inferred type."""

    name: str
    source: str  # "runtime", "annotation", "inferred", "unknown"
    confidence: float  # 0.0 to 1.0
    generic_args: List[str] = field(default_factory=list)  # For generics like List[int]

    def __str__(self) -> str:
        if self.generic_args:
            args = ", ".join(self.generic_args)
            return f"{self.name}[{args}]"
        return self.name

    @classmethod
    def unknown(cls) -> TypeInfo:
        """Create an unknown type."""
        return cls(name="Any", source="unknown", confidence=0.0)


@dataclass
class TypeExpectation:
    """Expected type for an operation."""

    expected_type: str
    operation: str
    reason: str

    def __str__(self) -> str:
        return f"{self.operation} expects {self.expected_type}: {self.reason}"


class TypeInferrer:
    """Combines static and dynamic type inference.

    Uses runtime values when available for high-confidence inference,
    falling back to static analysis from AST and type annotations.
    """

    # Type expectations for common operations
    TYPE_EXPECTATIONS = {
        "subscript": TypeExpectation(
            expected_type="Mapping or Sequence",
            operation="subscript access",
            reason="Object must support __getitem__",
        ),
        "attribute": TypeExpectation(
            expected_type="object",
            operation="attribute access",
            reason="Object must have the accessed attribute",
        ),
        "call": TypeExpectation(
            expected_type="Callable",
            operation="function call",
            reason="Object must be callable",
        ),
        "iteration": TypeExpectation(
            expected_type="Iterable",
            operation="iteration",
            reason="Object must be iterable",
        ),
        "addition": TypeExpectation(
            expected_type="number or sequence",
            operation="addition",
            reason="Object must support __add__",
        ),
        "comparison": TypeExpectation(
            expected_type="Comparable",
            operation="comparison",
            reason="Objects must support comparison operators",
        ),
        "membership": TypeExpectation(
            expected_type="Container",
            operation="membership test",
            reason="Object must support __contains__",
        ),
    }

    def __init__(
        self,
        source: str,
        dynamic_context: Optional[DynamicContext] = None,
    ):
        """Initialize the type inferrer.

        Args:
            source: Python source code to analyze
            dynamic_context: Optional runtime context for inference
        """
        self.source = source
        self.dynamic_context = dynamic_context
        self._ast: Optional[ast.Module] = None
        self._type_annotations: Dict[str, str] = {}
        self._function_signatures: Dict[str, Dict[str, str]] = {}
        self._parsed = False

    def _parse(self) -> None:
        """Parse source and extract type information."""
        if self._parsed:
            return

        try:
            self._ast = ast.parse(self.source)
            self._extract_annotations()
            self._extract_signatures()
        except SyntaxError:
            self._ast = None

        self._parsed = True

    def _extract_annotations(self) -> None:
        """Extract type annotations from AST."""
        if not self._ast:
            return

        class AnnotationExtractor(ast.NodeVisitor):
            def __init__(self, inferrer: TypeInferrer):
                self.inferrer = inferrer

            def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
                if isinstance(node.target, ast.Name) and node.annotation:
                    try:
                        self.inferrer._type_annotations[node.target.id] = ast.unparse(
                            node.annotation
                        )
                    except Exception:
                        pass
                self.generic_visit(node)

        AnnotationExtractor(self).visit(self._ast)

    def _extract_signatures(self) -> None:
        """Extract function signatures from AST."""
        if not self._ast:
            return

        class SignatureExtractor(ast.NodeVisitor):
            def __init__(self, inferrer: TypeInferrer):
                self.inferrer = inferrer

            def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
                self._extract_function_sig(node)
                self.generic_visit(node)

            def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
                self._extract_function_sig(node)
                self.generic_visit(node)

            def _extract_function_sig(
                self,
                node: ast.FunctionDef | ast.AsyncFunctionDef,
            ) -> None:
                sig: Dict[str, str] = {}

                # Extract argument types
                for arg in node.args.args:
                    if arg.annotation:
                        try:
                            sig[arg.arg] = ast.unparse(arg.annotation)
                        except Exception:
                            pass

                # Extract return type
                if node.returns:
                    try:
                        sig["return"] = ast.unparse(node.returns)
                    except Exception:
                        pass

                if sig:
                    self.inferrer._function_signatures[node.name] = sig

        SignatureExtractor(self).visit(self._ast)

    def infer_variable_type(self, var_name: str, line: int = 0) -> TypeInfo:
        """Infer the type of a variable.

        Args:
            var_name: Variable name
            line: Line number (optional, for context)

        Returns:
            TypeInfo with inferred type
        """
        self._parse()

        # First check dynamic context (most accurate)
        if self.dynamic_context:
            if var_name in self.dynamic_context.inferred_types:
                type_name = self.dynamic_context.inferred_types[var_name]
                return TypeInfo(
                    name=type_name,
                    source="runtime",
                    confidence=0.95,
                )

            # Check variable snapshots
            var = self.dynamic_context.get_variable_at_exception(var_name)
            if var:
                return TypeInfo(
                    name=var.type_name,
                    source="runtime",
                    confidence=0.95,
                )

        # Check type annotations
        if var_name in self._type_annotations:
            return TypeInfo(
                name=self._type_annotations[var_name],
                source="annotation",
                confidence=0.9,
            )

        # Try static inference
        return self._static_infer(var_name, line)

    def _static_infer(self, var_name: str, line: int) -> TypeInfo:
        """Static type inference from AST.

        Args:
            var_name: Variable name
            line: Line number

        Returns:
            TypeInfo with inferred type
        """
        if not self._ast:
            return TypeInfo.unknown()

        # H-122: Check for isinstance narrowing first (flow-sensitive)
        if line > 0:
            narrowed = self._infer_from_isinstance(var_name, line)
            if narrowed:
                return narrowed

        # Look for assignment patterns
        inferred = self._infer_from_assignments(var_name, line)
        if inferred:
            return inferred

        # Look for function parameters
        inferred = self._infer_from_parameters(var_name)
        if inferred:
            return inferred

        return TypeInfo.unknown()

    def _infer_from_assignments(
        self,
        var_name: str,
        line: int,
    ) -> Optional[TypeInfo]:
        """Infer type from assignment patterns.

        Args:
            var_name: Variable name
            line: Line number

        Returns:
            TypeInfo or None
        """
        if not self._ast:
            return None

        class AssignmentFinder(ast.NodeVisitor):
            def __init__(self):
                self.found_type: Optional[str] = None

            def visit_Assign(self, node: ast.Assign) -> None:
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id == var_name:
                        self.found_type = self._infer_value_type(node.value)
                self.generic_visit(node)

            def _infer_value_type(self, value: ast.expr) -> Optional[str]:
                if isinstance(value, ast.Constant):
                    return type(value.value).__name__
                if isinstance(value, ast.List):
                    return "list"
                if isinstance(value, ast.Dict):
                    return "dict"
                if isinstance(value, ast.Set):
                    return "set"
                if isinstance(value, ast.Tuple):
                    return "tuple"
                if isinstance(value, ast.Call):
                    if isinstance(value.func, ast.Name):
                        # Map known builtins to their return types
                        _BUILTIN_RETURN_TYPES = {
                            "len": "int", "abs": "int", "hash": "int",
                            "ord": "int", "round": "int", "id": "int",
                            "sorted": "list", "list": "list", "reversed": "iterator",
                            "range": "range", "enumerate": "enumerate",
                            "zip": "zip", "map": "map", "filter": "filter",
                            "str": "str", "repr": "str", "format": "str",
                            "chr": "str", "hex": "str", "oct": "str", "bin": "str",
                            "int": "int", "float": "float", "bool": "bool",
                            "complex": "complex", "bytes": "bytes", "bytearray": "bytearray",
                            "dict": "dict", "set": "set", "frozenset": "frozenset",
                            "tuple": "tuple", "type": "type",
                            "open": "TextIOWrapper", "input": "str",
                            "iter": "iterator", "next": None,
                            "min": None, "max": None, "sum": None,
                            "any": "bool", "all": "bool",
                            "isinstance": "bool", "issubclass": "bool",
                            "hasattr": "bool", "callable": "bool",
                            "getattr": None, "vars": "dict", "dir": "list",
                            "print": "NoneType", "super": None,
                        }
                        name = value.func.id
                        return _BUILTIN_RETURN_TYPES.get(name, name if name[0:1].isupper() else None)
                    if isinstance(value.func, ast.Attribute):
                        # Method call: obj.method() - cannot determine return type
                        # from name alone; return None instead of the method name
                        return None
                if isinstance(value, ast.ListComp):
                    return "list"
                if isinstance(value, ast.DictComp):
                    return "dict"
                if isinstance(value, ast.SetComp):
                    return "set"
                if isinstance(value, ast.GeneratorExp):
                    return "generator"
                return None

        finder = AssignmentFinder()
        finder.visit(self._ast)

        if finder.found_type:
            return TypeInfo(
                name=finder.found_type,
                source="inferred",
                confidence=0.6,
            )
        return None

    def _infer_from_parameters(self, var_name: str) -> Optional[TypeInfo]:
        """Infer type from function parameters.

        Args:
            var_name: Variable name

        Returns:
            TypeInfo or None
        """
        for func_name, sig in self._function_signatures.items():
            if var_name in sig:
                return TypeInfo(
                    name=sig[var_name],
                    source="annotation",
                    confidence=0.9,
                )
        return None

    def _infer_from_isinstance(
        self,
        var_name: str,
        line: int,
    ) -> Optional[TypeInfo]:
        """Narrow type from isinstance guards (flow-sensitive).

        Handles the pattern:
            if isinstance(x, SomeType):
                # x is SomeType here

        Args:
            var_name: Variable name
            line: Line number where the variable is used

        Returns:
            TypeInfo if narrowed by an isinstance guard, None otherwise
        """
        if not self._ast:
            return None

        class IsInstanceFinder(ast.NodeVisitor):
            def __init__(self):
                self.narrowed_type: Optional[str] = None

            def visit_If(self, node: ast.If) -> None:
                # Check if the test is isinstance(var_name, Type)
                guard_type = self._extract_isinstance_type(node.test, var_name)
                if guard_type and self._line_in_body(node, line):
                    self.narrowed_type = guard_type
                # Continue visiting nested if-statements
                self.generic_visit(node)

            def _extract_isinstance_type(
                self, test: ast.expr, target_var: str
            ) -> Optional[str]:
                """Extract the type from isinstance(var, Type)."""
                if not isinstance(test, ast.Call):
                    return None
                if not isinstance(test.func, ast.Name) or test.func.id != "isinstance":
                    return None
                if len(test.args) < 2:
                    return None
                # First arg must be the variable we're looking for
                if not isinstance(test.args[0], ast.Name) or test.args[0].id != target_var:
                    return None
                # Second arg is the type (or tuple of types)
                type_arg = test.args[1]
                if isinstance(type_arg, ast.Name):
                    return type_arg.id
                if isinstance(type_arg, ast.Attribute):
                    try:
                        return ast.unparse(type_arg)
                    except Exception:
                        return None
                # Tuple of types: isinstance(x, (int, str)) -> use first type
                if isinstance(type_arg, ast.Tuple) and type_arg.elts:
                    first = type_arg.elts[0]
                    if isinstance(first, ast.Name):
                        return first.id
                return None

            def _line_in_body(self, if_node: ast.If, target_line: int) -> bool:
                """Check if target_line falls inside the if-body."""
                if not if_node.body:
                    return False
                body_start = if_node.body[0].lineno
                body_end = max(
                    getattr(stmt, "end_lineno", stmt.lineno) or stmt.lineno
                    for stmt in if_node.body
                )
                return body_start <= target_line <= body_end

        finder = IsInstanceFinder()
        finder.visit(self._ast)

        if finder.narrowed_type:
            return TypeInfo(
                name=finder.narrowed_type,
                source="inferred",
                confidence=0.85,
            )
        return None

    def get_expected_type(self, operation: str) -> TypeExpectation:
        """Get the expected type for an operation.

        Args:
            operation: Operation name (e.g., "subscript", "call")

        Returns:
            TypeExpectation for the operation
        """
        return self.TYPE_EXPECTATIONS.get(
            operation,
            TypeExpectation(
                expected_type="Any",
                operation=operation,
                reason="Unknown operation",
            ),
        )

    def check_type_compatibility(
        self,
        var_name: str,
        operation: str,
    ) -> Optional[str]:
        """Check if a variable's type is compatible with an operation.

        Args:
            var_name: Variable name
            operation: Operation being performed

        Returns:
            Error message if incompatible, None if compatible
        """
        var_type = self.infer_variable_type(var_name)
        expected = self.get_expected_type(operation)

        # Simple compatibility check
        incompatible = self._is_incompatible(var_type.name, expected.expected_type)
        if incompatible:
            return f"Variable '{var_name}' has type '{var_type}' but {expected}"

        return None

    # Types known to NOT implement specific dunder protocols.
    # If a type is not in this set, we conservatively assume it might be
    # compatible (avoiding false positives for user-defined classes that
    # implement __call__, __iter__, __getitem__, etc. via dunder methods).
    _NOT_CALLABLE = {"int", "str", "float", "bool", "NoneType", "list", "dict", "tuple", "set", "frozenset", "bytes"}
    _NOT_ITERABLE = {"int", "float", "bool", "NoneType", "complex"}
    _NOT_SUBSCRIPTABLE = {"int", "float", "bool", "NoneType", "complex"}

    def _is_incompatible(self, actual: str, expected: str) -> bool:
        """Check if types are incompatible.

        Uses conservative negative checking: only returns True when the
        actual type is a well-known builtin that definitely does NOT
        support the expected protocol. User-defined types are assumed
        compatible because they may implement dunder methods (__call__,
        __iter__, __getitem__, etc.) or inherit from compatible bases.

        Args:
            actual: Actual type name
            expected: Expected type description

        Returns:
            True if definitely incompatible
        """
        # Unknown / Any types are always considered compatible
        if actual in ("Any", "unknown") or expected in ("Any", "object"):
            return False

        # None is incompatible with most operations
        if actual == "NoneType":
            return expected not in {"Any", "object", "Optional"}

        # For known builtins, check against protocol-specific deny lists.
        # User-defined types (not in deny list) are conservatively compatible.
        if expected == "Callable":
            return actual in self._NOT_CALLABLE

        if expected == "Iterable":
            return actual in self._NOT_ITERABLE

        if expected == "Mapping or Sequence":
            return actual in self._NOT_SUBSCRIPTABLE

        if expected == "Container":
            return actual in self._NOT_ITERABLE  # same set for membership

        if expected == "Comparable":
            # All builtins support comparison in some form
            return actual == "NoneType"

        return False

    def get_all_types(self) -> Dict[str, TypeInfo]:
        """Get all inferred types.

        Returns:
            Dict mapping variable names to TypeInfo
        """
        self._parse()

        all_types: Dict[str, TypeInfo] = {}

        # Add dynamic types first (highest priority)
        if self.dynamic_context:
            for name, type_name in self.dynamic_context.inferred_types.items():
                all_types[name] = TypeInfo(
                    name=type_name,
                    source="runtime",
                    confidence=0.95,
                )

        # Add annotated types
        for name, type_name in self._type_annotations.items():
            if name not in all_types:
                all_types[name] = TypeInfo(
                    name=type_name,
                    source="annotation",
                    confidence=0.9,
                )

        return all_types


def infer_types(
    source: str,
    dynamic_context: Optional[DynamicContext] = None,
) -> Dict[str, TypeInfo]:
    """Convenience function to infer types from source.

    Args:
        source: Python source code
        dynamic_context: Optional runtime context

    Returns:
        Dict mapping variable names to TypeInfo
    """
    inferrer = TypeInferrer(source, dynamic_context)
    return inferrer.get_all_types()
