"""
Unified RabbitMQ Client and Publisher for Bakery-IA Services

This module provides a standardized approach for all services to connect to RabbitMQ,
publish messages, and handle messaging lifecycle. It combines all messaging
functionality into a single, unified interface.
"""

import asyncio
import json
from typing import Dict, Any, Callable, Optional, Union
from datetime import datetime, date, timezone
import uuid
import structlog
from contextlib import suppress
from enum import Enum

try:
    import aio_pika
    from aio_pika import connect_robust, Message, DeliveryMode, ExchangeType
    AIO_PIKA_AVAILABLE = True
except ImportError:
    AIO_PIKA_AVAILABLE = False

logger = structlog.get_logger()


class EventType(Enum):
    """Event type enum for consistent event classification"""
    BUSINESS = "business"      # Business events like inventory changes, user actions
    ALERT = "alert"            # User-facing alerts requiring action
    NOTIFICATION = "notification"  # User-facing informational notifications
    RECOMMENDATION = "recommendation"  # User-facing recommendations
    SYSTEM = "system"          # System-level events


class EVENT_TYPES:
    """Static class for event type constants"""
    class INVENTORY:
        INGREDIENT_CREATED = "inventory.ingredient.created"
        STOCK_ADDED = "inventory.stock.added"
        STOCK_CONSUMED = "inventory.stock.consumed"
        LOW_STOCK_ALERT = "inventory.alert.low_stock"
        EXPIRATION_ALERT = "inventory.alert.expiration"
        STOCK_UPDATED = "inventory.stock.updated"
        STOCK_TRANSFERRED = "inventory.stock.transferred"
        STOCK_WASTED = "inventory.stock.wasted"

    class PRODUCTION:
        BATCH_CREATED = "production.batch.created"
        BATCH_STARTED = "production.batch.started"
        BATCH_COMPLETED = "production.batch.completed"
        EQUIPMENT_STATUS_CHANGED = "production.equipment.status_changed"

    class PROCUREMENT:
        PO_CREATED = "procurement.po.created"
        PO_APPROVED = "procurement.po.approved"
        PO_REJECTED = "procurement.po.rejected"
        DELIVERY_SCHEDULED = "procurement.delivery.scheduled"
        DELIVERY_RECEIVED = "procurement.delivery.received"
        DELIVERY_OVERDUE = "procurement.delivery.overdue"

    class FORECASTING:
        FORECAST_GENERATED = "forecasting.forecast.generated"
        FORECAST_UPDATED = "forecasting.forecast.updated"
        DEMAND_SPIKE_DETECTED = "forecasting.demand.spike_detected"
        WEATHER_IMPACT_FORECAST = "forecasting.weather.impact_forecast"

    class NOTIFICATION:
        NOTIFICATION_SENT = "notification.sent"
        NOTIFICATION_FAILED = "notification.failed"
        NOTIFICATION_DELIVERED = "notification.delivered"
        NOTIFICATION_OPENED = "notification.opened"

    class TENANT:
        TENANT_CREATED = "tenant.created"
        TENANT_UPDATED = "tenant.updated"
        TENANT_DELETED = "tenant.deleted"
        TENANT_MEMBER_ADDED = "tenant.member.added"
        TENANT_MEMBER_REMOVED = "tenant.member.removed"


def json_serializer(obj):
    """JSON serializer for objects not serializable by default json code"""
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    elif isinstance(obj, uuid.UUID):
        return str(obj)
    elif hasattr(obj, '__class__') and obj.__class__.__name__ == 'Decimal':
        # Handle Decimal objects from SQLAlchemy without importing decimal
        return float(obj)
    raise TypeError(f"Object of type {type(obj)} is not JSON serializable")


