#!/usr/bin/env python
# -*- coding: utf-8 -*-
import base64
import datetime as dt
import json
import logging
import re
import socket
import threading
import time
import uuid

import zmq
from lib.myconfig import MyConfig
from lib.mymessage.SysmonData import SysmonSchema
from lib.utils import float_to_datetime, datetime_now_float
from marshmallow import Schema, fields, post_load, ValidationError

logger = logging.getLogger()

DATAQUEUE_NAME = "dataflow"
ADMINQUEUE_NAME = "adminflow"

SUPPORTED_MESSAGE_CATEGORIES = {
    "info.sysmon" : SysmonSchema
}

###############################################################################################
###############################################################################################

class MessageSchema(Schema):
    uid = fields.Str()
    origin_host = fields.Str()
    origin_service = fields.Str()
    origin_uid = fields.Str()
    date_creation = fields.Float()
    topic = fields.Str()
    b64data = fields.Str()

    @post_load
    def make_object(self, data):
        msg = MessageObject()
        msg.date_creation = data["date_creation"]
        msg.origin_host = data["origin_host"]
        msg.origin_service = data["origin_service"]
        msg.origin_uid = data["origin_uid"]
        msg.uid = data["uid"]
        msg.topic = data["topic"]
        msg.b64data = data["b64data"]
        return msg

    def toObject(self, json_string):
        message = self.loads(json_string)
        return message.data

###############################################################################################
###############################################################################################

class MessageObject:

    def __init__(self, data=None,
                    topic="*"
                 ):
        # type: (object, object) -> object
        self.uid = str(uuid.uuid1())
        self.origin_host = socket.getfqdn()
        self.origin_service = None
        self.origin_uid = None
        self.date_creation = datetime_now_float()
        self.topic = topic
        self.set_data(data)

    def __repr__(self):
        return "<BusMessage> origin:%s.%s; topic:%s; date:%s; data:%s"%(self.origin_host, self.origin_service, self.topic,
                                                                            str(float_to_datetime(self.date_creation)),
                                                                            self.get_data())

    def set_data(self, data):
        if data is None:
            self.b64data = ""
            return
        if type(data) <> dict:
            logger.error("Data in message bus must be provided as 'dict' !!")
            self.b64data = ""
            return
        self.b64data = base64.b64encode(json.dumps(data))

    def get_data(self):
        if len(self.b64data) == 0:
            return None
        return json.loads(base64.b64decode(self.b64data))

    def toJson(self):
        schema = MessageSchema()
        json_result = schema.dumps(self)
        return json_result.data


###############################################################################################
###############################################################################################

cfg_data = MyConfig()
print cfg_data.configfilepath

SERVER_IP = cfg_data.get(["buscore","host",],"127.0.0.1")
logger.info("Bus Core Config IP: "+SERVER_IP)

PUBLISH_PORT = cfg_data.get(["buscore","internal_port",],1111)
logger.info("Bus Core internal queue port: "+str(PUBLISH_PORT))

MESSAGE_PORT = cfg_data.get(["buscore","port",],1981)
logger.info("Bus Core listening queue port: "+str(MESSAGE_PORT))

LISTEN_INTF_SRV = "tcp://*:%s"%PUBLISH_PORT
PUBLISH_INTF_SRV = "tcp://*:%s"%MESSAGE_PORT

LISTEN_INTF_CLT = "tcp://%s:%s"%(SERVER_IP,MESSAGE_PORT)
PUBLISH_INTF_CLT = "tcp://%s:%s"%(SERVER_IP,PUBLISH_PORT)

