"""
Cache Invalidation Notifier via Azure Service Bus.
Replaces the Casbin watcher with targeted per-user/role invalidation messages.

Reuses the robust Service Bus infrastructure (auto-reconnect, dead letter,
self-notification filtering) from the previous watcher implementation.
"""

import json
import logging
import os
import threading
import time
import uuid
from typing import Callable, List, Optional

from pydantic import BaseModel

logger = logging.getLogger(__name__)


class CacheInvalidationNotifierConfig(BaseModel):
    """Configuration for the cache invalidation notifier."""
    # Connection - use ONE of these two:
    connection_string: Optional[str] = None
    fully_qualified_namespace: Optional[str] = None  # For managed identity

    # Topic & Subscription
    topic_name: str = "rbac-cache-invalidation"
    subscription_name: Optional[str] = None  # Auto-set from RBAC_INSTANCE_ID env var

    # Behavior
    max_wait_time_seconds: int = 5
    max_message_count: int = 10
    message_ttl_seconds: int = 300  # 5 minutes
    auto_create_subscription: bool = True


class CacheInvalidationNotifier:
    """
    Publishes and receives targeted cache invalidation messages via Azure Service Bus.

    Message types:
      - user:  Invalidate a single user's cache
      - role:  Invalidate all users with a given role
      - all:   Invalidate everything

    Guarantees (inherited from previous watcher):
      - At-least-once delivery
      - Durable subscriptions
      - Dead letter queue for failed messages
      - Auto-reconnect with exponential backoff
      - Self-notification filtering
    """

    def __init__(self, config: CacheInvalidationNotifierConfig):
        self.config = config
        self._callback: Optional[Callable] = None
        self._mutex = threading.Lock()
        self._running = True

        # Unique instance ID per server process.
        # Priority: explicit subscription_name > RBAC_INSTANCE_ID env var > HOSTNAME-random
        # A random suffix is appended to HOSTNAME to guarantee uniqueness when
        # multiple servers share the same hostname (e.g. docker-compose replicas).
        if config.subscription_name:
            self.instance_id = config.subscription_name
        elif os.environ.get("RBAC_INSTANCE_ID"):
            self.instance_id = os.environ["RBAC_INSTANCE_ID"]
        else:
            hostname = os.environ.get("HOSTNAME", "rbac")
            self.instance_id = f"{hostname}-{uuid.uuid4().hex[:8]}"
        self.subscription_name = self.instance_id

        # Initialize client + sender
        self._client = None
        self._sender = None
        self._init_client()

        # Background receiver thread
        self._receiver_thread = threading.Thread(
            target=self._receiver_loop,
            daemon=True,
            name=f"rbac-notifier-{self.instance_id}",
        )
        self._subscribe_event = threading.Event()

    # =========================================================================
    # INITIALIZATION
    # =========================================================================

    def _init_client(self):
        from azure.servicebus import ServiceBusClient

        if self.config.connection_string:
            self._client = ServiceBusClient.from_connection_string(
                conn_str=self.config.connection_string,
                logging_enable=False,
            )
        elif self.config.fully_qualified_namespace:
            from azure.identity import DefaultAzureCredential
            self._client = ServiceBusClient(
                fully_qualified_namespace=self.config.fully_qualified_namespace,
                credential=DefaultAzureCredential(),
                logging_enable=False,
            )
        else:
            raise ValueError(
                "Provide either 'connection_string' or 'fully_qualified_namespace'"
            )

        self._sender = self._client.get_topic_sender(
            topic_name=self.config.topic_name
        )

    def _ensure_subscription(self):
        """Auto-create the subscription if it doesn't exist."""
        if not self.config.auto_create_subscription:
            return

        try:
            from azure.servicebus.management import ServiceBusAdministrationClient
            import datetime

            if self.config.connection_string:
                admin = ServiceBusAdministrationClient.from_connection_string(
                    conn_str=self.config.connection_string
                )
            else:
                from azure.identity import DefaultAzureCredential
                admin = ServiceBusAdministrationClient(
                    fully_qualified_namespace=self.config.fully_qualified_namespace,
                    credential=DefaultAzureCredential(),
                )

            with admin:
                try:
                    admin.get_subscription(
                        self.config.topic_name, self.subscription_name
                    )
                    logger.info(
                        f"[RBAC-Notifier] Subscription '{self.subscription_name}' exists"
                    )
                except Exception:
                    admin.create_subscription(
                        topic_name=self.config.topic_name,
                        subscription_name=self.subscription_name,
                        default_message_time_to_live=datetime.timedelta(
                            seconds=self.config.message_ttl_seconds
                        ),
                        dead_lettering_on_message_expiration=True,
                        max_delivery_count=5,
                    )
                    logger.info(
                        f"[RBAC-Notifier] Created subscription '{self.subscription_name}'"
                    )

        except ImportError:
            logger.warning(
                "[RBAC-Notifier] Cannot auto-create subscription. "
                "Create it manually in Azure Portal."
            )
        except Exception as e:
            logger.warning(f"[RBAC-Notifier] Subscription auto-create failed: {e}")

    # =========================================================================
    # PUBLIC API
    # =========================================================================

    def set_callback(self, callback: Callable) -> None:
        """Set the callback invoked when an invalidation message arrives."""
        with self._mutex:
            self._callback = callback

    def start(self) -> None:
        """Start the notifier. Call after set_callback."""
        logger.info(f"[RBAC-Notifier] ========================================")
        logger.info(f"[RBAC-Notifier] Starting Cache Invalidation Notifier")
        logger.info(f"[RBAC-Notifier] Instance ID:       {self.instance_id}")
        logger.info(f"[RBAC-Notifier] Topic:             {self.config.topic_name}")
        logger.info(f"[RBAC-Notifier] Subscription:      {self.subscription_name}")
        logger.info(f"[RBAC-Notifier] ========================================")

        self._ensure_subscription()
        self._receiver_thread.start()
        self._subscribe_event.wait(timeout=10)

        if self._subscribe_event.is_set():
            logger.info(
                f"[RBAC-Notifier] Notifier STARTED - subscribing to: "
                f"{self.config.topic_name}/{self.subscription_name}"
            )
        else:
            logger.error(f"[RBAC-Notifier] Notifier FAILED to start within timeout")

    def publish_user_invalidation(self, user_uuid: str) -> bool:
        """Publish a targeted invalidation for a single user."""
        return self._publish({
            "type": "user",
            "user_uuid": user_uuid,
            "instance_id": self.instance_id,
        })

    def publish_role_invalidation(
        self, role_uuid: str, affected_user_uuids: List[str]
    ) -> bool:
        """Publish a targeted invalidation for all users with a given role."""
        return self._publish({
            "type": "role",
            "role_uuid": role_uuid,
            "user_uuids": affected_user_uuids,
            "instance_id": self.instance_id,
        })

    def publish_all_invalidation(self) -> bool:
        """Publish a full cache invalidation."""
        return self._publish({
            "type": "all",
            "instance_id": self.instance_id,
        })

    def close(self) -> None:
        """Stop the notifier and release resources."""
        self._running = False
        try:
            if self._sender:
                self._sender.close()
            if self._client:
                self._client.close()
        except Exception:
            pass

    # =========================================================================
    # RECEIVER (background thread)
    # =========================================================================

    def _receiver_loop(self):
        backoff = 1

        while self._running:
            try:
                logger.info(
                    f"[RBAC-Notifier] Connecting receiver to "
                    f"topic='{self.config.topic_name}', subscription='{self.subscription_name}'"
                )

                receiver = self._client.get_subscription_receiver(
                    topic_name=self.config.topic_name,
                    subscription_name=self.subscription_name,
                    max_wait_time=self.config.max_wait_time_seconds,
                )

                if not self._subscribe_event.is_set():
                    self._subscribe_event.set()

                backoff = 1
                logger.info(
                    f"[RBAC-Notifier] Receiver CONNECTED - listening on: "
                    f"{self.config.topic_name}/{self.subscription_name}"
                )

                with receiver:
                    while self._running:
                        messages = receiver.receive_messages(
                            max_message_count=self.config.max_message_count,
                            max_wait_time=self.config.max_wait_time_seconds,
                        )
                        for msg in messages:
                            try:
                                self._process_message(msg, receiver)
                            except Exception as e:
                                logger.error(
                                    f"[RBAC-Notifier] Message processing error: {e}"
                                )
                                try:
                                    receiver.abandon_message(msg)
                                except Exception:
                                    pass

            except Exception as e:
                if not self._running:
                    break
                if not self._subscribe_event.is_set():
                    self._subscribe_event.set()
                logger.warning(
                    f"[RBAC-Notifier] Receiver error, reconnecting in {backoff}s: {e}"
                )
                time.sleep(backoff)
                backoff = min(backoff * 2, 30)

    @staticmethod
    def _extract_body(msg) -> str:
        """Reliably extract the message body as a string."""
        body = msg.body
        if isinstance(body, bytes):
            return body.decode("utf-8")
        if isinstance(body, str):
            return body
        # body can be a generator of bytes chunks in some SDK versions
        chunks = b"".join(chunk if isinstance(chunk, bytes) else chunk.encode("utf-8") for chunk in body)
        return chunks.decode("utf-8")

    def _process_message(self, msg, receiver):
        try:
            body = self._extract_body(msg)
            data = json.loads(body)
        except (json.JSONDecodeError, Exception) as e:
            logger.warning(
                f"[RBAC-Notifier] Could not parse message (error={e}), "
                f"raw body repr: {repr(msg.body)}"
            )
            receiver.complete_message(msg)
            return

        sender_id = data.get("instance_id", "")

        # Skip self-notifications
        if sender_id == self.instance_id:
            logger.debug(
                f"[RBAC-Notifier] Skipping self-notification (type={data.get('type')})"
            )
            receiver.complete_message(msg)
            return

        msg_type = data.get("type", "unknown")
        logger.info(
            f"[RBAC-Notifier] Processing invalidation: type={msg_type}, from={sender_id}"
        )

        # Execute callback
        with self._mutex:
            if self._callback is not None:
                try:
                    self._callback(data)
                    logger.info(f"[RBAC-Notifier] Invalidation callback executed successfully")
                except Exception as e:
                    logger.error(f"[RBAC-Notifier] Invalidation callback FAILED: {e}")
                    receiver.abandon_message(msg)
                    return
            else:
                logger.warning(f"[RBAC-Notifier] No callback set")

        receiver.complete_message(msg)

    # =========================================================================
    # PUBLISHER
    # =========================================================================

    def _publish(self, msg_dict: dict) -> bool:
        """Queue a message for background publishing (non-blocking)."""
        thread = threading.Thread(
            target=self._publish_sync,
            args=(msg_dict,),
            daemon=True,
        )
        thread.start()
        return True

    def _publish_sync(self, msg_dict: dict) -> None:
        from azure.servicebus import ServiceBusMessage

        for attempt in range(3):
            try:
                if self._sender is None:
                    self._sender = self._client.get_topic_sender(
                        topic_name=self.config.topic_name
                    )
                message = ServiceBusMessage(json.dumps(msg_dict))
                self._sender.send_messages(message)
                logger.info(
                    f"[RBAC-Notifier] Published invalidation: type={msg_dict.get('type')}"
                )
                return
            except Exception as e:
                logger.warning(
                    f"[RBAC-Notifier] Publish attempt {attempt + 1}/3 failed: {e}"
                )
                self._sender = None
                time.sleep(0.5)

        logger.error("[RBAC-Notifier] Publish failed after 3 attempts")
