"""
Codebase Indexer - Builds comprehensive index of the entire codebase

Performance optimizations:
- PERF-005: File caching with mtime-based invalidation
- PERF-006: Single-pass AST walking for all metadata
- PERF-008: File size limits to skip large files

Hybrid Retrieval Integration:
- Optional summary generation with LLM
- BM25 index building for keyword search
- Code graph building for relationship traversal
"""

import ast
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple

if TYPE_CHECKING:
    from groknroll.storage.database import Database

# PERF-008: Maximum file size to index (1MB)
MAX_FILE_SIZE = 1024 * 1024


@dataclass
class FileInfo:
    """Information about a source file"""

    path: Path
    relative_path: str
    size: int
    lines: int
    language: str
    mtime: float = 0.0  # PERF-005: Track modification time for caching
    content: Optional[str] = None
    functions: list[str] = field(default_factory=list)
    classes: list[str] = field(default_factory=list)
    imports: list[str] = field(default_factory=list)


@dataclass
class CodebaseIndex:
    """Complete index of the codebase"""

    root_path: Path
    files: dict[str, FileInfo] = field(default_factory=dict)
    functions: dict[str, list[str]] = field(default_factory=dict)  # function_name -> [file_paths]
    classes: dict[str, list[str]] = field(default_factory=dict)  # class_name -> [file_paths]
    imports: dict[str, set[str]] = field(default_factory=dict)  # module -> {files_that_import_it}
    total_files: int = 0
    total_lines: int = 0
    total_size: int = 0


