"""Classes and functions for event subscription management."""

import ssl
import threading
from pathlib import Path
from typing import Callable, Optional

import paho.mqtt.client as mqtt
import stomp
import urllib3.util
import yaml
from waldur_api_client.api.event_subscriptions import (
    event_subscriptions_create,
    event_subscriptions_destroy,
    event_subscriptions_retrieve,
)
from waldur_api_client.errors import UnexpectedStatus
from waldur_api_client.models.event_subscription import (
    EventSubscription as ClientEventSubscriptionObject,
)
from waldur_api_client.models.event_subscription_request import EventSubscriptionRequest

from waldur_site_agent.backend import logger
from waldur_site_agent.common import utils
from waldur_site_agent.common.structures import Offering
from waldur_site_agent.event_processing import handlers
from waldur_site_agent.event_processing.listener import WaldurListener, connect_to_stomp_server
from waldur_site_agent.event_processing.structures import EventSubscription, UserData

WALDUR_LISTENER_NAME = "waldur-listener"
OBJECT_TYPE_TO_HANDLER_STOMP = {
    "order": handlers.on_order_message_stomp,
    "user_role": handlers.on_user_role_message_stomp,
    "resource": handlers.on_resource_message_stomp,
    "importable_resources": handlers.on_importable_resources_message_stomp,
    "service_account": handlers.on_account_message_stomp,
    "course_account": handlers.on_account_message_stomp,
}
PID_FILE_PATH = "/var/run/waldur_site_agent.pid"


