# SPDX-License-Identifier: AGPL-3.0-or-later
"""Racket language analyzer using tree-sitter.

This module provides static analysis for Racket source code, extracting symbols
(functions, variables, structs) and edges (calls).

Racket is a general-purpose programming language in the Lisp/Scheme family,
known for its language-oriented programming features. It's widely used in
programming language research and computer science education.

Implementation approach:
- Uses tree-sitter-language-pack for Racket grammar
- Two-pass analysis: First pass collects all symbols, second pass extracts edges
- Handles Racket-specific constructs like define, struct, module+

Key constructs extracted:
- (define (name args) body) - function definitions
- (define name value) - variable definitions
- (struct name (fields)) - struct definitions
- (name args) - function calls
"""

import time
from pathlib import Path
from typing import Iterator, Optional, ClassVar, TYPE_CHECKING

from hypergumbo_core.discovery import find_files
from hypergumbo_core.ir import AnalysisRun, Edge, PASS_VERSION, Span, Symbol, make_pass_id
from hypergumbo_core.analyze.base import AnalysisResult, TreeSitterAnalyzer, make_symbol_id, populate_docstrings_from_tree
from hypergumbo_core.analyze.registry import register_analyzer

if TYPE_CHECKING:
    import tree_sitter

PASS_ID = make_pass_id("racket")



def find_racket_files(root: Path) -> Iterator[Path]:
    """Find all Racket files in the given directory."""
    for ext in ("*.rkt", "*.rktl", "*.rktd"):
        for path in find_files(root, [ext]):
            if path.is_file():
                yield path


def _get_node_text(node: "tree_sitter.Node") -> str:
    """Get the text content of a node."""
    return node.text.decode("utf-8", errors="replace")


def _is_define_form(node: "tree_sitter.Node") -> bool:
    """Check if a list node is a define form."""
    if node.type != "list":
        return False
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 2:
        return False  # pragma: no cover
    return children[0].type == "symbol" and _get_node_text(children[0]) == "define"


def _is_function_define(node: "tree_sitter.Node") -> bool:
    """Check if a define form defines a function (define (name args) body)."""
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 2:
        return False  # pragma: no cover
    # Second element should be a list (the function signature)
    return children[1].type == "list"


def _is_struct_form(node: "tree_sitter.Node") -> bool:
    """Check if a list node is a struct form."""
    if node.type != "list":
        return False
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 2:
        return False  # pragma: no cover
    return children[0].type == "symbol" and _get_node_text(children[0]) == "struct"


def _get_function_name(node: "tree_sitter.Node") -> Optional[str]:
    """Get the function name from a function define form."""
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 2:
        return None  # pragma: no cover
    sig_list = children[1]
    sig_children = [c for c in sig_list.children if c.type not in ("(", ")")]
    if sig_children and sig_children[0].type == "symbol":
        return _get_node_text(sig_children[0])
    return None  # pragma: no cover


def _get_function_params(node: "tree_sitter.Node") -> list[str]:
    """Get parameters from a function define form."""
    params = []
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 2:
        return params  # pragma: no cover
    sig_list = children[1]
    sig_children = [c for c in sig_list.children if c.type not in ("(", ")")]
    # Skip the first symbol (function name)
    for child in sig_children[1:]:
        if child.type == "symbol":
            params.append(_get_node_text(child))
    return params


def _get_variable_name(node: "tree_sitter.Node") -> Optional[str]:
    """Get the variable name from a variable define form."""
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 2:
        return None  # pragma: no cover
    if children[1].type == "symbol":
        return _get_node_text(children[1])
    return None  # pragma: no cover


def _get_struct_name(node: "tree_sitter.Node") -> Optional[str]:
    """Get the struct name from a struct form."""
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 2:
        return None  # pragma: no cover
    if children[1].type == "symbol":
        return _get_node_text(children[1])
    return None  # pragma: no cover


def _get_struct_fields(node: "tree_sitter.Node") -> list[str]:
    """Get field names from a struct form."""
    fields = []
    children = [c for c in node.children if c.type not in ("(", ")")]
    if len(children) < 3:
        return fields  # pragma: no cover
    if children[2].type == "list":
        for child in children[2].children:
            if child.type == "symbol":
                fields.append(_get_node_text(child))
    return fields


def _get_call_name(node: "tree_sitter.Node") -> Optional[str]:
    """Get the function name from a call expression."""
    children = [c for c in node.children if c.type not in ("(", ")")]
    if children and children[0].type == "symbol":
        return _get_node_text(children[0])
    return None  # pragma: no cover


