from __future__ import annotations

import json
import os
import shutil
import subprocess
from pathlib import Path

from auraindex.models import QueryCandidate, QueryContext
from auraindex.utils.text import normalize_whitespace

SUFFIX_LANGUAGE_MAP = {
    ".py": "python",
    ".php": "php",
    ".js": "javascript",
    ".jsx": "javascript",
    ".ts": "typescript",
    ".tsx": "tsx",
}


class WasmTreeSitterExtractor:
    """Tree-sitter WASM adapter for Python, JS/TS, and PHP source files."""

    def __init__(self, script_path: Path | None = None):
        self.script_path = script_path or Path(__file__).resolve().parents[1] / "scripts" / "wasm_sql_extractor.mjs"
        self.timeout_seconds = 25
        self._auto_grammar_dir = self._discover_default_grammar_dir()
        self.unavailable_reason = self._detect_unavailable_reason()

    def extract_file(self, file_path: Path) -> tuple[list[QueryCandidate], list[str]]:
        warnings: list[str] = []
        suffix = file_path.suffix.lower()
        language = SUFFIX_LANGUAGE_MAP.get(suffix)

        if language is None:
            return [], warnings

        if self.unavailable_reason:
            warnings.append(f"{self.unavailable_reason} Skipping {file_path}.")
            return [], warnings

        command = [
            "node",
            str(self.script_path),
            "--file",
            str(file_path),
            "--lang",
            language,
            "--grammar-dir",
            str(self._auto_grammar_dir),
        ]

        try:
            completed = subprocess.run(
                command,
                capture_output=True,
                text=True,
                timeout=self.timeout_seconds,
                check=False,
            )
        except FileNotFoundError:
            warnings.append("Node.js executable not found. Install Node.js to enable WASM extraction.")
            return [], warnings
        except subprocess.TimeoutExpired:
            warnings.append(f"WASM extractor timed out for {file_path}.")
            return [], warnings

        if completed.returncode != 0:
            detail = completed.stderr.strip() or completed.stdout.strip() or "unknown error"
            warnings.append(f"WASM extractor failed for {file_path}: {detail}")
            return [], warnings

        try:
            payload = json.loads(completed.stdout or "{}")
        except json.JSONDecodeError:
            warnings.append(f"WASM extractor returned invalid JSON for {file_path}.")
            return [], warnings

        for warning in payload.get("warnings", []):
            warnings.append(str(warning))

        candidates: list[QueryCandidate] = []
        for item in payload.get("queries", []):
            sql_text = normalize_whitespace(str(item.get("sql", "")).strip())
            if not sql_text:
                continue
            line = int(item.get("line", 1))
            confidence = float(item.get("confidence", 0.7))
            metadata = item.get("metadata", {})
            if not isinstance(metadata, dict):
                metadata = {"raw_metadata": metadata}

            candidates.append(
                QueryCandidate(
                    sql=sql_text,
                    context=QueryContext(
                        file_path=str(file_path),
                        line=line,
                        function=None,
                        language=language,
                        source_kind="wasm_sql",
                    ),
                    confidence=confidence,
                    metadata=metadata,
                )
            )

        return candidates, warnings

    def _discover_default_grammar_dir(self) -> Path | None:
        env_dir = os.getenv("AURAINDEX_WASM_GRAMMAR_DIR")
        if env_dir:
            env_path = Path(env_dir).expanduser().resolve()
            if env_path.exists():
                return env_path

        bundled_grammar_dir = self.script_path.parent / "grammars"
        if bundled_grammar_dir.exists():
            return bundled_grammar_dir

        search_roots: list[Path] = []
        cwd = Path.cwd().resolve()
        search_roots.extend([cwd, *cwd.parents])
        search_roots.append(Path(__file__).resolve().parents[2])

        seen: set[Path] = set()
        for root in search_roots:
            if root in seen:
                continue
            seen.add(root)
            node_modules_dir = root / "node_modules"
            if node_modules_dir.exists():
                return node_modules_dir
        return None

    def _detect_unavailable_reason(self) -> str | None:
        if not self.script_path.exists():
            return f"WASM extractor script not found: {self.script_path}"
        if shutil.which("node") is None:
            return "Node.js executable not found. Install Node.js to enable WASM extraction."
        if not self._auto_grammar_dir:
            return (
                "WASM grammar assets were not found. Reinstall AuraIndex or set AURAINDEX_WASM_GRAMMAR_DIR "
                "to a directory containing tree-sitter-*.wasm files."
            )
        if not self._has_tree_sitter_runtime(self._auto_grammar_dir):
            return (
                "WASM runtime assets were not found. Reinstall AuraIndex or install local runtime dependencies "
                "with: npm install web-tree-sitter tree-sitter-python tree-sitter-javascript "
                "tree-sitter-typescript tree-sitter-php."
            )
        return None

    def _has_tree_sitter_runtime(self, grammar_dir: Path) -> bool:
        bundled_vendor_dir = self.script_path.parent / "vendor"
        bundled_runtime_ok = (
            (bundled_vendor_dir / "tree-sitter.js").exists()
            and (bundled_vendor_dir / "tree-sitter.wasm").exists()
        )
        if bundled_runtime_ok:
            return True

        candidates = [
            grammar_dir / "web-tree-sitter",
            grammar_dir / "node_modules" / "web-tree-sitter",
            grammar_dir.parent / "node_modules" / "web-tree-sitter",
        ]
        return any(candidate.exists() for candidate in candidates)