class EventSubscriptionManager:
    """Class responsible for management over event subscriptions and MQTT clients."""

    def __init__(
        self,
        offering: Offering,
        on_connect_callback: Optional[Callable] = None,
        on_message_callback: Optional[Callable] = None,
        user_agent: str = "",
        observable_object_type: str = "",
    ) -> None:
        """Constructor."""
        self.waldur_rest_client = utils.get_client(
            offering.api_url, offering.api_token, user_agent, offering.verify_ssl
        )
        self.offering = offering
        self.user_agent = user_agent
        self.on_connect_callback = on_connect_callback
        self.on_message_callback = on_message_callback
        self.observable_object_type = observable_object_type

    def _read_pid_file(self) -> dict:
        content = {}
        logger.info("Reading event subscriptions info from %s", PID_FILE_PATH)
        try:
            with Path(PID_FILE_PATH).open("r", encoding="utf-8") as pid_file:
                content = yaml.safe_load(pid_file)
        except FileNotFoundError:
            return content
        return content or {}

    def _write_event_subscription_info_to_pidfile(
        self, event_subscription: EventSubscription
    ) -> None:
        """Write event subscription to file."""
        pid_file_content = self._read_pid_file()
        logger.info(
            "Writing %s event subscription info to %s", self.observable_object_type, PID_FILE_PATH
        )

        with Path(PID_FILE_PATH).open("w+", encoding="utf-8") as pid_file:
            payload = {self.observable_object_type: event_subscription["uuid"]}
            pid_file_content.update(payload)
            yaml.dump(pid_file_content, pid_file)

    def _delete_event_subscription_from_pidfile(self) -> None:
        """Delete event subscription from pidfile."""
        pid_file_content = self._read_pid_file()
        logger.info(
            "Deleting %s event subscription info from %s if exists",
            self.observable_object_type,
            PID_FILE_PATH,
        )
        pid_file_content.pop(self.observable_object_type, None)
        with Path(PID_FILE_PATH).open("w+", encoding="utf-8") as pid_file:
            yaml.dump(pid_file_content, pid_file)

    def create_event_subscription(self) -> Optional[EventSubscription]:
        """Create event subscription."""
        try:
            logger.info(
                "Creating event subscription for offering %s (%s), object type: %s",
                self.offering.name,
                self.offering.uuid,
                self.observable_object_type,
            )
            request_body = EventSubscriptionRequest(
                description=(
                    f"Event subscription for waldur site agent {self.user_agent}, "
                    f"observable object type: {self.observable_object_type}"
                ),
                observable_objects=[
                    {
                        "object_type": self.observable_object_type,
                    }
                ],
            )
            event_subscription = event_subscriptions_create.sync(
                client=self.waldur_rest_client, body=request_body
            )

        except UnexpectedStatus as e:
            logger.error("Failed to create event subscription: %s", e)
            return None
        else:
            logger.info(
                "Event subscription created: %s (%s)",
                event_subscription.uuid.hex,
                self.observable_object_type,
            )
            return EventSubscription(
                uuid=event_subscription.uuid.hex,
                user_uuid=event_subscription.user_uuid.hex,
                observable_objects=list(event_subscription.observable_objects),
            )

    def get_or_create_event_subscription(self) -> Optional[EventSubscription]:
        """Ger or create event subscription."""
        try:
            pid_file_content = self._read_pid_file()
            event_subscription_uuid = pid_file_content.get(self.observable_object_type)
            if event_subscription_uuid is not None:
                logger.info(
                    "Fetching the existing event subscription %s (%s) info",
                    event_subscription_uuid,
                    self.observable_object_type,
                )
                # Get the event subscription
                event_subscription: ClientEventSubscriptionObject = (
                    event_subscriptions_retrieve.sync(
                        client=self.waldur_rest_client, uuid=event_subscription_uuid
                    )
                )
                return EventSubscription(
                    uuid=event_subscription.uuid.hex,
                    user_uuid=event_subscription.user_uuid.hex,
                    observable_objects=list(event_subscription.observable_objects),
                )
        except Exception as e:
            logger.warning("Unable to get an event subscription %s: %s", event_subscription_uuid, e)

        event_subscription = self.create_event_subscription()
        if event_subscription:
            self._write_event_subscription_info_to_pidfile(event_subscription)
        return event_subscription

    def _setup_mqtt_consumer(self, event_subscription: EventSubscription) -> mqtt.Client:
        logger.info(
            "Setting up MQTT consumer for event subscription %s",
            event_subscription["uuid"],
        )
        mqtt_client = mqtt.Client(
            client_id=f"paho-mqtt-client-{event_subscription['uuid']}-{self.observable_object_type}",
            transport="websockets",
            callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
        )
        mqtt_client.ws_set_options(path="/rmqws", headers=None)
        vhost_name = event_subscription["user_uuid"]
        username = event_subscription["uuid"]
        username_full = f"{vhost_name}:{username}"
        password = self.offering.api_token
        mqtt_client.username_pw_set(username_full, password)
        if self.offering.websocket_use_tls:
            mqtt_client.tls_set()
        else:
            ssl_context = ssl.create_default_context()
            ssl_context.check_hostname = False
            ssl_context.verify_mode = ssl.CERT_NONE
            mqtt_client.tls_set_context(ssl_context)
            mqtt_client.tls_insecure_set(True)

        mqtt_client.on_connect = self.on_connect_callback
        mqtt_client.on_message = self.on_message_callback
        topic_postfix = self.observable_object_type
        userdata: UserData = {
            "event_subscription": event_subscription,
            "offering": self.offering,
            "user_agent": self.user_agent,
            "topic_postfix": topic_postfix,
        }
        mqtt_client.user_data_set(userdata)
        return mqtt_client

    def _setup_stomp_connection(
        self, event_subscription: EventSubscription
    ) -> stomp.WSStompConnection:
        logger.info(
            "Setting up STOMP connection for event subscription %s",
            event_subscription["uuid"],
        )
        # Mapped to a vhost in RabbitMQ bound to a Waldur User object
        vhost_name = event_subscription["user_uuid"]
        event_subscription_uuid = event_subscription["uuid"]
        # Mapped to a username in RabbitMQ bound to the Waldur EventSubscription object
        username = event_subscription_uuid
        queue_name = (
            f"subscription_{event_subscription_uuid}_"
            f"offering_{self.offering.uuid}_{self.observable_object_type}"
        )
        stomp_host = urllib3.util.parse_url(self.offering.api_url).host
        stomp_port = 443
        password = self.offering.api_token
        connection = stomp.WSStompConnection(
            host_and_ports=[(stomp_host, stomp_port)], ws_path="/rmqws-stomp", vhost=vhost_name
        )
        if self.offering.websocket_use_tls:
            connection.set_ssl(for_hosts=[(stomp_host, stomp_port)])

        callback_function = OBJECT_TYPE_TO_HANDLER_STOMP[self.observable_object_type]
        connection.set_listener(
            WALDUR_LISTENER_NAME,
            WaldurListener(
                connection,
                queue_name,
                username,
                password,
                callback_function,
                self.offering,
                self.user_agent,
            ),
        )

        def create_stomp_thread(callback: Callable) -> threading.Thread:
            thread = threading.Thread(
                target=callback,
                group=None,
                name=f"waldur-{self.observable_object_type}-listener",
            )
            thread.daemon = True  # Don't let thread prevent termination
            thread.start()
            return thread

        connection.transport.override_threading(create_stomp_thread)
        return connection

    def start_stomp_connection(
        self, event_subscription: EventSubscription
    ) -> Optional[stomp.Connection]:
        """Start STOMP connection."""
        connection = self._setup_stomp_connection(event_subscription)
        try:
            logger.info(
                "Starting STOMP connection for event subscription %s",
                event_subscription["uuid"],
            )
            connect_to_stomp_server(connection, event_subscription["uuid"], self.offering.api_token)
        except Exception as e:
            logger.error("Failed to start STOMP connection: %s", e)
            return None
        else:
            return connection

    def stop_stomp_connection(self, connection: stomp.WSStompConnection) -> None:
        """Stop the STOMP connection."""
        connection.remove_listener(WALDUR_LISTENER_NAME)
        connection.disconnect()

    def start_mqtt_consumer(self, event_subscription: EventSubscription) -> Optional[mqtt.Client]:
        """Start MQTT consumer."""
        try:
            mqtt_host = urllib3.util.parse_url(self.offering.api_url).host
            mqtt_port = 443
            logger.info(
                "Starting consumer for %s (%s), MQTT WS address: ws://%s:%s",
                self.offering.name,
                self.offering.uuid,
                mqtt_host,
                mqtt_port,
            )

            mqtt_client = self._setup_mqtt_consumer(event_subscription)

            try:
                topic_name = (
                    f"subscription/{event_subscription['uuid']}/offering/"
                    f"{self.offering.uuid}/{self.observable_object_type}"
                )
                logger.info("Connecting the consumer to the mqtt server, topic: %s", topic_name)
                mqtt_client.connect(mqtt_host, mqtt_port)
            except (ConnectionRefusedError, TimeoutError) as e:
                logger.error("Failed to connect to MQTT broker: %s", e)
                return None
            except Exception as e:
                logger.error("Unexpected error while connecting to MQTT broker: %s", e)
                return None

            try:
                logger.info("Starting the consumer")
                mqtt_client.loop_start()
                logger.info("MQTT consumer started")
            except Exception as e:
                logger.error("Failed to start MQTT consumer loop: %s", e)
                mqtt_client.disconnect()
                return None
            else:
                return mqtt_client

        except Exception as e:
            logger.error("Failed to start MQTT consumer: %s", e)
            return None

    def stop_mqtt_consumer(self, mqttc: mqtt.Client) -> None:
        """Stop the MQTT consumer."""
        mqttc.loop_stop()
        mqttc.disconnect()

    def delete_event_subscription(self, event_subscription: EventSubscription) -> None:
        """Delete the event subscription."""
        try:
            logger.info("Deleting event subscription %s", event_subscription["uuid"])
            event_subscriptions_destroy.sync_detailed(
                uuid=event_subscription["uuid"], client=self.waldur_rest_client
            )
            logger.info("Event subscription deleted: %s", event_subscription["uuid"])
            self._delete_event_subscription_from_pidfile()
        except UnexpectedStatus as e:
            logger.error(
                "Failed to delete event subscription %s: %s", event_subscription["uuid"], e
            )
