import json
import os
import shutil
import time
from datetime import datetime
from threading import Lock
from typing import (
    Optional,
    Dict,
    List,
)
import xml.etree.ElementTree as eTree

from application.model import (
    UpdateType,
    FiscalInfo,
)
from application.model.configuration import Configurations
from application.model.customexception import (
    DisabledFunctionality,
    ErrorCreatingBackup,
    ErrorDeletingContent,
    ErrorRecoveringFolder,
    ErrorExtractingLoaders,
    ErrorDownloadingLoaders,
    RollbackError,
    NotInsideUpdateWindow,
    ErrorValidatingOpenedUsers,
    ErrorUpdatingUserRoles,
    CannotDeletePdvWithOrders,
    ErrorDayIsOpen,
    InvalidBOHConfigurations,
)
from application.repository.api import LoaderUpdaterAPIRepository
from application.repository.pos import (
    LoaderUpdaterPOSRepository,
    VersionsPOSRepository,
    FiscalPOSRepository,
)
from application.service import (
    CommonService,
    UpdaterService,
    StaticConfigurationService,
)
from application.util import (
    LoggerUtil,
    ValidationUtil,
    settings,
)
from systools import sys_parsefilepath
from messagebus import MessageBus
from order_api import OrderService

from update_api.dto import PosUpdaterDto

GENESIS_PATH = "../../genesis"

logger = LoggerUtil.get_logger_name(__name__)


