from abc import ABCMeta, abstractmethod
from datetime import datetime
from logging import Logger
from threading import Timer

from application.interactor import UserUpdaterInteractor, MediaUpdaterInteractor, LoaderUpdaterInteractor, \
    CatalogUpdaterInteractor, BinaryUpdaterInteractor
from application.model import BaseThread
from application.model.configuration import Configurations
from application.model.customexception import NotInsideUpdateWindow, NotImplementedException, NoUpdateFound, \
    DisabledFunctionality
from application.repository.pos import UserUpdaterPOSRepository, MediaUpdaterPOSRepository, \
    LoaderUpdaterPOSRepository, CatalogUpdaterPOSRepository, BinaryUpdaterPOSRepository
from application.util import ValidationUtil
from typing import Union, Optional


class UpdaterManager(BaseThread):
    __metaclass__ = ABCMeta

    def __init__(
        self,               # type: UpdaterManager
        update_name,        # type: str
        configs,            # type: Configurations
        interactor,         # type: Union[UserUpdaterInteractor, MediaUpdaterInteractor, LoaderUpdaterInteractor, CatalogUpdaterInteractor, BinaryUpdaterInteractor] # noqa
        pos_repository,     # type: Union[UserUpdaterPOSRepository, MediaUpdaterPOSRepository, LoaderUpdaterPOSRepository, CatalogUpdaterPOSRepository, BinaryUpdaterPOSRepository] # noqa
        logger,             # type: Logger
    ):
        # type: (...) -> None

        super(UpdaterManager, self).__init__()
        self.update_name = update_name
        self.local_configs = configs.updaters[self.update_name]
        self.active_frequency = int(self.local_configs.active_frequency)
        self.interactor = interactor
        self.pos_repository = pos_repository
        self.logger = logger

        self.update_scheduled = {}

    def run(
        self,                   # type: UpdaterManager
        validate_window=True,   # type: Optional[bool]
    ):
        # type: (...) -> None

        if self.active_frequency == 0:
            self.logger.info("{} - Auto-update thread not active. Stopping manager".format(self.update_name))
            self.stop()
            return

        self.logger.info("{} - update thread manager started".format(self.update_name))

        self._execute(validate_window=validate_window)

    def _execute(self, validate_window):
        # type: (bool) -> None

        disabled_update = False
        while self.running:
            try:
                self.notify_pending_update()
                self._has_scheduled_next_update()

                self._check_update_window(validate_window)
                self.has_update_to_apply()

                self.update()
            except DisabledFunctionality:
                self.logger.info("{} - Auto-update thread disabled. Stopping manager".format(self.update_name))
                disabled_update = True
                self.stop()
                return
            except NotInsideUpdateWindow:
                self.logger.info("{} - Not inside update windows".format(self.update_name))
            except NoUpdateFound:
                self.logger.info("{} - No update found".format(self.update_name))
            except (Exception,):
                self.logger.exception("{} - Not expected exception on update".format(self.update_name))
            finally:
                if not disabled_update:
                    if not self.pos_repository.has_already_applied_update():
                        self.sleep(30)
                    else:
                        self.sleep(self.active_frequency)

    @abstractmethod
    def update(self):
        # type: () -> None

        raise NotImplementedException()

    @abstractmethod
    def notify_pending_update(
        self,   # type: UpdaterManager
    ):
        # type: (...) -> None

        raise NotImplementedError()

    def has_update_to_apply(self):
        # type: () -> None

        return

    def get_pending_remote_update_to_schedule(
        self,                  # type: UpdaterManager
        external_update_id,    # type: str
    ):
        # type: (...) -> Optional[datetime.time]

        return

    def get_pending_remote_update_id(self):
        # type: (...) -> Optional[str]

        return

    def _check_update_window(self, validate_window):
        # type: (bool) -> None

        if not validate_window or not self.pos_repository.has_already_applied_update():
            return

        start_time = self.local_configs.window.start_time
        end_time = self.local_configs.window.end_time
        if not ValidationUtil().is_inside_allowed_window(start_time, end_time):
            str_start_time = start_time.strftime("%H:%M:%S")
            str_end_time = end_time.strftime("%H:%M:%S")
            message = "Not inside allowed window. Start: {} / End: {}".format(str_start_time, str_end_time)
            self.logger.info(message)
            raise NotInsideUpdateWindow()

    def _has_scheduled_next_update(
        self,   # type: UpdaterManager
    ):
        # type: (...) -> Optional[bool]

        external_update_id = self.get_pending_remote_update_id()
        update_time = self.get_pending_remote_update_to_schedule(external_update_id=external_update_id)
        if not update_time:
            return

        timer = Timer(
            interval=(update_time - datetime.now()).total_seconds(),
            function=self._execute_timer,
            args=(external_update_id, )
        )
        timer.start()

        self.logger.info("Remote Loader Update scheduled for {}".format(update_time))

    def _execute_timer(
        self,                  # type: UpdaterManager
        external_update_id,    # type: str
    ):
        # type: (...) -> None

        if external_update_id in self.update_scheduled:
            del self.update_scheduled[external_update_id]

        self.update()
