"""
Vulnerability Reachability Analyzer - Pro Tier Feature.

[20251226_FEATURE] v3.2.9 - Pro tier reachability analysis.

This module analyzes whether vulnerable code is actually reachable in your project.
Reduces false positives by checking if vulnerable functions are actually called.
"""

import ast
import logging
from dataclasses import dataclass
from pathlib import Path

logger = logging.getLogger(__name__)


@dataclass
class ReachabilityResult:
    """Result of reachability analysis for a vulnerability."""

    is_reachable: bool
    confidence: float  # 0.0-1.0
    call_chain: list[str]  # Function call chain to vulnerable code
    evidence: str  # Human-readable explanation
    vulnerable_functions: list[str]  # Functions that are vulnerable
    called_functions: list[str]  # Functions actually called from your code


class VulnerabilityReachabilityAnalyzer:
    """
    Analyzes whether vulnerable dependencies are actually reachable.

    Pro tier feature that reduces false positives by checking if:
    - Vulnerable functions are imported
    - Vulnerable functions are actually called
    - Call chains exist from your code to vulnerable code
    """

    def __init__(self, project_root: str | Path):
        """
        Initialize the reachability analyzer.

        Args:
            project_root: Root directory of the project to analyze
        """
        self.project_root = Path(project_root)
        self._call_graph: dict[str, set[str]] = {}
        self._imports: dict[str, set[str]] = {}
        self._project_functions: set[str] = set()

    def analyze_vulnerability_reachability(
        self,
        package_name: str,
        vulnerable_functions: list[str] | None = None,
    ) -> ReachabilityResult:
        """
        Analyze if vulnerable package code is reachable.

        Args:
            package_name: Name of the vulnerable package
            vulnerable_functions: List of vulnerable function names (if known)

        Returns:
            ReachabilityResult with reachability assessment
        """
        # Build project call graph if not already done
        if not self._call_graph:
            self._build_call_graph()

        # Check if package is imported
        imported_modules = self._find_package_imports(package_name)
        if not imported_modules:
            return ReachabilityResult(
                is_reachable=False,
                confidence=0.95,
                call_chain=[],
                evidence=f"Package '{package_name}' is not imported in your code.",
                vulnerable_functions=vulnerable_functions or [],
                called_functions=[],
            )

        # If no specific vulnerable functions, assume package is used
        if not vulnerable_functions:
            return ReachabilityResult(
                is_reachable=True,
                confidence=0.6,
                call_chain=list(imported_modules)[:3],
                evidence=f"Package '{package_name}' is imported but specific vulnerable functions unknown. Assuming reachable.",
                vulnerable_functions=[],
                called_functions=list(imported_modules)[:10],
            )

        # Check if vulnerable functions are called
        called_vulns = []
        call_chains = []
        for func in vulnerable_functions:
            # Look for calls to this function
            full_name = f"{package_name}.{func}"
            if full_name in self._call_graph or func in self._imports.get(package_name, set()):
                called_vulns.append(func)
                # Find call chain
                chain = self._find_call_chain_to_function(full_name)
                if chain:
                    call_chains.append(chain)

        if called_vulns:
            # Vulnerable functions are called
            return ReachabilityResult(
                is_reachable=True,
                confidence=0.9,
                call_chain=call_chains[0] if call_chains else [],
                evidence=f"Vulnerable functions {called_vulns} are called from your code.",
                vulnerable_functions=vulnerable_functions,
                called_functions=called_vulns,
            )
        else:
            # Package imported but vulnerable functions not called
            return ReachabilityResult(
                is_reachable=False,
                confidence=0.7,
                call_chain=[],
                evidence=f"Package '{package_name}' is imported but vulnerable functions {vulnerable_functions} are not called.",
                vulnerable_functions=vulnerable_functions,
                called_functions=[],
            )

    def _build_call_graph(self) -> None:
        """Build call graph for the project."""
        python_files = list(self.project_root.rglob("*.py"))

        for py_file in python_files:
            # Skip virtual environments and test files for performance
            if any(part in py_file.parts for part in (".venv", "venv", "node_modules")):
                continue

            try:
                content = py_file.read_text(encoding="utf-8")
                tree = ast.parse(content, filename=str(py_file))
                self._extract_calls_and_imports(tree, py_file)
            except Exception as e:
                logger.debug(f"Failed to parse {py_file}: {e}")

    def _extract_calls_and_imports(self, tree: ast.AST, file_path: Path) -> None:
        """Extract function calls and imports from AST."""
        module_name = file_path.stem

        # Extract imports
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    imported = alias.name
                    if module_name not in self._imports:
                        self._imports[module_name] = set()
                    self._imports[module_name].add(imported)

            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    if node.module not in self._imports:
                        self._imports[node.module] = set()
                    for alias in node.names:
                        self._imports[node.module].add(alias.name)

            # Extract function definitions
            elif isinstance(node, ast.FunctionDef):
                func_full = f"{module_name}.{node.name}"
                self._project_functions.add(func_full)

                # Extract function calls within this function
                for child in ast.walk(node):
                    if isinstance(child, ast.Call):
                        called = self._get_call_name(child)
                        if called:
                            if func_full not in self._call_graph:
                                self._call_graph[func_full] = set()
                            self._call_graph[func_full].add(called)

    def _get_call_name(self, node: ast.Call) -> str | None:
        """Extract function name from a Call node."""
        if isinstance(node.func, ast.Name):
            return node.func.id
        elif isinstance(node.func, ast.Attribute):
            # Handle module.function calls
            parts = []
            current = node.func
            while isinstance(current, ast.Attribute):
                parts.append(current.attr)
                current = current.value
            if isinstance(current, ast.Name):
                parts.append(current.id)
            return ".".join(reversed(parts))
        return None

    def _find_package_imports(self, package_name: str) -> set[str]:
        """Find all imports of a package."""
        imports = set()
        for _module, imported_names in self._imports.items():
            if package_name in imported_names or any(name.startswith(package_name + ".") for name in imported_names):
                imports.update(imported_names)
        return imports

    def _find_call_chain_to_function(self, target_function: str) -> list[str]:
        """
        Find call chain from project code to target function.

        Uses BFS to find shortest path.
        """
        from collections import deque

        # Find project functions that call target
        queue: deque[tuple[str, list[str]]] = deque()

        # Start from project functions
        for func in self._project_functions:
            if func in self._call_graph:
                if target_function in self._call_graph[func]:
                    return [func, target_function]
                queue.append((func, [func]))

        # BFS to find path
        visited = set()
        max_depth = 5  # Limit search depth

        while queue and len(queue[0][1]) < max_depth:
            current, path = queue.popleft()

            if current in visited:
                continue
            visited.add(current)

            if current not in self._call_graph:
                continue

            for called in self._call_graph[current]:
                if called == target_function:
                    return path + [target_function]

                if called not in visited:
                    queue.append((called, path + [called]))

        return []  # No path found
