"""Symbol search engine for indexed codebases.

This module provides search functionality over the symbol index.
"""

from __future__ import annotations

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

from ieum.indexing.database import SymbolDatabase


@dataclass
class SearchResult:
    """A search result item."""

    name: str
    qualified_name: str
    type: str
    file_path: str
    relative_path: str
    line: int
    column: int
    signature: Optional[str] = None
    docstring: Optional[str] = None
    parent: Optional[str] = None
    score: float = 0.0

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "name": self.name,
            "qualified_name": self.qualified_name,
            "type": self.type,
            "file_path": self.file_path,
            "relative_path": self.relative_path,
            "line": self.line,
            "column": self.column,
            "signature": self.signature,
            "docstring": self.docstring,
            "parent": self.parent,
            "score": self.score,
        }


@dataclass
class SearchFilters:
    """Filters for symbol search."""

    symbol_type: Optional[str] = None
    file_pattern: Optional[str] = None
    parent: Optional[str] = None
    visibility: Optional[str] = None


class SymbolSearch:
    """Search engine for indexed symbols."""

    def __init__(
        self,
        db: SymbolDatabase | str | Path | None = None,
        project_path: str | Path | None = None,
    ):
        """Initialize the search engine.

        Args:
            db: Database instance or path. If None, uses default.
            project_path: Optional project path for scoped search.
        """
        if isinstance(db, SymbolDatabase):
            self.db = db
        elif db is not None:
            self.db = SymbolDatabase(db)
        else:
            self.db = SymbolDatabase()

        self.project_path = Path(project_path) if project_path else None
        self._project_id: Optional[int] = None

    @property
    def project_id(self) -> Optional[int]:
        """Get project ID for scoped searches."""
        if self._project_id is None and self.project_path:
            self._project_id = self.db.get_or_create_project(str(self.project_path))
        return self._project_id

    def search(
        self,
        query: str,
        filters: Optional[SearchFilters] = None,
        limit: int = 50,
    ) -> list[SearchResult]:
        """Search for symbols matching query.

        Args:
            query: Search query (supports FTS5 syntax).
            filters: Optional search filters.
            limit: Maximum results.

        Returns:
            List of SearchResult objects.
        """
        filters = filters or SearchFilters()

        results = self.db.search_symbols(
            query=query,
            project_id=self.project_id,
            symbol_type=filters.symbol_type,
            limit=limit,
        )

        return [self._row_to_result(row) for row in results]

    def search_by_name(
        self,
        name: str,
        exact: bool = False,
        filters: Optional[SearchFilters] = None,
    ) -> list[SearchResult]:
        """Search symbols by name.

        Args:
            name: Symbol name to search.
            exact: If True, match exact name.
            filters: Optional search filters.

        Returns:
            List of SearchResult objects.
        """
        results = self.db.search_by_name(
            name=name,
            project_id=self.project_id,
            exact=exact,
        )

        # Apply additional filters
        if filters:
            results = self._apply_filters(results, filters)

        return [self._row_to_result(row) for row in results]

    def find_definition(self, symbol_name: str) -> Optional[SearchResult]:
        """Find the definition of a symbol.

        Args:
            symbol_name: Symbol name to find.

        Returns:
            SearchResult or None if not found.
        """
        results = self.search_by_name(symbol_name, exact=True)

        # Prefer definitions over references
        definitions = [r for r in results if r.type in ("function", "class", "method")]

        if definitions:
            return definitions[0]
        return results[0] if results else None

    def find_references(
        self,
        symbol_name: str,
        include_definition: bool = True,
    ) -> list[dict]:
        """Find all references to a symbol.

        Args:
            symbol_name: Symbol name to find references for.
            include_definition: If True, include the definition.

        Returns:
            List of reference dictionaries.
        """
        refs = self.db.get_symbol_references(symbol_name, self.project_id)

        results = []
        for ref in refs:
            results.append({
                "file_path": ref.get("file_path"),
                "relative_path": ref.get("relative_path"),
                "line": ref.get("line"),
                "column": ref.get("column"),
                "context": ref.get("context"),
                "reference_type": ref.get("ref_type"),
            })

        if include_definition:
            definition = self.find_definition(symbol_name)
            if definition:
                results.insert(0, {
                    "file_path": definition.file_path,
                    "relative_path": definition.relative_path,
                    "line": definition.line,
                    "column": definition.column,
                    "context": definition.signature or definition.name,
                    "reference_type": "definition",
                })

        return results

    def get_file_symbols(self, file_path: str | Path) -> list[SearchResult]:
        """Get all symbols in a file.

        Args:
            file_path: Path to the file.

        Returns:
            List of symbols in the file.
        """
        results = self.db.get_file_symbols(str(file_path))
        return [self._row_to_result(row, file_path=str(file_path)) for row in results]

    def get_call_hierarchy(self, function_name: str) -> dict:
        """Get call hierarchy for a function.

        Args:
            function_name: Function name.

        Returns:
            Dictionary with callers and callees.
        """
        # Find the function definition
        definition = self.find_definition(function_name)

        # Find references (potential callers)
        refs = self.find_references(function_name, include_definition=False)

        # Group by file for better organization
        callers_by_file: dict[str, list[dict]] = {}
        for ref in refs:
            file_path = ref.get("relative_path", ref.get("file_path", "unknown"))
            if file_path not in callers_by_file:
                callers_by_file[file_path] = []
            callers_by_file[file_path].append(ref)

        return {
            "function": function_name,
            "definition": definition.to_dict() if definition else None,
            "callers": callers_by_file,
            "total_references": len(refs),
        }

    def get_type_hierarchy(self, class_name: str) -> dict:
        """Get type hierarchy for a class.

        Args:
            class_name: Class name.

        Returns:
            Dictionary with base classes and derived classes.
        """
        # Find the class definition
        definition = self.find_definition(class_name)

        # Search for classes that might inherit from this one
        # This is a simplified implementation - a more complete version
        # would parse inheritance relationships during indexing
        potential_derived = self.search(
            class_name,
            filters=SearchFilters(symbol_type="class"),
        )

        return {
            "class": class_name,
            "definition": definition.to_dict() if definition else None,
            "potential_derived": [r.to_dict() for r in potential_derived if r.name != class_name],
        }

    def get_symbols_by_type(
        self,
        symbol_type: str,
        limit: int = 100,
    ) -> list[SearchResult]:
        """Get all symbols of a specific type.

        Args:
            symbol_type: Type to filter (function, class, etc.).
            limit: Maximum results.

        Returns:
            List of matching symbols.
        """
        return self.search(
            "*",  # Match all
            filters=SearchFilters(symbol_type=symbol_type),
            limit=limit,
        )

    def _row_to_result(
        self,
        row: dict,
        file_path: Optional[str] = None,
    ) -> SearchResult:
        """Convert a database row to SearchResult."""
        return SearchResult(
            name=row.get("name", ""),
            qualified_name=row.get("qualified_name") or row.get("name", ""),
            type=row.get("type", "unknown"),
            file_path=row.get("file_path") or file_path or "",
            relative_path=row.get("relative_path", ""),
            line=row.get("line", 0),
            column=row.get("column", 0),
            signature=row.get("signature"),
            docstring=row.get("docstring"),
            parent=row.get("parent"),
        )

    def _apply_filters(
        self,
        results: list[dict],
        filters: SearchFilters,
    ) -> list[dict]:
        """Apply additional filters to results."""
        filtered = results

        if filters.symbol_type:
            filtered = [r for r in filtered if r.get("type") == filters.symbol_type]

        if filters.parent:
            filtered = [r for r in filtered if r.get("parent") == filters.parent]

        if filters.visibility:
            filtered = [r for r in filtered if r.get("visibility") == filters.visibility]

        if filters.file_pattern:
            import fnmatch
            filtered = [
                r for r in filtered
                if fnmatch.fnmatch(r.get("file_path", ""), filters.file_pattern)
            ]

        return filtered


