import logging
import os

from tree_sitter import Node

from .pipeline.language_registry import EXTENSION_MAP, LANGUAGE_CONFIGS

logger = logging.getLogger(__name__)


class CodeParser:
    """Tree-sitter based code parser with lazy language loading.

    Languages are loaded on first use via ``tree_sitter_language_pack`` and
    cached for the lifetime of the parser instance.  The set of supported
    languages and their AST node-type mappings are driven entirely by
    :mod:`language_registry` — no code changes needed to add a new language.
    """

    def __init__(self) -> None:
        # Lazily populated: lang_name → Parser | None (None = load failed)
        self._parsers: dict[str, object | None] = {}

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def parse_file(self, file_path: str, content: bytes) -> list[dict]:
        lang = self._get_lang_from_ext(file_path)
        if lang is None:
            return []

        parser = self._get_or_load_parser(lang)
        if parser is None:
            return []

        # Cast to Any to avoid mypy errors with tree_sitter.Parser
        from typing import Any, cast

        tree = cast(Any, parser).parse(content)
        root = tree.root_node

        results: list[dict] = []
        self._traverse(root, results, lang)
        return results

    # ------------------------------------------------------------------
    # Lazy loading
    # ------------------------------------------------------------------

    def _get_or_load_parser(self, lang_name: str) -> object | None:
        """Return a cached parser for *lang_name*, loading on first access."""
        if lang_name in self._parsers:
            return self._parsers[lang_name]

        try:
            from typing import Any, cast

            from tree_sitter_language_pack import get_parser  # type: ignore

            parser = get_parser(cast(Any, lang_name))
            self._parsers[lang_name] = parser
            return parser
        except Exception as e:
            logger.warning("Could not load language %s: %s", lang_name, e)
            self._parsers[lang_name] = None
            return None

    # ------------------------------------------------------------------
    # Extension → language mapping
    # ------------------------------------------------------------------

    @staticmethod
    def _get_lang_from_ext(file_path: str) -> str | None:
        ext = os.path.splitext(file_path)[1].lower()
        return EXTENSION_MAP.get(ext)

    # ------------------------------------------------------------------
    # AST traversal
    # ------------------------------------------------------------------

    def _traverse(self, node: Node, results: list[dict], lang: str) -> None:
        config = LANGUAGE_CONFIGS.get(lang)
        if config is None:
            return

        node_type = node.type

        if node_type in config.definition_types:
            name = self._get_name(node, lang)
            if name:
                results.append(self._make_entry("definition", node, name))

        elif node_type in config.import_types:
            import_name = self._get_import_name(node, lang)
            if import_name:
                results.append(self._make_entry("import", node, import_name))

        elif node_type in config.call_types:
            call_name = self._get_call_name(node, lang)
            if call_name:
                results.append(self._make_entry("call", node, call_name))

        for child in node.children:
            self._traverse(child, results, lang)

    @staticmethod
    def _make_entry(entry_type: str, node: Node, name: str) -> dict:
        return {
            "type": entry_type,
            "kind": node.type,
            "name": name,
            "start_line": node.start_point[0] + 1,
            "end_line": node.end_point[0] + 1,
            "content": node.text.decode("utf-8") if node.text else "",
        }

    # ------------------------------------------------------------------
    # Name extraction — generic with language-specific overrides
    # ------------------------------------------------------------------

    def _get_name(self, node: Node, lang: str) -> str | None:
        # 1. Direct child identifier (works for most grammars)
        for child in node.children:
            if child.type in ("identifier", "type_identifier", "name") and child.text:
                return child.text.decode("utf-8")

        # 2. Language-specific fallbacks
        if lang == "python":
            return self._get_name_python(node)
        if lang == "cpp":
            return self._get_name_cpp(node)

        # 3. Generic field-based fallback
        name_node = node.child_by_field_name("name")
        if name_node and name_node.text:
            return name_node.text.decode("utf-8")

        return None

    @staticmethod
    def _get_name_python(node: Node) -> str | None:
        name_node = node.child_by_field_name("name")
        if name_node and name_node.text:
            return name_node.text.decode("utf-8")
        return None

    @staticmethod
    def _get_name_cpp(node: Node) -> str | None:
        declarator = node.child_by_field_name("declarator")
        if declarator:
            curr: Node | None = declarator
            while curr:
                if curr.type == "identifier" and curr.text:
                    return curr.text.decode("utf-8")
                if curr.type in (
                    "function_declarator",
                    "reference_declarator",
                    "pointer_declarator",
                ):
                    curr = curr.child_by_field_name("declarator")
                else:
                    for child in curr.children:
                        if child.type == "identifier" and child.text:
                            return child.text.decode("utf-8")
                    if curr.child_count == 0 and curr.text:
                        return curr.text.decode("utf-8")
                    break

        name_node = node.child_by_field_name("name")
        if name_node and name_node.text:
            return name_node.text.decode("utf-8")
        return None

    # ------------------------------------------------------------------
    # Import name extraction
    # ------------------------------------------------------------------

    def _get_import_name(self, node: Node, lang: str) -> str | None:
        if lang == "python":
            return self._get_import_name_python(node)
        if lang == "go":
            return self._get_import_name_go(node)
        if lang in ("javascript", "typescript"):
            return self._get_import_name_js(node)
        if lang == "cpp":
            return self._get_import_name_cpp(node)
        # Generic: try full node text as fallback (e.g. `use std::io;`)
        return self._get_import_name_generic(node)

    @staticmethod
    def _get_import_name_python(node: Node) -> str | None:
        if node.type == "import_statement":
            # Handle 'import os', 'import sys as s'
            for child in node.children:
                if child.type == "dotted_name":
                    return child.text.decode("utf-8") if child.text else None
                if child.type == "aliased_import":
                    # For 'import sys as s', we want 'sys' (the source), not 's' (the local name)
                    name_node = child.child_by_field_name("name")
                    if name_node and name_node.text:
                        return name_node.text.decode("utf-8")
        elif node.type == "import_from_statement":
            # Handle 'from .local import module'
            module_node = node.child_by_field_name("module_name")
            if module_node:
                return module_node.text.decode("utf-8") if module_node.text else None

            # Handle relative imports (e.g. 'from . import module')
            # If module_name is missing, check for relative qualifiers
            for child in node.children:
                if child.type == "relative_import":
                    return child.text.decode("utf-8") if child.text else None

        return None

    @staticmethod
    def _get_import_name_go(node: Node) -> str | None:
        # Go: import "fmt" or import alias "fmt"
        if node.type == "import_spec":
            path_node = node.child_by_field_name("path")
            if path_node and path_node.text:
                return path_node.text.decode("utf-8").strip('"')

        # Handle top-level declaration if somehow we get called on it (though traversal should hit children)
        if node.type == "import_declaration":
            for child in node.children:
                if child.type == "import_spec":
                    # Return first one? This architecture assumes 1 node -> 1 name.
                    # Since we added import_spec to registry, we should focus on import_spec here.
                    pass
        return None

    @staticmethod
    def _get_import_name_js(node: Node) -> str | None:
        # JS/TS: import ... from 'source'
        if node.type == "import_statement":
            source_node = node.child_by_field_name("source")
            if source_node and source_node.text:
                return source_node.text.decode("utf-8").strip("'\"")
        return None

    @staticmethod
    def _get_import_name_cpp(node: Node) -> str | None:
        path_node = node.child_by_field_name("path")
        if path_node and path_node.text:
            raw = path_node.text.decode("utf-8")
            return raw.strip('"<>')
        return None

    @staticmethod
    def _get_import_name_generic(node: Node) -> str | None:
        """Best-effort import name extraction for languages without a dedicated handler."""
        # Try common field names used across grammars
        for field_name in ("name", "path", "source", "module_name"):
            child = node.child_by_field_name(field_name)
            if child and child.text:
                raw = child.text.decode("utf-8")
                return raw.strip('"<>')

        # Fall back to first identifier-like child
        for child in node.children:
            if (
                child.type
                in (
                    "identifier",
                    "scoped_identifier",
                    "dotted_name",
                    "string",
                    "use_list",
                )
                and child.text
            ):
                raw = child.text.decode("utf-8")
                return raw.strip('"<>')

        return None

    # ------------------------------------------------------------------
    # Call name extraction
    # ------------------------------------------------------------------

    @staticmethod
    def _get_call_name(node: Node, lang: str) -> str | None:  # noqa: ARG004
        """Extract the called function/method name from a call node."""
        func_node = node.child_by_field_name("function")
        if func_node and func_node.text:
            return func_node.text.decode("utf-8")

        # Some grammars use "method" or "name" instead of "function"
        for field_name in ("method", "name"):
            child = node.child_by_field_name(field_name)
            if child and child.text:
                return child.text.decode("utf-8")

        return None

    # ------------------------------------------------------------------
    # Backwards-compatible properties
    # ------------------------------------------------------------------

    @property
    def parsers(self) -> dict[str, object | None]:
        """Expose loaded parsers for test introspection."""
        return self._parsers

    @property
    def languages(self) -> dict[str, object | None]:
        """Expose loaded parsers (alias kept for backwards compatibility)."""
        return self._parsers