class _RacketExtractor:
    """Extraction logic for Racket source files."""

    def __init__(self, repo_root: Path, ts_analyzer: "RacketAnalyzer"):
        self.repo_root = repo_root
        self._ts_analyzer = ts_analyzer
        self.symbols: list[Symbol] = []
        self.edges: list[Edge] = []
        self._symbol_registry: dict[str, str] = {}  # name -> id
        self._run_id: str = ""

    def analyze(self) -> AnalysisResult:
        """Analyze all Racket files in the repository."""
        if not is_racket_tree_sitter_available():  # pragma: no cover
            # Defense-in-depth: RacketAnalyzer.analyze() gates on
            # _check_grammar_available() before constructing this extractor,
            # so this branch is unreachable through the public API.
            import warnings
            warnings.warn(
                "Racket analysis skipped: tree-sitter-language-pack not available",
                UserWarning,
                stacklevel=2,
            )
            return AnalysisResult(
                skipped=True,
                skip_reason="tree-sitter-language-pack not available",
            )

        import uuid as uuid_module
        from tree_sitter_language_pack import get_parser

        start_time = time.time()
        self._run_id = f"uuid:{uuid_module.uuid4()}"

        parser = get_parser("racket")
        racket_files = list(find_racket_files(self.repo_root))

        if not racket_files:
            return AnalysisResult()

        # Pass 1: Collect all symbols
        for path in racket_files:
            try:
                content = path.read_bytes()
                tree = parser.parse(content)
                before = len(self.symbols)
                self._extract_symbols(tree.root_node, path)
                populate_docstrings_from_tree(tree.root_node, content, self.symbols[before:])
            except Exception:  # pragma: no cover
                pass

        # Build symbol registry
        for sym in self.symbols:
            self._symbol_registry[sym.name] = sym.id

        # Pass 2: Extract edges
        for path in racket_files:
            try:
                content = path.read_bytes()
                tree = parser.parse(content)
                self._extract_edges(tree.root_node, path)
            except Exception:  # pragma: no cover
                pass

        elapsed = time.time() - start_time

        run = AnalysisRun(
            execution_id=self._run_id,
            run_signature="",
            pass_id=PASS_ID,
            version=PASS_VERSION,
            toolchain={"name": "racket", "version": "unknown"},
            duration_ms=int(elapsed * 1000),
        )

        return AnalysisResult(
            symbols=self.symbols,
            edges=self.edges,
            run=run,
        )

    def _extract_symbols(self, node: "tree_sitter.Node", path: Path) -> None:
        """Extract symbols from a syntax tree node."""
        if _is_define_form(node):
            if _is_function_define(node):
                name = _get_function_name(node)
                if name:
                    params = _get_function_params(node)
                    signature = f"(define ({name} {' '.join(params)}) ...)"
                    rel_path = str(path.relative_to(self.repo_root))

                    sym = Symbol(
                        id=make_symbol_id(
                            "racket", rel_path,
                            node.start_point[0] + 1, node.end_point[0] + 1,
                            name, "function",
                        ),
                        stable_id=self._ts_analyzer.compute_stable_id(node, kind="function"),
                        name=name,
                        kind="function",
                        language="racket",
                        path=rel_path,
                        span=Span(
                            start_line=node.start_point[0] + 1,
                            end_line=node.end_point[0] + 1,
                            start_col=node.start_point[1],
                            end_col=node.end_point[1],
                        ),
                        origin=PASS_ID,
                        signature=signature,
                        meta={"param_count": len(params)},
                    )
                    self.symbols.append(sym)
            else:
                name = _get_variable_name(node)
                if name:
                    rel_path = str(path.relative_to(self.repo_root))

                    sym = Symbol(
                        id=make_symbol_id(
                            "racket", rel_path,
                            node.start_point[0] + 1, node.end_point[0] + 1,
                            name, "variable",
                        ),
                        stable_id=self._ts_analyzer.compute_stable_id(node, kind="variable"),
                        name=name,
                        kind="variable",
                        language="racket",
                        path=rel_path,
                        span=Span(
                            start_line=node.start_point[0] + 1,
                            end_line=node.end_point[0] + 1,
                            start_col=node.start_point[1],
                            end_col=node.end_point[1],
                        ),
                        origin=PASS_ID,
                    )
                    self.symbols.append(sym)
            return  # Don't recursively process define children

        elif _is_struct_form(node):
            name = _get_struct_name(node)
            if name:
                fields = _get_struct_fields(node)
                rel_path = str(path.relative_to(self.repo_root))

                sym = Symbol(
                    id=make_symbol_id(
                        "racket", rel_path,
                        node.start_point[0] + 1, node.end_point[0] + 1,
                        name, "class",
                    ),
                    stable_id=self._ts_analyzer.compute_stable_id(node, kind="class"),
                    name=name,
                    kind="class",
                    language="racket",
                    path=rel_path,
                    span=Span(
                        start_line=node.start_point[0] + 1,
                        end_line=node.end_point[0] + 1,
                        start_col=node.start_point[1],
                        end_col=node.end_point[1],
                    ),
                    origin=PASS_ID,
                    meta={"field_count": len(fields), "fields": fields},
                )
                self.symbols.append(sym)
            return  # Don't recursively process struct children

        # Recursively process children
        for child in node.children:
            self._extract_symbols(child, path)

    def _extract_edges(self, node: "tree_sitter.Node", path: Path) -> None:
        """Extract edges from a syntax tree node."""
        # Skip processing inside define forms' signature lists
        if _is_define_form(node):
            if _is_function_define(node):
                children = [c for c in node.children if c.type not in ("(", ")")]
                # Skip define symbol (0), skip signature list (1), process body (2+)
                for child in children[2:]:
                    self._extract_edges(child, path)
            return  # Don't process other parts of define forms

        # Look for function calls (list expressions that aren't special forms)
        if node.type == "list":
            call_name = _get_call_name(node)
            if call_name:
                # Skip special forms and built-ins
                special_forms = {
                    "define", "lambda", "let", "let*", "letrec", "let-values",
                    "if", "cond", "case", "when", "unless", "begin", "set!",
                    "quote", "quasiquote", "unquote", "unquote-splicing",
                    "and", "or", "do", "delay", "define-syntax", "let-syntax",
                    "letrec-syntax", "syntax-rules", "syntax-case", "syntax",
                    "require", "provide", "module", "module+", "module*",
                    "struct", "class", "interface", "match", "match-let",
                    "for", "for/list", "for/vector", "for/hash", "for/and",
                    "for/or", "for/sum", "for/product", "for/fold",
                    "define-values", "define-struct", "define/contract",
                }
                builtins = {
                    "+", "-", "*", "/", "=", "<", ">", "<=", ">=", "eq?",
                    "eqv?", "equal?", "not", "null?", "pair?", "list?",
                    "number?", "string?", "symbol?", "procedure?", "boolean?",
                    "car", "cdr", "cons", "list", "append", "reverse",
                    "length", "map", "filter", "foldl", "foldr", "for-each",
                    "apply", "eval", "read", "write", "display", "displayln",
                    "newline", "printf", "format", "error", "raise",
                    "string-append", "string-length", "string-ref",
                    "number->string", "string->number", "symbol->string",
                    "string->symbol", "vector", "vector-ref", "vector-set!",
                    "make-vector", "vector-length", "hash", "hash-ref",
                    "hash-set", "make-hash", "call/cc", "values", "void",
                    "first", "second", "third", "rest", "empty", "empty?",
                    "build-list", "range", "in-range", "in-list",
                }

                if call_name not in special_forms and call_name not in builtins:
                    caller_id = self._find_enclosing_function(node, path)
                    if caller_id:
                        callee_id = self._symbol_registry.get(call_name)
                        confidence = 1.0 if callee_id else 0.6
                        if callee_id is None:
                            callee_id = f"racket:unresolved:{call_name}"

                        line = node.start_point[0] + 1
                        edge = Edge.create(
                            src=caller_id,
                            dst=callee_id,
                            edge_type="calls",
                            line=line,
                            origin=PASS_ID,
                            origin_run_id=self._run_id,
                            evidence_type="ast_call_direct",
                            confidence=confidence,
                            evidence_lang="racket",
                        )
                        self.edges.append(edge)

        # Recursively process children
        for child in node.children:
            self._extract_edges(child, path)

    def _find_enclosing_function(
        self, node: "tree_sitter.Node", path: Path
    ) -> Optional[str]:
        """Find the enclosing function for a node."""
        current = node.parent
        while current is not None:
            if _is_define_form(current) and _is_function_define(current):
                name = _get_function_name(current)
                if name:
                    return make_symbol_id(
                        "racket", str(path.relative_to(self.repo_root)),
                        current.start_point[0] + 1, current.end_point[0] + 1,
                        name, "function",
                    )
            current = current.parent
        return None  # pragma: no cover


