"""符号提取器 — tree-sitter Tag 提取 + Pygments 降级

从源文件提取函数/类/方法定义和引用（调用/import）。
tree-sitter 不可用时静默降级，不影响核心索引功能。
"""

from __future__ import annotations

import logging
from collections import namedtuple
from pathlib import Path

logger = logging.getLogger(__name__)

# ── 可选依赖检测 ──

_HAS_TREESITTER = False
try:
    from tree_sitter import Query, QueryCursor
    from tree_sitter_language_pack import get_language, get_parser

    _HAS_TREESITTER = True
except ImportError:
    pass

_HAS_GREP_AST = False
try:
    from grep_ast import filename_to_lang as _grep_ast_lang

    _HAS_GREP_AST = True
except ImportError:
    pass


# ── 数据结构 ──

Tag = namedtuple("Tag", "rel_fname fname line name kind")
"""符号标签

kind: "def" (定义) | "ref" (引用)
line: 0-based 行号，-1 表示未知（Pygments 降级）
"""

# .scm 查询文件目录
_QUERIES_DIR = Path(__file__).parent / "queries"

# 内置语言映射（grep-ast 不可用时的降级）
_LANG_MAP: dict[str, str] = {
    ".py": "python",
    ".js": "javascript",
    ".jsx": "javascript",
    ".ts": "typescript",
    ".tsx": "typescript",
    ".rs": "rust",
    ".go": "go",
    ".java": "java",
    ".c": "c",
    ".h": "c",
    ".cpp": "cpp",
    ".cc": "cpp",
    ".cxx": "cpp",
    ".hpp": "cpp",
    ".cs": "csharp",
    ".rb": "ruby",
    ".swift": "swift",
    ".lua": "lua",
    ".dart": "dart",
    ".el": "elisp",
    ".ex": "elixir",
    ".exs": "elixir",
    ".clj": "clojure",
    ".r": "r",
    ".R": "r",
    ".sol": "solidity",
}

# 符号提取文件大小上限（>1MB 跳过，防止 OOM）
_MAX_SYMBOL_FILE_SIZE = 1 * 1024 * 1024

# Query 缓存（language → Query 对象）
_query_cache: dict[str, Query | None] = {}


# ── 公共 API ──


def is_codegraph_available() -> bool:
    """检查 tree-sitter-language-pack 是否已安装"""
    return _HAS_TREESITTER


def filename_to_lang(fname: str) -> str | None:
    """文件名 → 语言名称

    优先用 grep-ast（覆盖 45+ 种语言），降级到内置映射。
    """
    if _HAS_GREP_AST:
        return _grep_ast_lang(fname)
    ext = Path(fname).suffix.lower()
    return _LANG_MAP.get(ext)


def extract_tags(fname: str, code: str, rel_fname: str) -> list[Tag]:
    """从源文件提取定义和引用标签

    Args:
        fname: 绝对文件路径（用于语言检测）
        code: 源代码文本
        rel_fname: 相对路径（存入 Tag.rel_fname）

    Returns:
        Tag 列表。tree-sitter 不可用或提取失败时返回空列表。
    """
    if not code or len(code) > _MAX_SYMBOL_FILE_SIZE:
        return []

    lang = filename_to_lang(fname)
    if not lang:
        return []

    # tree-sitter 路径
    if _HAS_TREESITTER:
        tags = _extract_with_treesitter(fname, code, rel_fname, lang)
        if tags is not None:
            return tags

    # Pygments 降级
    return _pygments_fallback(fname, code, rel_fname)


# ── tree-sitter 提取 ──


def _get_query(lang: str) -> Query | None:
    """获取语言的 .scm Query 对象（带缓存）"""
    if lang in _query_cache:
        return _query_cache[lang]

    scm_path = _QUERIES_DIR / f"{lang}-tags.scm"
    if not scm_path.exists():
        _query_cache[lang] = None
        return None

    try:
        language = get_language(lang)
        scm_text = scm_path.read_text(encoding="utf-8")
        query = Query(language, scm_text)
        _query_cache[lang] = query
        return query
    except Exception as e:
        logger.debug("加载 %s 的 .scm 查询失败: %s", lang, e)
        _query_cache[lang] = None
        return None


def _extract_with_treesitter(
    fname: str, code: str, rel_fname: str, lang: str
) -> list[Tag] | None:
    """用 tree-sitter 提取标签

    Returns:
        Tag 列表，如果该语言无 .scm 查询则返回 None（触发 Pygments 降级）。
    """
    query = _get_query(lang)
    if query is None:
        return None  # 触发 Pygments 降级

    try:
        parser = get_parser(lang)
        tree = parser.parse(bytes(code, "utf-8"))
    except (UnicodeEncodeError, Exception) as e:
        logger.debug("tree-sitter 解析失败 %s: %s", fname, e)
        return []

    try:
        cursor = QueryCursor(query)
        captures = cursor.captures(tree.root_node)
    except Exception as e:
        logger.debug("tree-sitter 查询失败 %s: %s", fname, e)
        return []

    tags = []
    # captures 是 dict[str, list[Node]]（tree-sitter 0.25+）
    for capture_name, nodes in captures.items():
        # 只关心 name.definition.* 和 name.reference.* 捕获
        if capture_name.startswith("name.definition."):
            kind = "def"
        elif capture_name.startswith("name.reference."):
            kind = "ref"
        else:
            continue

        for node in nodes:
            try:
                name = node.text.decode("utf-8")
            except (UnicodeDecodeError, AttributeError):
                continue
            line = node.start_point[0]
            tags.append(Tag(rel_fname, fname, line, name, kind))

    return tags


# ── Pygments 降级 ──


def _pygments_fallback(fname: str, code: str, rel_fname: str) -> list[Tag]:
    """Pygments 降级: 提取所有标识符作为伪引用

    Pygments 无法区分定义和引用，全部标记为 ref。
    仅在 tree-sitter 不可用或无 .scm 文件时使用。
    """
    try:
        from pygments.lexers import get_lexer_for_filename
        from pygments.token import Token
    except ImportError:
        return []

    try:
        lexer = get_lexer_for_filename(fname)
    except Exception:
        return []

    tags = []
    seen: set[str] = set()  # 去重
    for token_type, value in lexer.get_tokens(code):
        if token_type in Token.Name and len(value) >= 3 and value not in seen:
            seen.add(value)
            tags.append(Tag(rel_fname, fname, -1, value, "ref"))

    return tags
