from enum import IntEnum
from pprint import pformat
from re import match
from typing import Dict, Any, Optional, Union, List

from robotnikmq import Topic, Message, RobotnikConfig
from typeguard import typechecked


@typechecked
class Priority(IntEnum):
    INFO = 0
    ACTIVITY = 1
    WARNING = 2
    ERROR = 3
    CRITICAL = 4


INFO = {'info', 'information', 'i'}
ACTIVITY = {'activity', 'a'}
WARNING = {'warning', 'warn', 'w'}
ERROR = {'error', 'err', 'e'}
CRITICAL = {'critical', 'crit', 'c'}


@typechecked
def valid_priority(candidate: Any) -> bool:
    if isinstance(candidate, Priority):
        return True
    if isinstance(candidate, str):
        return any(candidate.lower() in P for P in [INFO, ACTIVITY, WARNING, ERROR, CRITICAL])
    if isinstance(candidate, int):
        return any(candidate == i for i in Priority)
    return False


@typechecked
def priority_of(candidate: Union[str, int]) -> Priority:
    if isinstance(candidate, str):
        if candidate.lower() in INFO:
            return Priority.INFO
        if candidate.lower() in ACTIVITY:
            return Priority.ACTIVITY
        if candidate.lower() in WARNING:
            return Priority.WARNING
        if candidate.lower() in ERROR:
            return Priority.ERROR
        if candidate.lower() in CRITICAL:
            return Priority.CRITICAL
    if isinstance(candidate, int):
        return Priority(candidate)
    raise ValueError(f'"{candidate}" cannot be mapped to a valid priority')


class NotificationMsg:
    @typechecked
    def __init__(self,
                 contents: Dict[str, Any],
                 alert_key: Union[str, List[str], None] = None,
                 desc: Optional[str] = None,
                 ttl: Optional[int] = None,
                 priority: Optional[Priority] = None):
        self.contents = contents
        self.alert_key = '['+']['.join(alert_key)+']' if isinstance(alert_key, List) else alert_key
        self.desc = desc
        self.ttl = ttl
        self.priority = priority or Priority.INFO

    @typechecked
    def broadcast(self, exchange: str, route: str, config: Optional[RobotnikConfig] = None) -> None:
        broadcast_msg(exchange, route, self, config)


@typechecked
class InfoMsg(NotificationMsg):
    def __init__(self,
                 contents: Dict[str, Any],
                 ttl: Optional[int] = None):
        super().__init__(contents=contents, ttl=ttl, priority=Priority.INFO)


@typechecked
class ActivityMsg(NotificationMsg):
    def __init__(self,
                 contents: Dict[str, Any],
                 ttl: Optional[int] = None):
        super().__init__(contents=contents, ttl=ttl, priority=Priority.ACTIVITY)


class AlertComparison:
    @typechecked
    def __init__(self, first: 'AlertMsg', second: 'AlertMsg'):
        self.key = first.key, second.key
        self.desc = first.desc, second.desc
        self.ttl = first.ttl, second.ttl
        self.priority = first.priority, second.priority
        self.contents = first.contents, second.contents

    @property
    def key_equal(self) -> bool:
        return self.key[0] == self.key[1]

    @property
    def desc_equal(self) -> bool:
        return self.desc[0] == self.desc[1]

    @property
    def ttl_equal(self) -> bool:
        return self.ttl[0] == self.ttl[1]

    @property
    def priority_equal(self) -> bool:
        return self.priority[0] == self.priority[1]

    @property
    def key_match(self) -> bool:
        return bool(match(*self.key) or match(self.key[1], self.key[0]))

    @property
    def desc_match(self) -> bool:
        return bool(match(*self.desc) or match(self.desc[1], self.desc[0]))