# Convenience functions
def search_symbols(
    query: str,
    project_path: Optional[str] = None,
    symbol_type: Optional[str] = None,
    limit: int = 50,
) -> list[dict]:
    """Search for symbols.

    Args:
        query: Search query.
        project_path: Optional project path.
        symbol_type: Optional type filter.
        limit: Maximum results.

    Returns:
        List of symbol dictionaries.
    """
    search = SymbolSearch(project_path)
    filters = SearchFilters(symbol_type=symbol_type) if symbol_type else None
    results = search.search(query, filters=filters, limit=limit)
    return [r.to_dict() for r in results]


def find_symbol(
    name: str,
    project_path: Optional[str] = None,
) -> Optional[dict]:
    """Find a symbol by exact name.

    Args:
        name: Symbol name.
        project_path: Optional project path.

    Returns:
        Symbol dictionary or None.
    """
    search = SymbolSearch(project_path)
    result = search.find_definition(name)
    return result.to_dict() if result else None


def find_references(
    symbol_name: str,
    project_path: Optional[str] = None,
) -> list[dict]:
    """Find all references to a symbol.

    Args:
        symbol_name: Symbol name.
        project_path: Optional project path.

    Returns:
        List of reference dictionaries.
    """
    search = SymbolSearch(project_path)
    return search.find_references(symbol_name)


def get_call_hierarchy(
    function_name: str,
    project_path: Optional[str] = None,
) -> dict:
    """Get call hierarchy for a function.

    Args:
        function_name: Function name.
        project_path: Optional project path.

    Returns:
        Call hierarchy dictionary.
    """
    search = SymbolSearch(project_path)
    return search.get_call_hierarchy(function_name)
