"""
Feature 21: Request/Response pattern
Feature 22: Pub/Sub pattern
Feature 23: Broadcast
Feature 24: Message queuing
Feature 25: Message priority
Feature 26: Message acknowledgment
Feature 27: Message retry
Feature 28: Message deduplication
Feature 30: End-to-end encryption
Feature 35: Message TTL
Feature 36: Async messaging
Feature 38: Message batching
Feature 40: Dead letter queue
"""
import uuid
import time
import hashlib
import asyncio
from typing import List, Dict, Any, Optional, Callable
from datetime import datetime
from dataclasses import dataclass, field


# ─── Standard Message Protocol (like A2A) ─────────────────────────────────────
@dataclass
class Message:
    content: str
    from_agent: str = "user"
    to_agent: str = "auto"
    msg_type: str = "task"          # task, response, broadcast, ack
    priority: int = 1               # Feature 25: 1=low, 2=medium, 3=high
    ttl: int = 300                  # Feature 35: seconds until expiry
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
    metadata: dict = field(default_factory=dict)
    acknowledged: bool = False      # Feature 26

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "from": self.from_agent,
            "to": self.to_agent,
            "type": self.msg_type,
            "content": self.content,
            "priority": self.priority,
            "ttl": self.ttl,
            "timestamp": self.timestamp,
            "metadata": self.metadata,
            "acknowledged": self.acknowledged,
        }

    def fingerprint(self) -> str:
        """Feature 28: deduplication fingerprint."""
        return hashlib.md5(f"{self.from_agent}{self.to_agent}{self.content}".encode()).hexdigest()

    def is_expired(self) -> bool:
        """Feature 35: check TTL."""
        created = datetime.fromisoformat(self.timestamp).timestamp()
        return time.time() - created > self.ttl

    @classmethod
    def from_dict(cls, data: dict) -> "Message":
        return cls(
            id=data.get("id", str(uuid.uuid4())),
            from_agent=data.get("from", "user"),
            to_agent=data.get("to", "auto"),
            msg_type=data.get("type", "task"),
            content=data.get("content", ""),
            priority=data.get("priority", 1),
            ttl=data.get("ttl", 300),
            timestamp=data.get("timestamp", datetime.utcnow().isoformat()),
            metadata=data.get("metadata", {}),
        )


# ─── Feature 24: Message Queue ────────────────────────────────────────────────
class MessageQueue:
    """Priority message queue with deduplication and dead letter queue."""

    def __init__(self):
        self._queue: List[Message] = []
        self._seen: set = set()                  # Feature 28: dedup
        self._dead_letter: List[Message] = []    # Feature 40: DLQ
        self._max_retries: Dict[str, int] = {}   # Feature 27: retry tracking

    def push(self, message: Message) -> bool:
        """Add message to queue. Returns False if duplicate."""
        fp = message.fingerprint()
        if fp in self._seen:
            return False                          # Feature 28: deduplicated
        self._seen.add(fp)
        # Feature 25: insert by priority (high first)
        inserted = False
        for i, m in enumerate(self._queue):
            if message.priority > m.priority:
                self._queue.insert(i, message)
                inserted = True
                break
        if not inserted:
            self._queue.append(message)
        return True

    def pop(self) -> Optional[Message]:
        """Get next message, skip expired ones."""
        while self._queue:
            msg = self._queue.pop(0)
            if not msg.is_expired():             # Feature 35: TTL check
                return msg
        return None

    def ack(self, message_id: str):
        """Feature 26: acknowledge message."""
        for msg in self._queue:
            if msg.id == message_id:
                msg.acknowledged = True
                return True
        return False

    def dead_letter(self, message: Message):
        """Feature 40: send to dead letter queue."""
        self._dead_letter.append(message)

    def get_dead_letters(self) -> List[Message]:
        return self._dead_letter.copy()

    def size(self) -> int:
        return len(self._queue)

    def clear(self):
        self._queue.clear()
        self._seen.clear()


# ─── Feature 22: Pub/Sub ──────────────────────────────────────────────────────
class PubSub:
    """Publish/Subscribe pattern for agent communication."""

    def __init__(self):
        self._subscribers: Dict[str, List[Callable]] = {}

    def subscribe(self, topic: str, callback: Callable):
        """Subscribe to a topic."""
        if topic not in self._subscribers:
            self._subscribers[topic] = []
        self._subscribers[topic].append(callback)

    def unsubscribe(self, topic: str, callback: Callable):
        if topic in self._subscribers:
            self._subscribers[topic].remove(callback)

    def publish(self, topic: str, message: Message) -> int:
        """Publish message to topic. Returns number of subscribers notified."""
        callbacks = self._subscribers.get(topic, [])
        for cb in callbacks:
            try:
                cb(message)
            except Exception as e:
                print(f"[PubSub] Subscriber error on topic '{topic}': {e}")
        return len(callbacks)

    async def publish_async(self, topic: str, message: Message):
        """Feature 36: async publish."""
        callbacks = self._subscribers.get(topic, [])
        for cb in callbacks:
            if asyncio.iscoroutinefunction(cb):
                await cb(message)
            else:
                cb(message)

    def topics(self) -> List[str]:
        return list(self._subscribers.keys())


# ─── Feature 23: Broadcast ────────────────────────────────────────────────────
class Broadcaster:
    """Broadcast messages to multiple agents at once."""

    def __init__(self, agents: list):
        self.agents = {a.name: a for a in agents}

    def broadcast(self, message: str, from_agent: str = "system") -> Dict[str, str]:
        """Send message to all agents, collect responses."""
        responses = {}
        for name, agent in self.agents.items():
            try:
                response = agent.complete([{"role": "user", "content": f"[Broadcast from {from_agent}]: {message}"}])
                responses[name] = response
            except Exception as e:
                responses[name] = f"Error: {e}"
        return responses

    async def broadcast_async(self, message: str, from_agent: str = "system") -> Dict[str, str]:
        """Feature 36: async broadcast."""
        async def call_agent(name, agent):
            loop = asyncio.get_event_loop()
            response = await loop.run_in_executor(
                None,
                lambda: agent.complete([{"role": "user", "content": f"[Broadcast from {from_agent}]: {message}"}])
            )
            return name, response

        tasks = [call_agent(n, a) for n, a in self.agents.items()]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return {name: resp for name, resp in results if not isinstance(resp, Exception)}


# ─── Feature 38: Message Batching ─────────────────────────────────────────────
class BatchProcessor:
    """Process multiple messages in batches."""

    def __init__(self, agent, batch_size: int = 5):
        self.agent = agent
        self.batch_size = batch_size

    def process_batch(self, messages: List[str]) -> List[str]:
        """Process a list of messages and return responses."""
        responses = []
        for i in range(0, len(messages), self.batch_size):
            batch = messages[i:i + self.batch_size]
            for msg in batch:
                try:
                    response = self.agent.complete([{"role": "user", "content": msg}])
                    responses.append(response)
                except Exception as e:
                    responses.append(f"Error: {e}")
        return responses
