"""
Event publisher for user containers.

Publishes events via dual ZMQ patterns:
- XPUB for informational events (fire-and-forget)
- DEALER for critical events (guaranteed delivery with ack)

The publisher automatically routes events based on:
1. Event priority (INFORMATIONAL always uses XPUB if subscribed)
2. Whether an active gateway session is subscribed (tracked via XPUB)
"""

import asyncio
import logging
import time
import uuid
from dataclasses import dataclass, field
from typing import Dict, Optional, Set

import zmq
import zmq.asyncio

from .types import (
    UserEvent,
    EventAck,
    Priority,
    AckStatus,
    serialize_user_event,
    deserialize_event_ack,
    get_event_type_name,
)
from .pending_store import PendingStore

logger = logging.getLogger(__name__)


@dataclass
class PendingEvent:
    """Tracks a pending critical event awaiting acknowledgment."""

    event: UserEvent
    sent_at: float
    retries: int = 0


class EventPublisher:
    """
    Publishes user events via dual ZMQ channels.

    - XPUB socket (bind): For informational events to active sessions
    - DEALER socket (connect): For critical events with guaranteed delivery

    The publisher tracks XPUB subscriptions to know when a gateway has an
    active session for this user. If subscribed, events go via XPUB (fast).
    If not subscribed and priority > INFORMATIONAL, events go via DEALER.

    Usage:
        publisher = EventPublisher(user_id="user-123")
        await publisher.start()

        # Informational event (dropped if no active session)
        await publisher.publish(UserEvent(
            event_type=EventType.INDICATOR_UPDATED,
            payload={"indicator": "RSI", "value": 65.5},
            delivery=DeliverySpec.informational(),
        ))

        # Critical event (guaranteed delivery)
        await publisher.publish(UserEvent(
            event_type=EventType.ORDER_FILLED,
            payload={"order_id": "123", "symbol": "BTC/USDT", ...},
            delivery=DeliverySpec.critical(),
        ))

        await publisher.stop()
    """

    def __init__(
        self,
        user_id: str,
        xpub_port: int = 5570,
        gateway_router_endpoint: str = "tcp://gateway:5571",
        ack_timeout: float = 30.0,
        max_retries: int = 3,
        pending_store_path: Optional[str] = None,
    ):
        """
        Initialize event publisher.

        Args:
            user_id: User ID for this container
            xpub_port: Port to bind XPUB socket on (gateway connects here)
            gateway_router_endpoint: Gateway ROUTER socket endpoint (we connect)
            ack_timeout: Seconds to wait for ack before retrying
            max_retries: Maximum retries for critical events
            pending_store_path: Path to persist pending events (for crash recovery)
        """
        self.user_id = user_id
        self.xpub_port = xpub_port
        self.gateway_router_endpoint = gateway_router_endpoint
        self.ack_timeout = ack_timeout
        self.max_retries = max_retries

        # ZMQ context and sockets
        self.ctx: Optional[zmq.asyncio.Context] = None
        self.xpub_socket: Optional[zmq.asyncio.Socket] = None
        self.dealer_socket: Optional[zmq.asyncio.Socket] = None

        # Track active subscriptions (set of topic strings)
        self.active_subscriptions: Set[str] = set()

        # Track pending critical events awaiting ack
        self.pending_events: Dict[str, PendingEvent] = {}

        # Persistent store for crash recovery
        self.pending_store = PendingStore(pending_store_path)

        # Background tasks
        self._subscription_task: Optional[asyncio.Task] = None
        self._ack_task: Optional[asyncio.Task] = None
        self._retry_task: Optional[asyncio.Task] = None
        self._running = False

        # Statistics
        self.stats = {
            "events_published": 0,
            "events_via_xpub": 0,
            "events_via_dealer": 0,
            "events_dropped": 0,
            "events_delivered": 0,
            "events_failed": 0,
            "retries": 0,
        }

    async def start(self) -> None:
        """Start the event publisher."""
        if self._running:
            logger.warning("Event publisher already running")
            return

        logger.info(
            "Starting event publisher: user_id=%s, xpub_port=%d, gateway=%s",
            self.user_id,
            self.xpub_port,
            self.gateway_router_endpoint,
        )

        # Create ZMQ context
        self.ctx = zmq.asyncio.Context()

        # Create XPUB socket for informational events
        self.xpub_socket = self.ctx.socket(zmq.XPUB)
        self.xpub_socket.setsockopt(zmq.XPUB_VERBOSE, 1)  # Receive all sub/unsub
        self.xpub_socket.bind(f"tcp://*:{self.xpub_port}")
        logger.info("XPUB socket bound on port %d", self.xpub_port)

        # Create DEALER socket for critical events
        self.dealer_socket = self.ctx.socket(zmq.DEALER)
        self.dealer_socket.setsockopt_string(zmq.IDENTITY, f"container-{self.user_id}")
        self.dealer_socket.connect(self.gateway_router_endpoint)
        logger.info("DEALER socket connected to %s", self.gateway_router_endpoint)

        # Load any persisted pending events
        persisted = await self.pending_store.load_pending()
        for event in persisted:
            self.pending_events[event.event_id] = PendingEvent(
                event=event,
                sent_at=time.time(),
                retries=0,
            )
        if persisted:
            logger.info("Loaded %d pending events from disk", len(persisted))

        # Start background tasks
        self._running = True
        self._subscription_task = asyncio.create_task(self._subscription_loop())
        self._ack_task = asyncio.create_task(self._ack_loop())
        self._retry_task = asyncio.create_task(self._retry_loop())

        # Resend any loaded pending events
        for pending in list(self.pending_events.values()):
            await self._send_via_dealer(pending.event)

        logger.info("Event publisher started")

    async def stop(self) -> None:
        """Stop the event publisher and persist pending events."""
        if not self._running:
            return

        logger.info("Stopping event publisher")
        self._running = False

        # Cancel background tasks
        for task in [self._subscription_task, self._ack_task, self._retry_task]:
            if task:
                task.cancel()
                try:
                    await task
                except asyncio.CancelledError:
                    pass

        # Persist pending critical events for crash recovery
        if self.pending_events:
            events = [pe.event for pe in self.pending_events.values()]
            await self.pending_store.save_pending(events)
            logger.info("Persisted %d pending events", len(events))

        # Close sockets
        if self.xpub_socket:
            self.xpub_socket.close()
        if self.dealer_socket:
            self.dealer_socket.close()
        if self.ctx:
            self.ctx.term()

        logger.info(
            "Event publisher stopped. Stats: %s",
            self.stats,
        )

    def has_active_subscriber(self) -> bool:
        """Check if any gateway is subscribed to this user's events."""
        topic = f"USER:{self.user_id}"
        return topic in self.active_subscriptions

    async def publish(self, event: UserEvent) -> None:
        """
        Publish an event via the appropriate channel.

        Routing logic:
        - INFORMATIONAL: XPUB only if subscribed, else drop
        - NORMAL/CRITICAL + subscribed: XPUB (fast path)
        - NORMAL/CRITICAL + not subscribed: DEALER (guaranteed)

        Args:
            event: Event to publish
        """
        # Ensure event has required fields
        if not event.event_id:
            event.event_id = str(uuid.uuid4())
        if not event.user_id:
            event.user_id = self.user_id
        if not event.timestamp:
            event.timestamp = int(time.time() * 1000)

        priority = event.delivery.priority
        has_subscriber = self.has_active_subscriber()

        logger.debug(
            "Publishing event: id=%s, type=%s, priority=%s, has_subscriber=%s",
            event.event_id,
            get_event_type_name(event.event_type),
            Priority(priority).name,
            has_subscriber,
        )

        self.stats["events_published"] += 1

        if priority == Priority.INFORMATIONAL:
            # Fire and forget - only send if someone's listening
            if has_subscriber:
                await self._send_via_xpub(event)
                self.stats["events_via_xpub"] += 1
            else:
                logger.debug(
                    "Dropping informational event (no subscriber): %s",
                    event.event_id,
                )
                self.stats["events_dropped"] += 1

        elif has_subscriber:
            # Active session exists - use fast path
            await self._send_via_xpub(event)
            self.stats["events_via_xpub"] += 1

        else:
            # No active session - use guaranteed delivery
            await self._send_via_dealer(event)
            self.stats["events_via_dealer"] += 1

            # Track for ack
            self.pending_events[event.event_id] = PendingEvent(
                event=event,
                sent_at=time.time(),
                retries=0,
            )

    async def _send_via_xpub(self, event: UserEvent) -> None:
        """Send event via XPUB socket (fire-and-forget)."""
        topic = f"USER:{self.user_id}"
        payload = serialize_user_event(event)

        await self.xpub_socket.send_multipart([topic.encode(), payload])

        logger.debug(
            "Sent event via XPUB: id=%s, type=%s",
            event.event_id,
            get_event_type_name(event.event_type),
        )

    async def _send_via_dealer(self, event: UserEvent) -> None:
        """Send event via DEALER socket (with ack tracking)."""
        payload = serialize_user_event(event)
        await self.dealer_socket.send(payload)

        logger.debug(
            "Sent event via DEALER: id=%s, type=%s",
            event.event_id,
            get_event_type_name(event.event_type),
        )

    async def _subscription_loop(self) -> None:
        """Process XPUB subscription/unsubscription messages."""
        logger.debug("Starting subscription loop")

        while self._running:
            try:
                # Poll with timeout to allow shutdown
                if await self.xpub_socket.poll(100):
                    msg = await self.xpub_socket.recv()

                    # First byte: 1 = subscribe, 0 = unsubscribe
                    # Remaining bytes: topic
                    is_subscribe = msg[0] == 1
                    topic = msg[1:].decode()

                    if is_subscribe:
                        self.active_subscriptions.add(topic)
                        logger.info("Gateway subscribed to topic: %s", topic)
                    else:
                        self.active_subscriptions.discard(topic)
                        logger.info("Gateway unsubscribed from topic: %s", topic)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error("Error in subscription loop: %s", e, exc_info=True)

        logger.debug("Subscription loop ended")

    async def _ack_loop(self) -> None:
        """Process EventAck messages from gateway."""
        logger.debug("Starting ack loop")

        while self._running:
            try:
                # Poll with timeout
                if await self.dealer_socket.poll(100):
                    payload = await self.dealer_socket.recv()
                    ack = deserialize_event_ack(payload)

                    logger.debug(
                        "Received ack: event_id=%s, status=%s",
                        ack.event_id,
                        AckStatus(ack.status).name,
                    )

                    if ack.event_id in self.pending_events:
                        pending = self.pending_events.pop(ack.event_id)

                        if ack.status == AckStatus.DELIVERED:
                            logger.info(
                                "Event delivered: id=%s, type=%s, via=%s",
                                ack.event_id,
                                get_event_type_name(pending.event.event_type),
                                ack.delivered_via.name if ack.delivered_via else "unknown",
                            )
                            self.stats["events_delivered"] += 1

                        elif ack.status == AckStatus.QUEUED:
                            logger.info(
                                "Event queued for delivery: id=%s",
                                ack.event_id,
                            )
                            self.stats["events_delivered"] += 1

                        elif ack.status == AckStatus.ACK_ERROR:
                            logger.error(
                                "Event delivery failed: id=%s, error=%s",
                                ack.event_id,
                                ack.error_message,
                            )
                            self.stats["events_failed"] += 1
                    else:
                        logger.debug("Received ack for unknown event: %s", ack.event_id)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error("Error in ack loop: %s", e, exc_info=True)

        logger.debug("Ack loop ended")

    async def _retry_loop(self) -> None:
        """Retry pending events that haven't been acked."""
        logger.debug("Starting retry loop")

        while self._running:
            try:
                await asyncio.sleep(5)  # Check every 5 seconds

                now = time.time()
                for event_id, pending in list(self.pending_events.items()):
                    if now - pending.sent_at > self.ack_timeout:
                        if pending.retries >= self.max_retries:
                            # Give up
                            logger.error(
                                "Event exceeded max retries, dropping: id=%s, type=%s",
                                event_id,
                                get_event_type_name(pending.event.event_type),
                            )
                            del self.pending_events[event_id]
                            self.stats["events_failed"] += 1
                        else:
                            # Retry
                            pending.retries += 1
                            pending.sent_at = now
                            await self._send_via_dealer(pending.event)
                            logger.info(
                                "Retrying event: id=%s, attempt=%d/%d",
                                event_id,
                                pending.retries,
                                self.max_retries,
                            )
                            self.stats["retries"] += 1

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error("Error in retry loop: %s", e, exc_info=True)

        logger.debug("Retry loop ended")

    def get_stats(self) -> Dict[str, int]:
        """Get publisher statistics."""
        return {
            **self.stats,
            "pending_events": len(self.pending_events),
            "active_subscriptions": len(self.active_subscriptions),
        }
