"""In-memory messaging adapter for ACB framework.

This module provides an in-memory messaging backend implementation supporting
both pub/sub and queue patterns. Suitable for development, testing, and
single-node deployments. Messages are stored in memory and will be lost on
application restart.

Features:
- Pub/sub pattern support (events)
- Queue pattern support (tasks)
- Priority-based message ordering
- Delayed message delivery
- Dead letter queue support
- Connection pooling simulation
- Health monitoring
- Batch operations
- Metrics collection

Implementation follows ACB adapter patterns:
- Dual interface (PubSubBackend + QueueBackend)
- Public/private method delegation
- Lazy client initialization
- Async context manager support
- CleanupMixin integration
"""

import heapq
import time
from collections import defaultdict, deque
from collections.abc import AsyncGenerator, AsyncIterator
from uuid import UUID

import asyncio
import typing as t
from contextlib import asynccontextmanager
from datetime import UTC, datetime, timedelta
from pydantic import Field

from acb.adapters import AdapterCapability, AdapterMetadata, AdapterStatus
from acb.adapters.messaging._base import (
    MessagePriority,
    MessagingCapability,
    MessagingConnectionError,
    MessagingOperationError,
    QueueFullError,
    QueueMessage,
)
from acb.adapters.messaging._base import (
    MessagingSettings as BaseMessagingSettings,
)
from acb.cleanup import CleanupMixin
from acb.depends import depends

LoggerType = t.Any

# Module metadata for adapter discovery
MODULE_METADATA = AdapterMetadata(
    module_id=UUID("66c46147-3eba-49a1-949a-b01df1710453"),
    name="Memory Messaging",
    category="messaging",
    provider="memory",
    version="2.0.0",
    acb_min_version="0.25.0",
    author="ACB Team",
    created_date="2025-10-08",
    last_modified="2025-10-08",
    status=AdapterStatus.STABLE,
    capabilities=[
        AdapterCapability.ASYNC_OPERATIONS,
        AdapterCapability.CONNECTION_POOLING,
    ],
    required_packages=[],
    description="In-memory messaging adapter for development and testing",
    settings_class="MemoryMessagingSettings",
)


class MemoryMessagingSettings(BaseMessagingSettings):
    """Settings specific to memory queue implementation."""

    # Memory limits
    max_memory_usage: int = Field(
        default=100_000_000,
        description="Maximum memory usage in bytes",
    )
    max_messages_per_topic: int = Field(
        default=10_000,
        description="Maximum messages per topic/queue",
    )

    # Message retention
    message_retention_seconds: int = Field(
        default=3600,
        description="How long to retain completed messages",
    )

    # Dead letter queue
    enable_dead_letter: bool = Field(
        default=True,
        description="Enable dead letter queue for failed messages",
    )
    dead_letter_retention_seconds: int = Field(
        default=86400,
        description="Dead letter message retention time",
    )

    # Rate limiting
    enable_rate_limiting: bool = Field(
        default=False,
        description="Enable per-topic rate limiting",
    )
    rate_limit_per_second: int = Field(
        default=100,
        description="Max messages per second per topic",
    )

    # Health monitoring
    health_check_interval: float = Field(
        default=60.0,
        description="Interval between health checks in seconds",
    )


class PriorityMessageItem:
    """Wrapper for priority queue ordering."""

    def __init__(self, message: QueueMessage, scheduled_time: float) -> None:
        """Initialize priority item.

        Args:
            message: Queue message
            scheduled_time: When message should be delivered
        """
        self.message = message
        self.scheduled_time = scheduled_time
        # Higher priority values have lower sort order (negate)
        self.priority = -message.priority.value
        self.created_at = time.time()

    def __lt__(self, other: "PriorityMessageItem") -> bool:
        """Compare items for heap ordering.

        Order by:
        1. Scheduled time (earliest first)
        2. Priority (highest first)
        3. Creation time (FIFO for same priority)
        """
        if self.scheduled_time != other.scheduled_time:
            return self.scheduled_time < other.scheduled_time
        if self.priority != other.priority:
            return self.priority < other.priority
        return self.created_at < other.created_at

    def __eq__(self, other: object) -> bool:
        """Check equality by message ID."""
        if not isinstance(other, PriorityMessageItem):
            return False
        return self.message.message_id == other.message.message_id