@typechecked
class AlertMsg(NotificationMsg):
    def __init__(self,
                 contents: Dict[str, Any],
                 key: Union[str, List[str]],
                 desc: str,
                 ttl: Optional[int] = None,
                 priority: Optional[Priority] = None,
                 from_msg: Optional[Message] = None):
        if priority is not None and priority < Priority.WARNING:
            raise ValueError('Alerts can only have a priority of WARNING (2) or higher')
        if not desc:
            raise ValueError('Alerts have to have a description')
        if not key:
            raise ValueError('Alerts have to have a key')
        super().__init__(desc=desc, priority=priority or Priority.WARNING,
                         ttl=ttl or 30, contents=contents)
        self.alert_key: str = '['+']['.join(key)+']' if isinstance(key, List) else key
        self.msg = from_msg

    def compare(self, other: 'AlertMsg') -> AlertComparison:
        return AlertComparison(self, other)

    @property
    def key(self) -> str:
        return self.alert_key

    @property
    def description(self) -> Optional[str]:
        return self.desc

    @staticmethod
    def of(msg: Message) -> 'AlertMsg':  # pylint: disable=C0103
        if 'ttl' not in msg.contents:
            raise ValueError(f'Message id {msg.msg_id} cannot be interpreted as an alert '
                             f'as it does not have a TTL: {pformat(msg.to_dict())}')
        if 'priority' not in msg.contents:
            raise ValueError(f'Message id {msg.msg_id} cannot be interpreted as an alert '
                             f'as it does not have a priority: {pformat(msg.to_dict())}')
        if not valid_priority(msg.contents['priority']):
            raise ValueError(f'Message id {msg.msg_id} cannot be interpreted as an alert '
                             f'as it does not have a valid priority: {pformat(msg.to_dict())}')
        if not isinstance(msg.contents['ttl'], int) or msg.contents['ttl'] < 0:
            raise ValueError(f'Message id {msg.msg_id} cannot be interpreted as an alert '
                             f'as it does not have a valid TTL: {pformat(msg.to_dict())}')
        if 'alert_key' not in msg.contents:
            raise ValueError(f'Message id {msg.msg_id} cannot be interpreted as an alert '
                             f'as it does not have an alert key: {pformat(msg.to_dict())}')
        if 'description' not in msg.contents:
            raise ValueError(f'Message id {msg.msg_id} cannot be interpreted as an alert '
                             f'as it does not have a description: {pformat(msg.to_dict())}')
        return AlertMsg(msg.contents, key=msg.contents['alert_key'],
                        desc=msg.contents['description'], ttl=msg.contents['ttl'],
                        priority=priority_of(msg.contents['priority']), from_msg=msg)


class WarningMsg(AlertMsg):
    @typechecked
    def __init__(self,
                 contents: Dict[str, Any],
                 key: Union[str, List[str]],
                 desc: str,
                 ttl: Optional[int] = None):
        if not desc:
            raise ValueError('Warnings (alerts) have to have a description')
        if not key:
            raise ValueError('Warnings (alerts) have to have a key')
        super().__init__(key=key, desc=desc, priority=Priority.WARNING,
                         ttl=ttl or 30, contents=contents)


class ErrorMsg(AlertMsg):
    @typechecked
    def __init__(self,
                 contents: Dict[str, Any],
                 key: Union[str, List[str]],
                 desc: str,
                 ttl: Optional[int] = None):
        if not desc:
            raise ValueError('Errors (alerts) have to have a description')
        if not key:
            raise ValueError('Errors (alerts) have to have a key')
        super().__init__(key=key, desc=desc, priority=Priority.ERROR,
                         ttl=ttl or 30, contents=contents)


class CriticalMsg(AlertMsg):
    @typechecked
    def __init__(self,
                 contents: Dict[str, Any],
                 key: Union[str, List[str]],
                 desc: str,
                 ttl: Optional[int] = None):
        if not desc:
            raise ValueError('Critical Alerts have to have a description')
        if not key:
            raise ValueError('Critical Alerts have to have a key')
        super().__init__(key=key, desc=desc, priority=Priority.CRITICAL,
                         ttl=ttl or 30, contents=contents)


