import json
import logging
import time
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple

import redis

from protean.port.broker import BaseBroker, BrokerCapabilities, DLQEntry, registry

if TYPE_CHECKING:
    from protean.domain import Domain

logger = logging.getLogger(__name__)

# Constants
DATA_FIELD = "data"
STREAM_ID_START = "0"
CONSUMER_GROUP_SEPARATOR = ":"
NEW_MESSAGES_MARK = ">"


class RedisBroker(BaseBroker):
    """Redis Streams as the Message Broker.

    This broker leverages Redis Streams' native consumer group functionality
    with built-in ack/nack support, providing true message queue semantics.
    """

    __broker__ = "redis"

    def __init__(self, name: str, domain: "Domain", conn_info: Dict) -> None:
        super().__init__(name, domain, conn_info)

        self.redis_instance = redis.Redis.from_url(conn_info["URI"])
        self._consumer_name = f"consumer-{int(time.time() * 1000)}"
        self._created_groups_set = set()
        self._group_creation_times = {}  # Track creation times for consistency

        # Add compatibility attributes for generic tests
        # Redis Streams handle these differently but tests expect these attributes
        self._max_retries = 3  # Default value for compatibility
        self._retry_delay = 1.0
        self._message_timeout = 300.0
        self._enable_dlq = False

    @property
    def capabilities(self) -> BrokerCapabilities:
        """Redis Streams provide ordered messaging with native consumer groups and blocking reads."""
        return (
            BrokerCapabilities.ORDERED_MESSAGING
            | BrokerCapabilities.BLOCKING_READ
            | BrokerCapabilities.DEAD_LETTER_QUEUE
        )

    @property
    def _created_groups(self):
        """Property to access created groups (allows for test monkeypatching)"""
        return self._created_groups_set

    def _publish(self, stream: str, message: dict) -> str:
        """Publish a message to Redis Stream using XADD"""
        serialized_message = {DATA_FIELD: json.dumps(message or {})}
        redis_stream_id = self.redis_instance.xadd(stream, serialized_message)
        return self._decode_if_bytes(redis_stream_id)

    def _get_next(self, stream: str, consumer_group: str) -> Optional[Tuple[str, dict]]:
        """Get next message from Redis Stream using consumer group

        Reads the next available message, prioritizing new messages over pending ones.
        This method relies on Redis Streams' native behavior where each call to
        XREADGROUP with ">" returns the next unread message.
        """
        self._ensure_group(consumer_group, stream)

        try:
            # Always try to read new messages first
            # Redis guarantees that each consumer in a group gets different messages
            response = self.redis_instance.xreadgroup(
                consumer_group,
                self._consumer_name,
                {stream: NEW_MESSAGES_MARK},
                count=1,
            )

            # If we got a new message, return it
            extracted = self._extract_message_from_response(response)
            if extracted:
                return extracted

            # No new messages, return None
            # We don't automatically read pending messages here because that would
            # cause the same message to be returned multiple times without ACK
            # Pending messages should only be read when explicitly retrying failed messages
            return None

        except redis.ResponseError as e:
            return self._handle_redis_error(e, stream, consumer_group)
        except Exception as e:
            logger.error(f"Unexpected error in _get_next: {e}")
            return None

    def _extract_message_from_response(self, response) -> Optional[Tuple[str, dict]]:
        """Extract message from Redis response"""
        if not (response and response[0][1]):
            return None

        _, messages = response[0]
        if not messages:
            return None

        message_id, fields = messages[0]
        redis_id_str = self._decode_if_bytes(message_id)
        message = self._deserialize_message(fields)
        return (redis_id_str, message)

    def _handle_redis_error(
        self, error: redis.ResponseError, stream: str, consumer_group: str
    ) -> Optional[Tuple[str, dict]]:
        """Handle Redis errors during message retrieval"""
        if "NOGROUP" in str(error):
            self._ensure_group(consumer_group, stream)
            return self._get_next(stream, consumer_group)

        logger.debug(f"Redis error in _get_next: {error}")
        return None

    def _deserialize_message(self, fields: dict) -> dict:
        """Deserialize the message from the fields"""
        data_field = self._extract_data_field(fields)
        if data_field is None:
            return {}

        try:
            data_str = self._decode_if_bytes(data_field)
            return json.loads(data_str)
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            logger.error(f"Failed to deserialize message: {e}")
            return {}

    def _extract_data_field(self, fields: dict):
        """Extract the data field from Redis fields"""
        for key, value in fields.items():
            key_str = self._decode_if_bytes(key)
            if key_str == DATA_FIELD:
                return value
        return None

    def _decode_if_bytes(self, value) -> str:
        """Convert bytes to string if needed, otherwise return as string"""
        if isinstance(value, bytes):
            return value.decode()
        return str(value)

    def _read(
        self, stream: str, consumer_group: str, no_of_messages: int
    ) -> List[Tuple[str, dict]]:
        """Read multiple messages from Redis Stream"""
        self._ensure_group(consumer_group, stream)

        messages = []
        try:
            # Read all requested messages at once to maintain order
            # First try to read new messages
            response = self.redis_instance.xreadgroup(
                consumer_group,
                self._consumer_name,
                {stream: NEW_MESSAGES_MARK},
                count=no_of_messages,
            )

            if response and response[0][1]:
                for message_id, fields in response[0][1]:
                    if fields:
                        redis_id_str = self._decode_if_bytes(message_id)
                        message = self._deserialize_message(fields)
                        messages.append((redis_id_str, message))

            # If we didn't get enough messages, try reading pending messages
            if len(messages) < no_of_messages:
                remaining = no_of_messages - len(messages)
                response = self.redis_instance.xreadgroup(
                    consumer_group, self._consumer_name, {stream: "0"}, count=remaining
                )

                if response and response[0][1]:
                    seen_ids = {msg[0] for msg in messages}  # Avoid duplicates
                    for message_id, fields in response[0][1]:
                        if fields:
                            redis_id_str = self._decode_if_bytes(message_id)
                            if redis_id_str not in seen_ids:
                                message = self._deserialize_message(fields)
                                messages.append((redis_id_str, message))
                                seen_ids.add(redis_id_str)
                                if len(messages) >= no_of_messages:
                                    break

        except redis.ResponseError as e:
            logger.error(f"Error reading messages: {e}")

        return messages[:no_of_messages]  # Ensure we don't return more than requested

    def _read_blocking(
        self,
        stream: str,
        consumer_group: str,
        consumer_name: str,
        timeout_ms: int = 5000,
        count: int = 1,
    ) -> List[Tuple[str, dict]]:
        """Read messages from Redis Stream using blocking mode with XREADGROUP.

        This method uses Redis's XREADGROUP with BLOCK parameter for efficient
        blocking reads, avoiding CPU waste from polling. It first checks for
        pending messages (from previous failed attempts) before reading new messages.

        Args:
            stream (str): The stream from which to read messages
            consumer_group (str): The consumer group identifier
            consumer_name (str): The unique consumer name within the group
            timeout_ms (int): Timeout in milliseconds to wait for messages (0 = block indefinitely)
            count (int): Maximum number of messages to read

        Returns:
            List[Tuple[str, dict]]: The list of (identifier, message) tuples
        """
        self._ensure_group(consumer_group, stream)

        try:
            # First, try to read pending messages (messages that were delivered but not ACKed)
            # Use "0" to read pending messages for this consumer
            response = self.redis_instance.xreadgroup(
                consumer_group,
                consumer_name,
                {stream: "0"},  # "0" means read pending messages
                count=count,
                block=0,  # Non-blocking for pending messages
            )

            # If we got pending messages, return them
            if response:
                messages = []
                for stream_name, stream_messages in response:
                    for message_id, fields in stream_messages:
                        if fields:  # Pending messages might have None fields if they've been claimed
                            redis_id_str = self._decode_if_bytes(message_id)
                            message = self._deserialize_message(fields)
                            messages.append((redis_id_str, message))
                if messages:
                    return messages

            # No pending messages, now try to read new messages with blocking
            # Limit blocking timeout to 1000ms (1 second) to ensure signal responsiveness
            # This allows the event loop to process signals more frequently
            effective_timeout = min(timeout_ms, 1000)

            response = self.redis_instance.xreadgroup(
                consumer_group,
                consumer_name,
                {stream: NEW_MESSAGES_MARK},
                count=count,
                block=effective_timeout,  # Block for at most 1 second
            )

            if not response:
                # Timeout occurred, no messages available
                return []

            # Extract messages from response
            messages = []
            for stream_name, stream_messages in response:
                for message_id, fields in stream_messages:
                    redis_id_str = self._decode_if_bytes(message_id)
                    message = self._deserialize_message(fields)
                    messages.append((redis_id_str, message))

            return messages

        except redis.ResponseError as e:
            if "NOGROUP" in str(e):
                # Consumer group doesn't exist — invalidate cache and recreate.
                # Use a single retry to avoid infinite recursion if the group
                # cannot be created (e.g. stream was flushed while engine runs).
                group_key = f"{stream}{CONSUMER_GROUP_SEPARATOR}{consumer_group}"
                self._created_groups_set.discard(group_key)
                self._ensure_group(consumer_group, stream)
                try:
                    response = self.redis_instance.xreadgroup(
                        consumer_group,
                        consumer_name,
                        {stream: NEW_MESSAGES_MARK},
                        count=count,
                        block=min(timeout_ms, 1000),
                    )
                    if not response:
                        return []
                    messages = []
                    for stream_name, stream_messages in response:
                        for message_id, fields in stream_messages:
                            redis_id_str = self._decode_if_bytes(message_id)
                            message = self._deserialize_message(fields)
                            messages.append((redis_id_str, message))
                    return messages
                except Exception as retry_err:
                    logger.error(f"Retry after NOGROUP failed: {retry_err}")
                    return []
            logger.error(f"Redis error in _read_blocking: {e}")
            return []
        except Exception as e:
            logger.error(f"Unexpected error in _read_blocking: {e}")
            return []

    def _ack(self, stream: str, identifier: str, consumer_group: str) -> bool:
        """Acknowledge message using Redis Streams XACK

        Redis natively handles ACK state - once ACKed, a message is removed from
        the pending list and cannot be ACKed again.
        """
        try:
            result = self.redis_instance.xack(stream, consumer_group, identifier)
            # result is the number of messages successfully acknowledged
            # 0 means the message was not pending (already ACKed or doesn't exist)
            # 1 means the message was successfully acknowledged
            return bool(result)
        except redis.ResponseError as e:
            logger.warning(f"Failed to ack message {identifier} in {stream}: {e}")
            return False
        except Exception as e:
            logger.error(f"Unexpected error during ack: {e}")
            return False

    def _nack(self, stream: str, identifier: str, consumer_group: str) -> bool:
        """Negative acknowledge - message remains in pending list for reprocessing

        In Redis Streams, NACK is implicit - a message stays pending until ACKed.
        We just verify the message is indeed pending.
        """
        try:
            # Ensure the consumer group exists first
            self._ensure_group(consumer_group, stream)

            # Check if the message is actually pending
            if not self._is_message_pending(stream, consumer_group, identifier):
                return False

            # In Redis Streams, the message automatically remains pending
            # No explicit NACK operation is needed
            logger.debug(
                f"Message {identifier} in {stream} remains pending for reprocessing"
            )
            return True

        except Exception as e:
            logger.error(f"Unexpected error during nack: {e}")
            return False

    def _is_message_pending(
        self, stream: str, consumer_group: str, identifier: str
    ) -> bool:
        """Check if message is in pending list for the consumer group"""
        try:
            pending_info = self.redis_instance.xpending_range(
                stream, consumer_group, min=identifier, max=identifier, count=1
            )
        except redis.ResponseError as e:
            logger.debug(f"Redis error during XPENDING: {e}")
            return False

        if not pending_info:
            logger.debug(
                f"Message {identifier} not found in pending list for {consumer_group}"
            )
            return False

        # Validate message info
        message_info = pending_info[0]
        message_id = self._decode_if_bytes(message_info["message_id"])

        if message_id != identifier:
            logger.debug(
                f"Message ID mismatch: expected {identifier}, got {message_id}"
            )
            return False

        return True

    # ------------------------------------------------------------------
    # DLQ Management
    # ------------------------------------------------------------------

    def _dlq_list(self, dlq_streams: list[str], limit: int) -> list[DLQEntry]:
        """List DLQ messages across specified DLQ streams."""
        entries: list[DLQEntry] = []
        for dlq_stream in dlq_streams:
            try:
                raw_messages = self.redis_instance.xrange(dlq_stream)
            except redis.ResponseError:
                # Stream doesn't exist
                continue

            for redis_id, fields in raw_messages:
                entry = self._parse_dlq_entry(dlq_stream, redis_id, fields)
                if entry:
                    entries.append(entry)

        # Sort by failed_at descending (newest first)
        entries.sort(key=lambda e: e.failed_at or "", reverse=True)
        return entries[:limit]

    def _dlq_inspect(self, dlq_stream: str, dlq_id: str) -> DLQEntry | None:
        """Inspect a specific DLQ message by ID."""
        try:
            raw_messages = self.redis_instance.xrange(
                dlq_stream, min=dlq_id, max=dlq_id, count=1
            )
        except redis.ResponseError:
            return None

        if not raw_messages:
            return None

        redis_id, fields = raw_messages[0]
        return self._parse_dlq_entry(dlq_stream, redis_id, fields)

    def _dlq_replay(self, dlq_stream: str, dlq_id: str, target_stream: str) -> bool:
        """Replay a single DLQ message back to its original stream."""
        try:
            raw_messages = self.redis_instance.xrange(
                dlq_stream, min=dlq_id, max=dlq_id, count=1
            )
        except redis.ResponseError:
            return False

        if not raw_messages:
            return False

        redis_id, fields = raw_messages[0]
        message = self._deserialize_message(fields)
        # Strip DLQ metadata before republishing
        message.pop("_dlq_metadata", None)
        self._publish(target_stream, message)
        self.redis_instance.xdel(dlq_stream, self._decode_if_bytes(redis_id))
        logger.info(f"Replayed DLQ message '{dlq_id}' to stream '{target_stream}'")
        return True

    def _dlq_replay_all(self, dlq_stream: str, target_stream: str) -> int:
        """Replay all DLQ messages from a stream."""
        try:
            raw_messages = self.redis_instance.xrange(dlq_stream)
        except redis.ResponseError:
            return 0

        replayed = 0
        for redis_id, fields in raw_messages:
            message = self._deserialize_message(fields)
            message.pop("_dlq_metadata", None)
            self._publish(target_stream, message)
            self.redis_instance.xdel(dlq_stream, self._decode_if_bytes(redis_id))
            replayed += 1
        return replayed

    def _dlq_purge(self, dlq_stream: str) -> int:
        """Purge all messages from a DLQ stream."""
        try:
            count = self.redis_instance.xlen(dlq_stream)
        except redis.ResponseError:
            return 0
        if count > 0:
            self.redis_instance.delete(dlq_stream)
        return count

    def _parse_dlq_entry(
        self, dlq_stream: str, redis_id: bytes | str, fields: dict
    ) -> DLQEntry | None:
        """Parse a raw Redis DLQ message into a DLQEntry."""
        message = self._deserialize_message(fields)
        if not message:
            return None

        dlq_meta = message.get("_dlq_metadata", {})
        redis_id_str = self._decode_if_bytes(redis_id)

        return DLQEntry(
            dlq_id=redis_id_str,
            original_id=dlq_meta.get("original_id", redis_id_str),
            stream=dlq_meta.get("original_stream", dlq_stream.removesuffix(":dlq")),
            consumer_group=dlq_meta.get("consumer_group", ""),
            payload=message,
            failure_reason=dlq_meta.get("failure_reason", "unknown"),
            failed_at=dlq_meta.get("failed_at"),
            retry_count=int(dlq_meta.get("retry_count", 0)),
            dlq_stream=dlq_stream,
        )

    def _ensure_group(self, group_name: str, stream: str) -> None:
        """Create consumer group if it doesn't exist"""
        group_key = f"{stream}{CONSUMER_GROUP_SEPARATOR}{group_name}"

        if group_key in self._created_groups:
            return

        try:
            self.redis_instance.xgroup_create(
                stream, group_name, id=STREAM_ID_START, mkstream=True
            )
            logger.debug(f"Created consumer group {group_name} for stream {stream}")
            self._created_groups_set.add(group_key)
            # Track creation time for consistency
            if group_name not in self._group_creation_times:
                self._group_creation_times[group_name] = time.time()
        except redis.ResponseError as e:
            if "BUSYGROUP" in str(e):
                self._created_groups_set.add(group_key)
                # Track creation time for existing groups too
                if group_name not in self._group_creation_times:
                    self._group_creation_times[group_name] = time.time()
            else:
                logger.warning(
                    f"Failed to create consumer group {group_name} for stream {stream}: {e}"
                )
        except Exception as e:
            logger.error(
                f"Error ensuring consumer group {group_name} for stream {stream}: {e}"
            )

    def _cleanup_stale_consumers(
        self, stream: str, group_name: str, current_consumer_name: str
    ) -> int:
        """Remove stale consumers from a consumer group.

        When an engine restarts with the same hostname and PID, old consumer
        entries persist in Redis.  This method identifies consumers that share
        the same ``{ClassName}-{hostname}-{pid}`` prefix as
        *current_consumer_name* but have a different random-hex suffix, and
        deletes them via ``XGROUP DELCONSUMER``.

        Consumers with pending (un-ACKed) messages are skipped so they can
        drain naturally.

        Returns the number of stale consumers removed.
        """
        # Extract prefix: everything before the last "-" (the random hex)
        last_dash = current_consumer_name.rfind("-")
        if last_dash == -1:
            return 0
        current_prefix = current_consumer_name[:last_dash]

        removed = 0
        try:
            consumers_info = self.redis_instance.xinfo_consumers(stream, group_name)
            for c in consumers_info:
                if not isinstance(c, dict):
                    continue
                name = self._get_field_value(c, "name")
                if name is None or name == current_consumer_name:
                    continue

                # Same class-hostname-pid prefix but different random hex?
                name_last_dash = name.rfind("-")
                if name_last_dash == -1 or name[:name_last_dash] != current_prefix:
                    continue

                pending = self._get_field_value(c, "pending", convert_to_int=True) or 0
                if pending > 0:
                    logger.debug(
                        f"Skipping stale consumer {name} "
                        f"with {pending} pending message(s)"
                    )
                    continue

                try:
                    self.redis_instance.xgroup_delconsumer(stream, group_name, name)
                    removed += 1
                    logger.debug(f"Removed stale consumer {name} from {group_name}")
                except Exception as e:
                    logger.debug(f"Failed to remove stale consumer {name}: {e}")
        except Exception as e:
            logger.debug(
                f"Error cleaning stale consumers for {group_name} on {stream}: {e}"
            )

        return removed

    def _info(self) -> dict:
        """Get information about consumer groups and consumers"""
        info = {"consumer_groups": {}}
        stream_info_nested = {}  # For Redis-specific structure

        try:
            streams_to_check = self._get_streams_to_check()

            for stream in streams_to_check:
                stream_info = self._get_stream_info(stream)
                if stream_info:
                    # Store nested structure for Redis-specific access
                    stream_info_nested[stream] = stream_info

                    # Flatten the structure to match inline broker format
                    for group_name, group_details in stream_info.items():
                        # Add created_at and consumer_count for compatibility
                        group_details["created_at"] = self._group_creation_times.get(
                            group_name, time.time()
                        )
                        group_details["consumer_count"] = len(
                            group_details["consumers"]
                        )
                        info["consumer_groups"][group_name] = group_details

            # Add nested structure for Redis-specific tests
            info["consumer_groups"].update(stream_info_nested)

        except Exception as e:
            logger.error(f"Error getting broker info: {e}")

        return info

    def _get_streams_to_check(self) -> set:
        """Get set of streams to check for info"""
        streams = set(self._subscribers.keys())

        # Add streams from created consumer groups
        for group_key in self._created_groups:
            if CONSUMER_GROUP_SEPARATOR in group_key:
                stream_name = group_key.split(CONSUMER_GROUP_SEPARATOR, 1)[0]
                streams.add(stream_name)

        return streams

    def _get_stream_info(self, stream: str) -> Optional[dict]:
        """Get info for a specific stream"""
        try:
            groups_info = self.redis_instance.xinfo_groups(stream)
            stream_info = {}

            for group_info in groups_info:
                if not isinstance(group_info, dict):
                    continue

                group_data = self._extract_group_data(group_info, stream)
                if group_data:
                    group_name, group_details = group_data
                    stream_info[group_name] = group_details

            return stream_info if stream_info else None

        except redis.ResponseError:
            # Stream might not exist
            return None

    def _extract_group_data(
        self, group_info: dict, stream: str
    ) -> Optional[Tuple[str, dict]]:
        """Extract group data from Redis group info"""
        try:
            # Handle both bytes and string keys
            group_name = self._get_field_value(group_info, "name")
            pending_count = self._get_field_value(
                group_info, "pending", convert_to_int=True
            )
            last_delivered_id = self._get_field_value(group_info, "last-delivered-id")

            if group_name is None:
                return None

            consumers_info = self.redis_instance.xinfo_consumers(stream, group_name)
            consumers = self._extract_consumers_data(consumers_info)

            return (
                str(group_name),
                {
                    "consumers": consumers,
                    "pending": pending_count,
                    "last_delivered_id": last_delivered_id,
                },
            )

        except Exception as e:
            logger.debug(f"Error extracting group data: {e}")
            return None

    def _extract_consumers_data(self, consumers_info) -> List[dict]:
        """Extract consumers data from Redis consumer info"""
        consumers = []
        for consumer_info in consumers_info:
            if not isinstance(consumer_info, dict):
                continue

            consumer_name = self._get_field_value(consumer_info, "name")
            consumer_pending = self._get_field_value(
                consumer_info, "pending", convert_to_int=True
            )

            if consumer_name is not None:
                consumers.append({"name": consumer_name, "pending": consumer_pending})

        return consumers

    def _get_field_value(
        self, info_dict: dict, field_name: str, convert_to_int: bool = False
    ):
        """Get field value from Redis info dict, handling both bytes and string keys"""
        for key_format in [field_name.encode(), field_name]:
            if key_format in info_dict:
                value = info_dict[key_format]
                decoded_value = self._decode_if_bytes(value)

                if convert_to_int:
                    try:
                        return int(decoded_value)
                    except (ValueError, TypeError):
                        logger.warning(
                            f"Failed to convert {decoded_value} to int for field {field_name}"
                        )
                        return 0

                return decoded_value
        return None

    def _ping(self) -> bool:
        """Test basic connectivity to Redis broker"""
        try:
            return self.redis_instance.ping()
        except Exception as e:
            logger.debug(f"Redis ping failed: {e}")
            return False

    def _calculate_message_counts(self) -> dict:
        """Calculate message counts across all streams"""
        try:
            streams_to_check = self._get_streams_to_check()
            total_messages = 0
            total_pending = 0

            for stream in streams_to_check:
                try:
                    # Get stream length (total messages)
                    stream_length = self.redis_instance.xlen(stream)
                    total_messages += stream_length

                    # Get pending messages for all consumer groups in this stream
                    try:
                        groups_info = self.redis_instance.xinfo_groups(stream)
                        for group_info in groups_info:
                            if isinstance(group_info, dict):
                                pending_count = self._get_field_value(
                                    group_info, "pending", convert_to_int=True
                                )
                                total_pending += (
                                    int(pending_count)
                                    if pending_count is not None
                                    else 0
                                )
                    except redis.ResponseError:
                        # Stream might not have consumer groups yet
                        pass

                except redis.ResponseError:
                    # Stream might not exist
                    pass

            # Failed messages would be those in DLQ or exceeded retry limits
            # For now, we don't track failed messages separately
            failed_count = 0

            return {
                "total_messages": total_messages,
                "in_flight": total_pending,  # Redis uses pending list for unacknowledged messages
                "failed": failed_count,  # Count nacked messages as failed
                "dlq": 0,  # Redis Streams don't have explicit DLQ
            }

        except Exception as e:
            logger.debug(f"Error calculating message counts: {e}")
            return {"total_messages": 0, "in_flight": 0, "failed": 0, "dlq": 0}

    def _calculate_streams_info(self) -> dict:
        """Calculate streams information"""
        try:
            streams_to_check = self._get_streams_to_check()
            # Filter out streams that don't actually exist
            existing_streams = []
            for stream in streams_to_check:
                try:
                    if self.redis_instance.xlen(stream) >= 0:  # Stream exists
                        existing_streams.append(stream)
                except redis.ResponseError:
                    # Stream doesn't exist, skip it
                    pass

            return {"count": len(existing_streams), "names": sorted(existing_streams)}
        except Exception as e:
            logger.debug(f"Error calculating streams info: {e}")
            return {"count": 0, "names": []}

    def _calculate_consumer_groups_info(self) -> dict:
        """Calculate consumer groups information"""
        try:
            # Get all unique consumer group names across all streams
            consumer_groups = set()

            # Access _created_groups safely in case it's patched to raise an exception
            created_groups = self._created_groups

            for group_key in created_groups:
                if CONSUMER_GROUP_SEPARATOR in group_key:
                    _, group_name = group_key.split(CONSUMER_GROUP_SEPARATOR, 1)
                    consumer_groups.add(group_name)

            return {"count": len(consumer_groups), "names": sorted(consumer_groups)}
        except Exception as e:
            logger.debug(f"Error calculating consumer groups info: {e}")
            return {"count": 0, "names": []}

    def _health_stats(self) -> dict:
        """Get Redis-specific health and performance statistics"""
        try:
            redis_info = self.redis_instance.info()

            # Calculate message counts across all streams
            message_counts = self._calculate_message_counts()

            # Calculate streams and consumer groups info
            streams_info = self._calculate_streams_info()
            consumer_groups_info = self._calculate_consumer_groups_info()

            # Extract key Redis metrics
            stats = {
                "healthy": True,
                "redis_version": redis_info.get("redis_version", "unknown"),
                "connected_clients": redis_info.get("connected_clients", 0),
                "used_memory": redis_info.get("used_memory", 0),
                "used_memory_human": redis_info.get("used_memory_human", "0B"),
                "used_memory_peak": redis_info.get("used_memory_peak", 0),
                "used_memory_peak_human": redis_info.get(
                    "used_memory_peak_human", "0B"
                ),
                "keyspace_hits": redis_info.get("keyspace_hits", 0),
                "keyspace_misses": redis_info.get("keyspace_misses", 0),
                "expired_keys": redis_info.get("expired_keys", 0),
                "evicted_keys": redis_info.get("evicted_keys", 0),
                "total_commands_processed": redis_info.get(
                    "total_commands_processed", 0
                ),
                "instantaneous_ops_per_sec": redis_info.get(
                    "instantaneous_ops_per_sec", 0
                ),
                "role": redis_info.get("role", "unknown"),
                "uptime_in_seconds": redis_info.get("uptime_in_seconds", 0),
                "tcp_port": redis_info.get("tcp_port", 0),
                "message_counts": message_counts,
                "streams": streams_info,
                "consumer_groups": consumer_groups_info,
                "memory_estimate_bytes": redis_info.get("used_memory", 0),
            }

            # Calculate hit rate if we have the data
            hits = stats["keyspace_hits"]
            misses = stats["keyspace_misses"]
            if hits + misses > 0:
                stats["hit_rate"] = hits / (hits + misses)
            else:
                stats["hit_rate"] = 0.0

            # Check for potential health issues
            if redis_info.get("loading", 0) == 1:
                stats["healthy"] = False
                stats["warning"] = "Redis is loading data from disk"

            if redis_info.get("rejected_connections", 0) > 0:
                stats["healthy"] = False
                stats["warning"] = "Redis has rejected connections"

            # Add configuration details (Redis has different config than manual brokers)
            stats["configuration"] = {
                "broker_type": "redis_streams",
                "consumer_name": self._consumer_name,
                "native_consumer_groups": True,
                "native_ack_nack": True,
                # Redis doesn't have these config options, but tests expect them
                "max_retries": self._max_retries,
                "retry_delay": self._retry_delay,
                "message_timeout": self._message_timeout,
                "enable_dlq": self._enable_dlq,
            }

            return stats

        except Exception as e:
            logger.error(f"Error getting Redis health stats: {e}")
            return {
                "healthy": False,
                "error": str(e),
                "message_counts": {
                    "total_messages": 0,
                    "in_flight": 0,
                    "failed": 0,
                    "dlq": 0,
                },
                "streams": {"count": 0, "names": []},
                "consumer_groups": {"count": 0, "names": []},
                "memory_estimate_bytes": 0,
                "configuration": {
                    "broker_type": "redis_streams",
                    "error": "Failed to get configuration",
                    "max_retries": self._max_retries,
                    "retry_delay": self._retry_delay,
                    "message_timeout": self._message_timeout,
                    "enable_dlq": self._enable_dlq,
                },
            }

    def _ensure_connection(self) -> bool:
        """Ensure connection to Redis broker is healthy, reconnect if necessary"""
        max_attempts = 3

        for attempt in range(max_attempts):
            try:
                # Test current connection
                if self.redis_instance.ping():
                    if attempt > 0:
                        logger.info(
                            f"Redis connection restored on attempt {attempt + 1}"
                        )
                    return True

            except Exception as e:
                logger.debug(f"Redis connection attempt {attempt + 1} failed: {e}")

            # Connection failed, try to reconnect (unless it's the last attempt)
            if attempt < max_attempts - 1:
                try:
                    logger.info(
                        f"Redis connection failed, attempting to reconnect (attempt {attempt + 1}/{max_attempts})..."
                    )
                    # Create a new Redis instance with the same connection info
                    self.redis_instance = redis.Redis.from_url(self.conn_info["URI"])
                except Exception as reconnect_error:
                    logger.error(
                        f"Failed to create new Redis connection: {reconnect_error}"
                    )

        logger.error(f"Failed to ensure Redis connection after {max_attempts} attempts")
        return False

    def _data_reset(self) -> None:
        """Flush all data in Redis instance for testing"""
        try:
            self.redis_instance.flushall()
            self._created_groups_set.clear()
            self._group_creation_times.clear()
        except Exception as e:
            logger.error(f"Error during data reset: {e}")


# Self-registration function for entry point
def register():
    """Register Redis broker with Protean if redis is available."""
    try:
        import redis  # noqa: F401

        registry.register("redis", "protean.adapters.broker.redis.RedisBroker")
        logger.debug("Redis broker registered successfully")
    except ImportError as e:
        # Redis not available, skip registration
        logger.debug(f"Redis broker not registered: redis package not available ({e})")
        pass