class RabbitMQHeartbeatMonitor:
    """Monitor to ensure heartbeats are processed during heavy operations"""

    def __init__(self, client):
        self.client = client
        self._monitor_task = None
        self._should_monitor = False

    async def start_monitoring(self):
        """Start heartbeat monitoring task"""
        if self._monitor_task and not self._monitor_task.done():
            return

        self._should_monitor = True
        self._monitor_task = asyncio.create_task(self._monitor_loop())

    async def stop_monitoring(self):
        """Stop heartbeat monitoring task"""
        self._should_monitor = False
        if self._monitor_task and not self._monitor_task.done():
            self._monitor_task.cancel()
            with suppress(asyncio.CancelledError):
                await self._monitor_task

    async def _monitor_loop(self):
        """Monitor loop that periodically yields control for heartbeat processing"""
        while self._should_monitor:
            # Yield control to allow heartbeat processing
            await asyncio.sleep(0.1)

            # Verify connection is still alive
            if self.client.connection and not self.client.connection.is_closed:
                # Check if connection is still responsive
                try:
                    # This is a lightweight check to ensure the connection is responsive
                    pass  # The heartbeat mechanism in aio_pika handles this internally
                except Exception as e:
                    logger.warning("Connection check failed", error=str(e))
                    self.client.connected = False
                    break
            else:
                logger.warning("Connection is closed, stopping monitor")
                break


class RabbitMQClient:
    """
    Universal RabbitMQ client for all bakery-ia microservices
    Handles all messaging patterns with proper fallbacks
    """

    def __init__(self, connection_url: str, service_name: str = "unknown"):
        self.connection_url = connection_url
        self.service_name = service_name
        self.connection = None
        self.channel = None
        self.connected = False
        self._reconnect_attempts = 0
        self._max_reconnect_attempts = 5
        self.heartbeat_monitor = RabbitMQHeartbeatMonitor(self)

    async def connect(self):
        """Connect to RabbitMQ with retry logic"""
        if not AIO_PIKA_AVAILABLE:
            logger.warning("aio-pika not available, messaging disabled", service=self.service_name)
            return False

        try:
            self.connection = await connect_robust(
                self.connection_url,
                heartbeat=600  # Increase heartbeat to 600 seconds (10 minutes) to prevent timeouts
            )
            self.channel = await self.connection.channel()
            await self.channel.set_qos(prefetch_count=100)  # Performance optimization

            self.connected = True
            self._reconnect_attempts = 0

            # Start heartbeat monitoring
            await self.heartbeat_monitor.start_monitoring()

            logger.info("Connected to RabbitMQ", service=self.service_name)
            return True

        except Exception as e:
            self.connected = False
            self._reconnect_attempts += 1
            logger.warning(
                "Failed to connect to RabbitMQ",
                service=self.service_name,
                error=str(e),
                attempt=self._reconnect_attempts
            )
            return False

    async def disconnect(self):
        """Disconnect from RabbitMQ with proper channel cleanup"""
        try:
            # Stop heartbeat monitoring first
            await self.heartbeat_monitor.stop_monitoring()

            # Close channel before connection to avoid "unexpected close" warnings
            if self.channel and not self.channel.is_closed:
                await self.channel.close()
                logger.debug("RabbitMQ channel closed", service=self.service_name)

            # Then close connection
            if self.connection and not self.connection.is_closed:
                await self.connection.close()
                logger.info("Disconnected from RabbitMQ", service=self.service_name)

            self.connected = False

        except Exception as e:
            logger.warning("Error during RabbitMQ disconnect",
                          service=self.service_name,
                          error=str(e))
            self.connected = False

    async def ensure_connected(self) -> bool:
        """Ensure connection is active, reconnect if needed"""
        if self.connected and self.connection and not self.connection.is_closed:
            return True

        if self._reconnect_attempts >= self._max_reconnect_attempts:
            logger.error("Max reconnection attempts reached", service=self.service_name)
            return False

        return await self.connect()

    async def publish_event(self, exchange_name: str, routing_key: str, event_data: Dict[str, Any],
                          persistent: bool = True) -> bool:
        """
        Universal event publisher with automatic fallback
        Returns True if published successfully, False otherwise
        """
        try:
            # Ensure we're connected
            if not await self.ensure_connected():
                logger.debug("Event not published - RabbitMQ unavailable",
                           service=self.service_name, routing_key=routing_key)
                return False

            # Declare exchange
            exchange = await self.channel.declare_exchange(
                exchange_name,
                ExchangeType.TOPIC,
                durable=True
            )

            # Prepare message with proper JSON serialization
            message_body = json.dumps(event_data, default=json_serializer)
            message = Message(
                message_body.encode(),
                delivery_mode=DeliveryMode.PERSISTENT if persistent else DeliveryMode.NOT_PERSISTENT,
                content_type="application/json",
                timestamp=datetime.now(),
                headers={
                    "source_service": self.service_name,
                    "event_id": event_data.get("event_id", str(uuid.uuid4()))
                }
            )

            # Publish message
            await exchange.publish(message, routing_key=routing_key)

            logger.debug("Event published successfully",
                        service=self.service_name,
                        exchange=exchange_name,
                        routing_key=routing_key,
                        size=len(message_body))
            return True

        except Exception as e:
            logger.error("Failed to publish event",
                        service=self.service_name,
                        exchange=exchange_name,
                        routing_key=routing_key,
                        error=str(e))
            self.connected = False  # Force reconnection on next attempt
            return False

    async def consume_events(self, exchange_name: str, queue_name: str,
                           routing_key: str, callback: Callable) -> bool:
        """Universal event consumer"""
        try:
            if not await self.ensure_connected():
                return False

            # Declare exchange
            exchange = await self.channel.declare_exchange(
                exchange_name,
                ExchangeType.TOPIC,
                durable=True
            )

            # Declare queue
            queue = await self.channel.declare_queue(
                queue_name,
                durable=True
            )

            # Bind queue to exchange
            await queue.bind(exchange, routing_key)

            # Set up consumer
            await queue.consume(callback)

            logger.info("Started consuming events",
                       service=self.service_name,
                       queue=queue_name,
                       routing_key=routing_key)
            return True

        except Exception as e:
            logger.error("Failed to start consuming events",
                        service=self.service_name,
                        error=str(e))
            return False

    # High-level convenience methods for common patterns
    async def publish_user_event(self, event_type: str, user_data: Dict[str, Any]) -> bool:
        """Publish user-related events"""
        return await self.publish_event("user.events", f"user.{event_type}", user_data)

    async def publish_training_event(self, event_type: str, training_data: Dict[str, Any]) -> bool:
        """Publish training-related events"""
        return await self.publish_event("training.events", f"training.{event_type}", training_data)

    async def publish_data_event(self, event_type: str, data: Dict[str, Any]) -> bool:
        """Publish data-related events"""
        return await self.publish_event("data.events", f"data.{event_type}", data)

    async def publish_forecast_event(self, event_type: str, forecast_data: Dict[str, Any]) -> bool:
        """Publish forecast-related events"""
        return await self.publish_event("forecast.events", f"forecast.{event_type}", forecast_data)


