import logging
import os
import socket
from datetime import datetime, timedelta
from threading import Lock, Condition, Thread
from typing import List, Dict
from uuid import uuid4

from messagebus import Event, Message, DefaultToken
from zeromqmessagehandler import MessageDecoder, ZeroMqMessageBus

from zeromqeventmanager.model.dto import SubscribeResponse
from ._ForwarderDevice import ForwarderDevice
from ._StreamerDevice import StreamerDevice

logger = logging.getLogger("ZeroMqEventManager.EventManager")


class AsyncSubscription(object):
    def __init__(self,
                 service_name,
                 sub_address,
                 pull_address,
                 publish_pull_address,
                 min_port,
                 max_port,
                 sub_id_lifetime=30):
        # type: (str, str, str, str, int, int, int) -> None
        self.service_name = service_name
        self.device = StreamerDevice(sub_address, pull_address, min_port, max_port)
        port = str(self.device.start())
        self.pull_address = pull_address + ":" + port
        self.publish_pull_address = publish_pull_address + ":" + port
        self.sub_id_lifetime = sub_id_lifetime
        self.subjects = {}  # type: Dict[str, Dict[str, str]]
        self.sub_id_subjects = {}  # type: Dict[str, Dict[str, str]]
        self.sub_id_last_seen = {}  # type: Dict[str, datetime]

    def add_subjects(self, sub_id, subjects):
        # type: (str, List[str]) -> None
        self.sub_id_last_seen[sub_id] = datetime.now()

        if sub_id not in self.sub_id_subjects:
            self.sub_id_subjects[sub_id] = {}
        sub_id_subjects = self.sub_id_subjects[sub_id]

        to_add = []
        for subject in subjects:
            if subject not in sub_id_subjects:
                sub_id_subjects[subject] = subject

            if subject not in self.subjects:
                self.subjects[subject] = {}
                to_add.append(subject)

            subject_sub_ids = self.subjects[subject]
            if sub_id not in subject_sub_ids:
                subject_sub_ids[sub_id] = sub_id

        if len(to_add) > 0:
            self.device.add_subjects(to_add)

    def remove_subjects(self, sub_id, subjects):
        # type: (str, List[str]) -> None
        self.sub_id_last_seen[sub_id] = datetime.now()

        if sub_id not in self.sub_id_subjects:
            return
        sub_id_subjects = self.sub_id_subjects[sub_id]

        to_remove = []
        for subject in subjects:
            if subject in sub_id_subjects:
                del sub_id_subjects[subject]
            if subject in self.subjects and sub_id in self.subjects[subject]:
                del self.subjects[subject][sub_id]

            if len(self.subjects[subject]) == 0:
                to_remove.append(subject)
                del self.subjects[subject]

            if len(self.sub_id_subjects[sub_id]) == 0:
                del self.sub_id_subjects[sub_id]

        if len(to_remove) > 0:
            self.device.remove_subjects(subjects)

    def update_last_seen(self, sub_id):
        if sub_id in self.sub_id_last_seen:
            self.sub_id_last_seen[sub_id] = datetime.now()
            return True
        return False

    def clean_invalid_subscriptions(self):
        valid_last_seen = datetime.now() - timedelta(seconds=self.sub_id_lifetime)
        sub_ids_to_remove = []
        for sub_id in self.sub_id_last_seen.keys():
            last_seen = self.sub_id_last_seen[sub_id]
            if last_seen < valid_last_seen:
                sub_ids_to_remove.append(sub_id)

        subject_sub_ids = {}
        for sub_id_to_remove in sub_ids_to_remove:
            for subject in self.sub_id_subjects[sub_id_to_remove]:
                if subject not in subject_sub_ids:
                    subject_sub_ids[subject] = {}
                subject_sub_ids[subject][sub_id_to_remove] = sub_id_to_remove

            del self.sub_id_subjects[sub_id_to_remove]
            del self.sub_id_last_seen[sub_id_to_remove]

        self.subjects = {}
        for sub_id in self.sub_id_last_seen.keys():
            for subject in self.sub_id_subjects[sub_id].keys():
                if subject not in self.subjects:
                    self.subjects[subject] = {}
                self.subjects[subject][sub_id] = sub_id

        return subject_sub_ids

    def has_subscriptions(self):
        return len(self.subjects) > 0

    def stop(self):
        self.device.stop()


