import json
import logging
from hashlib import md5
from typing import (
    Dict,
    List,
)

from crontab import CronTab
from messagebus import (
    Event,
    MessageBus,
)
from timeutil import (
    Clock,
    Sleeper,
)
from ._EventDefinition import EventDefinition
from ._InvalidCronTabExpression import InvalidCronTabExpression
from ._RedisLocker import RedisLocker

logger = logging.getLogger(__name__)


class CronTabInteractor(object):
    def __init__(
        self,
        message_bus: MessageBus,
        expression: str,
        event_definition: EventDefinition,
        clock: Clock,
        sleeper: Sleeper,
        locker: RedisLocker = None,
    ) -> None:

        self.message_bus = message_bus
        self.expression = expression
        self.event_definition = event_definition
        self.clock = clock
        self.sleeper = sleeper
        self.locker = locker

        try:
            self.entry = CronTab(expression)
        except ValueError:
            raise InvalidCronTabExpression(expression)

        self.headers_hash = self._calculate_headers_hash(headers=event_definition.headers)

    def execute(self):
        sleep_time = self.entry.next(self.clock.now(), default_utc=False)
        self.sleeper.sleep(sleep_time)

        if self.locker is not None:
            if not self.locker.must_publish(
                subject=f"{self.event_definition.subject}-{self.expression}-{self.headers_hash}",
                await_time=self.entry.next(self.clock.now(), default_utc=False),
            ):
                logger.debug("Skipping publication of: {}".format(self.event_definition.subject))
                return

        logger.info("Publishing event: {}".format(self.event_definition.subject))
        for headers in self.event_definition.headers:
            logger.info("Publishing event: {} for context {}".format(self.event_definition.subject, headers))
            self.message_bus.publish_event(self.build_event(headers))

    def build_event(self, headers: Dict[str, str]):
        return Event(
            subject=self.event_definition.subject,
            event_type=self.event_definition.event_type,
            data=self.event_definition.data,
            headers=headers,
        )

    @staticmethod
    def _calculate_headers_hash(headers: List[Dict[str, str]]) -> str:
        headers_str = json.dumps(headers)
        return md5(headers_str.encode('utf-8')).hexdigest()
