from __future__ import annotations

import hashlib
import importlib.metadata
import re
from pathlib import Path
from typing import Any, cast

from ace_lite.parsers.base import ImportEntry, ParseResult, ReferenceEntry, SymbolEntry
from ace_lite.parsers.languages import detect_language, module_name, normalize_languages
from ace_lite.parsers.markdown_parser import parse_markdown

tree_sitter_get_parser: Any | None
try:
    from tree_sitter_language_pack import get_parser as _tree_sitter_get_parser
except Exception:  # pragma: no cover
    tree_sitter_get_parser = None
else:  # pragma: no cover
    tree_sitter_get_parser = _tree_sitter_get_parser

get_parser: Any | None = tree_sitter_get_parser


def _is_generated_code(*, language: str, relative_path: str, source_text: str) -> bool:
    """Best-effort detection for auto-generated source files.

    This is used to tag index entries so ranking can down-weight generated code
    without hard-excluding it (generated code can still be relevant for some
    queries, e.g. protobuf or contract bindings).
    """

    basename = Path(str(relative_path or "")).name.lower()
    if not basename:
        return False

    if language == "go":
        if basename.endswith((".pb.go", "_generated.go", "_gen.go", "_mock.go")):
            return True
        if basename.startswith(("mock_", "zz_generated.")):
            return True

    if language == "python" and basename.endswith(("_pb2.py", "_pb2_grpc.py", "_grpc.py", "_generated.py")):
        return True

    if language in {"javascript", "typescript", "tsx"} and basename.endswith(
        (".generated.ts", ".generated.tsx", ".generated.js", ".generated.jsx")
    ):
        return True

    if language == "c_sharp" and basename.endswith((".designer.cs", ".generated.cs", ".g.cs", ".g.i.cs")):
        return True

    header_lines = source_text.splitlines()[:32]
    header = "\n".join(header_lines).lower()
    if not header:
        return False

    markers = (
        "code generated",
        "do not edit",
        "do not edit.",
        "autogenerated",
        "auto-generated",
        "this file was generated",
        "generated by",
        "generated from",
        "@generated",
        "autogenerated by",
        "generated by protoc",
    )
    if any(marker in header for marker in markers):
        return True

    path_lower = str(relative_path or "").lower()
    return ("/generated/" in path_lower or "/gen/" in path_lower) and (
        "generated" in header or "do not edit" in header
    )


