"""Python language adapter using the stdlib ast module.

This is the reference adapter that demonstrates the interface for
Phase 4's tree-sitter adapters. Uses ast.parse() for parsing and
implements import resolution following Python's import machinery.
"""

from __future__ import annotations

import ast
import logging
from pathlib import Path

from unfamiliar.analysis.models import (
    EntryPoint,
    FunctionDef,
    ParsedFile,
    ParsedImport,
)
from unfamiliar.constants import EntryType

logger = logging.getLogger(__name__)


class PythonAdapter:
    """Language adapter for Python source files using stdlib ast."""

    def language_name(self) -> str:
        """Return the lowercase name of the language."""
        return "python"

    def supported_extensions(self) -> frozenset[str]:
        """Return supported file extensions."""
        return frozenset({".py"})

    def can_handle(self, path: str) -> bool:
        """Check if this adapter can parse the given file."""
        return path.endswith(".py")

    def parse_file(self, path: Path, content: bytes) -> ParsedFile:
        """Parse a Python file and extract structure.

        Args:
            path: Relative path to the file.
            content: Raw file contents as bytes.

        Returns:
            ParsedFile with imports, definitions, entry points, and complexity.
            Returns empty ParsedFile on syntax errors (logs warning).
        """
        path_str = str(path)

        try:
            source = content.decode("utf-8", errors="replace")
        except (UnicodeDecodeError, LookupError) as e:
            logger.warning("Could not decode %s: %s", path_str, e)
            return ParsedFile(path=path_str, language="python")

        line_count = source.count("\n") + 1 if source else 0

        try:
            tree = ast.parse(source)
        except SyntaxError as e:
            logger.warning("Syntax error in %s: %s", path_str, e)
            return ParsedFile(path=path_str, language="python", line_count=line_count)

        # Extract all information
        imports = self._extract_imports(tree, path_str)
        definitions = self._extract_definitions(tree)
        entry_points = self._detect_entry_points(tree, path_str)
        complexity = self._calculate_complexity(tree)

        return ParsedFile(
            path=path_str,
            language="python",
            imports=imports,
            definitions=definitions,
            entry_points=entry_points,
            complexity=complexity,
            line_count=line_count,
        )

    def resolve_import(
        self,
        importing_file: str,
        import_target: str,
        repo_files: set[str],
    ) -> str | None:
        """Resolve a Python import to a file path in the repo.

        Handles:
        - Relative imports (.foo, ..bar)
        - Absolute imports (package.module)
        - Package imports (package/__init__.py)

        Args:
            importing_file: Path of the file containing the import.
            import_target: The raw import string (e.g., "os.path", ".utils").
            repo_files: Set of all file paths in the repository.

        Returns:
            Resolved file path relative to repo root, or None for external imports.
        """
        # Handle relative imports
        if import_target.startswith("."):
            resolved = self._resolve_relative_import(
                importing_file, import_target, repo_files
            )
            if resolved:
                return resolved
            return None

        # Try absolute import resolution
        return self._resolve_absolute_import(import_target, repo_files)

    def _extract_imports(self, tree: ast.AST, source_path: str) -> list[ParsedImport]:
        """Extract all import statements from the AST."""
        imports: list[ParsedImport] = []

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    imports.append(
                        ParsedImport(
                            source_path=source_path,
                            target=alias.name,
                            is_relative=False,
                            names=[alias.name],
                        )
                    )

            elif isinstance(node, ast.ImportFrom):
                module = node.module or ""
                is_relative = node.level > 0
                target = "." * node.level + module if is_relative else module

                # Extract imported names
                names = [alias.name for alias in node.names]

                imports.append(
                    ParsedImport(
                        source_path=source_path,
                        target=target,
                        is_relative=is_relative,
                        names=names,
                    )
                )

        return imports

    def _extract_definitions(self, tree: ast.AST) -> list[FunctionDef]:
        """Extract function and class definitions from the AST."""
        definitions: list[FunctionDef] = []
        method_nodes: set[int] = set()

        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                definitions.append(
                    FunctionDef(
                        name=node.name,
                        line_start=node.lineno,
                        line_end=node.end_lineno or node.lineno,
                        is_async=False,
                        kind="class",
                    )
                )
                # Track methods defined within this class
                for child in ast.iter_child_nodes(node):
                    if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
                        method_nodes.add(id(child))
                        is_async = isinstance(child, ast.AsyncFunctionDef)
                        definitions.append(
                            FunctionDef(
                                name=f"{node.name}.{child.name}",
                                line_start=child.lineno,
                                line_end=child.end_lineno or child.lineno,
                                is_async=is_async,
                                kind="method",
                            )
                        )

            elif isinstance(node, ast.FunctionDef):
                # Skip if this is a method we already added
                if id(node) in method_nodes:
                    continue

                # Top-level function — method_nodes check prevents duplicate entries
                # for functions that are also class methods
                definitions.append(
                    FunctionDef(
                        name=node.name,
                        line_start=node.lineno,
                        line_end=node.end_lineno or node.lineno,
                        is_async=False,
                        kind="function",
                    )
                )

            elif isinstance(node, ast.AsyncFunctionDef):
                # Skip if this is a method we already added
                if id(node) in method_nodes:
                    continue
                definitions.append(
                    FunctionDef(
                        name=node.name,
                        line_start=node.lineno,
                        line_end=node.end_lineno or node.lineno,
                        is_async=True,
                        kind="function",
                    )
                )

        return definitions

    def _detect_entry_points(self, tree: ast.AST, file_path: str) -> list[EntryPoint]:
        """Detect entry points in the Python file."""
        entry_points: list[EntryPoint] = []

        for node in ast.walk(tree):
            # Check for if __name__ == "__main__"
            if isinstance(node, ast.If) and self._is_main_guard(node):
                entry_points.append(
                    EntryPoint(
                        file_path=file_path,
                        entry_type=EntryType.MAIN,
                        description="if __name__ == '__main__' block",
                        line=node.lineno,
                    )
                )

            # Check for CLI app assignments (Flask, FastAPI, Typer)
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id == "app":
                        entry_type = self._detect_app_type(node.value)
                        if entry_type:
                            entry_points.append(
                                EntryPoint(
                                    file_path=file_path,
                                    entry_type=entry_type,
                                    description=f"App instance: {target.id}",
                                    line=node.lineno,
                                )
                            )

        return entry_points

    def _is_main_guard(self, node: ast.If) -> bool:
        """Check if an if statement is if __name__ == '__main__'."""
        test = node.test

        # Handle: if __name__ == "__main__"
        if not isinstance(test, ast.Compare):
            return False

        if not (len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)):
            return False

        left = test.left
        comparators = test.comparators

        if (
            isinstance(left, ast.Name)
            and left.id == "__name__"
            and len(comparators) == 1
        ):
            comp = comparators[0]
            if isinstance(comp, ast.Constant) and comp.value == "__main__":
                return True

        return False

    def _detect_app_type(self, value: ast.expr) -> EntryType | None:
        """Detect the type of app being created."""
        if isinstance(value, ast.Call):
            func = value.func

            # Check for Flask(), FastAPI(), Typer()
            if isinstance(func, ast.Name):
                if func.id in ("Flask", "FastAPI"):
                    return EntryType.API_ROUTE
                if func.id == "Typer":
                    return EntryType.CLI

            # Check for flask.Flask(), fastapi.FastAPI(), typer.Typer()
            if isinstance(func, ast.Attribute):
                if func.attr in ("Flask", "FastAPI"):
                    return EntryType.API_ROUTE
                if func.attr == "Typer":
                    return EntryType.CLI

        return None

    def _calculate_complexity(self, tree: ast.AST) -> int:
        """Calculate approximate cyclomatic complexity.

        Counts branching nodes: if, elif, for, while, except, with,
        and, or, assert, comprehensions.
        """
        complexity = 0

        for node in ast.walk(tree):
            if isinstance(
                node,
                (
                    ast.If,
                    ast.For,
                    ast.While,
                    ast.ExceptHandler,
                    ast.With,
                    ast.Assert,
                    ast.ListComp,
                    ast.SetComp,
                    ast.DictComp,
                    ast.GeneratorExp,
                ),
            ):
                complexity += 1
            # Count 'and' and 'or' in boolean expressions
            elif isinstance(node, ast.BoolOp):
                complexity += len(node.values) - 1

        return complexity

    def _resolve_relative_import(
        self,
        importing_file: str,
        import_target: str,
        repo_files: set[str],
    ) -> str | None:
        """Resolve a relative import to a file path."""
        # Count leading dots
        level = 0
        for char in import_target:
            if char == ".":
                level += 1
            else:
                break

        module_part = import_target[level:]

        # level=1 (.) means the current package directory — stay put.
        # level=2 (..) means the parent package — go up 1, etc.
        go_up = level - 1

        base = Path(importing_file).parent
        for _ in range(go_up):
            base = base.parent

        base_path = str(base)
        if base_path == ".":
            base_path = ""

        # Build candidate paths
        if module_part:
            module_rel = module_part.replace(".", "/")
            candidates = [
                f"{base_path}/{module_rel}.py" if base_path else f"{module_rel}.py",
                (
                    f"{base_path}/{module_rel}/__init__.py"
                    if base_path
                    else f"{module_rel}/__init__.py"
                ),
            ]
        else:
            # from . import foo - package level
            candidates = []

        for candidate in candidates:
            candidate = candidate.lstrip("/")
            if candidate in repo_files:
                return candidate

        return None

    def _resolve_absolute_import(
        self, import_target: str, repo_files: set[str]
    ) -> str | None:
        """Resolve an absolute import to a file path.

        Checks both direct paths (root-level packages) and src/-prefixed
        paths (src layout, e.g., "unfamiliar.config" → "src/unfamiliar/config.py").
        """
        module_path = import_target.replace(".", "/")

        candidates = [
            f"{module_path}.py",
            f"{module_path}/__init__.py",
        ]

        for candidate in candidates:
            if candidate in repo_files:
                return candidate

            # Support src/ layout and other directory-prefixed layouts
            suffix = "/" + candidate
            for repo_file in repo_files:
                if repo_file.endswith(suffix):
                    return repo_file

        return None
