"""Python AST parser using the built-in ast module.

This module provides Python code analysis using Python's native AST module
for accurate symbol extraction and code understanding.
"""

from __future__ import annotations

import ast
from pathlib import Path
from typing import Optional, Any

from ieum.ast.base import (
    BaseASTParser,
    Symbol,
    SymbolType,
    Reference,
    CodeRange,
    ParseResult,
    Visibility,
)


class PythonASTParser(BaseASTParser):
    """Python AST parser using the built-in ast module."""

    EXTENSIONS = [".py", ".pyi", ".pyw"]
    LANGUAGE = "python"

    def parse_file(self, file_path: str | Path) -> ParseResult:
        """Parse a Python file.

        Args:
            file_path: Path to the Python file.

        Returns:
            ParseResult with extracted symbols.
        """
        file_path = Path(file_path)
        result = ParseResult(file_path=str(file_path), language=self.LANGUAGE)

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                source = f.read()
            return self.parse_source(source, str(file_path))
        except FileNotFoundError:
            result.errors.append(f"File not found: {file_path}")
        except PermissionError:
            result.errors.append(f"Permission denied: {file_path}")
        except Exception as e:
            result.errors.append(f"Error reading file: {e}")

        return result

    def parse_source(self, source: str, file_path: str = "<string>") -> ParseResult:
        """Parse Python source code.

        Args:
            source: Python source code.
            file_path: Virtual file path.

        Returns:
            ParseResult with extracted symbols.
        """
        result = ParseResult(file_path=file_path, language=self.LANGUAGE)

        try:
            tree = ast.parse(source)
            self._extract_symbols(tree, result, source)
            self._extract_references(tree, result, source)
            self._extract_imports(tree, result)
        except SyntaxError as e:
            result.errors.append(f"Syntax error at line {e.lineno}: {e.msg}")
        except Exception as e:
            result.errors.append(f"Parse error: {e}")

        return result

    def _extract_symbols(
        self,
        tree: ast.AST,
        result: ParseResult,
        source: str,
        parent: Optional[str] = None,
    ) -> None:
        """Extract symbols from AST.

        Args:
            tree: AST node to process.
            result: ParseResult to populate.
            source: Original source code.
            parent: Parent symbol name for nested definitions.
        """
        for node in ast.walk(tree):
            symbol = self._node_to_symbol(node, result.file_path, source, parent)
            if symbol:
                result.symbols.append(symbol)

                # Process nested definitions (methods in classes)
                if isinstance(node, ast.ClassDef):
                    for child in node.body:
                        if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
                            nested = self._node_to_symbol(
                                child, result.file_path, source, node.name
                            )
                            if nested:
                                result.symbols.append(nested)

    def _node_to_symbol(
        self,
        node: ast.AST,
        file_path: str,
        source: str,
        parent: Optional[str] = None,
    ) -> Optional[Symbol]:
        """Convert an AST node to a Symbol.

        Args:
            node: AST node.
            file_path: Source file path.
            source: Original source code.
            parent: Parent symbol name.

        Returns:
            Symbol or None if node is not a symbol definition.
        """
        if isinstance(node, ast.FunctionDef):
            return self._create_function_symbol(
                node, file_path, source, parent, is_async=False
            )

        elif isinstance(node, ast.AsyncFunctionDef):
            return self._create_function_symbol(
                node, file_path, source, parent, is_async=True
            )

        elif isinstance(node, ast.ClassDef):
            return self._create_class_symbol(node, file_path, source, parent)

        elif isinstance(node, ast.Assign):
            # Module-level variable assignments
            if parent is None and hasattr(node, "lineno"):
                for target in node.targets:
                    if isinstance(target, ast.Name):
                        return Symbol(
                            name=target.id,
                            type=SymbolType.VARIABLE,
                            file_path=file_path,
                            range=CodeRange(
                                start_line=node.lineno,
                                start_column=node.col_offset,
                                end_line=node.end_lineno or node.lineno,
                                end_column=node.end_col_offset or 0,
                            ),
                        )

        return None

    def _create_function_symbol(
        self,
        node: ast.FunctionDef | ast.AsyncFunctionDef,
        file_path: str,
        source: str,
        parent: Optional[str],
        is_async: bool,
    ) -> Symbol:
        """Create a function symbol from AST node."""
        # Determine symbol type
        if parent:
            symbol_type = SymbolType.METHOD
            # Check for static/class methods
            is_static = any(
                isinstance(d, ast.Name) and d.id in ("staticmethod", "classmethod")
                for d in node.decorator_list
            )
        else:
            symbol_type = SymbolType.ASYNC_FUNCTION if is_async else SymbolType.FUNCTION
            is_static = False

        # Extract parameters
        parameters = self._extract_parameters(node.args)

        # Extract decorators
        decorators = []
        for decorator in node.decorator_list:
            if isinstance(decorator, ast.Name):
                decorators.append(decorator.id)
            elif isinstance(decorator, ast.Attribute):
                decorators.append(ast.unparse(decorator))
            elif isinstance(decorator, ast.Call):
                if isinstance(decorator.func, ast.Name):
                    decorators.append(decorator.func.id)

        # Build signature
        signature = self._build_signature(node)

        # Extract return type
        return_type = None
        if node.returns:
            try:
                return_type = ast.unparse(node.returns)
            except Exception:
                pass

        # Extract docstring
        docstring = ast.get_docstring(node)

        # Determine visibility
        visibility = Visibility.PRIVATE if node.name.startswith("_") else Visibility.PUBLIC
        if node.name.startswith("__") and not node.name.endswith("__"):
            visibility = Visibility.PRIVATE

        return Symbol(
            name=node.name,
            type=symbol_type,
            file_path=file_path,
            range=CodeRange(
                start_line=node.lineno,
                start_column=node.col_offset,
                end_line=node.end_lineno or node.lineno,
                end_column=node.end_col_offset or 0,
            ),
            parent=parent,
            signature=signature,
            docstring=docstring,
            visibility=visibility,
            is_static=is_static,
            is_async=is_async,
            return_type=return_type,
            parameters=parameters,
            decorators=decorators,
        )

    def _create_class_symbol(
        self,
        node: ast.ClassDef,
        file_path: str,
        source: str,
        parent: Optional[str],
    ) -> Symbol:
        """Create a class symbol from AST node."""
        # Extract base classes
        base_classes = []
        for base in node.bases:
            try:
                base_classes.append(ast.unparse(base))
            except Exception:
                pass

        # Extract decorators
        decorators = []
        for decorator in node.decorator_list:
            if isinstance(decorator, ast.Name):
                decorators.append(decorator.id)
            elif isinstance(decorator, ast.Attribute):
                try:
                    decorators.append(ast.unparse(decorator))
                except Exception:
                    pass

        # Extract docstring
        docstring = ast.get_docstring(node)

        return Symbol(
            name=node.name,
            type=SymbolType.CLASS,
            file_path=file_path,
            range=CodeRange(
                start_line=node.lineno,
                start_column=node.col_offset,
                end_line=node.end_lineno or node.lineno,
                end_column=node.end_col_offset or 0,
            ),
            parent=parent,
            docstring=docstring,
            decorators=decorators,
            base_classes=base_classes,
        )

    def _extract_parameters(self, args: ast.arguments) -> list[dict]:
        """Extract function parameters.

        Args:
            args: AST arguments node.

        Returns:
            List of parameter dictionaries.
        """
        parameters = []

        # Regular arguments
        defaults_offset = len(args.args) - len(args.defaults)
        for i, arg in enumerate(args.args):
            param = {
                "name": arg.arg,
                "type": None,
                "default": None,
            }
            if arg.annotation:
                try:
                    param["type"] = ast.unparse(arg.annotation)
                except Exception:
                    pass
            if i >= defaults_offset:
                try:
                    param["default"] = ast.unparse(args.defaults[i - defaults_offset])
                except Exception:
                    pass
            parameters.append(param)

        # *args
        if args.vararg:
            param = {"name": f"*{args.vararg.arg}", "type": None}
            if args.vararg.annotation:
                try:
                    param["type"] = ast.unparse(args.vararg.annotation)
                except Exception:
                    pass
            parameters.append(param)

        # **kwargs
        if args.kwarg:
            param = {"name": f"**{args.kwarg.arg}", "type": None}
            if args.kwarg.annotation:
                try:
                    param["type"] = ast.unparse(args.kwarg.annotation)
                except Exception:
                    pass
            parameters.append(param)

        return parameters

    def _build_signature(
        self,
        node: ast.FunctionDef | ast.AsyncFunctionDef,
    ) -> str:
        """Build function signature string.

        Args:
            node: Function AST node.

        Returns:
            Function signature string.
        """
        try:
            params = []
            for param in self._extract_parameters(node.args):
                p = param["name"]
                if param.get("type"):
                    p += f": {param['type']}"
                if param.get("default"):
                    p += f" = {param['default']}"
                params.append(p)

            sig = f"def {node.name}({', '.join(params)})"
            if node.returns:
                sig += f" -> {ast.unparse(node.returns)}"

            return sig
        except Exception:
            return f"def {node.name}(...)"

    def _extract_references(
        self,
        tree: ast.AST,
        result: ParseResult,
        source: str,
    ) -> None:
        """Extract symbol references from AST.

        Args:
            tree: AST to analyze.
            result: ParseResult to populate.
            source: Original source code.
        """
        source_lines = source.split("\n")

        for node in ast.walk(tree):
            # Function/method calls
            if isinstance(node, ast.Call):
                name = None
                if isinstance(node.func, ast.Name):
                    name = node.func.id
                elif isinstance(node.func, ast.Attribute):
                    name = node.func.attr

                if name and hasattr(node, "lineno"):
                    line = node.lineno - 1
                    context = source_lines[line] if line < len(source_lines) else ""
                    result.references.append(Reference(
                        symbol_name=name,
                        file_path=result.file_path,
                        line=node.lineno,
                        column=node.col_offset,
                        context=context.strip(),
                        reference_type="call",
                    ))

            # Name references (variables, etc.)
            elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
                if hasattr(node, "lineno"):
                    line = node.lineno - 1
                    context = source_lines[line] if line < len(source_lines) else ""
                    result.references.append(Reference(
                        symbol_name=node.id,
                        file_path=result.file_path,
                        line=node.lineno,
                        column=node.col_offset,
                        context=context.strip(),
                        reference_type="usage",
                    ))

    def _extract_imports(self, tree: ast.AST, result: ParseResult) -> None:
        """Extract import statements.

        Args:
            tree: AST to analyze.
            result: ParseResult to populate.
        """
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    result.imports.append({
                        "type": "import",
                        "module": alias.name,
                        "alias": alias.asname,
                        "line": node.lineno,
                    })

            elif isinstance(node, ast.ImportFrom):
                module = node.module or ""
                for alias in node.names:
                    result.imports.append({
                        "type": "from",
                        "module": module,
                        "name": alias.name,
                        "alias": alias.asname,
                        "line": node.lineno,
                    })

    def get_symbol_at_position(
        self,
        source: str,
        line: int,
        column: int,
    ) -> Optional[Symbol]:
        """Get the symbol at a specific position.

        Args:
            source: Source code.
            line: Line number (1-based).
            column: Column number (0-based).

        Returns:
            Symbol at position or None.
        """
        result = self.parse_source(source)

        # Find the most specific symbol containing this position
        best_match: Optional[Symbol] = None
        smallest_range = float("inf")

        for symbol in result.symbols:
            if symbol.range.contains(line, column):
                range_size = (
                    (symbol.range.end_line - symbol.range.start_line) * 1000
                    + (symbol.range.end_column - symbol.range.start_column)
                )
                if range_size < smallest_range:
                    smallest_range = range_size
                    best_match = symbol

        return best_match

    def get_all_functions(self, source: str) -> list[Symbol]:
        """Get all functions from source code.

        Args:
            source: Python source code.

        Returns:
            List of function symbols.
        """
        result = self.parse_source(source)
        return [
            s for s in result.symbols
            if s.type in (SymbolType.FUNCTION, SymbolType.METHOD, SymbolType.ASYNC_FUNCTION)
        ]

    def get_all_classes(self, source: str) -> list[Symbol]:
        """Get all classes from source code.

        Args:
            source: Python source code.

        Returns:
            List of class symbols.
        """
        result = self.parse_source(source)
        return [s for s in result.symbols if s.type == SymbolType.CLASS]