class TreeSitterEngine:
    def __init__(self, languages: list[str] | tuple[str, ...] | set[str] | None = None) -> None:
        self._languages = set(normalize_languages(languages))
        self._parsers: dict[str, Any] = {}

    @property
    def parser_version(self) -> str:
        try:
            return importlib.metadata.version("tree-sitter-language-pack")
        except Exception:  # pragma: no cover
            return "unknown"

    def parse_file(self, file_path: Path, root_path: Path) -> dict[str, Any] | None:
        language = detect_language(file_path)
        if language is None or language not in self._languages:
            return None

        source_bytes = file_path.read_bytes()
        try:
            stat_result = file_path.stat()
            mtime_ns = getattr(stat_result, "st_mtime_ns", None)
            if not isinstance(mtime_ns, int):
                mtime_seconds = float(getattr(stat_result, "st_mtime", 0.0) or 0.0)
                mtime_ns = int(mtime_seconds * 1_000_000_000)
            size_bytes = int(getattr(stat_result, "st_size", len(source_bytes)))
        except OSError:  # pragma: no cover - defensive
            mtime_ns = 0
            size_bytes = len(source_bytes)

        source_text = source_bytes.decode("utf-8", errors="replace").lstrip("\ufeff")
        sha256 = hashlib.sha256(source_bytes).hexdigest()
        relative_path = file_path.relative_to(root_path).as_posix()

        result = ParseResult(
            language=language,
            module=module_name(Path(relative_path), language),
            sha256=sha256,
            mtime_ns=max(0, int(mtime_ns)),
            size_bytes=max(0, int(size_bytes)),
        )
        result.generated = _is_generated_code(
            language=language,
            relative_path=relative_path,
            source_text=source_text,
        )

        if language == "markdown":
            symbols, imports, references = parse_markdown(
                source_text=source_text,
                relative_path=relative_path,
            )
            result.symbols.extend(symbols)
            result.imports.extend(imports)
            result.references.extend(references)
            return cast(dict[str, Any] | None, result.to_index_entry(path=relative_path))

        parser = self._resolve_parser(language)
        if parser is None:
            result.parse_error = "tree-sitter parser unavailable"
            return cast(dict[str, Any] | None, result.to_index_entry(path=relative_path))

        try:
            tree = parser.parse(source_text.encode("utf-8", errors="replace"))
            root = tree.root_node
            symbols, imports, references = self._extract(language=language, root=root, source_text=source_text)
            result.symbols.extend(symbols)
            result.imports.extend(imports)
            result.references.extend(references)
        except Exception as exc:  # pragma: no cover
            result.parse_error = f"parse_error: {exc}"

        return cast(dict[str, Any] | None, result.to_index_entry(path=relative_path))

    def _resolve_parser(self, language: str) -> Any | None:
        if language in self._parsers:
            return self._parsers[language]

        if get_parser is None:
            self._parsers[language] = None
            return None

        try:
            parser = get_parser(language)
        except Exception:
            parser = None
        self._parsers[language] = parser
        return parser

    def _extract(
        self,
        *,
        language: str,
        root: Any,
        source_text: str,
    ) -> tuple[list[SymbolEntry], list[ImportEntry], list[ReferenceEntry]]:
        if language == "python":
            return self._extract_python(root=root)
        if language in {"javascript", "typescript", "tsx"}:
            return self._extract_js_like(root=root)
        if language == "go":
            return self._extract_go(root=root)
        if language == "solidity":
            return self._extract_solidity(root=root)
        if language in {"rust", "java", "ruby", "php", "kotlin", "swift", "c_sharp", "bash", "lua"}:
            return self._extract_generic_named_declarations(root=root)
        if language in {"c", "cpp"}:
            return self._extract_c_like(root=root)
        return ([], [], [])

    def _extract_solidity(self, *, root: Any) -> tuple[list[SymbolEntry], list[ImportEntry], list[ReferenceEntry]]:
        symbols: list[SymbolEntry] = []
        imports: list[ImportEntry] = []
        references: list[ReferenceEntry] = []

        def parse_import(node: Any) -> None:
            text = self._node_text(node).strip()
            if not text:
                return
            lineno = node.start_point[0] + 1

            alias_node = node.child_by_field_name("alias") if hasattr(node, "child_by_field_name") else None
            alias = self._node_text(alias_node).strip() if alias_node is not None else ""

            quoted = re.findall(r"\"([^\"]+)\"|'([^']+)'", text)
            module = ""
            for a, b in quoted[::-1]:
                module = (a or b or "").strip()
                if module:
                    break

            name = ""
            brace_match = re.search(r"import\\s*\\{([^}]+)\\}\\s*from\\s*", text, re.IGNORECASE)
            if brace_match:
                identifiers = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", brace_match.group(1))
                if identifiers:
                    name = ",".join(identifiers[:8])

            if module or alias or name:
                imports.append(
                    ImportEntry(
                        module=module or None,
                        name=name or None,
                        alias=alias or None,
                        lineno=lineno,
                        type="import",
                    )
                )

        def scope_kind(node_type: str) -> str | None:
            lowered = node_type.lower()
            if lowered in {"contract_declaration", "interface_declaration", "library_declaration"}:
                return "type"
            if lowered in {"struct_declaration", "enum_declaration"}:
                return "type"
            if lowered == "constructor_definition":
                return "method"
            if lowered == "function_definition":
                return "function"
            if lowered == "modifier_definition":
                return "modifier"
            if lowered == "event_definition":
                return "event"
            if lowered == "error_declaration":
                return "error"
            return None

        def walk(node: Any, scope: list[str]) -> None:
            node_type = str(getattr(node, "type", "") or "")
            if node_type == "import_directive":
                parse_import(node)

            kind = scope_kind(node_type)
            name = ""
            if kind is not None:
                name_node = node.child_by_field_name("name") if hasattr(node, "child_by_field_name") else None
                name = self._node_text(name_node).strip() if name_node is not None else ""
                if node_type == "constructor_definition":
                    name = "constructor"
                if not name:
                    name = self._find_identifier_like(node, max_depth=3) or ""

            next_scope = scope
            if kind is not None and name:
                qualified = ".".join([*scope, name]) if scope else name
                symbols.append(self._symbol(name, qualified, kind, node))
                if node_type in {"contract_declaration", "interface_declaration", "library_declaration"}:
                    next_scope = [*scope, name]

            for child in getattr(node, "children", []) or []:
                if getattr(child, "is_named", False):
                    walk(child, next_scope)

        walk(root, [])
        return symbols, imports, references

    def _extract_python(self, *, root: Any) -> tuple[list[SymbolEntry], list[ImportEntry], list[ReferenceEntry]]:
        symbols: list[SymbolEntry] = []
        imports: list[ImportEntry] = []
        references: list[ReferenceEntry] = []
        seen_references: set[tuple[str, int]] = set()

        def walk(node: Any, scope: list[str]) -> None:
            node_type = node.type
            next_scope = scope

            if node_type == "class_definition":
                class_name = self._node_text(node.child_by_field_name("name")) or "<anonymous_class>"
                qualified = ".".join([*scope, class_name]) if scope else class_name
                symbols.append(self._symbol(class_name, qualified, "class", node))
                next_scope = [*scope, class_name]

            elif node_type == "function_definition":
                func_name = self._node_text(node.child_by_field_name("name")) or "<anonymous_function>"
                qualified = ".".join([*scope, func_name]) if scope else func_name
                symbols.append(self._symbol(func_name, qualified, "function", node))
                next_scope = [*scope, func_name]

            elif node_type == "import_statement":
                imports.extend(self._extract_python_imports(node))

            elif node_type == "import_from_statement":
                imports.extend(self._extract_python_from_imports(node))

            elif node_type == "assignment" and not scope:
                for name in self._extract_python_module_constants(node):
                    symbols.append(self._symbol(name, name, "constant", node))

            elif node_type == 'call':
                reference = self._extract_python_call_reference(node)
                self._append_reference(references, reference, seen_references)

            for child in node.children:
                if child.is_named:
                    walk(child, next_scope)

        walk(root, [])
        return symbols, imports, references

    def _extract_python_module_constants(self, node: Any) -> list[str]:
        left = node.child_by_field_name("left")
        if left is None:
            return []

        targets: list[str] = []
        seen: set[str] = set()

        def walk_target(current: Any) -> None:
            if current.type == "identifier":
                name = self._node_text(current)
                if name and name.isupper() and name not in seen:
                    targets.append(name)
                    seen.add(name)
                return
            for child in current.children:
                if child.is_named:
                    walk_target(child)

        walk_target(left)
        return targets

    def _extract_js_like(self, *, root: Any) -> tuple[list[SymbolEntry], list[ImportEntry], list[ReferenceEntry]]:
        symbols: list[SymbolEntry] = []
        imports: list[ImportEntry] = []
        references: list[ReferenceEntry] = []

        def walk(node: Any, scope: list[str]) -> None:
            node_type = node.type
            next_scope = scope

            if node_type == "class_declaration":
                class_name = self._node_text(node.child_by_field_name("name")) or "<anonymous_class>"
                qualified = ".".join([*scope, class_name]) if scope else class_name
                symbols.append(self._symbol(class_name, qualified, "class", node))
                next_scope = [*scope, class_name]

            elif node_type == "function_declaration":
                func_name = self._node_text(node.child_by_field_name("name")) or "<anonymous_function>"
                qualified = ".".join([*scope, func_name]) if scope else func_name
                symbols.append(self._symbol(func_name, qualified, "function", node))
                next_scope = [*scope, func_name]

            elif node_type == "method_definition":
                method_name = self._node_text(node.child_by_field_name("name")) or "<anonymous_method>"
                qualified = ".".join([*scope, method_name]) if scope else method_name
                symbols.append(self._symbol(method_name, qualified, "method", node))
                next_scope = [*scope, method_name]

            elif node_type == "import_statement":
                imports.extend(self._extract_js_imports(node))

            elif node_type == "export_statement":
                declaration = node.child_by_field_name("declaration")
                if declaration is not None:
                    walk(declaration, scope)
                return

            for child in node.children:
                if child.is_named:
                    walk(child, next_scope)

        walk(root, [])
        return symbols, imports, references

    def _extract_go(self, *, root: Any) -> tuple[list[SymbolEntry], list[ImportEntry], list[ReferenceEntry]]:
        symbols: list[SymbolEntry] = []
        imports: list[ImportEntry] = []
        references = self._extract_go_references(root)

        for node in root.children:
            if not node.is_named:
                continue

            if node.type == "function_declaration":
                name = self._node_text(node.child_by_field_name("name")) or "<anonymous_function>"
                symbols.append(self._symbol(name, name, "function", node))

            elif node.type == "method_declaration":
                receiver = self._extract_go_receiver_type(node)
                method_name = self._node_text(node.child_by_field_name("name")) or "<anonymous_method>"
                qualified = f"{receiver}.{method_name}" if receiver else method_name
                symbols.append(self._symbol(method_name, qualified, "method", node))

            elif node.type == "type_declaration":
                type_name = self._extract_go_type_name(node)
                if type_name:
                    symbols.append(self._symbol(type_name, type_name, "type", node))

            elif node.type == "import_declaration":
                imports.extend(self._extract_go_imports(node))

        return symbols, imports, references

    def _extract_generic_named_declarations(
        self,
        *,
        root: Any,
    ) -> tuple[list[SymbolEntry], list[ImportEntry], list[ReferenceEntry]]:
        symbols: list[SymbolEntry] = []
        imports: list[ImportEntry] = []
        references: list[ReferenceEntry] = []

        container_kinds = {
            "class",
            "interface",
            "trait",
            "protocol",
            "struct",
            "enum",
            "record",
            "module",
            "namespace",
            "object",
        }

        def kind_for(node_type: str) -> str | None:
            lowered = node_type.lower()
            if "class" in lowered:
                return "class"
            if any(token in lowered for token in ("interface", "trait", "protocol", "struct", "enum", "record", "type")):
                return "type"
            if "constructor" in lowered:
                return "method"
            if "method" in lowered:
                return "method"
            if "function" in lowered:
                return "function"
            if lowered in {"mod_item", "module_declaration"} or "module" in lowered or "namespace" in lowered:
                return "module"
            if "const" in lowered or "constant" in lowered:
                return "constant"
            return None

        def should_scope(node_type: str) -> bool:
            lowered = node_type.lower()
            if lowered in {"mod_item", "module_declaration"}:
                return True
            return any(token in lowered for token in container_kinds)

        def maybe_import(node: Any) -> None:
            node_type = str(getattr(node, "type", "") or "")
            lowered = node_type.lower()
            if "import" not in lowered and lowered not in {"use_declaration", "using_directive"}:
                return

            text = self._node_text(node).strip()
            if not text:
                return

            lineno = node.start_point[0] + 1

            # Rust: use foo::bar;
            match = re.match(r"^\s*use\s+(.+?);?\s*$", text)
            if match:
                imports.append(ImportEntry(module=match.group(1).strip(), lineno=lineno, type="import"))
                return

            # Java: import [static] foo.bar.Baz;
            match = re.match(r"^\s*import\s+(?:static\s+)?(.+?);?\s*$", text)
            if match:
                imports.append(ImportEntry(module=match.group(1).strip(), lineno=lineno, type="import"))
                return

            # C#: using Foo.Bar;
            match = re.match(r"^\s*using\s+(.+?);?\s*$", text)
            if match:
                imports.append(ImportEntry(module=match.group(1).strip(), lineno=lineno, type="import"))
                return

        def walk(node: Any, scope: list[str]) -> None:
            node_type = str(getattr(node, "type", "") or "")
            maybe_import(node)

            name_node = node.child_by_field_name("name") if hasattr(node, "child_by_field_name") else None
            name = self._node_text(name_node).strip() if name_node is not None else ""
            node_kind = kind_for(node_type)
            if node_kind is not None and not name:
                name = self._find_identifier_like(node, max_depth=2) or ""

            next_scope = scope
            if name and node_kind is not None:
                qualified = ".".join([*scope, name]) if scope else name
                symbols.append(self._symbol(name, qualified, node_kind, node))
                if should_scope(node_type):
                    next_scope = [*scope, name]

            for child in getattr(node, "children", []) or []:
                if getattr(child, "is_named", False):
                    walk(child, next_scope)

        walk(root, [])
        return symbols, imports, references

    def _find_identifier_like(self, node: Any, *, max_depth: int) -> str | None:
        """Find a reasonable identifier-like name within a node.

        Used as a best-effort fallback for grammars where declarations don't expose
        a stable `name` field (e.g. some Kotlin declarations).
        """

        if node is None or max_depth < 0:
            return None

        node_type = str(getattr(node, "type", "") or "")
        if node_type in {
            "identifier",
            "type_identifier",
            "field_identifier",
            "simple_identifier",
            "scoped_identifier",
            "qualified_identifier",
            "constant",
        }:
            value = self._node_text(node).strip()
            return value or None

        for child in getattr(node, "children", []) or []:
            if not getattr(child, "is_named", False):
                continue
            found = self._find_identifier_like(child, max_depth=max_depth - 1)
            if found:
                return found
        return None

    def _extract_c_like(self, *, root: Any) -> tuple[list[SymbolEntry], list[ImportEntry], list[ReferenceEntry]]:
        symbols: list[SymbolEntry] = []
        imports: list[ImportEntry] = []
        references: list[ReferenceEntry] = []

        def find_identifier(node: Any, *, max_depth: int) -> str | None:
            if max_depth < 0 or node is None:
                return None
            node_type = str(getattr(node, "type", "") or "")
            if node_type in {"identifier", "type_identifier", "field_identifier"}:
                value = self._node_text(node).strip()
                return value or None
            for child in getattr(node, "children", []) or []:
                if not getattr(child, "is_named", False):
                    continue
                found = find_identifier(child, max_depth=max_depth - 1)
                if found:
                    return found
            return None

        for node in root.children:
            if not node.is_named:
                continue

            node_type = node.type

            if node_type == "function_definition":
                declarator = node.child_by_field_name("declarator")
                name = find_identifier(declarator, max_depth=6)
                if name:
                    symbols.append(self._symbol(name, name, "function", node))

            elif node_type in {"struct_specifier", "enum_specifier", "union_specifier", "class_specifier"}:
                name_node = node.child_by_field_name("name")
                name = self._node_text(name_node).strip() if name_node is not None else ""
                if not name:
                    name = find_identifier(node, max_depth=4) or ""
                if name:
                    symbols.append(self._symbol(name, name, "type", node))

            elif node_type in {"type_definition", "typedef_declaration"}:
                name = find_identifier(node, max_depth=6)
                if name:
                    symbols.append(self._symbol(name, name, "type", node))

            elif node_type == "preproc_include":
                text = self._node_text(node).strip()
                lineno = node.start_point[0] + 1
                match = re.search(r"#\s*include\s*[<\"]([^>\"]+)[>\"]", text)
                if match:
                    imports.append(ImportEntry(module=match.group(1).strip(), lineno=lineno, type="import"))

        return symbols, imports, references

    def _extract_python_call_reference(self, node: Any) -> ReferenceEntry | None:
        lineno = node.start_point[0] + 1
        callee = node.child_by_field_name('function')
        if callee is None:
            return None

        if callee.type == 'identifier':
            name = self._node_text(callee).strip()
            if not name:
                return None
            return ReferenceEntry(name=name, qualified_name=name, lineno=lineno, kind='call')

        if callee.type == 'attribute':
            return self._extract_attribute_reference(callee, lineno=lineno)

        return None

    def _extract_attribute_reference(self, node: Any, *, lineno: int) -> ReferenceEntry | None:
        owner = self._node_text(node.child_by_field_name('object')).strip()
        attribute = self._node_text(node.child_by_field_name('attribute')).strip()
        if not owner and not attribute:
            return None

        qualified_name = f'{owner}.{attribute}' if owner and attribute else (attribute or owner)
        name = attribute or owner
        if not name:
            return None
        return ReferenceEntry(name=name, qualified_name=qualified_name, lineno=lineno, kind='call')

    def _extract_go_references(self, root: Any) -> list[ReferenceEntry]:
        references: list[ReferenceEntry] = []
        seen: set[tuple[str, int]] = set()

        def walk(node: Any) -> None:
            if node.type == 'call_expression':
                reference = self._extract_go_call_reference(node)
                self._append_reference(references, reference, seen)
            for child in node.children:
                if child.is_named:
                    walk(child)

        walk(root)
        return references

    def _extract_go_call_reference(self, node: Any) -> ReferenceEntry | None:
        lineno = node.start_point[0] + 1
        callee = node.child_by_field_name('function')
        if callee is None:
            return None

        if callee.type == 'identifier':
            name = self._node_text(callee).strip()
            if not name:
                return None
            return ReferenceEntry(name=name, qualified_name=name, lineno=lineno, kind='call')

        if callee.type == 'selector_expression':
            owner = self._node_text(callee.child_by_field_name('operand')).strip()
            name = self._node_text(callee.child_by_field_name('field')).strip()
            if not owner and not name:
                return None
            qualified_name = f'{owner}.{name}' if owner and name else (name or owner)
            label = name or owner
            if not label:
                return None
            return ReferenceEntry(name=label, qualified_name=qualified_name, lineno=lineno, kind='call')

        return None

    @staticmethod
    def _append_reference(
        references: list[ReferenceEntry],
        reference: ReferenceEntry | None,
        seen: set[tuple[str, int]],
    ) -> None:
        if reference is None:
            return

        key = (str(reference.qualified_name or reference.name), int(reference.lineno))
        if key in seen:
            return

        seen.add(key)
        references.append(reference)

    def _extract_python_imports(self, node: Any) -> list[ImportEntry]:
        text = self._node_text(node)
        lineno = node.start_point[0] + 1
        imports: list[ImportEntry] = []

        if not text.startswith("import "):
            return imports

        names_blob = text[len("import ") :]
        for raw_part in names_blob.split(","):
            part = raw_part.strip()
            if not part:
                continue
            module_name = part
            alias = None
            if " as " in part:
                module_name, alias = [token.strip() for token in part.split(" as ", 1)]
            imports.append(ImportEntry(module=module_name, alias=alias, lineno=lineno, type="import"))

        return imports

    def _extract_python_from_imports(self, node: Any) -> list[ImportEntry]:
        text = self._node_text(node)
        lineno = node.start_point[0] + 1
        match = re.match(r"from\s+([\.\w]+)\s+import\s+(.+)", text)
        if not match:
            return []

        module = match.group(1)
        names_blob = match.group(2)
        imports: list[ImportEntry] = []
        for raw_part in names_blob.split(","):
            part = raw_part.strip()
            if not part:
                continue
            name = part
            alias = None
            if " as " in part:
                name, alias = [token.strip() for token in part.split(" as ", 1)]
            imports.append(ImportEntry(module=module, name=name, alias=alias, lineno=lineno, type="from"))

        return imports

    def _extract_js_imports(self, node: Any) -> list[ImportEntry]:
        text = self._node_text(node)
        lineno = node.start_point[0] + 1
        module = None
        from_match = re.search(r"from\s+[\"']([^\"']+)[\"']", text)
        if from_match:
            module = from_match.group(1)
        else:
            bare_match = re.search(r"import\s+[\"']([^\"']+)[\"']", text)
            if bare_match:
                module = bare_match.group(1)

        if not module:
            return []

        return [ImportEntry(module=module, lineno=lineno, type="import")]

    def _extract_go_imports(self, node: Any) -> list[ImportEntry]:
        imports: list[ImportEntry] = []
        lineno = node.start_point[0] + 1

        def walk(current: Any) -> None:
            if current.type == "import_spec":
                alias = None
                module = None
                for child in current.children:
                    if child.type == "package_identifier":
                        alias = self._node_text(child)
                    if child.type in {"interpreted_string_literal", "raw_string_literal"}:
                        module = self._node_text(child).strip('"`')
                imports.append(ImportEntry(module=module, alias=alias, lineno=lineno, type="import"))

            for child in current.children:
                if child.is_named:
                    walk(child)

        walk(node)
        return imports

    def _extract_go_receiver_type(self, node: Any) -> str | None:
        receiver_node = node.child_by_field_name("receiver")
        if receiver_node is None:
            return None

        text = self._node_text(receiver_node)
        match = re.search(r"\*?([A-Za-z_][A-Za-z0-9_]*)", text)
        if not match:
            return None
        return match.group(1)

    def _extract_go_type_name(self, node: Any) -> str | None:
        for child in node.children:
            if child.type == "type_spec":
                name_node = child.child_by_field_name("name")
                if name_node is not None:
                    return self._node_text(name_node)
                for grandchild in child.children:
                    if grandchild.type == "type_identifier":
                        return self._node_text(grandchild)
        return None

    @staticmethod
    def _node_text(node: Any | None) -> str:
        if node is None:
            return ""
        raw = getattr(node, "text", b"")
        if isinstance(raw, (bytes, bytearray)):
            return bytes(raw).decode("utf-8", errors="replace")
        if isinstance(raw, str):
            return raw
        return str(raw)

    @staticmethod
    def _symbol(name: str, qualified_name: str, kind: str, node: Any) -> SymbolEntry:
        return SymbolEntry(
            name=name,
            qualified_name=qualified_name,
            kind=kind,
            lineno=node.start_point[0] + 1,
            end_lineno=node.end_point[0] + 1,
        )


__all__ = ["TreeSitterEngine"]
