"""Tree-sitter parser for multi-language code analysis."""

from __future__ import annotations

import subprocess
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterator

# Language file extensions mapping
LANGUAGE_EXTENSIONS: dict[str, str] = {
    ".py": "python",
    ".pyi": "python",
    ".js": "javascript",
    ".jsx": "javascript",
    ".ts": "typescript",
    ".tsx": "tsx",
    ".c": "c",
    ".h": "c",
    ".cpp": "cpp",
    ".hpp": "cpp",
    ".cc": "cpp",
    ".cxx": "cpp",
    ".go": "go",
    ".rs": "rust",
    ".java": "java",
    ".rb": "ruby",
    ".php": "php",
}


@dataclass
class ParsedNode:
    """Represents a parsed syntax node."""

    type: str
    name: str
    start_line: int
    end_line: int
    start_col: int
    end_col: int
    children: list[ParsedNode] = field(default_factory=list)
    parent_name: str | None = None
    signature: str | None = None
    body_start: int | None = None


@dataclass
class ParsedFile:
    """Result of parsing a file."""

    file_path: str
    language: str
    nodes: list[ParsedNode]
    imports: list[str]
    errors: list[str]


class TreeSitterParser:
    """Multi-language parser using tree-sitter CLI or Python bindings."""

    def __init__(self) -> None:
        """Initialize the parser."""
        self._check_tree_sitter_available()

    def _check_tree_sitter_available(self) -> bool:
        """Check if tree-sitter CLI is available."""
        try:
            result = subprocess.run(
                ["tree-sitter", "--version"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            return result.returncode == 0
        except (subprocess.SubprocessError, FileNotFoundError):
            return False

    def detect_language(self, file_path: str | Path) -> str | None:
        """Detect language from file extension."""
        path = Path(file_path)
        return LANGUAGE_EXTENSIONS.get(path.suffix.lower())

    def parse_file(self, file_path: str | Path) -> ParsedFile:
        """Parse a file and extract syntax nodes."""
        path = Path(file_path)
        if not path.exists():
            return ParsedFile(
                file_path=str(path),
                language="unknown",
                nodes=[],
                imports=[],
                errors=[f"File not found: {path}"],
            )

        language = self.detect_language(path)
        if not language:
            return ParsedFile(
                file_path=str(path),
                language="unknown",
                nodes=[],
                imports=[],
                errors=[f"Unsupported file type: {path.suffix}"],
            )

        try:
            source = path.read_text(encoding="utf-8", errors="replace")
            return self.parse_source(source, language, str(path))
        except Exception as e:
            return ParsedFile(
                file_path=str(path),
                language=language,
                nodes=[],
                imports=[],
                errors=[f"Parse error: {e}"],
            )

    def parse_source(
        self,
        source: str,
        language: str,
        file_path: str = "<string>",
    ) -> ParsedFile:
        """Parse source code and extract syntax nodes."""
        nodes: list[ParsedNode] = []
        imports: list[str] = []
        errors: list[str] = []

        # Use language-specific parsing
        if language == "python":
            nodes, imports, errors = self._parse_python(source)
        elif language in ("javascript", "typescript", "tsx"):
            nodes, imports, errors = self._parse_javascript(source)
        elif language in ("c", "cpp"):
            nodes, imports, errors = self._parse_cpp(source)
        else:
            # Fallback to basic parsing
            nodes, imports, errors = self._parse_generic(source, language)

        return ParsedFile(
            file_path=file_path,
            language=language,
            nodes=nodes,
            imports=imports,
            errors=errors,
        )

    def _parse_python(
        self, source: str
    ) -> tuple[list[ParsedNode], list[str], list[str]]:
        """Parse Python source code."""
        import ast

        nodes: list[ParsedNode] = []
        imports: list[str] = []
        errors: list[str] = []

        try:
            tree = ast.parse(source)
        except SyntaxError as e:
            return nodes, imports, [f"Syntax error: {e}"]

        for node in ast.walk(tree):
            # Extract imports
            if isinstance(node, ast.Import):
                for alias in node.names:
                    imports.append(alias.name)
            elif isinstance(node, ast.ImportFrom):
                module = node.module or ""
                for alias in node.names:
                    imports.append(f"{module}.{alias.name}" if module else alias.name)

            # Extract functions
            elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
                args = self._get_python_args(node)
                returns = ""
                if node.returns:
                    returns = f" -> {ast.unparse(node.returns)}"

                is_async = isinstance(node, ast.AsyncFunctionDef)
                prefix = "async def" if is_async else "def"

                parsed = ParsedNode(
                    type="function",
                    name=node.name,
                    start_line=node.lineno,
                    end_line=node.end_lineno or node.lineno,
                    start_col=node.col_offset,
                    end_col=node.end_col_offset or 0,
                    signature=f"{prefix} {node.name}({args}){returns}",
                    body_start=node.body[0].lineno if node.body else None,
                )
                nodes.append(parsed)

            # Extract classes
            elif isinstance(node, ast.ClassDef):
                bases = ", ".join(ast.unparse(b) for b in node.bases)
                parsed = ParsedNode(
                    type="class",
                    name=node.name,
                    start_line=node.lineno,
                    end_line=node.end_lineno or node.lineno,
                    start_col=node.col_offset,
                    end_col=node.end_col_offset or 0,
                    signature=f"class {node.name}({bases})" if bases else f"class {node.name}",
                    body_start=node.body[0].lineno if node.body else None,
                )
                nodes.append(parsed)

        return nodes, imports, errors

    def _get_python_args(self, func) -> str:
        """Get function arguments as string."""
        import ast

        args = []

        # Regular args
        defaults_start = len(func.args.args) - len(func.args.defaults)
        for i, arg in enumerate(func.args.args):
            arg_str = arg.arg
            if arg.annotation:
                arg_str += f": {ast.unparse(arg.annotation)}"
            if i >= defaults_start:
                default = func.args.defaults[i - defaults_start]
                arg_str += f" = {ast.unparse(default)}"
            args.append(arg_str)

        # *args
        if func.args.vararg:
            arg_str = f"*{func.args.vararg.arg}"
            if func.args.vararg.annotation:
                arg_str += f": {ast.unparse(func.args.vararg.annotation)}"
            args.append(arg_str)

        # **kwargs
        if func.args.kwarg:
            arg_str = f"**{func.args.kwarg.arg}"
            if func.args.kwarg.annotation:
                arg_str += f": {ast.unparse(func.args.kwarg.annotation)}"
            args.append(arg_str)

        return ", ".join(args)

    def _parse_javascript(
        self, source: str
    ) -> tuple[list[ParsedNode], list[str], list[str]]:
        """Parse JavaScript/TypeScript source code using regex patterns."""
        import re

        nodes: list[ParsedNode] = []
        imports: list[str] = []
        errors: list[str] = []

        lines = source.split("\n")

        # Import patterns
        import_patterns = [
            r"import\s+.*?\s+from\s+['\"]([^'\"]+)['\"]",
            r"import\s+['\"]([^'\"]+)['\"]",
            r"require\s*\(\s*['\"]([^'\"]+)['\"]\s*\)",
        ]

        for pattern in import_patterns:
            for match in re.finditer(pattern, source):
                imports.append(match.group(1))

        # Function patterns
        func_patterns = [
            # function name() {}
            r"(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)",
            # const name = function() {}
            r"(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(([^)]*)\)",
            # const name = () => {}
            r"(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>",
            # const name = async () => {}
            r"(?:const|let|var)\s+(\w+)\s*=\s*async\s+\([^)]*\)\s*=>",
            # method() {} in class
            r"^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{",
        ]

        for i, line in enumerate(lines, 1):
            for pattern in func_patterns:
                match = re.search(pattern, line)
                if match:
                    name = match.group(1)
                    if name not in ("if", "for", "while", "switch", "catch"):
                        nodes.append(ParsedNode(
                            type="function",
                            name=name,
                            start_line=i,
                            end_line=i,  # Would need brace matching for accurate end
                            start_col=0,
                            end_col=len(line),
                            signature=line.strip(),
                        ))

        # Class patterns
        class_pattern = r"(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?"
        for i, line in enumerate(lines, 1):
            match = re.search(class_pattern, line)
            if match:
                name = match.group(1)
                extends = match.group(2)
                sig = f"class {name}"
                if extends:
                    sig += f" extends {extends}"
                nodes.append(ParsedNode(
                    type="class",
                    name=name,
                    start_line=i,
                    end_line=i,
                    start_col=0,
                    end_col=len(line),
                    signature=sig,
                ))

        return nodes, imports, errors

    def _parse_cpp(
        self, source: str
    ) -> tuple[list[ParsedNode], list[str], list[str]]:
        """Parse C/C++ source code using regex patterns."""
        import re

        nodes: list[ParsedNode] = []
        imports: list[str] = []
        errors: list[str] = []

        lines = source.split("\n")

        # Include patterns
        include_pattern = r'#include\s*[<"]([^>"]+)[>"]'
        for match in re.finditer(include_pattern, source):
            imports.append(match.group(1))

        # Function pattern (simplified)
        # Matches: return_type function_name(args)
        func_pattern = r"^\s*(?:[\w:*&<>,\s]+)\s+(\w+)\s*\(([^)]*)\)\s*(?:const)?\s*(?:override)?\s*(?:final)?\s*\{"

        for i, line in enumerate(lines, 1):
            # Skip preprocessor and comments
            stripped = line.strip()
            if stripped.startswith("#") or stripped.startswith("//"):
                continue

            match = re.search(func_pattern, line)
            if match:
                name = match.group(1)
                # Skip control structures
                if name not in ("if", "for", "while", "switch", "catch", "else"):
                    nodes.append(ParsedNode(
                        type="function",
                        name=name,
                        start_line=i,
                        end_line=i,
                        start_col=0,
                        end_col=len(line),
                        signature=stripped.rstrip("{").strip(),
                    ))

        # Class/struct pattern
        class_pattern = r"^\s*(?:class|struct)\s+(\w+)(?:\s*:\s*(?:public|private|protected)\s+(\w+))?"
        for i, line in enumerate(lines, 1):
            match = re.search(class_pattern, line)
            if match:
                name = match.group(1)
                parent = match.group(2)
                sig = f"class {name}"
                if parent:
                    sig += f" : {parent}"
                nodes.append(ParsedNode(
                    type="class",
                    name=name,
                    start_line=i,
                    end_line=i,
                    start_col=0,
                    end_col=len(line),
                    signature=sig,
                    parent_name=parent,
                ))

        return nodes, imports, errors

    def _parse_generic(
        self, source: str, language: str
    ) -> tuple[list[ParsedNode], list[str], list[str]]:
        """Generic fallback parser using common patterns."""
        return [], [], [f"No specific parser for {language}, using generic"]

    def parse_directory(
        self,
        directory: str | Path,
        extensions: list[str] | None = None,
        exclude_dirs: list[str] | None = None,
    ) -> Iterator[ParsedFile]:
        """Parse all files in a directory."""
        path = Path(directory)
        exclude = set(exclude_dirs or [".git", "node_modules", "__pycache__", ".venv", "venv"])

        if extensions:
            ext_set = {e if e.startswith(".") else f".{e}" for e in extensions}
        else:
            ext_set = set(LANGUAGE_EXTENSIONS.keys())

        for file_path in path.rglob("*"):
            if file_path.is_file() and file_path.suffix in ext_set:
                # Check if in excluded directory
                if any(ex in file_path.parts for ex in exclude):
                    continue
                yield self.parse_file(file_path)
