"""
Event reporting API for plugins.
"""
from time import time
from builtins import int
from hashlib import md5
from enum import IntEnum
from threading import Lock
from typing import Dict
import inspect
import ruxit.api.selectors
from ruxit.utils.murmur3 import Murmur3
import logging

logger = logging.getLogger(__name__)


class EventType(IntEnum):
    HOST_VM_STARTED = 57
    HOST_VM_SHUTDOWN = 58
    VM_MOTION = 59
    VM_LAUNCH_FAILED = 65

    PERFORMANCE_EVENT = 67
    ERROR_EVENT = 68
    AVAILABILITY_EVENT = 69
    RESOURCE_CONTENTION_EVENT = 70
    CUSTOM_INFO = 71
    CUSTOM_DEPLOYMENT = 72
    CUSTOM_ANNOTATION = 73


class EventMetadataKey(IntEnum):
    VM_DESTINATION_HOST_ID = 155
    VM_SOURCE_HOST_ID = 156
    HOST_AFFECTED_VM_ID = 157
    PROJECT_ID = 158
    OPENSTACK_REASON = 162

    CUSTOM_DESCRIPTION = 163
    CUSTOM_TITLE = 164
    CUSTOM_PROPERTIES = 165
    CUSTOM_ANNOTATION_TYPE = 166
    CUSTOM_SOURCE = 167
    CUSTOM_PROJECT = 168
    CUSTOM_VERSION = 169
    CUSTOM_CI_BACK_LINK = 170
    CUSTOM_REMEDIATION_ACTION_LINK = 171
    CUSTOM_DEPLOYMENT_NAME = 172


# event methadata - it's non-mutable object
class EventMetadata:
    TITLE_STR_LIMIT = 1024
    OTHER_STR_LIMIT = 10240
    PROPERTIES_ELEMENTS_LIMIT = 100
    PROPERTIES_KEY_STR_LIMIT = 100
    PROPERTIES_VALUE_STR_LIMIT = 4000

    def __init__(self, key: int, value):
        self._key = key
        self._value = value

    @property
    def key(self):
        return self._key

    @property
    def is_boolean(self):
        return type(self._value) is bool

    @property
    def is_float(self):
        return type(self._value) is float

    @property
    def is_int(self):
        return type(self._value) is int

    @property
    def is_custom_properties(self):
        return type(self._value) is dict

    @property
    def is_not_set(self):
        return self._value is None

    @property
    def value(self):
        if self.is_boolean or self.is_float or self.is_int or self.is_not_set:
            return self._value
        elif self.is_custom_properties:
            # limit the number of elements and length of strings
            no = 0
            trimmed_properties = {}
            for key, value in self._value.items():
                if no >= self.PROPERTIES_ELEMENTS_LIMIT:
                    logger.info("Event properties dictionary reached max size = %d and has been trimmed. First rejecterd element key: %s", self.PROPERTIES_ELEMENTS_LIMIT, str(key))
                    break
                trimmed_properties[trim_end_string(key, self.PROPERTIES_KEY_STR_LIMIT)] = trim_end_string(value, self.PROPERTIES_VALUE_STR_LIMIT)
                no += 1
            return trimmed_properties
        else:
            # limit the length of strings
            if self._key is EventMetadataKey.CUSTOM_TITLE:
                str_limit = self.TITLE_STR_LIMIT
            else:
                str_limit = self.OTHER_STR_LIMIT
        return str(trim_end_string(self._value, str_limit))

    def __repr__(self):
        return "EventMetadata(key=%s,value=%s)" % (self.key, self.value)


class _BaseEvent:

    def __init__(self,
                 identifier: str,
                 event_type: int,
                 entity_selector: ruxit.api.selectors.FromPluginSelector(),
                 metadata: list=[]
                 ):
        self._id = identifier
        self._event_type = event_type
        self._entity_selector = entity_selector
        self._metadata = metadata
        self._correlated = False

    @property
    def event_type(self):
        return self._event_type

    @property
    def identifier(self):
        return self._id

    @property
    def entity_selector(self):
        return self._entity_selector

    @property
    def metadata(self):
        return frozenset(self._metadata)

    def isCorrelated(self):
        return self._correlated

    def get_value(self, key):
        return next((x.value for x in self._metadata if x.key == key), "")