class BusCore(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self._stopevent = threading.Event()

        # Initialize the ZeroMQ context
        self.context = zmq.Context()
        self._init_reception_queue()
        self._init_emission_queue()


    def _init_reception_queue(self):
        # Configure ZeroMQ to receive messages
        self.zmq_recv = self.context.socket(zmq.SUB)
        # The communication is made on socket 1112
        self.zmq_recv.bind(LISTEN_INTF_SRV)
        self.zmq_recv.setsockopt(zmq.SUBSCRIBE, '')  # subscribe to everything
        logger.info("Bus Core Receiving messages on "+LISTEN_INTF_SRV)

    def _init_emission_queue(self):
        # Configure ZeroMQ to send messages
        self.zmq_send = self.context.socket(zmq.PUB)
        # The communication is made on socket 1111
        self.zmq_send.bind(PUBLISH_INTF_SRV)
        logger.info("Bus Core Sending messages on "+PUBLISH_INTF_SRV)

    def publish(self, msg):
        """self.channel.basic_publish(exchange='',
                              routing_key=DATAQUEUE_NAME,
                              body=msg.msg())"""
        json_result = msg.toJson()

        logger.info("Core Bus publishing message : %s"%str(msg))

        try:
            self.zmq_send.send(json_result)
        except zmq.ZMQError as err:
            logger.info("Error while trying to publish the message : %s"%str(err))
            return False

        return True

    def run(self):
        logger.info("Bus core start listening messages")
        while not self._stopevent.isSet():
            # Command to wait for an incoming message
            try:
                # Capture the message and store it
                body = self.zmq_recv.recv(zmq.NOBLOCK)
            except zmq.ZMQError as err:
                #logger.error("ZMQ Error : %s" % str(err))
                time.sleep(0.01)
                continue

            schema = MessageSchema(strict=True)
            try:
                message = schema.toObject(body)
            except ValidationError as err:
                logger.error("Invalid message format: %s, dropping it" % str(body))
                logger.error("    ERRORS: %s" % str(err.messages))
                return

            logger.debug("Bus Core received a message : %s" % str(message))
            #self.client_list.client_message(message)
            """self._db.update_buselement(bus_uid=message.origin_uid,
                                       host=message.origin_host,
                                       service=message.origin_service,
                                       event_date=message.date_creation)
            """
            self.publish(message)

        logger.info("Bus core stopped listening message.")

    def stop(self):
        self._stopevent.set( )
        self.zmq_send.close()
        self.zmq_recv.close()
        self.context.term()
        logger.info("Bus core cleaned and terminated. Thank you !")

##############################################################################################""

class BusElement(threading.Thread):

    def __init__(self, service_name,
                 callback=None,
                 filter=None,
                 ignore_my_msg=True):
        threading.Thread.__init__(self)
        self._stopevent = threading.Event()
        # Initialize the ZeroMQ context
        self.context = zmq.Context()
        self._init_reception_queue()
        self._init_emission_queue()
        self._ignore_me = ignore_my_msg

        self.registered_topics = []

        self.service_name = service_name
        self.bus_uid = str(uuid.uuid1())
        self.set_callback(callback=callback)

    def register_topic(self, topic_pattern):
        if topic_pattern == "*":
            regex = topic_pattern
        else:
            regex = re.compile(topic_pattern)
        self.registered_topics.append(regex)

    def set_callback(self, callback):
        self._callback = callback

    def _init_reception_queue(self):
        # Configure ZeroMQ to receive messages
        self.zmq_recv = self.context.socket(zmq.SUB)
        # The communication is made on socket 1112
        logger.info("Receiving messages on " + LISTEN_INTF_CLT)
        self.zmq_recv.connect(LISTEN_INTF_CLT)
        self.zmq_recv.setsockopt(zmq.SUBSCRIBE, '')  # subscribe to everything

    def _init_emission_queue(self):
        # Configure ZeroMQ to send messages
        self.zmq_send = self.context.socket(zmq.PUB)
        logger.info("Sending messages on " + PUBLISH_INTF_CLT)
        # The communication is made on socket 1111
        self.zmq_send.connect(PUBLISH_INTF_CLT)


    def __internal_data_cb(self, body):
        schema = MessageSchema(strict=True)
        try:
            message = schema.toObject(body)
        except ValidationError as err:
            logger.error("Invalid message format: %s, dropping it" % str(body))
            logger.error("    ERRORS: %s" % str(err.messages))
            return
        pass
        if (message.origin_uid == self.bus_uid) and self._ignore_me is True:
            #ignore message published by me !!
            return

        for topic in self.registered_topics:
            if topic == "*" or topic.match(message.topic) is not None:
                if self._callback is not None:
                    logger.info("Message triggered: %s" % str(message))
                    self._callback(message)

    def publish(self, msg):
        """self.channel.basic_publish(exchange='',
                              routing_key=DATAQUEUE_NAME,
                              body=msg.msg())"""
        msg.origin_uid = self.bus_uid
        msg.origin_service = self.service_name
        json_result = msg.toJson()

        logger.info("Publishing message : %s" % str(msg))

        try:
            self.zmq_send.send(json_result)
        except zmq.ZMQError as err:
            logger.info("ZMQ error trying to send message: %s"%str(err))

    def run(self):
        logger.info("Bus client start listening messages on bus")
        self.publish(MessageObject(topic="admin.start"))
        dt_start = dt.datetime.now()
        while not self._stopevent.isSet():
            # Command to wait for an incoming message
            try:
                # Capture the message and store it
                body = self.zmq_recv.recv(zmq.NOBLOCK)
                #logger.debug("Message received : %s"%str(body))
            except zmq.ZMQError as err:
                #logger.error("ZMQ error message : %s" % str(err))
                time.sleep(0.01)
                dt_now = dt.datetime.now()
                if (dt_now-dt_start).seconds > 30:
                    self.publish(MessageObject(topic="admin.heartbeat"))
                    dt_start = dt_now
                continue

            self.__internal_data_cb(body)
        logger.info("Bus client stopped listening messages on bus")

    def stop(self):
        self.publish(MessageObject(topic="admin.terminated"))
        time.sleep(0.5)
        self._stopevent.set( )
        self.zmq_send.close()
        self.zmq_recv.close()
        self.context.term()
        logger.info("Bus client cleaned and terminated. Thank you !")