class CodebaseIndexer:
    """
    Indexes the entire codebase for the Oracle Agent

    Extracts:
    - File structure and contents
    - Function and class definitions
    - Import relationships
    - Documentation strings

    Performance optimizations:
    - PERF-005: Caches indexed files, skips unchanged files on rebuild
    - PERF-006: Single-pass AST walking extracts all metadata at once
    - PERF-008: Skips files larger than MAX_FILE_SIZE (1MB)
    """

    SUPPORTED_EXTENSIONS = {
        ".py": "python",
        ".js": "javascript",
        ".ts": "typescript",
        ".jsx": "javascript",
        ".tsx": "typescript",
        ".md": "markdown",
        ".txt": "text",
        ".yaml": "yaml",
        ".yml": "yaml",
        ".json": "json",
        ".toml": "toml",
    }

    IGNORE_DIRS = {
        "__pycache__",
        ".git",
        ".venv",
        "venv",
        "node_modules",
        "dist",
        "build",
        ".pytest_cache",
        ".egg-info",
        "rlm_old_backup",
        ".vscode",
        ".idea",
        "logs",
        "agent_logs",
    }

    def __init__(
        self,
        root_path: Path,
        database: Optional["Database"] = None,
        project_id: Optional[int] = None,
    ):
        """
        Initialize indexer with repository root.

        Args:
            root_path: Path to repository root
            database: Optional database for hybrid retrieval persistence
            project_id: Project ID for database operations (required if database provided)
        """
        self.root_path = Path(root_path).resolve()
        self.index = CodebaseIndex(root_path=self.root_path)
        # PERF-005: Cache of indexed files (relative_path -> (mtime, FileInfo))
        self._file_cache: Dict[str, Tuple[float, FileInfo]] = {}

        # Hybrid retrieval components (lazy initialized)
        self.database = database
        self.project_id = project_id
        self._summary_generator = None
        self._bm25_index = None
        self._code_graph = None
        self._hybrid_retriever = None

    def build_index(self, include_content: bool = True) -> CodebaseIndex:
        """
        Build complete codebase index

        PERF-005: Uses caching to skip unchanged files on rebuild.

        Args:
            include_content: Whether to include full file contents in index

        Returns:
            CodebaseIndex with all extracted information
        """
        print(f"🔍 Indexing codebase at: {self.root_path}")

        cached_count = 0
        indexed_count = 0

        # Walk directory tree
        for root, dirs, files in os.walk(self.root_path):
            # Filter out ignored directories
            dirs[:] = [d for d in dirs if d not in self.IGNORE_DIRS]

            root_path = Path(root)

            for file in files:
                file_path = root_path / file
                ext = file_path.suffix.lower()

                if ext in self.SUPPORTED_EXTENSIONS:
                    was_cached = self._index_file(file_path, include_content)
                    if was_cached:
                        cached_count += 1
                    else:
                        indexed_count += 1

        print(f"✅ Indexed {self.index.total_files} files ({self.index.total_lines:,} lines)")
        if cached_count > 0:
            print(f"   ({cached_count} from cache, {indexed_count} newly indexed)")
        return self.index

    def _should_reindex(self, file_path: Path, relative_path: str) -> bool:
        """
        Check if file needs to be re-indexed based on mtime.

        PERF-005: Caching optimization - skip unchanged files.

        Args:
            file_path: Path to file
            relative_path: Relative path for cache lookup

        Returns:
            True if file needs re-indexing, False if cache is valid
        """
        if relative_path not in self._file_cache:
            return True

        try:
            current_mtime = file_path.stat().st_mtime
            cached_mtime, _ = self._file_cache[relative_path]
            return current_mtime > cached_mtime
        except OSError:
            return True

    def _index_file(self, file_path: Path, include_content: bool) -> bool:
        """
        Index a single file with caching support.

        PERF-005: Uses cache if file unchanged.
        PERF-008: Skips files larger than MAX_FILE_SIZE.

        Args:
            file_path: Path to file to index
            include_content: Whether to include content

        Returns:
            True if used cache, False if newly indexed
        """
        try:
            relative_path = str(file_path.relative_to(self.root_path))

            # PERF-008: Skip files larger than limit
            try:
                size = file_path.stat().st_size
                if size > MAX_FILE_SIZE:
                    return False
            except OSError:
                return False

            # PERF-005: Check cache before re-indexing
            if not self._should_reindex(file_path, relative_path):
                # Use cached file info
                _, cached_info = self._file_cache[relative_path]
                self.index.files[relative_path] = cached_info
                self.index.total_files += 1
                self.index.total_lines += cached_info.lines
                self.index.total_size += cached_info.size

                # Re-add to index maps
                for func_name in cached_info.functions:
                    if func_name not in self.index.functions:
                        self.index.functions[func_name] = []
                    if relative_path not in self.index.functions[func_name]:
                        self.index.functions[func_name].append(relative_path)

                for class_name in cached_info.classes:
                    if class_name not in self.index.classes:
                        self.index.classes[class_name] = []
                    if relative_path not in self.index.classes[class_name]:
                        self.index.classes[class_name].append(relative_path)

                for module in cached_info.imports:
                    if module not in self.index.imports:
                        self.index.imports[module] = set()
                    self.index.imports[module].add(relative_path)

                return True  # Used cache

            # Read file content
            content = file_path.read_text(encoding="utf-8", errors="ignore")
            lines = content.count("\n") + 1
            mtime = file_path.stat().st_mtime

            ext = file_path.suffix.lower()
            language = self.SUPPORTED_EXTENSIONS[ext]

            # Create file info
            file_info = FileInfo(
                path=file_path,
                relative_path=relative_path,
                size=size,
                lines=lines,
                language=language,
                mtime=mtime,
                content=content if include_content else None,
            )

            # Extract Python-specific metadata
            if language == "python":
                self._extract_python_info(file_info, content)

            # Store in index and cache
            self.index.files[relative_path] = file_info
            self._file_cache[relative_path] = (mtime, file_info)
            self.index.total_files += 1
            self.index.total_lines += lines
            self.index.total_size += size

            return False  # Newly indexed

        except Exception as e:
            print(f"⚠️  Error indexing {file_path}: {e}")
            return False

    def _extract_python_info(self, file_info: FileInfo, content: str):
        """
        Extract Python-specific information using AST.

        PERF-006: Single-pass AST walking extracts functions, classes,
        and imports in one traversal instead of multiple passes.
        """
        try:
            tree = ast.parse(content)

            # PERF-006: Extract all info in one pass
            functions, classes, imports = self._extract_all_in_one_pass(tree)

            # Update file info
            file_info.functions = functions
            file_info.classes = classes
            file_info.imports = imports

            # Update global indexes
            for func_name in functions:
                if func_name not in self.index.functions:
                    self.index.functions[func_name] = []
                self.index.functions[func_name].append(file_info.relative_path)

            for class_name in classes:
                if class_name not in self.index.classes:
                    self.index.classes[class_name] = []
                self.index.classes[class_name].append(file_info.relative_path)

            for module in imports:
                if module not in self.index.imports:
                    self.index.imports[module] = set()
                self.index.imports[module].add(file_info.relative_path)

        except SyntaxError:
            # Skip files with syntax errors
            pass

    def _extract_all_in_one_pass(
        self, tree: ast.AST
    ) -> Tuple[list[str], list[str], list[str]]:
        """
        Extract functions, classes, and imports in a single AST traversal.

        PERF-006: Optimized single-pass extraction instead of multiple
        isinstance() checks per node type.

        Args:
            tree: Parsed AST tree

        Returns:
            Tuple of (functions, classes, imports) lists
        """
        functions = []
        classes = []
        imports = []

        for node in ast.walk(tree):
            node_type = type(node)

            if node_type is ast.FunctionDef or node_type is ast.AsyncFunctionDef:
                functions.append(node.name)

            elif node_type is ast.ClassDef:
                classes.append(node.name)

            elif node_type is ast.Import:
                for alias in node.names:
                    imports.append(alias.name)

            elif node_type is ast.ImportFrom:
                if node.module:
                    imports.append(node.module)

        return functions, classes, imports

    def get_file_info(self, relative_path: str) -> Optional[FileInfo]:
        """Get information about a specific file"""
        return self.index.files.get(relative_path)

    def find_function(self, function_name: str) -> list[str]:
        """Find all files containing a function"""
        return self.index.functions.get(function_name, [])

    def find_class(self, class_name: str) -> list[str]:
        """Find all files containing a class"""
        return self.index.classes.get(class_name, [])

    def find_importers(self, module_name: str) -> set[str]:
        """Find all files that import a module"""
        return self.index.imports.get(module_name, set())

    def search_files(self, pattern: str) -> list[FileInfo]:
        """Search for files matching a pattern"""
        results = []
        for relative_path, file_info in self.index.files.items():
            if pattern.lower() in relative_path.lower():
                results.append(file_info)
        return results

    def get_summary(self) -> str:
        """Get a summary of the indexed codebase"""
        summary = [
            "Codebase Summary",
            "=" * 80,
            f"Root: {self.index.root_path}",
            f"Total Files: {self.index.total_files}",
            f"Total Lines: {self.index.total_lines:,}",
            f"Total Size: {self.index.total_size / 1024:.1f} KB",
            "",
            f"Functions: {len(self.index.functions)}",
            f"Classes: {len(self.index.classes)}",
            f"Modules: {len(self.index.imports)}",
        ]

        # Language breakdown
        lang_counts = {}
        for file_info in self.index.files.values():
            lang = file_info.language
            lang_counts[lang] = lang_counts.get(lang, 0) + 1

        summary.append("")
        summary.append("Files by Language:")
        for lang, count in sorted(lang_counts.items(), key=lambda x: -x[1]):
            summary.append(f"  {lang}: {count}")

        return "\n".join(summary)

    # =========================================================================
    # Hybrid Retrieval Integration
    # =========================================================================

    def build_hybrid_index(
        self,
        include_content: bool = True,
        generate_summaries: bool = True,
        build_bm25: bool = True,
        build_graph: bool = True,
        backend: str = "anthropic",
        model: str = "claude-sonnet-4-20250514",
        verbose: bool = False,
    ) -> CodebaseIndex:
        """
        Build complete hybrid index with summaries, BM25, and code graph.

        This is the recommended method for large codebases that need
        intelligent retrieval. It builds:
        1. Standard file index (functions, classes, imports)
        2. LLM-generated summaries at all scope levels
        3. BM25 inverted index for keyword search
        4. Code dependency graph for relationship traversal

        Args:
            include_content: Whether to include file contents
            generate_summaries: Whether to generate LLM summaries
            build_bm25: Whether to build BM25 index
            build_graph: Whether to build code graph
            backend: LLM backend for summaries
            model: Model name for summaries
            verbose: Whether to print progress

        Returns:
            CodebaseIndex with all extracted information
        """
        if not self.database or not self.project_id:
            raise ValueError(
                "Database and project_id required for hybrid indexing. "
                "Initialize with database=db, project_id=id"
            )

        # Step 1: Build standard index
        self.build_index(include_content=include_content)

        # Step 2: Generate summaries
        if generate_summaries:
            self._ensure_summary_generator(backend, model, verbose)
            self._summary_generator.summarize_codebase(
                self.root_path,
                self.index.files,
            )

        # Step 3: Build BM25 index
        if build_bm25:
            self._ensure_bm25_index(verbose)
            self._bm25_index.index_summaries()

        # Step 4: Build code graph
        if build_graph:
            self._ensure_code_graph(verbose)
            self._code_graph.build_graph(self.index.files)

        return self.index

    def get_hybrid_retriever(self, verbose: bool = False) -> Any:
        """
        Get or create the hybrid retriever for this index.

        Returns:
            HybridRetriever instance configured for this codebase
        """
        if not self.database or not self.project_id:
            raise ValueError("Database and project_id required for hybrid retriever")

        if self._hybrid_retriever is None:
            from groknroll.oracle.hybrid_retriever import HybridRetriever

            self._ensure_bm25_index(verbose)
            self._ensure_code_graph(verbose)

            self._hybrid_retriever = HybridRetriever(
                database=self.database,
                project_id=self.project_id,
                bm25_index=self._bm25_index,
                code_graph=self._code_graph,
                verbose=verbose,
            )
            # Set content cache from indexed files
            self._hybrid_retriever.set_content_cache(self.index.files)

        return self._hybrid_retriever

    def _ensure_summary_generator(
        self, backend: str, model: str, verbose: bool
    ) -> None:
        """Ensure summary generator is initialized."""
        if self._summary_generator is None:
            from groknroll.oracle.summary_generator import CodeSummaryGenerator

            self._summary_generator = CodeSummaryGenerator(
                database=self.database,
                project_id=self.project_id,
                backend=backend,
                model=model,
                verbose=verbose,
            )

    def _ensure_bm25_index(self, verbose: bool) -> None:
        """Ensure BM25 index is initialized."""
        if self._bm25_index is None:
            from groknroll.oracle.bm25_index import BM25Index

            self._bm25_index = BM25Index(
                database=self.database,
                project_id=self.project_id,
                verbose=verbose,
            )

    def _ensure_code_graph(self, verbose: bool) -> None:
        """Ensure code graph builder is initialized."""
        if self._code_graph is None:
            from groknroll.oracle.code_graph import CodeGraphBuilder

            self._code_graph = CodeGraphBuilder(
                database=self.database,
                project_id=self.project_id,
                verbose=verbose,
            )

    def get_changed_files(self) -> list[str]:
        """
        Get list of files that have changed since last indexing.

        Useful for incremental updates.

        Returns:
            List of relative paths to changed files
        """
        changed = []
        for root, dirs, files in os.walk(self.root_path):
            dirs[:] = [d for d in dirs if d not in self.IGNORE_DIRS]
            root_path = Path(root)

            for file in files:
                file_path = root_path / file
                ext = file_path.suffix.lower()

                if ext in self.SUPPORTED_EXTENSIONS:
                    relative_path = str(file_path.relative_to(self.root_path))
                    if self._should_reindex(file_path, relative_path):
                        changed.append(relative_path)

        return changed