class SyncSubscription(object):
    def __init__(self, subject):
        # type: (str) -> None
        self.subject = subject
        self.services = {}  # type: Dict[str, str]

    def add_service(self, sub_id, service_name):
        # type: (str, str) -> None
        self.services[sub_id] = service_name

    def remove_service(self, sub_id):
        # type: (str) -> None
        if sub_id in self.services:
            del self.services[sub_id]

    def has_subscriptions(self):
        return len(self.services) > 0

    def get_subscribed_services(self):
        subs = {}
        for sub_id in self.services.keys():
            service_name = self.services[sub_id]
            subs[service_name] = service_name
        return subs.keys()


class EventManager(object):
    def __init__(self, message_bus,
                 pub_address=None,
                 sub_address=None,
                 pull_address=None,
                 min_port=5001,
                 max_port=5100,
                 clean_interval=None,
                 sub_id_lifetime=None):
        # type: (ZeroMqMessageBus, str, str, str, int, int, int, int) -> None
        self.message_bus = message_bus
        self.pub_address = self.publish_pub_address = pub_address
        if self.pub_address is None:
            self.pub_address = "tcp://*:5000"

        if self.publish_pub_address is None:
            if "SERVICE_ADDRESS" in os.environ:
                self.publish_pub_address = "{}:{}".format(self._get_address_without_port(os.environ["SERVICE_ADDRESS"]),
                                                          5000)
            elif "HOSTNAME" in os.environ:
                self.publish_pub_address = "tcp://{}:5000".format(os.environ["HOSTNAME"])

        if self.publish_pub_address is None:
            self.publish_pub_address = "tcp://localhost:5000"

        self.sub_address = sub_address
        if self.sub_address is None:
            self.sub_address = "tcp://*:5001"
        self.pull_address = self.publish_pull_address = pull_address
        if self.pull_address is None:
            self.pull_address = "tcp://*"

        if self.publish_pull_address is None:
            if "SERVICE_ADDRESS" in os.environ:
                self.publish_pull_address = self._get_address_without_port(os.environ["SERVICE_ADDRESS"])
            elif "HOSTNAME" in os.environ:
                self.publish_pull_address = "tcp://{}".format(os.environ["HOSTNAME"])

        if self.publish_pull_address is None:
            self.publish_pull_address = "tcp://localhost"

        self.min_port = min_port
        self.max_port = max_port
        self.clean_interval = clean_interval
        if self.clean_interval is None:
            self.clean_interval = 300
        self.sub_id_lifetime = sub_id_lifetime
        if self.clean_interval is None:
            self.clean_interval = 30

        self.forwarder_device = ForwarderDevice(self.pub_address, self.sub_address)
        self.forwarder_device.start()

        self.subscription_lock = Lock()
        self.clean_condition = Condition()
        self.running = True
        self.clean_thread = Thread(target=self.run_clean_thread)
        self.clean_thread.daemon = True
        self.clean_thread.start()

        self.sub_id_async_subscription = {}  # type: Dict[str, AsyncSubscription]
        self.service_name_async_subscriptions = {}  # type: Dict[str, AsyncSubscription]
        self.sync_subscriptions = {}  # type: Dict[str, SyncSubscription]

    def subscribe(self, sub_id, service_name, subjects):
        # type: (str, str, List[str]) -> SubscribeResponse
        if sub_id is None:
            logger.debug("New subscription received: %s", service_name)
            sub_id = str(uuid4())
            logger.debug("Created subscription id: %s", sub_id)
        with self.subscription_lock:
            async_subscriptions = self.subscribe_async_events(service_name, sub_id, subjects)
            self.subscribe_sync_events(service_name, sub_id, subjects)

        logger.debug("Subscription correctly processed: %s", async_subscriptions.publish_pull_address)

        return SubscribeResponse(sub_id,
                                 self.publish_pub_address,
                                 async_subscriptions.publish_pull_address.replace("*", "localhost"))

    def unsubscribe(self, sub_id, service_name, subjects):
        # type: (str, str, List[str]) -> None
        with self.subscription_lock:
            self.unsubscribe_async_events(service_name, sub_id, subjects)
            self.unsubscribe_sync_events(sub_id, subjects)

    def publish_sync_event(self, event):
        # type: (Event) -> Message
        logger.debug("Publishing sync event: %s", event.subject)
        if event.subject not in self.sync_subscriptions:
            logger.debug("Nobody subscribed to this event")
            return Message(DefaultToken.TK_EVT_EVENT)
        with self.subscription_lock:
            service_names = self.sync_subscriptions[event.subject].get_subscribed_services()

        event_message = MessageDecoder.get_message_from_event(event)
        for service_name in service_names:
            logger.debug("Sending the event to: %s", service_name)
            reply = self.message_bus.send_message(service_name, event_message)
            if reply.token != DefaultToken.TK_EVT_EVENT:
                logger.debug("Received reply from service (%s). Interrupting the loop", reply.token)
                return reply

        return Message(DefaultToken.TK_EVT_EVENT)

    def keep_alive(self, sub_id):
        logger.debug("Receiving keep alive from: %s", sub_id)
        with self.subscription_lock:
            if sub_id not in self.sub_id_async_subscription:
                raise Exception("Subscription id not found")
            self.sub_id_async_subscription[sub_id].update_last_seen(sub_id)

    def stop(self):
        self.running = False
        with self.clean_condition:
            self.clean_condition.notify_all()
        self.forwarder_device.stop()
        with self.subscription_lock:
            for subscription in self.service_name_async_subscriptions.values():
                subscription.stop()

    def unsubscribe_sync_events(self, sub_id, subjects):
        for subject in subjects:
            if subject in self.sync_subscriptions:
                self.sync_subscriptions[subject].remove_service(sub_id)
                if not self.sync_subscriptions[subject].has_subscriptions():
                    del self.sync_subscriptions[subject]

    def unsubscribe_async_events(self, service_name, sub_id, subjects):
        if service_name in self.service_name_async_subscriptions:
            subscription = self.service_name_async_subscriptions[service_name]
            subscription.remove_subjects(sub_id, subjects)
            if not subscription.has_subscriptions():
                del self.service_name_async_subscriptions[service_name]

    def subscribe_sync_events(self, service_name, sub_id, subjects):
        for subject in subjects:
            if subject not in self.sync_subscriptions:
                self.sync_subscriptions[subject] = SyncSubscription(subject)
            sync_subscription = self.sync_subscriptions[subject]
            sync_subscription.add_service(sub_id, service_name)

    def subscribe_async_events(self, service_name, sub_id, subjects):
        if service_name not in self.service_name_async_subscriptions:
            self.service_name_async_subscriptions[service_name] = \
                AsyncSubscription(service_name,
                                  self.sub_address.replace("*", "localhost"),
                                  self.pull_address,
                                  self.publish_pull_address,
                                  self.min_port,
                                  self.max_port,
                                  self.sub_id_lifetime)

        subscription = self.service_name_async_subscriptions[service_name]
        self.sub_id_async_subscription[sub_id] = subscription
        subscription.add_subjects(sub_id, subjects)
        return subscription

    @staticmethod
    def _get_address_without_port(address):
        if not address.startswith("tcp://"):
            raise Exception("The SERVICE_ADDRESS must start with tcp://")

        final_index = len(address)
        port_start_index = address.find(":", 6)
        if port_start_index >= 0:
            final_index = address.index(":")

        return address[:final_index]

    def run_clean_thread(self):
        while self.running:
            try:
                with self.clean_condition:
                    self.clean_condition.wait(self.clean_interval)
                self.clean_invalid_subscriptions()
            except:
                logger.exception("Error on clean thread")

    def clean_invalid_subscriptions(self):
        with self.subscription_lock:
            service_names_to_remove = []
            for service_name in self.service_name_async_subscriptions.keys():
                async_subscription = self.service_name_async_subscriptions[service_name]
                subject_sub_ids = async_subscription.clean_invalid_subscriptions()
                if not async_subscription.has_subscriptions():
                    service_names_to_remove.append(service_name)
                    async_subscription.stop()

                for subject in subject_sub_ids.keys():
                    for sub_id in subject_sub_ids[subject]:
                        if sub_id in self.sub_id_async_subscription:
                            del self.sub_id_async_subscription[sub_id]
                        self.sync_subscriptions[subject].remove_service(sub_id)
                        if not self.sync_subscriptions[subject].has_subscriptions():
                            del self.sync_subscriptions[subject]

            for service_name in service_names_to_remove:
                del self.service_name_async_subscriptions[service_name]
