"""
cvc.agent.grounding — Output Validation Layer: API Grounding Gate.

This module validates that code generated by the LLM references REAL APIs,
functions, and imports. It sits between the LLM's response and the user,
catching hallucinated APIs before they cause errors.

Two modes:
  1. **Reactive** (default): After code is written, scan for unknown imports
     and flag them with a warning + suggestion to fetch docs.
  2. **Proactive** (tool-gated): The agent must call `lookup_api` before
     referencing an unfamiliar API. Returns real signatures or "not found".

This closes the gap between CVC's memory layer (defensive) and Context Hub's
documentation layer (preventive) — adding STRUCTURAL VALIDATION of output.
"""

from __future__ import annotations

import ast
import json
import logging
import re
from pathlib import Path
from typing import Any

logger = logging.getLogger("cvc.agent.grounding")


# ---------------------------------------------------------------------------
# Known standard library modules (no doc needed)
# ---------------------------------------------------------------------------

_PYTHON_STDLIB = frozenset({
    "os", "sys", "json", "re", "math", "time", "datetime", "pathlib",
    "collections", "functools", "itertools", "typing", "dataclasses",
    "logging", "subprocess", "shutil", "tempfile", "hashlib", "base64",
    "uuid", "enum", "abc", "io", "copy", "pprint", "textwrap",
    "contextlib", "asyncio", "threading", "multiprocessing", "socket",
    "http", "urllib", "email", "html", "xml", "csv", "sqlite3",
    "unittest", "doctest", "argparse", "configparser", "struct",
    "secrets", "string", "operator", "pickle", "shelve", "gzip",
    "zipfile", "tarfile", "glob", "fnmatch", "stat", "platform",
    "signal", "warnings", "traceback", "inspect", "dis", "gc",
    "weakref", "array", "queue", "heapq", "bisect", "decimal",
    "fractions", "random", "statistics", "pdb", "profile", "timeit",
})

_NODE_BUILTINS = frozenset({
    "fs", "path", "http", "https", "url", "crypto", "os", "util",
    "stream", "buffer", "events", "child_process", "cluster",
    "net", "dns", "tls", "zlib", "readline", "assert", "console",
    "process", "querystring", "string_decoder", "timers", "vm",
    "worker_threads", "perf_hooks",
})


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

def extract_python_imports(code: str) -> list[dict[str, str]]:
    """
    Extract import statements from Python code.

    Returns list of {module, name, full} where:
    - module: top-level package (e.g. "openai", "stripe")
    - name: specific import (e.g. "OpenAI", "Stripe")
    - full: complete import path (e.g. "openai.OpenAI")
    """
    imports: list[dict[str, str]] = []

    try:
        tree = ast.parse(code)
    except SyntaxError:
        # Fallback to regex for partial/invalid code
        return _extract_imports_regex(code)

    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                top = alias.name.split(".")[0]
                imports.append({
                    "module": top,
                    "name": alias.asname or alias.name,
                    "full": alias.name,
                })
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                top = node.module.split(".")[0]
                for alias in node.names:
                    imports.append({
                        "module": top,
                        "name": alias.asname or alias.name,
                        "full": f"{node.module}.{alias.name}",
                    })

    return imports


def extract_js_imports(code: str) -> list[dict[str, str]]:
    """Extract import/require statements from JavaScript/TypeScript code."""
    imports: list[dict[str, str]] = []

    # ES6 imports: import X from 'package'
    for match in re.finditer(
        r'''import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]''',
        code,
    ):
        pkg = match.group(1)
        top = pkg.split("/")[0]
        if top.startswith("@"):
            top = "/".join(pkg.split("/")[:2])
        imports.append({"module": top, "name": pkg, "full": pkg})

    # CommonJS: require('package')
    for match in re.finditer(r'''require\s*\(\s*['"]([^'"]+)['"]\s*\)''', code):
        pkg = match.group(1)
        top = pkg.split("/")[0]
        if top.startswith("@"):
            top = "/".join(pkg.split("/")[:2])
        imports.append({"module": top, "name": pkg, "full": pkg})

    return imports


def _extract_imports_regex(code: str) -> list[dict[str, str]]:
    """Regex fallback for Python imports when AST parsing fails."""
    imports: list[dict[str, str]] = []

    # import X
    for match in re.finditer(r'^import\s+([\w.]+)', code, re.MULTILINE):
        mod = match.group(1)
        top = mod.split(".")[0]
        imports.append({"module": top, "name": mod, "full": mod})

    # from X import Y
    for match in re.finditer(r'^from\s+([\w.]+)\s+import', code, re.MULTILINE):
        mod = match.group(1)
        top = mod.split(".")[0]
        imports.append({"module": top, "name": mod, "full": mod})

    return imports


# ---------------------------------------------------------------------------
# Grounding validation
# ---------------------------------------------------------------------------

