"""
Caching service for map data to improve performance and reduce database load.
"""

import hashlib
import json
import time
from typing import Any

from django.core.cache import cache
from django.utils import timezone

from .data_structures import (
    ClusterData,
    GeoBounds,
    MapFilters,
    MapResponse,
    QueryPerformanceMetrics,
    UnifiedLocation,
)


class MapCacheService:
    """
    Handles caching of map data with geographic partitioning and intelligent invalidation.
    """

    # Cache configuration
    DEFAULT_TTL = 3600  # 1 hour
    CLUSTER_TTL = 7200  # 2 hours (clusters change less frequently)
    LOCATION_DETAIL_TTL = 1800  # 30 minutes
    BOUNDS_CACHE_TTL = 1800  # 30 minutes

    # Cache key prefixes
    CACHE_PREFIX = "thrillwiki_map"
    LOCATIONS_PREFIX = f"{CACHE_PREFIX}:locations"
    CLUSTERS_PREFIX = f"{CACHE_PREFIX}:clusters"
    BOUNDS_PREFIX = f"{CACHE_PREFIX}:bounds"
    DETAIL_PREFIX = f"{CACHE_PREFIX}:detail"
    STATS_PREFIX = f"{CACHE_PREFIX}:stats"

    # Geographic partitioning settings
    GEOHASH_PRECISION = 6  # ~1.2km precision for cache partitioning

    def __init__(self):
        self.cache_stats = {
            "hits": 0,
            "misses": 0,
            "invalidations": 0,
            "geohash_partitions": 0,
        }

    def get_locations_cache_key(
        self,
        bounds: GeoBounds | None,
        filters: MapFilters | None,
        zoom_level: int | None = None,
    ) -> str:
        """Generate cache key for location queries."""
        key_parts = [self.LOCATIONS_PREFIX]

        if bounds:
            # Use geohash for spatial locality
            geohash = self._bounds_to_geohash(bounds)
            key_parts.append(f"geo:{geohash}")

        if filters:
            # Create deterministic hash of filters
            filter_hash = self._hash_filters(filters)
            key_parts.append(f"filters:{filter_hash}")

        if zoom_level is not None:
            key_parts.append(f"zoom:{zoom_level}")

        return ":".join(key_parts)

    def get_clusters_cache_key(
        self,
        bounds: GeoBounds | None,
        filters: MapFilters | None,
        zoom_level: int,
    ) -> str:
        """Generate cache key for cluster queries."""
        key_parts = [self.CLUSTERS_PREFIX, f"zoom:{zoom_level}"]

        if bounds:
            geohash = self._bounds_to_geohash(bounds)
            key_parts.append(f"geo:{geohash}")

        if filters:
            filter_hash = self._hash_filters(filters)
            key_parts.append(f"filters:{filter_hash}")

        return ":".join(key_parts)

    def get_location_detail_cache_key(self, location_type: str, location_id: int) -> str:
        """Generate cache key for individual location details."""
        return f"{self.DETAIL_PREFIX}:{location_type}:{location_id}"

    def cache_locations(
        self,
        cache_key: str,
        locations: list[UnifiedLocation],
        ttl: int | None = None,
    ) -> None:
        """Cache location data."""
        try:
            # Convert locations to serializable format
            cache_data = {
                "locations": [loc.to_dict() for loc in locations],
                "cached_at": timezone.now().isoformat(),
                "count": len(locations),
            }

            cache.set(cache_key, cache_data, ttl or self.DEFAULT_TTL)
        except Exception as e:
            # Log error but don't fail the request
            print(f"Cache write error for key {cache_key}: {e}")

    def cache_clusters(
        self,
        cache_key: str,
        clusters: list[ClusterData],
        ttl: int | None = None,
    ) -> None:
        """Cache cluster data."""
        try:
            cache_data = {
                "clusters": [cluster.to_dict() for cluster in clusters],
                "cached_at": timezone.now().isoformat(),
                "count": len(clusters),
            }

            cache.set(cache_key, cache_data, ttl or self.CLUSTER_TTL)
        except Exception as e:
            print(f"Cache write error for clusters {cache_key}: {e}")

    def cache_map_response(self, cache_key: str, response: MapResponse, ttl: int | None = None) -> None:
        """Cache complete map response."""
        try:
            cache_data = response.to_dict()
            cache_data["cached_at"] = timezone.now().isoformat()

            cache.set(cache_key, cache_data, ttl or self.DEFAULT_TTL)
        except Exception as e:
            print(f"Cache write error for response {cache_key}: {e}")

    def get_cached_locations(self, cache_key: str) -> list[UnifiedLocation] | None:
        """Retrieve cached location data."""
        try:
            cache_data = cache.get(cache_key)
            if not cache_data:
                self.cache_stats["misses"] += 1
                return None

            self.cache_stats["hits"] += 1

            # Convert back to UnifiedLocation objects
            locations = []
            for loc_data in cache_data["locations"]:
                # Reconstruct UnifiedLocation from dictionary
                locations.append(self._dict_to_unified_location(loc_data))

            return locations

        except Exception as e:
            print(f"Cache read error for key {cache_key}: {e}")
            self.cache_stats["misses"] += 1
            return None

    def get_cached_clusters(self, cache_key: str) -> list[ClusterData] | None:
        """Retrieve cached cluster data."""
        try:
            cache_data = cache.get(cache_key)
            if not cache_data:
                self.cache_stats["misses"] += 1
                return None

            self.cache_stats["hits"] += 1

            # Convert back to ClusterData objects
            clusters = []
            for cluster_data in cache_data["clusters"]:
                clusters.append(self._dict_to_cluster_data(cluster_data))

            return clusters

        except Exception as e:
            print(f"Cache read error for clusters {cache_key}: {e}")
            self.cache_stats["misses"] += 1
            return None

    def get_cached_map_response(self, cache_key: str) -> MapResponse | None:
        """Retrieve cached map response."""
        try:
            cache_data = cache.get(cache_key)
            if not cache_data:
                self.cache_stats["misses"] += 1
                return None

            self.cache_stats["hits"] += 1

            # Convert back to MapResponse object
            return self._dict_to_map_response(cache_data["data"])

        except Exception as e:
            print(f"Cache read error for response {cache_key}: {e}")
            self.cache_stats["misses"] += 1
            return None

    def invalidate_location_cache(self, location_type: str, location_id: int | None = None) -> None:
        """Invalidate cache for specific location or all locations of a type."""
        try:
            if location_id:
                # Invalidate specific location detail
                detail_key = self.get_location_detail_cache_key(location_type, location_id)
                cache.delete(detail_key)

            # Invalidate related location and cluster caches
            # In a production system, you'd want more sophisticated cache
            # tagging
            cache.delete_many([f"{self.LOCATIONS_PREFIX}:*", f"{self.CLUSTERS_PREFIX}:*"])

            self.cache_stats["invalidations"] += 1

        except Exception as e:
            print(f"Cache invalidation error: {e}")

    def invalidate_bounds_cache(self, bounds: GeoBounds) -> None:
        """Invalidate cache for specific geographic bounds."""
        try:
            geohash = self._bounds_to_geohash(bounds)
            pattern = f"{self.LOCATIONS_PREFIX}:geo:{geohash}*"

            # In production, you'd use cache tagging or Redis SCAN
            # For now, we'll invalidate broader patterns
            cache.delete_many([pattern])

            self.cache_stats["invalidations"] += 1

        except Exception as e:
            print(f"Bounds cache invalidation error: {e}")

    def clear_all_map_cache(self) -> None:
        """Clear all map-related cache data."""
        try:
            cache.delete_many(
                [
                    f"{self.LOCATIONS_PREFIX}:*",
                    f"{self.CLUSTERS_PREFIX}:*",
                    f"{self.BOUNDS_PREFIX}:*",
                    f"{self.DETAIL_PREFIX}:*",
                ]
            )

            self.cache_stats["invalidations"] += 1

        except Exception as e:
            print(f"Cache clear error: {e}")

    def get_cache_stats(self) -> dict[str, Any]:
        """Get cache performance statistics."""
        total_requests = self.cache_stats["hits"] + self.cache_stats["misses"]
        hit_rate = (self.cache_stats["hits"] / total_requests * 100) if total_requests > 0 else 0

        return {
            "hits": self.cache_stats["hits"],
            "misses": self.cache_stats["misses"],
            "hit_rate_percent": round(hit_rate, 2),
            "invalidations": self.cache_stats["invalidations"],
            "geohash_partitions": self.cache_stats["geohash_partitions"],
        }

    def record_performance_metrics(self, metrics: QueryPerformanceMetrics) -> None:
        """Record query performance metrics for analysis."""
        try:
            # 5-minute buckets
            stats_key = f"{self.STATS_PREFIX}:performance:{int(time.time() // 300)}"

            current_stats = cache.get(
                stats_key,
                {
                    "query_count": 0,
                    "total_time_ms": 0,
                    "cache_hits": 0,
                    "db_queries": 0,
                },
            )

            current_stats["query_count"] += 1
            current_stats["total_time_ms"] += metrics.query_time_ms
            current_stats["cache_hits"] += 1 if metrics.cache_hit else 0
            current_stats["db_queries"] += metrics.db_query_count

            cache.set(stats_key, current_stats, 3600)  # Keep for 1 hour

        except Exception as e:
            print(f"Performance metrics recording error: {e}")

    def _bounds_to_geohash(self, bounds: GeoBounds) -> str:
        """Convert geographic bounds to geohash for cache partitioning."""
        # Use center point of bounds for geohash
        center_lat = (bounds.north + bounds.south) / 2
        center_lng = (bounds.east + bounds.west) / 2

        # Simple geohash implementation (in production, use a library)
        return self._encode_geohash(center_lat, center_lng, self.GEOHASH_PRECISION)

    def _encode_geohash(self, lat: float, lng: float, precision: int) -> str:
        """Simple geohash encoding implementation."""
        # This is a simplified implementation
        # In production, use the `geohash` library
        lat_range = [-90.0, 90.0]
        lng_range = [-180.0, 180.0]

        geohash = ""
        bits = 0
        bit_count = 0
        even_bit = True

        while len(geohash) < precision:
            if even_bit:
                # longitude
                mid = (lng_range[0] + lng_range[1]) / 2
                if lng >= mid:
                    bits = (bits << 1) + 1
                    lng_range[0] = mid
                else:
                    bits = bits << 1
                    lng_range[1] = mid
            else:
                # latitude
                mid = (lat_range[0] + lat_range[1]) / 2
                if lat >= mid:
                    bits = (bits << 1) + 1
                    lat_range[0] = mid
                else:
                    bits = bits << 1
                    lat_range[1] = mid

            even_bit = not even_bit
            bit_count += 1

            if bit_count == 5:
                # Convert 5 bits to base32 character
                geohash += "0123456789bcdefghjkmnpqrstuvwxyz"[bits]
                bits = 0
                bit_count = 0

        return geohash

    def _hash_filters(self, filters: MapFilters) -> str:
        """Create deterministic hash of filters for cache keys."""
        filter_dict = filters.to_dict()
        # Sort to ensure consistent ordering
        filter_str = json.dumps(filter_dict, sort_keys=True)
        return hashlib.md5(filter_str.encode()).hexdigest()[:8]

    def _dict_to_unified_location(self, data: dict[str, Any]) -> UnifiedLocation:
        """Convert dictionary back to UnifiedLocation object."""
        from .data_structures import LocationType

        return UnifiedLocation(
            id=data["id"],
            type=LocationType(data["type"]),
            name=data["name"],
            coordinates=list(data["coordinates"]),
            address=data.get("address"),
            metadata=data.get("metadata", {}),
            type_data=data.get("type_data", {}),
            cluster_weight=data.get("cluster_weight", 1),
            cluster_category=data.get("cluster_category", "default"),
        )

    def _dict_to_cluster_data(self, data: dict[str, Any]) -> ClusterData:
        """Convert dictionary back to ClusterData object."""
        from .data_structures import LocationType

        bounds = GeoBounds(**data["bounds"])
        types = {LocationType(t) for t in data["types"]}

        representative = None
        if data.get("representative"):
            representative = self._dict_to_unified_location(data["representative"])

        return ClusterData(
            id=data["id"],
            coordinates=list(data["coordinates"]),
            count=data["count"],
            types=types,
            bounds=bounds,
            representative_location=representative,
        )

    def _dict_to_map_response(self, data: dict[str, Any]) -> MapResponse:
        """Convert dictionary back to MapResponse object."""
        locations = [self._dict_to_unified_location(loc) for loc in data.get("locations", [])]
        clusters = [self._dict_to_cluster_data(cluster) for cluster in data.get("clusters", [])]

        bounds = None
        if data.get("bounds"):
            bounds = GeoBounds(**data["bounds"])

        return MapResponse(
            locations=locations,
            clusters=clusters,
            bounds=bounds,
            total_count=data.get("total_count", 0),
            filtered_count=data.get("filtered_count", 0),
            zoom_level=data.get("zoom_level"),
            clustered=data.get("clustered", False),
        )


# Global cache service instance
map_cache = MapCacheService()
