"""
checker.py — Cross-references extracted LLM references against the codebase
index. Produces a list of verified references and hallucinations with
similarity suggestions.
"""

from __future__ import annotations

import difflib
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple

from hallucination_grep.extractor import CodeReference, ReferenceKind
from hallucination_grep.indexer import CodebaseIndex


@dataclass
class CheckResult:
    """Result for a single code reference."""

    reference: CodeReference
    verified: bool
    # Where it was found, if verified
    found_at: Optional[str] = None  # "filename:line"
    # Closest match if not verified
    suggestion: Optional[str] = None  # "similar_name in file:line"
    suggestion_name: Optional[str] = None
    confidence: float = 0.0  # 0.0 = definitely hallucinated, 1.0 = verified


@dataclass
class HallucinationReport:
    hallucinations: List[CheckResult] = field(default_factory=list)
    verified: List[CheckResult] = field(default_factory=list)
    total_references: int = 0
    input_lines: int = 0
    codebase_root: str = ""
    python_files_indexed: int = 0
    total_files_indexed: int = 0

    @property
    def hallucination_count(self) -> int:
        return len(self.hallucinations)

    @property
    def verified_count(self) -> int:
        return len(self.verified)

    @property
    def hallucination_rate(self) -> float:
        if self.total_references == 0:
            return 0.0
        return self.hallucination_count / self.total_references


# ---------------------------------------------------------------------------
# Stdlib modules — used to skip false positives on known stdlib imports
# ---------------------------------------------------------------------------
STDLIB_MODULES: Set[str] = {
    "os", "sys", "re", "io", "abc", "ast", "csv", "cmd", "cgi", "ssl",
    "ftp", "smb", "xml", "json", "math", "copy", "glob", "gzip", "html",
    "http", "uuid", "time", "enum", "stat", "code", "dis", "pty", "nis",
    "fnmatch", "shutil", "struct", "signal", "socket", "select", "pickle",
    "hashlib", "base64", "string", "random", "decimal", "fractions",
    "calendar", "datetime", "pathlib", "logging", "urllib", "email",
    "sqlite3", "zipfile", "tarfile", "tempfile", "textwrap", "unicodedata",
    "threading", "multiprocessing", "subprocess", "collections", "itertools",
    "functools", "operator", "contextlib", "dataclasses", "traceback",
    "warnings", "weakref", "gc", "inspect", "importlib", "pkgutil",
    "platform", "getpass", "getopt", "argparse", "configparser", "pprint",
    "typing", "types", "numbers", "array", "queue", "heapq", "bisect",
    "statistics", "secrets", "hmac", "unittest", "doctest", "timeit",
    "profile", "cProfile", "pdb", "faulthandler", "tracemalloc",
    "concurrent", "asyncio", "selectors", "xml.etree.ElementTree",
    "xml.dom", "html.parser", "urllib.parse", "urllib.request",
    "http.client", "http.server", "email.mime", "os.path",
    # Common third-party packages that are well-known
    "click", "rich", "requests", "flask", "django", "fastapi", "sqlalchemy",
    "pydantic", "numpy", "pandas", "pytest", "setuptools", "pip",
    "aiohttp", "httpx", "boto3", "celery", "redis", "pymongo",
    "psycopg2", "aiomysql", "alembic", "jinja2", "werkzeug",
    "cryptography", "paramiko", "fabric", "invoke", "dotenv",
    "pyyaml", "toml", "black", "mypy", "flake8", "isort",
    "attrs", "cattrs", "marshmallow", "cerberus",
    "difflib", "abc", "pathlib", "typing_extensions",
}


