"""Base abstractions for multi-language code parsing.

Provides the Function dataclass, ParsingEngine enum, LanguageParser ABC,
and TreeSitterParser base class that encapsulates shared tree-sitter logic.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any

from tree_sitter import Language, Node, Parser, Query, QueryCursor


class ParsingEngine(Enum):
    """Supported parsing engines."""

    TREE_SITTER = auto()
    LIBCLANG = auto()
    PYGMENTS = auto()
    VERIBLE = auto()


@dataclass
class Function:
    """Represents a parsed function definition."""

    name: str = ""
    signature: str = ""
    return_type: str = ""
    line: int = -1
    start: int = -1
    end: int = -1
    ccm: int = -1
    modified_ccm: int = -1
    statements: int = -1
    fqdn: str = ""


class LanguageParser(ABC):
    """Abstract base class that every language parser must implement."""

    # Subclasses should set these class-level attributes
    language_name: str = ""
    file_extensions: list[str] = []
    engine: ParsingEngine = ParsingEngine.TREE_SITTER

    @classmethod
    def can_handle(cls, filename: str) -> bool:
        """Check if this parser can handle the given filename by extension."""
        import os

        ext = os.path.splitext(filename)[1].lower()
        return ext in cls.file_extensions

    @abstractmethod
    def parse(self, code: bytes) -> Any:
        """Parse *code* and return the syntax tree root node."""

    @abstractmethod
    def get_function_definitions(self, code: bytes) -> list[Function]:
        """Extract all function definitions from *code*."""


# ---------------------------------------------------------------------------
# Tree-sitter shared base
# ---------------------------------------------------------------------------

class TreeSitterParser(LanguageParser):
    """Concrete base class that factors out all shared tree-sitter logic.

    Language-specific subclasses override a small number of hooks:
    - ``_get_ts_language()``
    - ``_get_function_query()``
    - ``_extract_function()``
    - ``_compute_cyclomatic_complexity()``
    - ``_compute_fqdn()``
    """

    engine = ParsingEngine.TREE_SITTER

    def __init__(self) -> None:
        ts_lang = self._get_ts_language()
        self._language = Language(ts_lang)
        self._parser = Parser(self._language)

    # -- public API ----------------------------------------------------------

    def parse(self, code: bytes) -> Node:
        """Parse *code* and return the AST root node."""
        tree = self._parser.parse(code)
        return tree.root_node

    def get_function_definitions(self, code: bytes) -> list[Function]:
        """Extract all function definitions from *code*."""
        root = self.parse(code)
        functions: list[Function] = []

        for node in self._query_function_nodes(root):
            func = self._extract_function(node, code)
            func.ccm = self._compute_cyclomatic_complexity(node)
            func.modified_ccm = self._compute_modified_cyclomatic_complexity(node)
            func.statements = self._count_statements(node)
            func.fqdn = self._compute_fqdn(node, func.name)
            functions.append(func)

        return sorted(functions, key=lambda f: f.start)

    # -- tree-sitter helpers -------------------------------------------------

    def capture_nodes(self, root: Node, query_str: str) -> dict[str, list[Node]]:
        """Run a tree-sitter query and return ``{capture_name: [nodes]}``."""
        query = Query(self._language, query_str)
        cursor = QueryCursor(query)
        return cursor.captures(root)

    # -- hooks for subclasses ------------------------------------------------

    @staticmethod
    @abstractmethod
    def _get_ts_language() -> Any:
        """Return the tree-sitter language object (e.g. ``tree_sitter_c.language()``)."""

    @staticmethod
    @abstractmethod
    def _get_function_query() -> str:
        """Return a tree-sitter S-expression that captures top-level function nodes
        as ``@func``."""

    @abstractmethod
    def _extract_function(self, node: Node, code: bytes) -> Function:
        """Build a ``Function`` from a captured function *node*."""

    def _query_function_nodes(self, root: Node) -> list[Node]:
        """Return all function-definition nodes using the subclass query."""
        captures = self.capture_nodes(root, self._get_function_query())
        return captures.get("func", [])

    def _compute_cyclomatic_complexity(self, node: Node) -> int:
        """Compute *traditional* cyclomatic complexity for a function *node*.

        Counts each ``case`` as a separate decision point.
        """
        complexity = 1
        decision_types = self._get_decision_node_types()
        self._walk_complexity(node, decision_types, complexity_counter := [complexity])
        return complexity_counter[0]

    def _compute_modified_cyclomatic_complexity(self, node: Node) -> int:
        """Compute *modified* cyclomatic complexity for a function *node*.

        Treats the whole ``switch`` as a single decision instead of counting
        each ``case`` separately.
        """
        complexity = 1
        decision_types = self._get_modified_decision_node_types()
        self._walk_complexity(node, decision_types, complexity_counter := [complexity])
        return complexity_counter[0]

    def _walk_complexity(
        self, node: Node, decision_types: set[str], counter: list[int]
    ) -> None:
        if node.type in decision_types:
            counter[0] += 1
        for child in node.children:
            self._walk_complexity(child, decision_types, counter)

    def _get_decision_node_types(self) -> set[str]:
        """Return node types for *traditional* cyclomatic complexity.

        Each ``case`` counts as a separate decision point.
        Subclasses should override to provide language-specific decision types.
        """
        return {
            "if_statement",
            "for_statement",
            "while_statement",
            "case_statement",
            "catch_clause",
            "&&",
            "||",
        }

    def _get_modified_decision_node_types(self) -> set[str]:
        """Return node types for *modified* cyclomatic complexity.

        The whole ``switch`` counts as one decision instead of each ``case``.
        Subclasses should override to provide language-specific decision types.
        """
        return {
            "if_statement",
            "for_statement",
            "while_statement",
            "switch_statement",
            "catch_clause",
            "&&",
            "||",
        }

    def _count_statements(self, node: Node) -> int:
        """Count the number of statements inside a function *node*."""
        stmt_types = self._get_statement_node_types()
        counter = [0]
        self._walk_statements(node, stmt_types, counter)
        return counter[0]

    def _walk_statements(
        self, node: Node, stmt_types: set[str], counter: list[int]
    ) -> None:
        if node.type in stmt_types:
            counter[0] += 1
        for child in node.children:
            self._walk_statements(child, stmt_types, counter)

    def _get_statement_node_types(self) -> set[str]:
        """Return node types considered as statements.

        Subclasses should override to provide language-specific statement types.
        """
        return {
            "expression_statement",
            "declaration",
            "return_statement",
            "if_statement",
            "for_statement",
            "while_statement",
            "do_statement",
            "switch_statement",
            "break_statement",
            "continue_statement",
            "goto_statement",
            "case_statement",
        }

    def _compute_fqdn(self, node: Node, name: str) -> str:
        """Compute the fully-qualified name by walking ancestor nodes.

        Default implementation joins parent class/namespace/module names.
        Subclasses may override for language-specific scoping rules.
        """
        parts: list[str] = []
        current = node.parent
        while current is not None:
            part = self._get_scope_name(current)
            if part:
                parts.append(part)
            current = current.parent
        parts.reverse()
        parts.append(name)
        return "::".join(parts) if len(parts) > 1 else name

    def _get_scope_name(self, node: Node) -> str | None:
        """Extract a scope name (class, namespace, etc.) from a parent *node*.

        Subclasses should override to recognise language-specific scoping nodes.
        """
        return None