@typechecked
def broadcast_msg(exchange: str, route: str, notification: NotificationMsg,
                  config: Optional[RobotnikConfig] = None) -> None:
    broadcast(exchange=exchange,
              route=route,
              priority=notification.priority,
              contents=notification.contents,
              ttl=notification.ttl,
              description=notification.desc,
              alert_key=notification.alert_key,
              config=config)


@typechecked
def broadcast_alert_msg(exchange: str, route: str, alert: AlertMsg,
                        config: Optional[RobotnikConfig] = None) -> None:
    broadcast(exchange=exchange,
              route=route,
              priority=alert.priority,
              contents=alert.contents,
              ttl=alert.ttl,
              description=alert.desc,
              alert_key=alert.alert_key,
              config=config)


@typechecked
def broadcast(exchange: str,
              route: str,
              priority: Priority,
              contents: Dict[str, Any],
              ttl: Optional[int] = None,
              description: Optional[str] = None,
              alert_key: Optional[str] = None,
              config: Optional[RobotnikConfig] = None):
    _contents: Dict[str, Any] = {'priority': priority.value}
    if priority.value >= 2:
        assert description is not None, 'Alerts (e.g. WARNING, ERROR, CRITICAL) must have a description'
        assert ttl is not None, 'Alerts (e.g. WARNING, ERROR, CRITICAL) must have a ttl (to clear an alert, set the ttl to 0)'
        assert alert_key is not None, 'Alerts (e.g. WARNING, ERROR, CRITICAL) must have an alert_key'
    if ttl is not None:
        _contents['ttl'] = ttl
    if description is not None:
        _contents['description'] = description
    if alert_key is not None:
        _contents['alert_key'] = alert_key
    _contents.update(contents)
    route += f'.{priority.name.lower()}'
    Topic(exchange=exchange, config=config).broadcast(Message(contents=_contents),
                                                      routing_key=route)


@typechecked
def broadcast_info(exchange: str, route: str, contents: Dict[str, Any],
                   config: Optional[RobotnikConfig] = None):
    broadcast(exchange, route, priority=Priority.INFO, contents=contents, config=config)


@typechecked
def broadcast_activity(exchange: str, route: str, contents: Dict[str, Any],
                       config: Optional[RobotnikConfig] = None):
    broadcast(exchange, route, priority=Priority.ACTIVITY, contents=contents, config=config)


@typechecked
def broadcast_alert(exchange: str,
                    route: str,
                    description: str,
                    alert_key: str,
                    contents: Dict[str, Any],
                    ttl: int = 30,
                    priority: Priority = Priority.WARNING,
                    config: Optional[RobotnikConfig] = None) -> None:
    broadcast(exchange, route, ttl=ttl, priority=priority, contents=contents,
              config=config, description=description, alert_key=alert_key)


@typechecked
def broadcast_warning(exchange: str, route: str, desc: str, alert_key: str,
                      contents: Dict[str, Any], ttl: int = 30,
                      config: Optional[RobotnikConfig] = None):
    broadcast_alert(exchange, route, description=desc, alert_key=alert_key, contents=contents,
                    ttl=ttl, priority=Priority.WARNING, config=config)


@typechecked
def broadcast_error(exchange: str, route: str, desc: str, alert_key: str,
                    contents: Dict[str, Any], ttl: int = 30,
                    config: Optional[RobotnikConfig] = None):
    broadcast_alert(exchange, route, description=desc, alert_key=alert_key, contents=contents,
                    ttl=ttl, priority=Priority.ERROR, config=config)


@typechecked
def broadcast_critical(exchange: str, route: str, desc: str, alert_key: str,
                       contents: Dict[str, Any], ttl: int = 30,
                       config: Optional[RobotnikConfig] = None):
    broadcast_alert(exchange, route, description=desc, alert_key=alert_key, contents=contents,
                    ttl=ttl, priority=Priority.CRITICAL, config=config)
