"""
Search Cache Utility
Provides intelligent caching for search results to avoid repeated queries.
Includes TTL, LRU eviction, and query normalization.
"""

import hashlib
import time
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, List, Optional

from loguru import logger
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from ..config.paths import get_cache_directory
from ..database.models import Base, SearchCache as SearchCacheModel


class SearchCache:
    """
    Persistent cache for search results with TTL and LRU eviction.
    Stores results in SQLite for persistence across sessions.
    """

    def __init__(
        self,
        cache_dir: str = None,
        max_memory_items: int = 1000,
        default_ttl: int = 3600,
    ):
        """
        Initialize search cache.

        Args:
            cache_dir: Directory for cache database. Defaults to data/__CACHE_DIR__
            max_memory_items: Maximum items in memory cache
            default_ttl: Default time-to-live in seconds (1 hour default)
        """
        self.max_memory_items = max_memory_items
        self.default_ttl = default_ttl

        # Setup cache directory
        if cache_dir is None:
            cache_dir = get_cache_directory() / "search_cache"
        else:
            cache_dir = Path(cache_dir)

        cache_dir.mkdir(parents=True, exist_ok=True)
        self.db_path = cache_dir / "search_cache.db"

        # Initialize database
        self._init_db()

        # In-memory cache for frequently accessed items
        self._memory_cache = {}
        self._access_times = {}

    def _init_db(self):
        """Initialize SQLite database for persistent cache using SQLAlchemy."""
        try:
            # Create engine and session
            self.engine = create_engine(f"sqlite:///{self.db_path}")
            Base.metadata.create_all(
                self.engine, tables=[SearchCacheModel.__table__]
            )
            self.Session = sessionmaker(bind=self.engine)
        except Exception:
            logger.exception("Failed to initialize search cache database")

    def _normalize_query(self, query: str) -> str:
        """Normalize query for consistent caching."""
        # Convert to lowercase and remove extra whitespace
        normalized = " ".join(query.lower().strip().split())

        # Remove common punctuation that doesn't affect search
        normalized = normalized.replace('"', "").replace("'", "")

        return normalized

    def _get_query_hash(
        self, query: str, search_engine: str = "default"
    ) -> str:
        """Generate hash for query + search engine combination."""
        normalized_query = self._normalize_query(query)
        cache_key = f"{search_engine}:{normalized_query}"
        return hashlib.md5(cache_key.encode()).hexdigest()

    def _cleanup_expired(self):
        """Remove expired entries from database."""
        try:
            current_time = int(time.time())
            with self.Session() as session:
                deleted = (
                    session.query(SearchCacheModel)
                    .filter(SearchCacheModel.expires_at < current_time)
                    .delete()
                )
                session.commit()
                if deleted > 0:
                    logger.debug(f"Cleaned up {deleted} expired cache entries")
        except Exception:
            logger.exception("Failed to cleanup expired cache entries")

    def _evict_lru_memory(self):
        """Evict least recently used items from memory cache."""
        if len(self._memory_cache) <= self.max_memory_items:
            return

        # Sort by access time and remove oldest
        sorted_items = sorted(self._access_times.items(), key=lambda x: x[1])
        items_to_remove = (
            len(self._memory_cache) - self.max_memory_items + 100
        )  # Remove extra for efficiency

        for query_hash, _ in sorted_items[:items_to_remove]:
            self._memory_cache.pop(query_hash, None)
            self._access_times.pop(query_hash, None)

    def get(
        self, query: str, search_engine: str = "default"
    ) -> Optional[List[Dict[str, Any]]]:
        """
        Get cached search results for a query.

        Args:
            query: Search query
            search_engine: Search engine identifier for cache partitioning

        Returns:
            Cached results or None if not found/expired
        """
        query_hash = self._get_query_hash(query, search_engine)
        current_time = int(time.time())

        # Check memory cache first
        if query_hash in self._memory_cache:
            entry = self._memory_cache[query_hash]
            if entry["expires_at"] > current_time:
                self._access_times[query_hash] = current_time
                logger.debug(f"Cache hit (memory) for query: {query[:50]}...")
                return entry["results"]
            else:
                # Expired, remove from memory
                self._memory_cache.pop(query_hash, None)
                self._access_times.pop(query_hash, None)

        # Check database cache
        try:
            with self.Session() as session:
                cache_entry = (
                    session.query(SearchCacheModel)
                    .filter(
                        SearchCacheModel.query_hash == query_hash,
                        SearchCacheModel.expires_at > current_time,
                    )
                    .first()
                )

                if cache_entry:
                    results = cache_entry.results

                    # Update access statistics
                    cache_entry.access_count += 1
                    cache_entry.last_accessed = current_time
                    session.commit()

                    # Add to memory cache
                    self._memory_cache[query_hash] = {
                        "results": results,
                        "expires_at": cache_entry.expires_at,
                    }
                    self._access_times[query_hash] = current_time
                    self._evict_lru_memory()

                    logger.debug(
                        f"Cache hit (database) for query: {query[:50]}..."
                    )
                    return results

        except Exception:
            logger.exception("Failed to retrieve from search cache")

        logger.debug(f"Cache miss for query: {query[:50]}...")
        return None

    def put(
        self,
        query: str,
        results: List[Dict[str, Any]],
        search_engine: str = "default",
        ttl: Optional[int] = None,
    ) -> bool:
        """
        Store search results in cache.

        Args:
            query: Search query
            results: Search results to cache
            search_engine: Search engine identifier
            ttl: Time-to-live in seconds (uses default if None)

        Returns:
            True if successfully cached
        """
        if not results:  # Don't cache empty results
            return False

        query_hash = self._get_query_hash(query, search_engine)
        current_time = int(time.time())
        expires_at = current_time + (ttl or self.default_ttl)

        try:
            # Store in database
            with self.Session() as session:
                # Check if entry exists
                existing = (
                    session.query(SearchCacheModel)
                    .filter_by(query_hash=query_hash)
                    .first()
                )

                if existing:
                    # Update existing entry
                    existing.query_text = self._normalize_query(query)
                    existing.results = results
                    existing.created_at = current_time
                    existing.expires_at = expires_at
                    existing.access_count = 1
                    existing.last_accessed = current_time
                else:
                    # Create new entry
                    cache_entry = SearchCacheModel(
                        query_hash=query_hash,
                        query_text=self._normalize_query(query),
                        results=results,
                        created_at=current_time,
                        expires_at=expires_at,
                        access_count=1,
                        last_accessed=current_time,
                    )
                    session.add(cache_entry)

                session.commit()

            # Store in memory cache
            self._memory_cache[query_hash] = {
                "results": results,
                "expires_at": expires_at,
            }
            self._access_times[query_hash] = current_time
            self._evict_lru_memory()

            logger.debug(f"Cached results for query: {query[:50]}...")
            return True

        except Exception:
            logger.exception("Failed to store in search cache")
            return False

    def invalidate(self, query: str, search_engine: str = "default") -> bool:
        """Invalidate cached results for a specific query."""
        query_hash = self._get_query_hash(query, search_engine)

        try:
            # Remove from memory
            self._memory_cache.pop(query_hash, None)
            self._access_times.pop(query_hash, None)

            # Remove from database
            with self.Session() as session:
                deleted = (
                    session.query(SearchCacheModel)
                    .filter_by(query_hash=query_hash)
                    .delete()
                )
                session.commit()

            logger.debug(f"Invalidated cache for query: {query[:50]}...")
            return deleted > 0

        except Exception:
            logger.exception("Failed to invalidate cache")
            return False

    def clear_all(self) -> bool:
        """Clear all cached results."""
        try:
            self._memory_cache.clear()
            self._access_times.clear()

            with self.Session() as session:
                session.query(SearchCacheModel).delete()
                session.commit()

            logger.info("Cleared all search cache")
            return True

        except Exception:
            logger.exception("Failed to clear search cache")
            return False

    def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        try:
            current_time = int(time.time())
            with self.Session() as session:
                # Total entries
                total_entries = (
                    session.query(SearchCacheModel)
                    .filter(SearchCacheModel.expires_at > current_time)
                    .count()
                )

                # Total expired entries
                expired_entries = (
                    session.query(SearchCacheModel)
                    .filter(SearchCacheModel.expires_at <= current_time)
                    .count()
                )

                # Average access count
                from sqlalchemy import func

                avg_access_result = (
                    session.query(func.avg(SearchCacheModel.access_count))
                    .filter(SearchCacheModel.expires_at > current_time)
                    .scalar()
                )
                avg_access = avg_access_result or 0

            return {
                "total_valid_entries": total_entries,
                "expired_entries": expired_entries,
                "memory_cache_size": len(self._memory_cache),
                "average_access_count": round(avg_access, 2),
                "cache_hit_potential": (
                    f"{(total_entries / (total_entries + 1)) * 100:.1f}%"
                    if total_entries > 0
                    else "0%"
                ),
            }

        except Exception as e:
            logger.exception("Failed to get cache stats")
            return {"error": str(e)}


# Global cache instance
_global_cache = None


def get_search_cache() -> SearchCache:
    """Get global search cache instance."""
    global _global_cache
    if _global_cache is None:
        _global_cache = SearchCache()
    return _global_cache


@lru_cache(maxsize=100)
def normalize_entity_query(entity: str, constraint: str) -> str:
    """
    Normalize entity + constraint combination for consistent caching.
    Uses LRU cache for frequent normalizations.
    """
    # Remove quotes and normalize whitespace
    entity_clean = " ".join(entity.strip().lower().split())
    constraint_clean = " ".join(constraint.strip().lower().split())

    # Create canonical form
    return f"{entity_clean} {constraint_clean}"