class LoaderUpdaterService(object):
    def __init__(
        self,
        configs,              # type: Configurations
        working_on_update,    # type: Lock
        api_repository,       # type: LoaderUpdaterAPIRepository
        pos_repository,       # type: LoaderUpdaterPOSRepository
        version_repository,   # type: VersionsPOSRepository,
        updater_service,      # type: UpdaterService
        message_bus,          # type: MessageBus
        fiscal_repository,    # type: FiscalPOSRepository
    ):
        # type: (...) -> None # noqa

        self.update_type = UpdateType.loader
        self.update_name = self.update_type.name
        self.enabled = configs.updaters[self.update_name].enabled
        self.local_configs = configs.updaters[self.update_name]
        self.store_id = configs.store_id

        self.working_on_update = working_on_update
        self.api_repository = api_repository
        self.pos_repository = pos_repository
        self.version_repository = version_repository
        self.updater_service = updater_service
        self.message_bus = message_bus
        self.fiscal_repository = fiscal_repository

        data_path = sys_parsefilepath("{HVDATADIR}")
        work_dir = sys_parsefilepath(configs.backup_directory)
        self.data_relative_path = "data"
        self.server_relative_path = "data/server"
        self.databases_relative_path = "data/server/databases"
        self.data_path = os.path.join(data_path, GENESIS_PATH, self.data_relative_path)
        self.server_path = os.path.join(self.data_path, "server")
        self.databases_path = os.path.join(data_path, GENESIS_PATH, self.databases_relative_path)
        default_work_path = os.path.join(work_dir, self.update_type.name)
        self.downloads_path = os.path.join(default_work_path, "downloads")
        self.backups_path = os.path.join(default_work_path, "backups")
        self.work_path = os.path.join(default_work_path, "work_path")
        self.payments_json = os.path.join(self.server_path, "bundles/payments.json")
        self.genesis_operator_levels_json = os.path.join(self.server_path, "bundles/operatorLevels.json")
        self.htdocs_path = os.path.join(data_path, 'htdocs')
        self.htdocs_kui_path = os.path.join(self.htdocs_path, 'kui')
        self.sui_static_path = os.path.join(data_path, 'htdocs/sui/static')
        self.loaders_zip = "loaders.zip"

        if not os.path.exists(self.backups_path):
            os.makedirs(self.backups_path)
        if not os.path.exists(self.downloads_path):
            os.makedirs(self.downloads_path)

    def update_loaders(
        self,                       # type: LoaderUpdaterService
        restart_hv=False,           # type: bool
        retry_on_error=False,       # type: bool
        data=None,                  # type: Optional[PosUpdaterDto]
    ):
        # type: (...) -> None

        settings.set_update_control(self.update_name, None)
        if not self.enabled:
            settings.set_update_control(self.update_name, "$UPDATE_DISABLED")
            raise DisabledFunctionality()

        rollback_necessary = False
        backup_package_zip = ''

        try:
            logger.info("Waiting Updating Loaders")
            with self.working_on_update:
                logger.info("Updating loaders")
                settings.set_update_control(self.update_name, "$UPDATING_LOADERS")
                settings.set_update_control("updating", True)

                external_update_id = None
                last_loader_update_id = 0
                obtained_date = None

                last_loader_update = self.pos_repository.get_last_loader_update()
                if last_loader_update is not None:
                    external_update_id = last_loader_update.server_update_identifier
                    last_loader_update_id = int(last_loader_update.update_id)
                    obtained_date = last_loader_update.obtained_date

                self._delete_directories()
                changed_pos_ctrl_info = self.download_and_validate_loaders()

                settings.set_update_control(self.update_name, "$LOADERS_BACKUP")
                backup_package_zip = self._create_backup()
                rollback_necessary = True

                settings.set_update_control(self.update_name, "$APPLYING_NEW_LOADERS")
                current_update_id = str(last_loader_update_id + 1)
                self.pos_repository.insert_new_loader_update(
                    update_id=current_update_id,
                    update_name="Loaders",
                    external_update_id=external_update_id,
                    obtained_date=obtained_date,
                )

                self.pos_repository.delete_update_line(
                    external_update_id=external_update_id,
                    update_id=last_loader_update_id,
                )

                self._extract_new_loaders(self.data_path)
                self._recover_folders()

                settings.set_update_control(self.update_name, "$UPDATING_USER_FUNCTIONS")
                self.update_operator_levels()

                self.updater_service.update_databases_with_json_config(
                    update_name=self.update_name,
                    settings=settings,
                )

                self.pos_repository.mark_loader_update_as_applied()

                StaticConfigurationService.update_static_loaders()
                StaticConfigurationService.run_databases_script()
                settings.set_update_control(self.update_name, "$SUCCESS_UPDATE")

                if changed_pos_ctrl_info is not None:
                    self._update_pos_ctrl_info(changed_pos_ctrl_info)

                self._update_ui_customizations()
                self._update_fiscal_id()

                rollback_necessary = False
                self.send_result_message(
                    current_update_id=current_update_id,
                )
                if restart_hv:
                    self._delete_directories()
                    logger.info("Restarting Hypervisor to apply new Loaders")
                    self.pos_repository.common_pos_repository.restart_hv(
                        pos_id=(data and data.pos_id) or "",
                        user_id=(data and data.user_id) or "",
                        event_type="POS_UPDATER_LOADER",
                    )
                    time.sleep(5)

                logger.info("Loaders update fully concluded with success!")
        except NotInsideUpdateWindow:
            settings.set_update_control(self.update_name, "$NOT_INSIDE_UPDATE_WINDOW")
            logger.info("Sleeping until enter the update window")
            raise
        except ErrorValidatingOpenedUsers:
            settings.set_update_control(self.update_name, "$UPDATE_LOADERS_ERRORVALIDATINGOPENEDUSERS")
            logger.info("Error validating loaders. Necessary to close all the operators and the day")
            raise
        except (Exception,) as ex:
            if rollback_necessary:
                with self.working_on_update:
                    self._rollback(backup_package_zip)

            if isinstance(ex, ErrorDownloadingLoaders):
                settings.set_update_control(self.update_name, ex.message)
            else:
                settings.set_update_control(self.update_name, "$UNKNOWN_ERROR")

            if not retry_on_error:
                self.pos_repository.mark_loader_update_as_error()

            logger.exception("Not expected exception on loaders update")
            raise
        finally:
            self._delete_directories()
            settings.set_update_control("updating", False)

    def download_and_validate_loaders(self):
        settings.set_update_control(self.update_name, "$GETTING_NEW_LOADERS")
        last_versions, _ = self.version_repository.get_last_versions()
        if not last_versions:
            return None

        self._download_new_loaders(last_versions=last_versions)

        self.updater_service.create_dir(self.work_path)
        self._extract_new_loaders(os.path.join(self.work_path, self.data_relative_path))

        settings.set_update_control(self.update_name, "$VALIDATING_NEW_LOADERS")
        current_pos_ctrl_dir = os.path.join(self.work_path, self.server_relative_path, 'bundles', 'posctrl')
        changed_pos_ctrl_info = None
        if os.path.exists(current_pos_ctrl_dir):
            changed_pos_ctrl_info = self._could_apply_new_loaders()

        return changed_pos_ctrl_info

    def _update_pos_ctrl_info(self, pos_ctrl_type):
        # type: (Dict) -> None

        for pos_id, value in pos_ctrl_type.items():
            user_control_type = 1 if value.get("userControlType") == "QSR" else 2
            self.pos_repository.update_pos_ctrl_info(
                pos_id=pos_id,
                user_control_type=user_control_type,
                pos_function=value.get("posFunction")
            )

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

        try:
            if os.path.exists(self.genesis_operator_levels_json) and os.path.exists(self.sui_static_path):
                current_operator_levels = os.path.join(self.sui_static_path, 'operatorLevels.json')

                with open(self.genesis_operator_levels_json, 'rb') as f:
                    new_operator_level = json.load(f)

                has_some_configuration = sum([len(x) for x in new_operator_level["levels"].values()]) > 0
                if has_some_configuration:
                    self._create_support_menu_buttons(
                        operator_level=new_operator_level
                    )
                    self.updater_service.save_json(
                        data=new_operator_level,
                        destination_path=current_operator_levels
                    )
                else:
                    self.updater_service.delete_file(
                        path=current_operator_levels
                    )
        except (Exception,) as e:
            logger.info("Error processing operatorLevels.json: {}".format(e))
            raise ErrorUpdatingUserRoles()

    def _update_fiscal_id(
        self,   # type: LoaderUpdaterService
    ):
        # type: (...) -> None

        logger.info("Starting update fiscal id")

        current_fiscal_wrapper_dir = os.path.join(self.work_path, self.server_relative_path, "bundles", "fiscalwrapper")
        boh_fiscal_wrapper_dir = os.path.join(self.server_path, "bundles", "fiscalwrapper")
        if not os.path.exists(boh_fiscal_wrapper_dir) or not os.path.exists(current_fiscal_wrapper_dir):
            logger.error("Fiscal wrapper loaders not found")
            return

        boh_fiscal_info = self._get_fiscal_wrapper_info(fiscal_dir=boh_fiscal_wrapper_dir)
        pos_fiscal_info = self._get_fiscal_wrapper_info(fiscal_dir=current_fiscal_wrapper_dir)
        if not boh_fiscal_info or not pos_fiscal_info:
            logger.error("Fiscal info not found. BOH: {} - POS: {}".format(boh_fiscal_info, pos_fiscal_info))
            return

        if boh_fiscal_info.nf_type == pos_fiscal_info.nf_type:
            logger.info("NF type has not changed")
            return

        fiscal_id = boh_fiscal_info.initial_order_id
        logger.info("New fiscal id: {}".format(fiscal_id))

        self.fiscal_repository.set_fiscal_id(fiscal_id=fiscal_id)


    def send_result_message(
        self,                 # type: LoaderUpdaterService
        current_update_id,    # type: str
    ):
        # type: (...) -> None

        last_versions, versions_dates = self.version_repository.get_last_versions()
        body = self._get_update_notify_body(
            last_versions=last_versions,
            versions_dates=versions_dates,
        )
        success = self.api_repository.send_result_message(
            body=body,
        )
        if success:
            self.pos_repository.update_loader_notified_date(
                update_id=current_update_id,
            )

    def _get_update_notify_body(
        self,               # type: LoaderUpdaterService
        last_versions,      # type: Dict[str, str]
        versions_dates,     # type: Dict[str, str]
    ):
        # type: (...) -> Dict[str, str]

        src_version = last_versions.get(UpdateType.binaries.value).split("|")[1].replace("SRC:", "")
        src_applied_date = versions_dates.get(UpdateType.binaries.value)
        src_applied_date = datetime.strptime(src_applied_date, '%d/%m/%Y %H:%M')
        loader_applied_date = datetime.strptime(versions_dates.get(UpdateType.loader.value), '%d/%m/%Y %H:%M')
        last_loader_update = self.pos_repository.get_last_loader_update()
        server_update_identifier = last_loader_update.server_update_identifier if last_loader_update else None

        return {
            "storeId": self.store_id,
            "srcVersion": src_version,
            "srcAppliedDate": src_applied_date.isoformat(),
            "menuVersion": last_versions.get(UpdateType.catalog.value).split("_")[0],
            "loaderAppliedDate": loader_applied_date.isoformat(),
            "externalUpdateId": server_update_identifier,
        }

    @staticmethod
    def _create_support_menu_buttons(
        operator_level,     # type: Dict
    ):
        # type: (...) -> None

        rows = int(operator_level["rows"])
        support_menu = {
            "function": "SUPPORT_MENU",
            "row": rows,
            "column": 0,
            "color": "#274E13"
        }
        update_users = {
            "function": "UPDATE_USERS",
            "row": rows,
            "column": 1,
            "color": "#274E13"
        }

        operator_level["levels"]["30"] = operator_level["levels"]["10"][:]
        operator_level["levels"]["30"].extend([support_menu, update_users])

    @staticmethod
    def _validate_users(
        model  # type: eTree.XML
    ):
        # type: (...) -> None

        operators = model.find("Operators").findall("Operator")
        for operator in operators:
            status = operator.get("state")
            if status != "LOGGEDOUT":
                logger.exception("can't update loader with operator status: {}".format(status))
                raise ErrorValidatingOpenedUsers("NEED_TO_CLOSE_OPENED_USERS")

    def _could_apply_new_loaders(self):
        # type: (...) -> Dict

        boh_pos_ctrl_dir = os.path.join(self.work_path, self.server_relative_path, 'bundles', 'posctrl')
        current_pos_ctrl_dir = os.path.join(self.server_path, 'bundles', 'posctrl')
        if not os.path.exists(current_pos_ctrl_dir) or not os.path.exists(boh_pos_ctrl_dir):
            return {}

        boh_loaders = self._get_pos_ctrl_info(boh_pos_ctrl_dir)
        pos_loaders = self._get_pos_ctrl_info(current_pos_ctrl_dir)
        table_service_has_opened_day = self._table_service_has_opened_day(pos_loaders)

        order_service = OrderService(
            pos_id=pos_loaders.keys()[0],
            message_bus=self.message_bus,
        )
        pos_instances = self._get_pos_instances(order_service=order_service)
        if len(pos_instances) > len(boh_loaders.keys()):
            difference = len(pos_instances) - len(boh_loaders.keys())
            pos_ids_to_delete = pos_instances[::-1][:difference]
            self._validate_orders_db(
                pos_ids=pos_ids_to_delete,
                order_service=order_service,
            )
            self._validate_pos_ids_to_delete(pos_ids_to_delete=pos_ids_to_delete)

        changed_pos_ctrl_info = {}
        for pos_id, boh_pos_ctrl_info in boh_loaders.items():
            if pos_id == 0:
                continue

            need_validate_pos = self._need_validate_pos(
                pos_id=pos_id,
                pos_loaders=pos_loaders,
                boh_pos_ctrl_info=boh_pos_ctrl_info,
                table_service_has_opened_day=table_service_has_opened_day,
                changed_pos_ctrl_info=changed_pos_ctrl_info
            )
            if not need_validate_pos:
                continue

            model = self.pos_repository.get_model(pos_id=pos_id)
            self._validate_users(model=model)
            if self._day_is_opened_in_pos(model=model):
                logger.exception("Trying to update loader with day not closed")
                raise ErrorDayIsOpen()

        return changed_pos_ctrl_info

    @staticmethod
    def _get_pos_instances(
        order_service,    # type: OrderService
    ):
        # type: (...) -> List[int]

        order_config = order_service.get_order_config()

        return range(order_config["instances"])

    def _validate_pos_ids_to_delete(
        self,                 # type: LoaderUpdaterService
        pos_ids_to_delete,    # type: List[int]
    ):
        # type: (...) -> None

        for pos_id in pos_ids_to_delete:
            if pos_id in pos_ids_to_delete:
                model = self.pos_repository.get_model(pos_id=pos_id)
                self._validate_users(model=model)
                if self._day_is_opened_in_pos(model=model):
                    logger.exception("Error, trying to update loader with day not closed")
                    raise ErrorDayIsOpen()

    @staticmethod
    def _validate_orders_db(
        pos_ids,          # type: List[int]
        order_service,    # type: OrderService
    ):
        # type: (...) -> None

        for pos_id in pos_ids:
            orders_by_pos_id = order_service.list_orders(
                pos_id=pos_id,
                limit=1,
            )
            if len(orders_by_pos_id) > 0:
                raise CannotDeletePdvWithOrders()

    def _need_validate_pos(
        self,
        pos_id,
        pos_loaders,
        boh_pos_ctrl_info,
        table_service_has_opened_day,
        changed_pos_ctrl_info,
    ):

        if pos_id == 0:
            return False

        is_a_new_pos = pos_id not in pos_loaders
        if is_a_new_pos:
            self._validate_new_pos(
                boh_pos_ctrl_info=boh_pos_ctrl_info,
                table_service_has_opened_day=table_service_has_opened_day,
            )
            return False

        pos_pos_ctrl_info = pos_loaders[pos_id]
        same_control_type = boh_pos_ctrl_info.user_control_type == pos_pos_ctrl_info.user_control_type
        same_pos_function = boh_pos_ctrl_info.pos_function == pos_pos_ctrl_info.pos_function
        if not same_control_type or not same_pos_function:
            changed_pos_ctrl_info[pos_id] = {
                "userControlType": boh_pos_ctrl_info.user_control_type,
                "posFunction": boh_pos_ctrl_info.pos_function,
            }
            logger.info("POS{}: has a different user_control_type {} -> {}".format(
                pos_id,
                pos_pos_ctrl_info.user_control_type,
                boh_pos_ctrl_info.user_control_type
            ))

        same_pod_type = boh_pos_ctrl_info.pod_type == pos_pos_ctrl_info.pod_type
        if not same_pod_type:
            logger.info("POS{}: has a different pod_type {} -> {}".format(
                pos_id,
                pos_pos_ctrl_info.pod_type,
                boh_pos_ctrl_info.pod_type
            ))

        return not (same_control_type and same_pod_type)

    @staticmethod
    def _validate_new_pos(
        boh_pos_ctrl_info,                  # type: PosCtrlInfo
        table_service_has_opened_day,       # type: bool
    ):
        # type: (...) -> None

        if boh_pos_ctrl_info.user_control_type == "QSR":
            return

        if not table_service_has_opened_day:
            return

        logger.exception("New TS POS being added with TS day opened")
        raise ErrorDayIsOpen()

    def _table_service_has_opened_day(
        self,               # type: LoaderUpdaterService
        pos_loaders,        # type: Dict
    ):
        # type: (...) -> bool

        for pos_id, pos_ctrl_info in pos_loaders.items():
            if pos_ctrl_info.user_control_type != "TS":
                continue

            model = self.pos_repository.get_model(pos_id=pos_id)
            return self._day_is_opened_in_pos(model=model)

        return False

    @staticmethod
    def _get_pos_ctrl_info(
        pos_ctrl_dir,       # type: str
    ):
        # type: (...) -> Dict

        loaders = {}
        loaders_paths = os.listdir(pos_ctrl_dir) or []
        loaders_paths = [x for x in loaders_paths if ".cfg" in x]
        for path in loaders_paths:
            xml_loader = eTree.parse(os.path.join(pos_ctrl_dir, path))

            user_control_type = xml_loader.find(".//*[@name='{}']/string".format("userControlType"))
            user_control_type = user_control_type.text if user_control_type is not None else None

            pod_type = xml_loader.find(".//*[@name='{}']/string".format("pod"))
            pod_type = pod_type.text if pod_type is not None else None

            pos_function = xml_loader.find(".//*[@name='{}']/string".format("posFunction"))
            pos_function = pos_function.text if pos_function is not None else None

            pos_id = int(path.split("loader_")[1].split(".")[0])
            loaders[pos_id] = PosCtrlInfo(
                pos_id=pos_id,
                user_control_type=user_control_type,
                pod_type=pod_type,
                pos_function=pos_function,
            )

        return loaders

    @staticmethod
    def _get_fiscal_wrapper_info(
        fiscal_dir,     # type: str
    ):
        # type: (...) -> Optional[FiscalInfo]

        loaders_paths = os.listdir(fiscal_dir) or []
        loaders_paths = [x for x in loaders_paths if ".cfg" in x]
        find_field_name = ".//*[@name='{}']/string"
        for path in loaders_paths:
            xml_loader = eTree.parse(os.path.join(fiscal_dir, path))
            initial_state = xml_loader.find(find_field_name.format("InitialState"))
            initial_state = initial_state.text if initial_state is not None else None
            if initial_state != "automatic":
                continue

            nf_type = xml_loader.find(find_field_name.format("NFType"))
            nf_type = nf_type.text if nf_type is not None else None

            initial_order_id = xml_loader.find(find_field_name.format("initial_order_id"))
            initial_order_id = initial_order_id.text if initial_order_id is not None else 0

            return FiscalInfo(
                nf_type=nf_type,
                initial_order_id=int(initial_order_id),
            )

        return None

    def _delete_directories(self):
        self.updater_service.delete_directory(self.work_path)

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

        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)
            logger.info(message)
            raise NotInsideUpdateWindow()

    def _download_new_loaders(
        self,               # type: LoaderUpdaterService
        last_versions,      # type: Dict[int, str]
    ):
        # type: (...) -> None

        try:
            logger.info("Downloading new loaders")
            file_path = os.path.join(self.downloads_path, self.loaders_zip)
            self.updater_service.create_dir(path=self.downloads_path)
            self.api_repository.download_loaders(
                file_path=file_path,
                last_versions=last_versions
            )
            logger.info("Loaders successfully downloaded")
        except InvalidBOHConfigurations:
            logger.exception("Invalid BOH configuration")
            raise
        except (Exception,) as ex:
            logger.exception("Error downloading loaders")
            raise ErrorDownloadingLoaders(ex.message)

    def _create_backup(self):
        # type: () -> str

        try:
            logger.info("Creating loaders backup")

            backup_zip_name = "loaders_{}".format(datetime.now().strftime("%Y%m%d_%H%M%S"))
            backup_package = os.path.join(self.backups_path, backup_zip_name)
            backup_package_zip = os.path.join(self.backups_path, backup_zip_name) + ".zip"

            zip_path = os.path.join(self.work_path, self.data_relative_path)
            self.updater_service.copy_directory(self.data_path, zip_path)
            self.updater_service.save_directory_zip(zip_path, backup_package)
            self.updater_service.test_zip_file(backup_package_zip)
            self._clean_backups()

            logger.info("Loaders backup successfully created!")
        except (Exception,):
            logger.exception("Error creating loaders backup")
            raise ErrorCreatingBackup()

        return backup_package_zip

    def _clean_backups(
        self,   # type: LoaderUpdaterService
    ):
        # type: (...) -> None

        list_of_files = os.listdir(self.backups_path)
        if len(list_of_files) > 5:
            full_path_files = [os.path.join(self.backups_path, x) for x in list_of_files]
            full_path_files = sorted(full_path_files, key=lambda p: os.path.getmtime(p))
            files_to_delete = full_path_files[:-5]
            for backup_file in files_to_delete:
                try:
                    CommonService.delete_file_or_directory(path=backup_file)
                except (Exception,) as e:
                    logger.exception("Failed to remove backup file {}. Error {}".format(backup_file, e))

    def _delete_content(self, path):
        # type: (str) -> None

        try:
            logger.info("Deleting path {}".format(path))

            self.updater_service.delete_directory(path)

            logger.info("Path deleted")
        except (Exception,):
            logger.exception("Error deleting path")
            raise ErrorDeletingContent()

    def _extract_new_loaders(self, path):
        # type: (str) -> None

        try:
            logger.info("Extracting loaders in data path")

            self.updater_service.create_dir(path)
            zip_path = os.path.join(self.downloads_path, self.loaders_zip)
            self.updater_service.extract_zip(zip_path, path)

            logger.info("Loaders extracted in data path")
        except (Exception,):
            logger.exception("Error extracting loaders to data path")
            raise ErrorExtractingLoaders()

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

        current_folder = None
        try:
            work_path_server_folder = os.path.join(self.work_path, self.server_relative_path)
            if not os.path.exists(work_path_server_folder):
                return

            for current_folder in os.listdir(work_path_server_folder):
                if current_folder == "bundles":
                    continue

                logger.info("Recovering {} folder".format(current_folder))
                folder_path = os.path.join(self.server_path, current_folder)
                work_path_path = os.path.join(work_path_server_folder, current_folder)
                if not os.path.isdir(work_path_path):
                    continue

                self.updater_service.copy_directory(work_path_path, folder_path)
                logger.info("{} folder recovered".format(current_folder))
        except (Exception,):
            logger.exception("Error recovering {}".format(current_folder))
            raise ErrorRecoveringFolder()

    def _rollback(self, backup_package_zip):
        # type: (str) -> None

        logger.info("Starting rollback")

        retries = 0
        max_retries = 2

        while True:
            try:
                self._delete_content(self.data_path)
                self.updater_service.extract_zip(backup_package_zip, self.data_path)
            except (Exception,):
                logger.exception("Error while executing rollback")
                if retries == max_retries:
                    raise RollbackError()

                retries += 1
                continue

            logger.info("Rollback finished")
            break

    def _update_ui_customizations(
        self,
    ):
        # (...) -> None

        try:
            customization_kui_dir = os.path.join(
                self.server_path,
                "bundles",
                "customization",
                "kui",
            )
            if not os.path.isdir(customization_kui_dir):
                return

            self.updater_service.create_dir(
                path=self.htdocs_kui_path,
                delete_if_exists=False,
            )

            os_walk = os.walk(customization_kui_dir)
            for step_index, (base_dir, dirs, files) in enumerate(os_walk):
                for dir_name in dirs:
                    file_path = os.path.join(self.htdocs_kui_path, dir_name)
                    self.updater_service.create_dir(
                        path=file_path,
                        delete_if_exists=False,
                    )

                last_path_component = os.path.split(base_dir)[1]
                use_last_path_component = step_index != 0
                for file_name in files:
                    src_file = os.path.join(
                        base_dir,
                        file_name,
                    )
                    if use_last_path_component:
                        dst_file = os.path.join(
                            self.htdocs_kui_path,
                            last_path_component,
                            file_name,
                        )
                    else:
                        dst_file = os.path.join(
                            self.htdocs_kui_path,
                            file_name,
                        )
                    shutil.copy2(
                        src_file,
                        dst_file,
                    )
        except (Exception,):
            logger.exception("Error updating customization files")

    @staticmethod
    def _day_is_opened_in_pos(
        model   # type: eTree.XML
    ):
        # type: (...) -> bool

        pos_state = model.find("PosState").get("state")
        return pos_state not in ("CLOSED", "UNDEFINED")


class PosCtrlInfo(object):
    def __init__(
        self,
        pos_id,
        user_control_type,
        pod_type,
        pos_function,
    ):
        # type: (...) -> None

        self.pod_type = pod_type
        self.pos_id = pos_id
        self.user_control_type = user_control_type
        self.pos_function = pos_function