class RacketAnalyzer(TreeSitterAnalyzer):
    """Tree-sitter-based analyzer for Racket source files.

    Extracts functions, variables, structs, and call edges.
    Uses language_pack_name for the racket grammar.
    """

    lang = "racket"
    file_patterns: ClassVar[list[str]] = ["**/*.rkt", "**/*.rktl", "**/*.rktd"]
    language_pack_name = "racket"

    def analyze(self, repo_root: Path, max_files: int | None = None) -> AnalysisResult:
        """Run Racket analysis with cross-file symbol registry."""
        if not self._check_grammar_available():
            import warnings
            warnings.warn(
                f"{self.lang} analysis skipped: grammar not available. "
                f"Install the required tree-sitter grammar package.",
                UserWarning,
                stacklevel=2,
            )
            return AnalysisResult(
                skipped=True,
                skip_reason=f"{self.lang} tree-sitter grammar not available",
            )

        extractor = _RacketExtractor(repo_root, self)
        return extractor.analyze()


_analyzer = RacketAnalyzer()


def is_racket_tree_sitter_available() -> bool:
    """Check if tree-sitter-language-pack with Racket support is available."""
    return _analyzer._check_grammar_available()


@register_analyzer("racket")
def analyze_racket(repo_root: Path) -> AnalysisResult:
    """Analyze Racket source files in a repository.

    Args:
        repo_root: Root directory of the repository to analyze

    Returns:
        AnalysisResult containing symbols, edges, and analysis metadata
    """
    return _analyzer.analyze(repo_root)
