"""
Python AST-based docstring checker.

This module uses Python's ast module for accurate detection of functions and classes
that are missing docstrings. It handles edge cases that regex-based approaches miss:
- Decorated functions (where decorator comes before def)
- Nested functions and classes
- Async functions
- Properties and other special methods
"""

import ast
import sys
from pathlib import Path


class DocstringChecker(ast.NodeVisitor):
    """AST visitor that checks for missing docstrings in functions and classes."""

    def __init__(self, filename: str):
        """Initialize the checker.

        Args:
            filename: Path to the file being checked (for error reporting)
        """
        self.filename = filename
        self.missing: list[tuple[str, int, str]] = []  # (type, line, name)
        self.private_names: set[str] = set()  # Track private functions/classes

    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:  # noqa: N802
        """Visit a function definition.

        Args:
            node: The function definition AST node
        """
        self._check_docstring(node, "function")
        self.generic_visit(node)

    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:  # noqa: N802
        """Visit an async function definition.

        Args:
            node: The async function definition AST node
        """
        self._check_docstring(node, "async function")
        self.generic_visit(node)

    def visit_ClassDef(self, node: ast.ClassDef) -> None:  # noqa: N802
        """Visit a class definition.

        Args:
            node: The class definition AST node
        """
        self._check_docstring(node, "class")
        self.generic_visit(node)

    def _check_docstring(
        self,
        node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
        node_type: str,
    ) -> None:
        """Check if a function or class has a docstring.

        Args:
            node: The AST node to check
            node_type: Type of node ("function", "async function", or "class")
        """
        # Skip private functions/classes (starting with _) unless they're special methods
        if node.name.startswith("_") and not (
            node.name.startswith("__") and node.name.endswith("__")
        ):
            return

        # Check for docstring
        docstring = ast.get_docstring(node)
        if docstring is None:
            self.missing.append((node_type, node.lineno, node.name))


def check_file(filepath: Path) -> tuple[bool, list[tuple[str, int, str]]]:
    """Check a Python file for missing docstrings.

    Args:
        filepath: Path to the Python file to check

    Returns:
        Tuple of (has_definitions, missing_docstrings) where:
        - has_definitions: True if file contains any functions/classes
        - missing_docstrings: List of (type, line, name) for missing docstrings
    """
    try:
        with open(filepath, encoding="utf-8") as f:
            source = f.read()

        # Parse the Python file
        tree = ast.parse(source, filename=str(filepath))

        # Check for docstrings
        checker = DocstringChecker(str(filepath))
        checker.visit(tree)

        # Determine if file has any function/class definitions
        has_definitions = any(
            isinstance(  # noqa: UP038
                node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
            )
            for node in ast.walk(tree)
        )

        return has_definitions, checker.missing

    except SyntaxError as e:
        # File has syntax errors - can't parse
        print(
            f"⚠ Warning: Syntax error in {filepath}:{e.lineno} - skipping docstring check",
            file=sys.stderr,
        )
        return False, []
    except UnicodeDecodeError:
        # File encoding issue
        print(
            f"⚠ Warning: Encoding error in {filepath} - skipping docstring check",
            file=sys.stderr,
        )
        return False, []
    except Exception as e:
        # Other errors
        print(
            f"⚠ Warning: Error checking {filepath}: {e} - skipping",
            file=sys.stderr,
        )
        return False, []


def check_files(filepaths: list[Path]) -> int:
    """Check multiple Python files for missing docstrings.

    Args:
        filepaths: List of paths to Python files to check

    Returns:
        Exit code: 0 if all checked files have docstrings,
                  1 if missing docstrings found,
                  2 for usage errors
    """
    if not filepaths:
        print("Usage: check-docstrings <file1.py> [file2.py ...]", file=sys.stderr)
        return 2

    found_issues = False

    for filepath in filepaths:
        if not filepath.exists():
            print(f"⚠ Warning: {filepath} does not exist - skipping", file=sys.stderr)
            continue

        if not filepath.is_file():
            print(f"⚠ Warning: {filepath} is not a file - skipping", file=sys.stderr)
            continue

        has_definitions, missing = check_file(filepath)

        if missing:
            found_issues = True
            print(f"⚠ {filepath} is missing docstrings:")
            for node_type, line, name in missing:
                print(f"  Line {line}: {node_type} '{name}'")

    return 1 if found_issues else 0