class EventMessage:
    """Standardized event message structure"""

    def __init__(
        self,
        event_type: str,
        tenant_id: Union[str, uuid.UUID],
        service_name: str,
        data: Dict[str, Any],
        event_class: str = "business",  # business, alert, notification, recommendation
        correlation_id: Optional[str] = None,
        trace_id: Optional[str] = None,
        severity: Optional[str] = None,  # For alerts: urgent, high, medium, low
        source: Optional[str] = None
    ):
        self.event_type = event_type
        self.tenant_id = str(tenant_id) if isinstance(tenant_id, uuid.UUID) else tenant_id
        self.service_name = service_name
        self.data = data
        self.event_class = event_class
        self.correlation_id = correlation_id or str(uuid.uuid4())
        self.trace_id = trace_id or str(uuid.uuid4())
        self.severity = severity
        self.source = source or service_name
        self.timestamp = datetime.now(timezone.utc).isoformat()

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for message publishing"""
        result = {
            "event_type": self.event_type,
            "tenant_id": self.tenant_id,
            "service_name": self.service_name,
            "data": self.data,
            "event_class": self.event_class,
            "correlation_id": self.correlation_id,
            "trace_id": self.trace_id,
            "timestamp": self.timestamp
        }

        if self.severity:
            result["severity"] = self.severity
        if self.source:
            result["source"] = self.source

        return result


class UnifiedEventPublisher:
    """Unified publisher for all event types - business events, alerts, notifications, recommendations"""

    def __init__(self, rabbitmq_client: RabbitMQClient, service_name: str):
        self.rabbitmq = rabbitmq_client
        self.service_name = service_name
        self.exchange = "events.exchange"

    async def publish_event(
        self,
        event_type: str,
        tenant_id: Union[str, uuid.UUID],
        data: Dict[str, Any],
        event_class: str = "business",
        severity: Optional[str] = None
    ) -> bool:
        """
        Publish a standardized event using the unified messaging pattern.

        Args:
            event_type: Type of event (e.g., 'inventory.ingredient.created')
            tenant_id: Tenant identifier
            data: Event payload data
            event_class: One of 'business', 'alert', 'notification', 'recommendation'
            severity: Alert severity (for alert events only)
        """
        # Determine event domain and event type separately for alert processor
        # The event_type should be just the specific event name, domain should be extracted separately
        if '.' in event_type and event_class in ["alert", "notification", "recommendation"]:
            # For events like "inventory.critical_stock_shortage", split into domain and event
            parts = event_type.split('.', 1)  # Split only on first dot
            event_domain = parts[0]
            actual_event_type = parts[1]
        else:
            # For simple event types or business events, use as-is
            event_domain = "general" if event_class == "business" else self.service_name
            actual_event_type = event_type

        # For the message payload that goes to alert processor, use the expected MinimalEvent format
        if event_class in ["alert", "notification", "recommendation"]:
            # Format for alert processor (uses MinimalEvent schema)
            event_payload = {
                "tenant_id": str(tenant_id),
                "event_class": event_class,
                "event_domain": event_domain,
                "event_type": actual_event_type,  # Just the specific event name, not domain.event_name
                "service": self.service_name,  # Changed from service_name to service
                "metadata": data,  # Changed from data to metadata
                "timestamp": datetime.now(timezone.utc).isoformat()
            }
            if severity:
                event_payload["severity"] = severity  # Include severity for alerts
        else:
            # Format for business events (standard format)
            event_payload = {
                "event_type": event_type,
                "tenant_id": str(tenant_id),
                "service_name": self.service_name,
                "data": data,
                "event_class": event_class,
                "timestamp": datetime.now(timezone.utc).isoformat()
            }
            if severity:
                event_payload["severity"] = severity

        # Determine routing key based on event class
        # For routing, we can still use the original event_type format since it's for routing purposes
        if event_class == "alert":
            routing_key = f"alert.{event_domain}.{severity or 'medium'}"
        elif event_class == "notification":
            routing_key = f"notification.{event_domain}.info"
        elif event_class == "recommendation":
            routing_key = f"recommendation.{event_domain}.{severity or 'medium'}"
        else:  # business events
            routing_key = f"business.{event_type.replace('.', '_')}"

        try:
            success = await self.rabbitmq.publish_event(
                exchange_name=self.exchange,
                routing_key=routing_key,
                event_data=event_payload
            )

            if success:
                logger.info(
                    "event_published",
                    tenant_id=str(tenant_id),
                    event_type=event_type,
                    event_class=event_class,
                    severity=severity
                )
            else:
                logger.error(
                    "event_publish_failed",
                    tenant_id=str(tenant_id),
                    event_type=event_type
                )

            return success

        except Exception as e:
            logger.error(
                "event_publish_error",
                tenant_id=str(tenant_id),
                event_type=event_type,
                error=str(e)
            )
            return False

    # Business event methods
    async def publish_business_event(
        self,
        event_type: str,
        tenant_id: Union[str, uuid.UUID],
        data: Dict[str, Any]
    ) -> bool:
        """Publish a business event (inventory changes, user actions, etc.)"""
        return await self.publish_event(
            event_type=event_type,
            tenant_id=tenant_id,
            data=data,
            event_class="business"
        )

    # Alert methods
    async def publish_alert(
        self,
        event_type: str,
        tenant_id: Union[str, uuid.UUID],
        severity: str,  # urgent, high, medium, low
        data: Dict[str, Any]
    ) -> bool:
        """Publish an alert (actionable by user)"""
        return await self.publish_event(
            event_type=event_type,
            tenant_id=tenant_id,
            data=data,
            event_class="alert",
            severity=severity
        )

    # Notification methods
    async def publish_notification(
        self,
        event_type: str,
        tenant_id: Union[str, uuid.UUID],
        data: Dict[str, Any]
    ) -> bool:
        """Publish a notification (informational to user)"""
        return await self.publish_event(
            event_type=event_type,
            tenant_id=tenant_id,
            data=data,
            event_class="notification"
        )

    # Recommendation methods
    async def publish_recommendation(
        self,
        event_type: str,
        tenant_id: Union[str, uuid.UUID],
        data: Dict[str, Any],
        severity: Optional[str] = None
    ) -> bool:
        """Publish a recommendation (suggestion to user)"""
        return await self.publish_event(
            event_type=event_type,
            tenant_id=tenant_id,
            data=data,
            event_class="recommendation",
            severity=severity
        )


class ServiceMessagingManager:
    """Manager class to handle messaging lifecycle for services"""

    def __init__(self, service_name: str, rabbitmq_url: str):
        self.service_name = service_name
        self.rabbitmq_url = rabbitmq_url
        self.rabbitmq_client = None
        self.publisher = None

    async def setup(self):
        """Setup the messaging system for the service"""
        try:
            self.rabbitmq_client = RabbitMQClient(self.rabbitmq_url, self.service_name)
            success = await self.rabbitmq_client.connect()
            if success:
                self.publisher = UnifiedEventPublisher(self.rabbitmq_client, self.service_name)
                logger.info(f"{self.service_name} messaging manager setup completed")
                return True
            else:
                logger.error(f"{self.service_name} messaging manager setup failed")
                return False
        except Exception as e:
            logger.error(f"Error during {self.service_name} messaging manager setup", error=str(e))
            return False

    async def cleanup(self):
        """Cleanup the messaging system for the service"""
        try:
            if self.rabbitmq_client:
                await self.rabbitmq_client.disconnect()
                logger.info(f"{self.service_name} messaging manager cleanup completed")
                return True
            return True  # If no client to clean up, consider it successful
        except Exception as e:
            logger.error(f"Error during {self.service_name} messaging manager cleanup", error=str(e))
            return False

    @property
    def is_ready(self):
        """Check if the messaging system is ready for use"""
        return (self.publisher is not None and
                self.rabbitmq_client is not None and
                self.rabbitmq_client.connected)


# Utility functions for easy service integration
async def initialize_service_publisher(service_name: str, rabbitmq_url: str):
    """
    Initialize a service-specific publisher using the unified messaging system.

    Args:
        service_name: Name of the service (e.g., 'notification-service', 'forecasting-service')
        rabbitmq_url: RabbitMQ connection URL

    Returns:
        UnifiedEventPublisher instance or None if initialization failed
    """
    try:
        rabbitmq_client = RabbitMQClient(rabbitmq_url, service_name)
        success = await rabbitmq_client.connect()
        if success:
            publisher = UnifiedEventPublisher(rabbitmq_client, service_name)
            logger.info(f"{service_name} unified messaging publisher initialized")
            return rabbitmq_client, publisher
        else:
            logger.warning(f"{service_name} unified messaging publisher failed to connect")
            return None, None
    except Exception as e:
        logger.error(f"Failed to initialize {service_name} unified messaging publisher", error=str(e))
        return None, None


async def cleanup_service_publisher(rabbitmq_client):
    """
    Cleanup messaging for a service.

    Args:
        rabbitmq_client: The RabbitMQ client to disconnect

    Returns:
        True if cleanup was successful, False otherwise
    """
    try:
        if rabbitmq_client:
            await rabbitmq_client.disconnect()
            logger.info("Service messaging cleanup completed")
            return True
        return True  # If no client to clean up, consider it successful
    except Exception as e:
        logger.error("Error during service messaging cleanup", error=str(e))
        return False