def validate_code_grounding(
    code: str,
    language: str = "python",
    workspace: Path | None = None,
) -> dict[str, Any]:
    """
    Validate that code references real, known APIs.

    Returns a grounding report:
    {
        "grounded": bool,          # True if all imports are known
        "unknown_imports": [...],   # Imports not in stdlib or project deps
        "suggestions": [...],       # Recommended actions
        "known_imports": [...],     # Verified imports
    }
    """
    if language in ("python", "py"):
        imports = extract_python_imports(code)
        stdlib = _PYTHON_STDLIB
        deps = _get_python_deps(workspace) if workspace else set()
    elif language in ("javascript", "js", "typescript", "ts"):
        imports = extract_js_imports(code)
        stdlib = _NODE_BUILTINS
        deps = _get_node_deps(workspace) if workspace else set()
    else:
        return {"grounded": True, "unknown_imports": [], "suggestions": [], "known_imports": []}

    known: list[dict[str, str]] = []
    unknown: list[dict[str, str]] = []

    # Also track internal project imports
    internal_modules = _get_internal_modules(workspace) if workspace else set()

    for imp in imports:
        mod = imp["module"]
        if mod in stdlib:
            known.append(imp)
        elif mod in deps:
            known.append(imp)
        elif mod in internal_modules:
            known.append(imp)
        elif mod.startswith("."):
            # Relative imports are always internal
            known.append(imp)
        else:
            unknown.append(imp)

    suggestions: list[str] = []
    if unknown:
        for u in unknown:
            suggestions.append(
                f"Unknown import '{u['full']}' — use `fetch_docs` to get "
                f"documentation for '{u['module']}' before using this API."
            )

    return {
        "grounded": len(unknown) == 0,
        "unknown_imports": unknown,
        "suggestions": suggestions,
        "known_imports": known,
    }


def check_api_exists(
    symbol: str,
    workspace: Path | None = None,
    language: str = "python",
) -> dict[str, Any]:
    """
    Check if an API symbol exists in the project's installed packages.

    This is the `lookup_api` tool implementation. The agent calls this
    BEFORE using an unfamiliar API to verify it exists.

    Returns:
    {
        "exists": bool,
        "source": "stdlib"|"installed"|"project"|"unknown",
        "info": str,  # signature or description if found
    }
    """
    parts = symbol.split(".")
    top_module = parts[0]

    # Check stdlib
    if language in ("python", "py") and top_module in _PYTHON_STDLIB:
        return {
            "exists": True,
            "source": "stdlib",
            "info": f"'{top_module}' is a Python standard library module.",
        }
    if language in ("javascript", "js", "typescript", "ts") and top_module in _NODE_BUILTINS:
        return {
            "exists": True,
            "source": "stdlib",
            "info": f"'{top_module}' is a Node.js built-in module.",
        }

    # Check project dependencies
    if workspace:
        if language in ("python", "py"):
            deps = _get_python_deps(workspace)
        else:
            deps = _get_node_deps(workspace)

        if top_module in deps:
            return {
                "exists": True,
                "source": "installed",
                "info": (
                    f"'{top_module}' is an installed dependency. "
                    f"Use `fetch_docs` to get current API documentation."
                ),
            }

    # Check internal modules
    if workspace:
        internal = _get_internal_modules(workspace)
        if top_module in internal:
            return {
                "exists": True,
                "source": "project",
                "info": f"'{top_module}' is a project-internal module.",
            }

    return {
        "exists": False,
        "source": "unknown",
        "info": (
            f"'{symbol}' was not found in installed packages or project modules. "
            f"This may be hallucinated. Check the correct package name or "
            f"use `fetch_docs` to look up the real API."
        ),
    }


# ---------------------------------------------------------------------------
# Dependency detection helpers
# ---------------------------------------------------------------------------

def _get_python_deps(workspace: Path | None) -> set[str]:
    """Get installed Python package names from the project."""
    deps: set[str] = set()

    if not workspace:
        return deps

    # From pyproject.toml
    pyproject = workspace / "pyproject.toml"
    if pyproject.exists():
        try:
            text = pyproject.read_text(encoding="utf-8")
            # Parse [project.dependencies] and [tool.poetry.dependencies]
            for match in re.finditer(
                r'^\s*"?([a-zA-Z0-9_-]+)', text, re.MULTILINE
            ):
                pkg = match.group(1).lower().replace("-", "_")
                deps.add(pkg)
        except OSError:
            pass

    # From requirements.txt
    reqs = workspace / "requirements.txt"
    if reqs.exists():
        try:
            for line in reqs.read_text(encoding="utf-8").splitlines():
                line = line.strip()
                if line and not line.startswith("#"):
                    # Strip version specifiers
                    pkg = re.split(r'[>=<!\[\];]', line)[0].strip()
                    if pkg:
                        deps.add(pkg.lower().replace("-", "_"))
        except OSError:
            pass

    return deps


def _get_node_deps(workspace: Path | None) -> set[str]:
    """Get installed Node.js package names from the project."""
    deps: set[str] = set()

    if not workspace:
        return deps

    pkg_json = workspace / "package.json"
    if pkg_json.exists():
        try:
            data = json.loads(pkg_json.read_text(encoding="utf-8"))
            for section in ("dependencies", "devDependencies", "peerDependencies"):
                for pkg in data.get(section, {}):
                    deps.add(pkg)
        except (json.JSONDecodeError, OSError):
            pass

    return deps


def _get_internal_modules(workspace: Path | None) -> set[str]:
    """Detect internal project modules (Python packages, src dirs)."""
    modules: set[str] = set()

    if not workspace:
        return modules

    # Python: directories with __init__.py
    for init in workspace.rglob("__init__.py"):
        rel = init.parent.relative_to(workspace)
        parts = rel.parts
        if parts and not any(
            p.startswith(".") or p in ("node_modules", "venv", ".venv", "__pycache__")
            for p in parts
        ):
            modules.add(parts[0])

    # Also add immediate Python files as importable modules
    for py_file in workspace.glob("*.py"):
        if py_file.stem != "__init__":
            modules.add(py_file.stem)

    # Node: check package.json name
    pkg_json = workspace / "package.json"
    if pkg_json.exists():
        try:
            data = json.loads(pkg_json.read_text(encoding="utf-8"))
            name = data.get("name", "")
            if name:
                modules.add(name)
        except (json.JSONDecodeError, OSError):
            pass

    return modules
