"""Codebase indexer for symbol extraction and storage.

This module provides the main indexing functionality that parses files
and stores symbols in the database.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Callable, Any

from ieum.indexing.database import SymbolDatabase, compute_file_hash
from ieum.ast.base import get_parser_for_file, ParseResult


@dataclass
class IndexResult:
    """Result of an indexing operation."""

    success: bool = True
    files_indexed: int = 0
    files_skipped: int = 0
    files_failed: int = 0
    symbols_found: int = 0
    errors: list[str] = field(default_factory=list)


class CodebaseIndexer:
    """Indexes codebase for fast symbol search."""

    # File extensions to index
    INDEXABLE_EXTENSIONS = {
        ".py", ".pyi", ".pyw",  # Python
        ".cpp", ".cxx", ".cc", ".c",  # C/C++ source
        ".h", ".hpp", ".hxx", ".hh",  # C/C++ headers
        ".js", ".jsx",  # JavaScript
        ".ts", ".tsx",  # TypeScript
    }

    # Directories to ignore
    IGNORE_DIRS = {
        ".git", ".svn", ".hg",
        "__pycache__", ".pytest_cache",
        "node_modules", "bower_components",
        ".venv", "venv", "env",
        "build", "dist", "target",
        ".tox", ".nox",
        ".eggs", "*.egg-info",
        ".mypy_cache", ".ruff_cache",
        "site-packages",
    }

    # Files to ignore
    IGNORE_FILES = {
        "__init__.py",  # Usually minimal content
    }

    def __init__(
        self,
        project_path: str | Path,
        db: Optional[SymbolDatabase] = None,
    ):
        """Initialize the indexer.

        Args:
            project_path: Root path of the project.
            db: Optional database instance.
        """
        self.project_path = Path(project_path).resolve()
        self.db = db or SymbolDatabase()
        self.project_id: Optional[int] = None

    def _should_index(self, file_path: Path) -> bool:
        """Check if a file should be indexed.

        Args:
            file_path: Path to check.

        Returns:
            True if file should be indexed.
        """
        # Check extension
        if file_path.suffix.lower() not in self.INDEXABLE_EXTENSIONS:
            return False

        # Check filename
        if file_path.name in self.IGNORE_FILES:
            return False

        # Check path components
        for part in file_path.parts:
            if part in self.IGNORE_DIRS:
                return False
            # Also check patterns like *.egg-info
            for ignore in self.IGNORE_DIRS:
                if "*" in ignore and part.endswith(ignore.replace("*", "")):
                    return False

        return True

    def _get_files_to_index(self) -> list[Path]:
        """Get list of files to index.

        Returns:
            List of file paths.
        """
        files = []

        for path in self.project_path.rglob("*"):
            if path.is_file() and self._should_index(path):
                files.append(path)

        return sorted(files)

    def index_project(
        self,
        force: bool = False,
        progress_callback: Optional[Callable[[int, int, str], None]] = None,
    ) -> IndexResult:
        """Index the entire project.

        Args:
            force: If True, reindex all files even if unchanged.
            progress_callback: Optional callback(current, total, file_path).

        Returns:
            IndexResult with statistics.
        """
        result = IndexResult()

        # Get or create project
        self.project_id = self.db.get_or_create_project(str(self.project_path))

        # Get files to index
        files = self._get_files_to_index()
        total = len(files)

        for i, file_path in enumerate(files):
            if progress_callback:
                progress_callback(i + 1, total, str(file_path))

            try:
                indexed = self.index_file(file_path, force=force)
                if indexed:
                    result.files_indexed += 1
                    result.symbols_found += indexed
                else:
                    result.files_skipped += 1
            except Exception as e:
                result.files_failed += 1
                result.errors.append(f"{file_path}: {e}")

        # Update project stats
        self.db.update_project_stats(self.project_id)

        result.success = result.files_failed == 0
        return result

    def index_file(
        self,
        file_path: str | Path,
        force: bool = False,
    ) -> int:
        """Index a single file.

        Args:
            file_path: Path to the file.
            force: If True, reindex even if unchanged.

        Returns:
            Number of symbols indexed, or 0 if skipped.
        """
        file_path = Path(file_path).resolve()

        # Ensure project is set
        if self.project_id is None:
            self.project_id = self.db.get_or_create_project(str(self.project_path))

        # Check if file needs reindexing
        current_hash = compute_file_hash(file_path)
        stored_hash = self.db.get_file_hash(str(file_path))

        if not force and stored_hash == current_hash:
            return 0  # File unchanged

        # Get parser
        parser = get_parser_for_file(file_path)
        if not parser:
            return 0  # No parser available

        # Parse file
        parse_result = parser.parse_file(file_path)
        if not parse_result.success:
            # Log errors but continue
            pass

        # Add file to database
        file_id = self.db.add_file(
            project_id=self.project_id,
            file_path=str(file_path),
            file_hash=current_hash,
            language=parse_result.language,
        )

        # Add symbols
        symbol_count = 0
        for symbol in parse_result.symbols:
            self.db.add_symbol(file_id, symbol.to_dict())
            symbol_count += 1

        # Add references
        for ref in parse_result.references:
            self.db.add_reference(file_id, ref.to_dict())

        # Update file symbol count
        self.db.update_file_symbol_count(file_id)

        return symbol_count

    def index_files(
        self,
        file_paths: list[str | Path],
        force: bool = False,
    ) -> IndexResult:
        """Index specific files.

        Args:
            file_paths: List of file paths.
            force: If True, reindex even if unchanged.

        Returns:
            IndexResult with statistics.
        """
        result = IndexResult()

        for file_path in file_paths:
            try:
                indexed = self.index_file(file_path, force=force)
                if indexed:
                    result.files_indexed += 1
                    result.symbols_found += indexed
                else:
                    result.files_skipped += 1
            except Exception as e:
                result.files_failed += 1
                result.errors.append(f"{file_path}: {e}")

        if self.project_id:
            self.db.update_project_stats(self.project_id)

        result.success = result.files_failed == 0
        return result

    def remove_file(self, file_path: str | Path) -> None:
        """Remove a file from the index.

        Args:
            file_path: Path to remove.
        """
        self.db.remove_file(str(file_path))

        if self.project_id:
            self.db.update_project_stats(self.project_id)

    def update_index(
        self,
        changed_files: list[str | Path],
    ) -> IndexResult:
        """Update index for changed files.

        Args:
            changed_files: List of files that changed.

        Returns:
            IndexResult with statistics.
        """
        return self.index_files(changed_files, force=True)

    def get_stats(self) -> dict:
        """Get index statistics.

        Returns:
            Dictionary with stats.
        """
        stats = self.db.get_stats(self.project_id)
        return {
            "total_files": stats.total_files,
            "total_symbols": stats.total_symbols,
            "total_references": stats.total_references,
            "last_indexed": stats.last_indexed.isoformat() if stats.last_indexed else None,
            "index_size_bytes": stats.index_size_bytes,
        }


# Convenience functions
def index_project(
    project_path: str | Path,
    force: bool = False,
    progress_callback: Optional[Callable[[int, int, str], None]] = None,
) -> dict:
    """Index a project.

    Args:
        project_path: Project root path.
        force: If True, reindex all files.
        progress_callback: Optional progress callback.

    Returns:
        Dictionary with indexing results.
    """
    indexer = CodebaseIndexer(project_path)
    result = indexer.index_project(force=force, progress_callback=progress_callback)

    return {
        "success": result.success,
        "files_indexed": result.files_indexed,
        "files_skipped": result.files_skipped,
        "files_failed": result.files_failed,
        "symbols_found": result.symbols_found,
        "errors": result.errors,
    }


def update_index(
    project_path: str | Path,
    changed_files: list[str | Path],
) -> dict:
    """Update index for changed files.

    Args:
        project_path: Project root path.
        changed_files: List of changed files.

    Returns:
        Dictionary with update results.
    """
    indexer = CodebaseIndexer(project_path)
    result = indexer.update_index(changed_files)

    return {
        "success": result.success,
        "files_indexed": result.files_indexed,
        "symbols_found": result.symbols_found,
        "errors": result.errors,
    }


def get_index_stats(project_path: str | Path) -> dict:
    """Get index statistics.

    Args:
        project_path: Project root path.

    Returns:
        Statistics dictionary.
    """
    indexer = CodebaseIndexer(project_path)
    return indexer.get_stats()
