import logging

import libcst as cst
from libcst import MetadataWrapper
from libcst.metadata import ByteSpanPositionProvider, PositionProvider

from py_agent.models.testable import Position, Testable

logger = logging.getLogger(__name__)


def get_current_class_name(current_class_node: cst.ClassDef | None) -> str | None:
    if current_class_node is None:
        return None
    return current_class_node.name.value


def _has_public_methods(class_node: cst.ClassDef) -> bool:
    """Check if a class has at least one public (non-private, non-dunder) method."""
    for stmt in class_node.body.body:
        if isinstance(stmt, cst.FunctionDef):
            name = stmt.name.value
            if not name.startswith("_"):
                return True
    return False


class TestablesVisitor(cst.CSTVisitor):
    METADATA_DEPENDENCIES = (PositionProvider, ByteSpanPositionProvider)

    def __init__(
        self,
        module: cst.Module,
        metadata_wrapper: MetadataWrapper,
        absolute_file_path: str,
        relative_file_path: str,
        project_path: str,
    ):
        super().__init__()
        self.module = module
        self.metadata_wrapper = metadata_wrapper
        self.testables: list[Testable] = []
        self.current_class_node: cst.ClassDef | None = None
        self.absolute_file_path = absolute_file_path
        self.relative_file_path = relative_file_path
        self.project_path = project_path
        self.class_stack: list[cst.ClassDef] = []
        self.function_stack: list[cst.FunctionDef] = []

    def visit_FunctionDef(self, function_node: cst.FunctionDef) -> bool | None:
        self.function_stack.append(function_node)
        if len(self.function_stack) > 1:
            return False

        class_name = get_current_class_name(self.current_class_node)
        position_metadata = self.get_metadata(PositionProvider, function_node)
        bytespan_metadata = self.get_metadata(ByteSpanPositionProvider, function_node)
        position = Position.from_metadata(position_metadata, bytespan_metadata)
        name = function_node.name.value
        testable = Testable(
            type=Testable.get_type("function", class_name),
            name=name,
            position=position,
            parent_name=class_name,
            file_path=self.absolute_file_path,
            can_create_tests=not name.startswith("_"),
        )
        self.testables.append(testable)
        return None

    def leave_FunctionDef(self, node: cst.FunctionDef) -> None:
        self.function_stack.pop()

    @staticmethod
    def _is_dataclass(class_node: cst.ClassDef) -> bool:
        for decorator in class_node.decorators:
            deco = decorator.decorator
            if isinstance(deco, cst.Name) and deco.value == "dataclass":
                return True
            if (
                isinstance(deco, cst.Call)
                and isinstance(deco.func, cst.Name)
                and deco.func.value == "dataclass"
            ):
                return True
        return False

    @staticmethod
    def _has_custom_methods(class_node: cst.ClassDef) -> bool:
        for stmt in class_node.body.body:
            if isinstance(stmt, cst.FunctionDef):
                return True
        return False

    def visit_ClassDef(self, class_node: cst.ClassDef) -> bool | None:
        self.class_stack.append(class_node)
        if len(self.class_stack) > 1:
            return False
        self.current_class_node = class_node

        # Skip pure dataclasses with no custom methods
        if self._is_dataclass(class_node) and not self._has_custom_methods(class_node):
            logger.info(f"Skipping pure dataclass: {class_node.name.value}")
            return None

        position_metadata = self.get_metadata(PositionProvider, class_node)
        bytespan_metadata = self.get_metadata(ByteSpanPositionProvider, class_node)
        position = Position.from_metadata(position_metadata, bytespan_metadata)
        testable = Testable(
            type="class",
            name=class_node.name.value,
            position=position,
            file_path=self.absolute_file_path,
            can_create_tests=_has_public_methods(class_node),
        )
        self.testables.append(testable)
        return None

    def leave_ClassDef(self, class_node: cst.ClassDef) -> None:
        self.class_stack.pop()
        self.current_class_node = None
