"""Redis connection management with reconnect / error detection."""
from __future__ import annotations

import logging
import random
import threading
import time
from typing import Any, Optional

logger = logging.getLogger(__name__)


class QueueError(Exception):
    """Base error for queue operations."""


class RedisConnection:
    """Manages the Redis client, including reconnects with exponential backoff.

    A single ``Redis`` client (and its underlying ``ConnectionPool``) is kept
    for the lifetime of the process.  On transient failures the pool's
    connections are recycled via ``disconnect()`` so redis-py opens fresh TCP
    sockets on the next command — without creating a new client or leaking
    old connection pools.
    """

    def __init__(
        self,
        redis_client: Any = None,
        redis_url: Optional[str] = None,
        redis_password: Optional[str] = None,
        reconnect_delay: int = 1,
        max_reconnect_delay: int = 30,
    ) -> None:
        self.redis_url = redis_url
        self.redis_password = redis_password
        self.client = redis_client or self._build(redis_url, redis_password)
        self.reconnect_delay = reconnect_delay
        self.max_reconnect_delay = max_reconnect_delay
        self._reconnect_lock = threading.Lock()
        self._generation = 0

    # -- helpers ---------------------------------------------------------

    @staticmethod
    def _build(redis_url: Optional[str], password: Optional[str]):
        if not redis_url:
            raise QueueError("Redis URL required to build client")
        try:
            import redis  # type: ignore
        except ImportError as exc:  # pragma: no cover
            raise QueueError("redis library required") from exc
        return redis.Redis.from_url(
            redis_url,
            password=password,
            decode_responses=True,
            socket_timeout=10,
            socket_connect_timeout=5,
            retry_on_timeout=True,
            health_check_interval=15,
        )

    @staticmethod
    def is_connection_error(exc: Exception) -> bool:
        try:
            import redis  # type: ignore
            return isinstance(exc, (redis.exceptions.ConnectionError, redis.exceptions.TimeoutError, OSError))
        except Exception:
            return isinstance(exc, (ConnectionError, TimeoutError, OSError))

    def reconnect(self, stop_flag=None, on_reconnect=None) -> None:
        """Thread-safe reconnect with jittered backoff.

        Instead of creating a new ``Redis`` client, this disconnects all
        pooled connections and verifies liveness with a ``PING``.  redis-py
        will transparently open fresh TCP sockets on the next command.

        All reconnect calls are serialized via ``_reconnect_lock``.  If
        another thread already completed a reconnect since the caller
        observed a failure, this is a no-op (checked via generation counter).

        Args:
            stop_flag: A callable returning bool, or a bool. When truthy,
                       the reconnect loop exits without establishing a connection.
            on_reconnect: Optional callback invoked after a successful reconnect.
        """
        gen_before = self._generation

        with self._reconnect_lock:
            if self._generation != gen_before:
                # Another thread already reconnected; use the fresh connection.
                if on_reconnect:
                    try:
                        on_reconnect()
                    except Exception:
                        logger.warning("Post-reconnect callback failed", exc_info=True)
                return

            delay = self.reconnect_delay
            while True:
                if callable(stop_flag) and stop_flag():
                    return
                if not callable(stop_flag) and stop_flag:
                    return
                try:
                    # Drop all pooled sockets so redis-py opens fresh ones.
                    self.client.connection_pool.disconnect()
                    self.client.ping()
                    self._generation += 1
                    logger.info("Reconnected to Redis")
                    if on_reconnect:
                        try:
                            on_reconnect()
                        except Exception:
                            logger.warning("Post-reconnect callback failed", exc_info=True)
                    return
                except Exception as exc:
                    jitter = random.uniform(0, delay * 0.3)
                    logger.error("Reconnect to Redis failed: %s (retrying in %.1fs)", exc, delay + jitter)
                    time.sleep(delay + jitter)
                    delay = min(delay * 2, self.max_reconnect_delay)

    def reconnect_if_needed(self) -> None:
        """Convenience alias — all reconnects go through ``reconnect()``."""
        self.reconnect()