class MemoryMessaging(CleanupMixin):
    """In-memory queue backend implementation.

    Provides a full-featured queue implementation that runs entirely in memory.
    Suitable for development, testing, and single-process applications.

    Implements both PubSubBackend and QueueBackend protocols (UnifiedMessagingBackend).

    Capabilities:
    - Priority-based message ordering
    - Delayed message delivery
    - Dead letter queue
    - Rate limiting (optional)
    - Batch operations
    - Health monitoring

    Limitations:
    - Messages lost on restart (no persistence)
    - No cross-process communication
    - No distributed clustering
    """

    def __init__(self, settings: MemoryMessagingSettings | None = None) -> None:
        """Initialize memory messaging.

        Args:
            settings: Memory messaging configuration
        """
        super().__init__()
        self._settings: MemoryMessagingSettings = settings or MemoryMessagingSettings()

        # Connection management
        self._client: MemoryMessaging | None = None
        self._connection_lock = asyncio.Lock()
        self._shutdown_event = asyncio.Event()

        # Message storage
        self._queues: dict[str, list[PriorityMessageItem]] = defaultdict(list)
        self._delayed_messages: list[PriorityMessageItem] = []
        self._processing_messages: dict[UUID, QueueMessage] = {}
        self._dead_letter_messages: dict[UUID, tuple[QueueMessage, str]] = {}

        # Rate limiting
        self._rate_limiter: dict[str, deque[float]] = defaultdict(deque)

        # Metrics
        self._memory_usage = 0
        self._total_messages = 0
        self._messages_sent = 0
        self._messages_received = 0
        self._messages_acknowledged = 0
        self._messages_rejected = 0

        # Background tasks
        self._delayed_processor_task: asyncio.Task[None] | None = None
        self._cleanup_task: asyncio.Task[None] | None = None
        self._logger: LoggerType | None = None
        self._connected: bool = False

    @property
    def logger(self) -> LoggerType:
        """Lazy-initialize logger.

        Returns:
            Logger instance
        """
        if self._logger is None:
            from acb.adapters import import_adapter

            logger_cls = import_adapter("logger")
            self._logger = t.cast(LoggerType, depends.get_sync(logger_cls))
        return self._logger

    # ========================================================================
    # Public Lifecycle Methods
    # ========================================================================

    async def connect(self) -> None:
        """Establish connection to the in-memory backend."""
        await self._connect()

    async def disconnect(self) -> None:
        """Disconnect from the in-memory backend."""
        await self._disconnect()

    # ========================================================================
    # Abstract Implementation Methods (Private)
    # ========================================================================

    async def _ensure_client(self) -> "MemoryMessaging":
        """Ensure queue client is initialized.

        For memory queue, "client" is just the internal state.
        This method follows ACB patterns for consistency.

        Returns:
            Self reference (memory queue uses internal state)
        """
        if self._client is None:
            async with self._connection_lock:
                if self._client is None:
                    # For memory queue, "client" is just self
                    self._client = self
        return self._client

    async def _connect(self) -> None:
        """Connect to memory queue backend.

        Initializes background tasks for delayed message processing
        and periodic cleanup.
        """
        if self._connected:
            return

        async with self._connection_lock:
            if self._connected:
                return

            # Initialize client
            await self._ensure_client()

            # Start background tasks
            self._delayed_processor_task = asyncio.create_task(
                self._process_delayed_messages(),
            )
            if self._settings.enable_dead_letter:
                self._cleanup_task = asyncio.create_task(
                    self._periodic_cleanup(),
                )

            self._connected = True
            self.logger.info("Memory queue connected")

    async def _disconnect(self) -> None:
        """Disconnect from memory queue backend.

        Stops background tasks and clears state.
        """
        if not self._connected:
            return

        async with self._connection_lock:
            if not self._connected:
                return

            # Signal shutdown
            self._shutdown_event.set()

            # Stop background tasks
            if self._delayed_processor_task:
                self._delayed_processor_task.cancel()
                try:
                    await self._delayed_processor_task
                except asyncio.CancelledError:
                    pass

            if self._cleanup_task:
                self._cleanup_task.cancel()
                try:
                    await self._cleanup_task
                except asyncio.CancelledError:
                    pass

            # Clear state
            self._queues.clear()
            self._delayed_messages.clear()
            self._processing_messages.clear()
            self._dead_letter_messages.clear()
            self._rate_limiter.clear()

            self._client = None
            self._connected = False
            self.logger.info("Memory queue disconnected")

    async def _health_check(self) -> dict[str, t.Any]:
        """Perform health check on memory queue.

        Returns:
            Health status with metrics
        """
        total_pending = sum(len(queue) for queue in self._queues.values())
        memory_percent = (
            self._memory_usage / self._settings.max_memory_usage * 100
            if self._settings.max_memory_usage > 0
            else 0
        )

        return {
            "connected": self._connected,
            "healthy": self._connected and memory_percent < 90,
            "memory_usage_bytes": self._memory_usage,
            "memory_usage_percent": memory_percent,
            "total_messages": self._total_messages,
            "pending_messages": total_pending,
            "delayed_messages": len(self._delayed_messages),
            "processing_messages": len(self._processing_messages),
            "dead_letter_messages": len(self._dead_letter_messages),
            "messages_sent": self._messages_sent,
            "messages_received": self._messages_received,
            "messages_acknowledged": self._messages_acknowledged,
            "messages_rejected": self._messages_rejected,
            "queue_count": len(self._queues),
        }

    async def _send(
        self,
        message: QueueMessage,
        timeout: float | None = None,
    ) -> str:
        """Send a message to the queue.

        Args:
            message: Message to send
            timeout: Operation timeout (unused for memory)

        Returns:
            Message ID

        Raises:
            MessagingConnectionError: If not connected
            QueueFullError: If queue is full
            MessagingOperationError: If send fails
        """
        if not self._connected:
            raise MessagingConnectionError("Queue not connected")

        # Check memory limits
        message_size = self._estimate_message_size(message)
        if self._memory_usage + message_size > self._settings.max_memory_usage:
            raise QueueFullError(
                f"Memory limit exceeded: {self._memory_usage} + {message_size} "
                f"> {self._settings.max_memory_usage}",
            )

        # Check queue size limits
        if len(self._queues[message.queue]) >= self._settings.max_messages_per_topic:
            raise QueueFullError(
                f"Queue {message.queue} is full "
                f"({self._settings.max_messages_per_topic} messages)",
            )

        # Check rate limiting
        if self._settings.enable_rate_limiting:
            if not await self._check_rate_limit(message.queue):
                raise MessagingOperationError(
                    f"Rate limit exceeded for queue {message.queue}",
                )

        # Calculate scheduled time
        current_time = time.time()
        if message.delay_seconds > 0:
            scheduled_time = current_time + message.delay_seconds
        else:
            # Immediate messages use priority ordering
            scheduled_time = 0.0

        # Create priority item
        item = PriorityMessageItem(message, scheduled_time)

        # Add to appropriate queue
        if scheduled_time > current_time:
            # Delayed message
            heapq.heappush(self._delayed_messages, item)
        else:
            # Immediate message
            heapq.heappush(self._queues[message.queue], item)

        # Update metrics
        self._memory_usage += message_size
        self._total_messages += 1
        self._messages_sent += 1

        self.logger.debug(
            f"Sent message {message.message_id} to queue {message.queue}",
        )

        return str(message.message_id)

    async def _receive(
        self,
        topic: str,
        timeout: float | None = None,
    ) -> QueueMessage | None:
        """Receive a message from a topic.

        Args:
            topic: Topic to receive from
            timeout: Operation timeout (unused for memory)

        Returns:
            Message or None if queue is empty

        Raises:
            MessagingConnectionError: If not connected
        """
        if not self._connected:
            raise MessagingConnectionError("Queue not connected")

        # Check if queue has messages
        queue = self._queues[topic]
        if not queue:
            return None

        # Get highest priority message
        item = heapq.heappop(queue)
        message = item.message

        # Move to processing
        self._processing_messages[message.message_id] = message

        # Update metrics
        self._messages_received += 1

        self.logger.debug(
            f"Received message {message.message_id} from topic {topic}",
        )

        return message

    async def _acknowledge(
        self,
        message: QueueMessage,
        timeout: float | None = None,
    ) -> None:
        """Acknowledge successful message processing.

        Args:
            message: Message to acknowledge
            timeout: Operation timeout (unused for memory)

        Raises:
            MessagingOperationError: If message not in processing state
        """
        if message.message_id not in self._processing_messages:
            raise MessagingOperationError(
                f"Message {message.message_id} not in processing state",
            )

        # Remove from processing
        del self._processing_messages[message.message_id]

        # Update memory usage
        message_size = self._estimate_message_size(message)
        self._memory_usage = max(0, self._memory_usage - message_size)
        self._total_messages = max(0, self._total_messages - 1)

        # Update metrics
        self._messages_acknowledged += 1

        self.logger.debug(f"Acknowledged message {message.message_id}")

    async def _reject(
        self,
        message: QueueMessage,
        requeue: bool = False,
        timeout: float | None = None,
    ) -> None:
        """Reject a message (negative acknowledgment).

        Args:
            message: Message to reject
            requeue: Whether to requeue the message
            timeout: Operation timeout (unused for memory)

        Raises:
            MessagingOperationError: If message not in processing state
        """
        if message.message_id not in self._processing_messages:
            raise MessagingOperationError(
                f"Message {message.message_id} not in processing state",
            )

        # Remove from processing
        del self._processing_messages[message.message_id]

        # Update metrics
        self._messages_rejected += 1

        if requeue and message.retry_count < message.max_retries:
            # Requeue with incremented retry count
            message.retry_count += 1
            item = PriorityMessageItem(message, time.time())
            heapq.heappush(self._queues[message.queue], item)

            self.logger.debug(
                f"Requeued message {message.message_id} "
                f"(retry {message.retry_count}/{message.max_retries})",
            )
        else:
            # Move to dead letter queue
            if self._settings.enable_dead_letter:
                reason = (
                    "Max retries exceeded"
                    if message.retry_count >= message.max_retries
                    else "Message rejected without requeue"
                )
                self._dead_letter_messages[message.message_id] = (message, reason)

                self.logger.warning(
                    f"Message {message.message_id} moved to dead letter queue: "
                    f"{reason}",
                )
            else:
                # Just discard
                message_size = self._estimate_message_size(message)
                self._memory_usage = max(0, self._memory_usage - message_size)
                self._total_messages = max(0, self._total_messages - 1)

    @asynccontextmanager  # type: ignore[arg-type]
    async def _subscribe(
        self,
        topic: str,
        prefetch: int | None = None,
    ) -> AsyncIterator[AsyncGenerator[QueueMessage]]:
        """Subscribe to a topic and stream messages.

        Args:
            topic: Topic to subscribe to
            prefetch: Number of messages to prefetch

        Yields:
            Async generator yielding messages
        """

        async def message_generator() -> AsyncGenerator[QueueMessage]:
            """Generate messages from the topic."""
            while not self._shutdown_event.is_set():
                try:
                    message = await self._receive(topic, timeout=1.0)
                    if message:
                        yield message
                    else:
                        # No messages available, wait a bit
                        await asyncio.sleep(0.1)
                except asyncio.CancelledError:
                    break
                except Exception as e:
                    self.logger.error(f"Error in message generator: {e}")
                    await asyncio.sleep(1.0)

        try:
            yield message_generator()
        finally:
            # Cleanup on exit
            pass

    async def _create_queue(
        self,
        name: str,
        **options: t.Any,
    ) -> None:
        """Create a new queue.

        Args:
            name: Queue name
            **options: Queue options (ignored for memory)
        """
        # For memory queue, queues are created automatically
        if name not in self._queues:
            self._queues[name] = []
            self.logger.info(f"Created queue: {name}")

    async def _delete_queue(
        self,
        name: str,
        if_empty: bool = False,
    ) -> None:
        """Delete a queue.

        Args:
            name: Queue name
            if_empty: Only delete if queue is empty

        Raises:
            MessagingOperationError: If queue not empty and if_empty=True
        """
        if name not in self._queues:
            return

        queue = self._queues[name]
        if if_empty and queue:
            raise MessagingOperationError(
                f"Cannot delete non-empty queue: {name}",
            )

        # Update memory usage
        for item in queue:
            message_size = self._estimate_message_size(item.message)
            self._memory_usage = max(0, self._memory_usage - message_size)
            self._total_messages = max(0, self._total_messages - 1)

        del self._queues[name]
        self.logger.info(f"Deleted queue: {name}")

    async def _purge_queue(
        self,
        name: str,
    ) -> int:
        """Remove all messages from a queue.

        Args:
            name: Queue name

        Returns:
            Number of messages removed
        """
        if name not in self._queues:
            return 0

        queue = self._queues[name]
        count = len(queue)

        # Update memory usage
        for item in queue:
            message_size = self._estimate_message_size(item.message)
            self._memory_usage = max(0, self._memory_usage - message_size)
            self._total_messages = max(0, self._total_messages - 1)

        queue.clear()
        self.logger.info(f"Purged {count} messages from queue: {name}")

        return count

    async def _get_queue_size(
        self,
        name: str,
    ) -> int:
        """Get number of messages in a queue.

        Args:
            name: Queue name

        Returns:
            Number of pending messages
        """
        return len(self._queues.get(name, []))

    async def _list_queues(
        self,
        pattern: str | None = None,
    ) -> list[str]:
        """List all queues.

        Args:
            pattern: Optional pattern to filter queues (unused for memory)

        Returns:
            List of queue names
        """
        queue_names = list(self._queues.keys())

        if pattern:
            # Simple prefix matching
            queue_names = [name for name in queue_names if name.startswith(pattern)]

        return sorted(queue_names)

    # ========================================================================
    # Utility Methods
    # ========================================================================

    def get_capabilities(self) -> set[MessagingCapability]:
        """Get supported capabilities.

        Returns:
            Set of capabilities
        """
        return {
            MessagingCapability.BASIC_QUEUE,
            MessagingCapability.PUB_SUB,
            MessagingCapability.PRIORITY_QUEUE,
            MessagingCapability.DELAYED_MESSAGES,
            MessagingCapability.DEAD_LETTER_QUEUE,
            MessagingCapability.BATCH_OPERATIONS,
            MessagingCapability.PATTERN_SUBSCRIBE,
            MessagingCapability.BROADCAST,
        }

    def supports_capability(self, capability: MessagingCapability) -> bool:
        """Check if a capability is supported.

        Args:
            capability: Capability to check

        Returns:
            True if supported
        """
        return capability in self.get_capabilities()

    # QueueBackend interface methods
    async def enqueue(
        self,
        queue: str,
        message: bytes,
        priority: MessagePriority = MessagePriority.NORMAL,
        delay_seconds: float = 0.0,
        headers: dict[str, str] | None = None,
    ) -> str:
        """Add a message to a queue.

        Args:
            queue: Queue name
            message: Message payload as bytes
            priority: Message priority
            delay_seconds: Delay before message becomes available
            headers: Optional message headers

        Returns:
            Message ID for tracking
        """
        if not self._connected:
            raise MessagingConnectionError("Queue not connected")

        # Check memory limits
        fake_message = QueueMessage(
            queue=queue,
            payload=message,
            priority=priority,
            delay_seconds=delay_seconds,
            headers=headers or {},
        )
        message_size = self._estimate_message_size(fake_message)
        if self._memory_usage + message_size > self._settings.max_memory_usage:
            raise QueueFullError(
                f"Memory limit exceeded: {self._memory_usage} + {message_size} "
                f"> {self._settings.max_memory_usage}",
            )

        # Check queue size limits
        if len(self._queues[queue]) >= self._settings.max_messages_per_topic:
            raise QueueFullError(
                f"Queue {queue} is full "
                f"({self._settings.max_messages_per_topic} messages)",
            )

        # Check rate limiting
        if self._settings.enable_rate_limiting:
            if not await self._check_rate_limit(queue):
                raise MessagingOperationError(
                    f"Rate limit exceeded for queue {queue}",
                )

        # Calculate scheduled time
        current_time = time.time()
        if delay_seconds > 0:
            scheduled_time = current_time + delay_seconds
        else:
            # Immediate messages use priority ordering
            scheduled_time = 0.0

        # Create priority item
        queue_message = QueueMessage(
            queue=queue,
            payload=message,
            priority=priority,
            delay_seconds=delay_seconds,
            headers=headers or {},
        )
        item = PriorityMessageItem(queue_message, scheduled_time)

        # Add to appropriate queue
        if scheduled_time > current_time:
            # Delayed message
            heapq.heappush(self._delayed_messages, item)
        else:
            # Immediate message
            heapq.heappush(self._queues[queue], item)

        # Update metrics
        self._memory_usage += message_size
        self._total_messages += 1
        self._messages_sent += 1

        self.logger.debug(
            f"Sent message {queue_message.message_id} to queue {queue}",
        )

        return str(queue_message.message_id)

    async def dequeue(
        self,
        queue: str,
        timeout: float | None = None,
        visibility_timeout: float = 30.0,
    ) -> QueueMessage | None:
        """Remove and return a message from a queue.

        Args:
            queue: Queue name
            timeout: Wait timeout in seconds (None for no wait)
            visibility_timeout: Time before message returns to queue if not acked

        Returns:
            Message if available, None otherwise
        """
        if not self._connected:
            raise MessagingConnectionError("Queue not connected")

        # Check if queue has messages
        queue_list = self._queues[queue]
        if not queue_list:
            return None

        # Get highest priority message
        item = heapq.heappop(queue_list)
        message = item.message

        # Move to processing
        self._processing_messages[message.message_id] = message

        # Update metrics
        self._messages_received += 1

        self.logger.debug(
            f"Received message {message.message_id} from queue {queue}",
        )

        return message

    async def acknowledge(
        self,
        queue: str,
        message_id: str,
    ) -> None:
        """Acknowledge successful processing of a message.

        Args:
            queue: Queue name
            message_id: ID of message to acknowledge
        """
        if message_id not in self._processing_messages:
            raise MessagingOperationError(
                f"Message {message_id} not in processing state",
            )

        # Remove from processing
        message = self._processing_messages[message_id]
        del self._processing_messages[message_id]

        # Update memory usage
        message_size = self._estimate_message_size(message)
        self._memory_usage = max(0, self._memory_usage - message_size)
        self._total_messages = max(0, self._total_messages - 1)

        # Update metrics
        self._messages_acknowledged += 1

        self.logger.debug(f"Acknowledged message {message_id}")

    async def reject(
        self,
        queue: str,
        message_id: str,
        requeue: bool = True,
    ) -> None:
        """Reject a message, optionally requeuing it.

        Args:
            queue: Queue name
            message_id: ID of message to reject
            requeue: Whether to return message to queue
        """
        if message_id not in self._processing_messages:
            raise MessagingOperationError(
                f"Message {message_id} not in processing state",
            )

        message = self._processing_messages[message_id]
        # Remove from processing
        del self._processing_messages[message_id]

        # Update metrics
        self._messages_rejected += 1

        if requeue and message.retry_count < message.max_retries:
            # Requeue with incremented retry count
            updated_message = QueueMessage(
                message_id=message.message_id,
                timestamp=message.timestamp,
                queue=message.queue,
                payload=message.payload,
                priority=message.priority,
                delay_seconds=message.delay_seconds,
                max_retries=message.max_retries,
                retry_count=message.retry_count + 1,
                correlation_id=message.correlation_id,
                headers=message.headers,
            )
            item = PriorityMessageItem(updated_message, time.time())
            heapq.heappush(self._queues[message.queue], item)

            self.logger.debug(
                f"Requeued message {message_id} "
                f"(retry {updated_message.retry_count}/{message.max_retries})",
            )
        else:
            # Move to dead letter queue
            if self._settings.enable_dead_letter:
                reason = (
                    "Max retries exceeded"
                    if message.retry_count >= message.max_retries
                    else "Message rejected without requeue"
                )
                self._dead_letter_messages[message.message_id] = (message, reason)

                self.logger.warning(
                    f"Message {message_id} moved to dead letter queue: {reason}",
                )
            else:
                # Just discard
                message_size = self._estimate_message_size(message)
                self._memory_usage = max(0, self._memory_usage - message_size)
                self._total_messages = max(0, self._total_messages - 1)

    async def purge_queue(self, queue: str) -> int:
        """Remove all messages from a queue.

        Returns:
            Number of messages purged
        """
        if queue not in self._queues:
            return 0

        queue_list = self._queues[queue]
        count = len(queue_list)

        if count > 0:
            # Update memory usage
            for item in queue_list:
                message_size = self._estimate_message_size(item.message)
                self._memory_usage = max(0, self._memory_usage - message_size)
                self._total_messages = max(0, self._total_messages - 1)

            queue_list.clear()
            self.logger.info(f"Purged {count} messages from queue: {queue}")

        return count

    async def get_queue_stats(self, queue: str) -> dict[str, t.Any]:
        """Get statistics for a queue.

        Returns:
            Dict with stats like message_count, consumer_count, etc.
        """
        queue_list = self._queues.get(queue, [])

        # Count messages by priority
        priority_counts = {}
        for item in queue_list:
            priority_name = item.message.priority.name
            priority_counts[priority_name] = priority_counts.get(priority_name, 0) + 1

        return {
            "name": queue,
            "message_count": len(queue_list),
            "priority_distribution": priority_counts,
            "oldest_message_time": min(
                (item.created_at for item in queue_list), default=None
            ),
            "newest_message_time": max(
                (item.created_at for item in queue_list), default=None
            ),
        }

    async def send_to_dlq(
        self,
        queue: str,
        message: QueueMessage,
        reason: str,
    ) -> None:
        """Send a failed message to the dead letter queue.

        Default implementation enqueues to {queue}_dlq.
        Backends can override for custom DLQ handling.
        """
        if self._settings.enable_dead_letter:
            self._dead_letter_messages[message.message_id] = (message, reason)

            # Update metrics
            self._total_messages = max(0, self._total_messages - 1)
            message_size = self._estimate_message_size(message)
            self._memory_usage = max(0, self._memory_usage - message_size)

            self.logger.info(f"Message {message.message_id} sent to DLQ: {reason}")
        else:
            # If DLQ is disabled, just remove from processing without storing
            self.logger.info(f"Message {message.message_id} failed (no DLQ): {reason}")

    async def get_queue_info(self, queue_name: str) -> dict[str, t.Any]:
        """Get information about a queue."""
        return await self.get_queue_stats(queue_name)

    async def list_queues(self) -> list[str]:
        """List all available queues."""
        return list(self._queues.keys())

    # ========================================================================
    # Background Tasks
    # ========================================================================

    async def _process_delayed_messages(self) -> None:
        """Background task to move delayed messages to main queues."""
        while not self._shutdown_event.is_set():
            try:
                current_time = time.time()
                moved_count = self._move_ready_messages(current_time)

                if moved_count > 0:
                    self.logger.debug(
                        f"Moved {moved_count} delayed messages to queues",
                    )

                # Sleep until next check
                sleep_time = self._calculate_sleep_time(current_time)
                await asyncio.sleep(sleep_time)

            except asyncio.CancelledError:
                break
            except Exception as e:
                self.logger.exception(f"Delayed processor error: {e}")
                await asyncio.sleep(1.0)

    def _move_ready_messages(self, current_time: float) -> int:
        """Move delayed messages that are ready to main queues.

        Args:
            current_time: Current timestamp

        Returns:
            Number of messages moved
        """
        moved_count = 0

        while self._delayed_messages:
            if self._delayed_messages[0].scheduled_time <= current_time:
                item = heapq.heappop(self._delayed_messages)
                heapq.heappush(self._queues[item.message.queue], item)
                moved_count += 1
            else:
                break

        return moved_count

    def _calculate_sleep_time(self, current_time: float) -> float:
        """Calculate optimal sleep time until next message processing.

        Args:
            current_time: Current timestamp

        Returns:
            Sleep time in seconds
        """
        if not self._delayed_messages:
            return 1.0

        next_time = self._delayed_messages[0].scheduled_time
        return min(1.0, max(0.1, next_time - current_time))

    async def _periodic_cleanup(self) -> None:
        """Background task for periodic cleanup."""
        while not self._shutdown_event.is_set():
            try:
                # Clean up old dead letter messages
                await self._cleanup_dead_letter_messages()

                # Sleep until next cleanup
                await asyncio.sleep(self._settings.health_check_interval)

            except asyncio.CancelledError:
                break
            except Exception as e:
                self.logger.exception(f"Cleanup error: {e}")
                await asyncio.sleep(self._settings.health_check_interval)

    async def _cleanup_dead_letter_messages(self) -> None:
        """Clean up old dead letter messages."""
        if not self._settings.enable_dead_letter:
            return

        current_time = datetime.now(tz=UTC)
        ttl = timedelta(seconds=self._settings.dead_letter_retention_seconds)

        # Find expired messages
        expired_ids = [
            message_id
            for message_id, (message, _) in self._dead_letter_messages.items()
            if (current_time - message.timestamp) > ttl
        ]

        # Remove expired messages
        for message_id in expired_ids:
            message, _ = self._dead_letter_messages[message_id]
            del self._dead_letter_messages[message_id]

            # Update memory usage
            message_size = self._estimate_message_size(message)
            self._memory_usage = max(0, self._memory_usage - message_size)
            self._total_messages = max(0, self._total_messages - 1)

        if expired_ids:
            self.logger.debug(
                f"Cleaned up {len(expired_ids)} expired dead letter messages",
            )

    async def _check_rate_limit(self, topic: str) -> bool:
        """Check if topic is within rate limits.

        Args:
            topic: Topic name

        Returns:
            True if under rate limit
        """
        if not self._settings.enable_rate_limiting:
            return True

        current_time = time.time()
        window_start = current_time - 1.0  # 1 second window

        # Clean old timestamps
        rate_queue = self._rate_limiter[topic]
        while rate_queue and rate_queue[0] < window_start:
            rate_queue.popleft()

        # Check if under limit
        if len(rate_queue) < self._settings.rate_limit_per_second:
            rate_queue.append(current_time)
            return True

        return False

    def _estimate_message_size(self, message: QueueMessage) -> int:
        """Estimate memory usage of a message.

        Args:
            message: Message to estimate

        Returns:
            Estimated size in bytes
        """
        import sys

        try:
            # Estimate based on payload and metadata
            payload_size = len(message.payload)
            metadata_size = (
                sys.getsizeof(message.queue)
                + sys.getsizeof(message.message_id)
                + sys.getsizeof(message.headers)
            )
            return payload_size + metadata_size + 500  # Base overhead
        except Exception:
            return 1000  # Default if calculation fails

    # ========================================================================
    # Async Context Manager Support
    # ========================================================================

    async def __aenter__(self) -> "MemoryQueue":
        """Enter async context.

        Returns:
            Self
        """
        await self.connect()
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: t.Any,
    ) -> None:
        """Exit async context.

        Args:
            exc_type: Exception type
            exc_val: Exception value
            exc_tb: Exception traceback
        """
        await self.disconnect()


# Export with role-specific names for DI
# These allow the same implementation to be used for both patterns
MemoryPubSub = MemoryMessaging  # For events system (pub/sub)
MemoryQueue = MemoryMessaging  # For tasks system (queues)

Messaging = MemoryMessaging
MessagingSettings = MemoryMessagingSettings

depends.set(Messaging, "memory")
