import json
import logging
import os
import threading
from typing import Any, Callable, Optional

import redis

logger = logging.getLogger(__name__)


class CacheService:
    INVALIDATION_CHANNEL = "hieromem:cache:invalidate"
    VERSION_PREFIX = "hieromem:cache:version"

    def __init__(self, redis_url: str = None):
        self.redis_url = redis_url or os.getenv("REDIS_URL", "redis://localhost:6379/0")
        self._invalidation_callbacks: list[Callable[[str, int | None], None]] = []
        self._pubsub_thread: Optional[threading.Thread] = None
        self._local_versions: dict[str, int] = {}

        try:
            self.redis = redis.from_url(self.redis_url, decode_responses=True)
            # Ping to check connection
            self.redis.ping()
            logger.info(f"CacheService connected to {self.redis_url}")

            # Start pub/sub listener in background
            self._start_pubsub_listener()
        except Exception as e:
            logger.warning(f"CacheService failed to connect to Redis: {e}")
            self.redis = None

    def get(self, key: str) -> Optional[Any]:
        if not self.redis:
            return None
        try:
            val = self.redis.get(key)
            return json.loads(val) if val else None
        except Exception as e:
            logger.warning(f"Cache get failed for {key}: {e}")
            return None

    def set(self, key: str, value: Any, ttl: int = 3600) -> None:
        if not self.redis:
            return
        try:
            self.redis.setex(key, ttl, json.dumps(value))
        except Exception as e:
            logger.warning(f"Cache set failed for {key}: {e}")

    def delete(self, key: str) -> None:
        if not self.redis:
            return
        try:
            self.redis.delete(key)
        except Exception as e:
            logger.warning(f"Cache delete failed for {key}: {e}")

    def publish_invalidation(self, namespace: str, *, version: int | None = None) -> None:
        """Publish a cache invalidation event to all instances."""
        if not self.redis:
            return
        try:
            payload = {"namespace": namespace, "action": "invalidate"}
            if version is not None:
                payload["version"] = version
            message = json.dumps(payload)
            self.redis.publish(self.INVALIDATION_CHANNEL, message)
            logger.debug(f"Published cache invalidation for namespace: {namespace}")
        except Exception as e:
            logger.warning(f"Failed to publish invalidation: {e}")

    def subscribe_to_invalidations(self, callback: Callable[[str, int | None], None]) -> None:
        """Register a callback to be called when cache invalidation events are received."""
        self._invalidation_callbacks.append(callback)
        logger.debug(f"Registered invalidation callback: {callback.__name__}")

    def get_version(self, namespace: str) -> int:
        """Get the cache version for a namespace."""
        key = f"{self.VERSION_PREFIX}:{namespace}"
        if not self.redis:
            return self._local_versions.get(namespace, 0)
        try:
            val = self.redis.get(key)
            if val is None:
                self.redis.set(key, 0)
                return 0
            return int(val)
        except Exception as e:
            logger.warning(f"Cache version get failed for {namespace}: {e}")
            return self._local_versions.get(namespace, 0)

    def bump_version(self, namespace: str) -> int:
        """Increment the cache version for a namespace and return it."""
        key = f"{self.VERSION_PREFIX}:{namespace}"
        if not self.redis:
            self._local_versions[namespace] = self._local_versions.get(namespace, 0) + 1
            return self._local_versions[namespace]
        try:
            version = int(self.redis.incr(key))
            return version
        except Exception as e:
            logger.warning(f"Cache version bump failed for {namespace}: {e}")
            self._local_versions[namespace] = self._local_versions.get(namespace, 0) + 1
            return self._local_versions[namespace]

    def _start_pubsub_listener(self) -> None:
        """Start a background thread to listen for pub/sub messages."""
        if not self.redis:
            return

        def listen():
            try:
                pubsub = self.redis.pubsub()
                pubsub.subscribe(self.INVALIDATION_CHANNEL)
                logger.info(f"Subscribed to {self.INVALIDATION_CHANNEL}")

                for message in pubsub.listen():
                    if message['type'] == 'message':
                        try:
                            data = json.loads(message['data'])
                            namespace = data.get('namespace')
                            if namespace:
                                version = data.get("version")
                                # Call all registered callbacks
                                for callback in self._invalidation_callbacks:
                                    try:
                                        callback(namespace, version)
                                    except Exception as e:
                                        logger.error(f"Invalidation callback failed: {e}")
                        except Exception as e:
                            logger.warning(f"Failed to process invalidation message: {e}")
            except Exception as e:
                logger.error(f"Pub/sub listener failed: {e}")

        self._pubsub_thread = threading.Thread(target=listen, daemon=True)
        self._pubsub_thread.start()

    def close(self) -> None:
        """Close Redis connection and stop pub/sub listener."""
        if self.redis:
            try:
                self.redis.close()
            except Exception as e:
                logger.warning(f"Failed to close Redis connection: {e}")