class HallucinationChecker:
    """Cross-references LLM-extracted references against the codebase index."""

    def __init__(self, index: CodebaseIndex, codebase_root: str) -> None:
        self._index = index
        self._root = Path(codebase_root).resolve()

        # Build flat lists for similarity matching
        self._all_function_names: List[str] = list(index.functions.keys())
        self._all_class_names: List[str] = list(index.classes.keys())
        self._all_method_names: List[str] = list(index.methods.keys())
        self._all_variable_names: List[str] = list(index.variables.keys())
        self._all_files: List[str] = list(index.files)
        self._all_module_exports: Dict[str, Set[str]] = index.module_exports

    def check(
        self,
        references: List[CodeReference],
        input_text: str,
    ) -> HallucinationReport:
        report = HallucinationReport(
            total_references=len(references),
            input_lines=len(input_text.splitlines()),
            codebase_root=str(self._root),
            python_files_indexed=self._index.python_files_count,
            total_files_indexed=self._index.total_files_count,
        )

        for ref in references:
            result = self._check_reference(ref)
            if result.verified:
                report.verified.append(result)
            else:
                report.hallucinations.append(result)

        return report

    # ------------------------------------------------------------------
    # Dispatch per kind
    # ------------------------------------------------------------------

    def _check_reference(self, ref: CodeReference) -> CheckResult:
        dispatch = {
            ReferenceKind.FUNCTION: self._check_function,
            ReferenceKind.CLASS: self._check_class,
            ReferenceKind.FILE: self._check_file,
            ReferenceKind.IMPORT: self._check_import,
            ReferenceKind.METHOD: self._check_method,
            ReferenceKind.VARIABLE: self._check_variable,
        }
        handler = dispatch.get(ref.kind)
        if handler is None:
            return CheckResult(reference=ref, verified=False, confidence=0.0)
        return handler(ref)

    # ------------------------------------------------------------------
    # Kind-specific checks
    # ------------------------------------------------------------------

    def _check_function(self, ref: CodeReference) -> CheckResult:
        name = ref.name
        if name in self._index.functions:
            fdefs = self._index.functions[name]
            fdef = fdefs[0]
            location = f"{fdef.file}:{fdef.line}"
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=location,
                confidence=1.0,
            )

        suggestion, score = self._closest(name, self._all_function_names)
        return CheckResult(
            reference=ref,
            verified=False,
            suggestion=self._fmt_function_suggestion(suggestion) if suggestion else None,
            suggestion_name=suggestion,
            confidence=score,
        )

    def _check_class(self, ref: CodeReference) -> CheckResult:
        name = ref.name
        if name in self._index.classes:
            cdefs = self._index.classes[name]
            cdef = cdefs[0]
            location = f"{cdef.file}:{cdef.line}"
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=location,
                confidence=1.0,
            )

        suggestion, score = self._closest(name, self._all_class_names)
        return CheckResult(
            reference=ref,
            verified=False,
            suggestion=self._fmt_class_suggestion(suggestion) if suggestion else None,
            suggestion_name=suggestion,
            confidence=score,
        )

    def _check_file(self, ref: CodeReference) -> CheckResult:
        name = ref.name
        # Try exact match first
        if name in self._index.files:
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=name,
                confidence=1.0,
            )

        # Try stripping leading ./ or ../
        normalized = name.lstrip("./").lstrip("../")
        if normalized in self._index.files:
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=normalized,
                confidence=1.0,
            )

        # Try matching just the filename part
        basename = os.path.basename(name)
        matches = [f for f in self._index.files if os.path.basename(f) == basename]
        if matches:
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=matches[0],
                confidence=0.85,
            )

        # Absolute path check
        abs_path = Path(name)
        if not abs_path.is_absolute():
            abs_path = self._root / name
        if abs_path.exists():
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=str(abs_path),
                confidence=1.0,
            )

        suggestion, score = self._closest(name, self._all_files)
        return CheckResult(
            reference=ref,
            verified=False,
            suggestion=suggestion if suggestion else None,
            suggestion_name=suggestion,
            confidence=score,
        )

    def _check_import(self, ref: CodeReference) -> CheckResult:
        module = ref.import_module or ""
        name = ref.import_name

        # Check if this is a known stdlib/third-party module (skip)
        root_module = module.split(".")[0]
        if root_module in STDLIB_MODULES:
            # Treat as verified — we're checking project code, not stdlib
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=f"stdlib/third-party: {module}",
                confidence=1.0,
            )

        # Check if module exists in indexed modules
        if module in self._all_module_exports:
            if name is None or name == "*":
                return CheckResult(
                    reference=ref,
                    verified=True,
                    found_at=f"module: {module}",
                    confidence=1.0,
                )
            exports = self._all_module_exports[module]
            if name in exports:
                return CheckResult(
                    reference=ref,
                    verified=True,
                    found_at=f"{module}.{name}",
                    confidence=1.0,
                )

            # Module exists but name not found — check all exports for similar
            all_exports = [f"{mod}.{n}" for mod, names in self._all_module_exports.items() for n in names]
            suggestion, score = self._closest(
                f"{module}.{name}", all_exports, cutoff=0.5
            )
            return CheckResult(
                reference=ref,
                verified=False,
                suggestion=suggestion,
                suggestion_name=suggestion,
                confidence=score,
            )

        # Try path-based lookup: "from config import X" -> look for config.py
        module_as_file = module.replace(".", "/") + ".py"
        if module_as_file in self._index.files or module.replace(".", "/") + "/__init__.py" in self._index.files:
            # The module file exists but we didn't pick it up — check variables/functions
            if name and (name in self._index.functions or name in self._index.classes or name in self._index.variables):
                return CheckResult(
                    reference=ref,
                    verified=True,
                    found_at=f"{module_as_file} -> {name}",
                    confidence=0.9,
                )
            # Module exists but name not found
            suggestion = None
            suggestion_score = 0.0
            if name:
                suggestion, suggestion_score = self._closest(
                    name,
                    self._all_function_names + self._all_class_names + self._all_variable_names,
                )
            return CheckResult(
                reference=ref,
                verified=False,
                suggestion=suggestion,
                suggestion_name=suggestion,
                confidence=suggestion_score,
            )

        # Module not found at all
        all_module_names = list(self._all_module_exports.keys())
        suggestion, score = self._closest(module, all_module_names, cutoff=0.4)
        return CheckResult(
            reference=ref,
            verified=False,
            suggestion=f"{suggestion} (module)" if suggestion else None,
            suggestion_name=suggestion,
            confidence=score,
        )

    def _check_method(self, ref: CodeReference) -> CheckResult:
        name = ref.name
        if name in self._index.methods:
            entries = self._index.methods[name]
            class_name, file_path, line = entries[0]
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=f"{file_path}:{line} (in {class_name})",
                confidence=1.0,
            )

        # Also check plain functions
        if name in self._index.functions:
            fdefs = self._index.functions[name]
            fdef = fdefs[0]
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=f"{fdef.file}:{fdef.line}",
                confidence=0.9,
            )

        suggestion, score = self._closest(
            name, self._all_method_names + self._all_function_names
        )
        return CheckResult(
            reference=ref,
            verified=False,
            suggestion=self._fmt_method_suggestion(suggestion) if suggestion else None,
            suggestion_name=suggestion,
            confidence=score,
        )

    def _check_variable(self, ref: CodeReference) -> CheckResult:
        name = ref.name
        if name in self._index.variables:
            locs = self._index.variables[name]
            file_path, line = locs[0]
            return CheckResult(
                reference=ref,
                verified=True,
                found_at=f"{file_path}:{line}",
                confidence=1.0,
            )

        suggestion, score = self._closest(name, self._all_variable_names)
        return CheckResult(
            reference=ref,
            verified=False,
            suggestion=suggestion,
            suggestion_name=suggestion,
            confidence=score,
        )

    # ------------------------------------------------------------------
    # Similarity helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _closest(
        name: str,
        pool: List[str],
        cutoff: float = 0.6,
        n: int = 1,
    ) -> Tuple[Optional[str], float]:
        """Return the closest match and its similarity score."""
        if not pool:
            return None, 0.0

        matches = difflib.get_close_matches(name, pool, n=n, cutoff=cutoff)
        if not matches:
            return None, 0.0

        best = matches[0]
        ratio = difflib.SequenceMatcher(None, name, best).ratio()
        return best, round(ratio, 2)

    def _fmt_function_suggestion(self, name: Optional[str]) -> Optional[str]:
        if name is None or name not in self._index.functions:
            return name
        fdef = self._index.functions[name][0]
        return f"{name}() in {fdef.file}:{fdef.line}"

    def _fmt_class_suggestion(self, name: Optional[str]) -> Optional[str]:
        if name is None or name not in self._index.classes:
            return name
        cdef = self._index.classes[name][0]
        return f"{name} in {cdef.file}:{cdef.line}"

    def _fmt_method_suggestion(self, name: Optional[str]) -> Optional[str]:
        if name is None:
            return None
        if name in self._index.methods:
            class_name, file_path, line = self._index.methods[name][0]
            return f"{name}() in {class_name} ({file_path}:{line})"
        if name in self._index.functions:
            fdef = self._index.functions[name][0]
            return f"{name}() in {fdef.file}:{fdef.line}"
        return name