# event is non-mutable object
# contenttype is default from protobuf message
# eventId is automatic
# agentId, timestamp, smin, smax are set by OS Agent
# required: type, entity_id
class Event(_BaseEvent):
    __event_id_generator = 1
    __event_id_lock = Lock()

    def __init__(self, event_type: int, entity_selector: ruxit.api.selectors.FromPluginSelector(),
                 metadata: list=[]):
        super().__init__(self.__get_event_id(event_type), event_type, entity_selector, metadata)

    @staticmethod
    def __get_event_id(type):
        with(Event.__event_id_lock):
            id = Event.__event_id_generator
            Event.__event_id_generator += 1

        m = md5()
        byteorder = "little"
        m.update(type.to_bytes(64, byteorder))
        m.update(round(time() * 100).to_bytes(64, byteorder))
        m.update(id.to_bytes(64, byteorder))
        bytes = m.digest()
        first_part = 0
        second_part = 0
        for index in range(8):
            first_part = first_part << 8
            first_part |= bytes[index]
            second_part = second_part << 8
            second_part |= bytes[index + 8]
        return str(first_part ^ second_part)


class CorrelationEvent(_BaseEvent):

    def __init__(self, event_type: int, entity_selector: ruxit.api.selectors.FromPluginSelector(),
                 metadata: list=[]):
        super().__init__(str(-1),
                         event_type,
                         entity_selector,
                         metadata)
        self._correlated = True

    @staticmethod
    def get_correlation_id(entity_id, event_type, event_title):
        input_bytes = entity_id.to_bytes(8, byteorder="big") \
                      +event_type.to_bytes(64, byteorder="big") \
                      +event_title.encode('utf-8')
        return str(Murmur3().murmur3(input_bytes))
    

#get information about plugin from stack
def _get_plugin_name(stack_depth: int) -> str:
    the_stack = inspect.stack()
    frame = inspect.stack()[stack_depth]
    return "file {} line {}".format(frame.filename, frame.lineno)
    
#generator for dictionary with maximum counter
def _iterate_dict (the_items, max_size: int):
    count = min(len(the_items), max_size)
    for k, v in the_items:
        if count <= 0: 
            return
        count -= 1
        yield k, v

def trim_end_string(string, limit):
    if len(string) > limit:
        return string[:limit - 3] + "..."
    else:
        return string
        
# if props doesn't meet custom properties rules 
# than CustomPropException is thrown             
def check_custom_properties (properties: Dict[str,str],
                             stack_depth : int, 
                             max_size: int=EventMetadata.PROPERTIES_ELEMENTS_LIMIT, 
                             max_key: int=EventMetadata.PROPERTIES_KEY_STR_LIMIT, 
                             max_value: int=EventMetadata.PROPERTIES_VALUE_STR_LIMIT
                             ) -> Dict[str,str]:
    if len(properties) > max_size:
        logger.info("Size of custom parameters is greater then {} for plugin {}"
                     .format(max_size, _get_plugin_name(stack_depth+1)))
        changed = {key: value for key, value in _iterate_dict(properties.items(), max_size)}
        properties = changed
                       
    changed = []                                       
    for key, value in properties.items():
        if len(key) > max_key:
            logger.info("Length of name {} of custom parameter is greater than {} for plugin {}"
                         .format(key, max_key, _get_plugin_name(stack_depth+1)))
            changed.append((key, trim_end_string(key,max_key)))
        if value and len(value) > max_value:
            logger.info("Length of value {} of custom parameter {} is greater than {} for plugin {}"
                         .format(value, key, max_value, _get_plugin_name(stack_depth+1)))
            properties[key] = trim_end_string(value, max_value)
    for key, new_key in changed:
        properties[new_key] = properties.pop(key)
    return properties