import os
import shutil
import zipfile
import time
import sqlite3

from threading import Lock
from systools import sys_parsefilepath
from typing import List

from application.model import UpdateData, UpdateType, UpdateStep, UpdateStatus
from application.model.configuration import Configurations
from application.model.customexception import (
    DisabledFunctionality,
    DataBasesNotFound,
    ErrorApplyingUpdate,
    ErrorCheckingGenesisDir,
    ErrorCheckingPackageIntegrity,
    ErrorInsertingNewUpdateVersion,
    ErrorIntegrityDatabase,
    ErrorRetrievingPendingUpdate,
    NoCatalogDirectoryFoundException,
    NoUpdateFound,
    NotInsideUpdateWindow,
)
from application.repository.api import CatalogUpdaterAPIRepository
from application.repository.pos import CatalogUpdaterPOSRepository
from application.service import UpdaterService, StaticConfigurationService
from application.util import LoggerUtil, settings

logger = LoggerUtil.get_logger_name(__name__)
GENESIS_PATH = os.path.join("..", "..", "genesis")
HT_DOCS_POS = "htdocs/pos"


class CatalogUpdaterInteractor(object):

    def __init__(self, configs, working_on_update, api_repository, pos_repository, updater_service, enabled):
        # type: (Configurations, Lock, CatalogUpdaterAPIRepository, CatalogUpdaterPOSRepository, UpdaterService, bool) -> None # noqa

        self.working_on_update = working_on_update
        self.update_type = UpdateType.catalog
        self.update_name = self.update_type.name
        self.api_repository = api_repository
        self.pos_repository = pos_repository
        self.updater_service = updater_service
        self.enabled = enabled

        data_path = sys_parsefilepath("{HVDATADIR}")
        work_dir = sys_parsefilepath(configs.backup_directory)

        self.databases_path = os.path.join(data_path, "databases")
        self.bundles_path = os.path.join(data_path, "bundles")
        self.htdocs_pos_path = os.path.join(data_path, HT_DOCS_POS)
        self.genesis_databases_path = os.path.join(data_path, GENESIS_PATH, "data/server/databases")
        self.genesis_campaign_databases_path = os.path.join(data_path, GENESIS_PATH, "data/server/databases-campaign")
        self.genesis_htdocs_pos_path = os.path.join(data_path, GENESIS_PATH, "data/server/htdocs/pos")
        self.catalog_json = os.path.join(self.genesis_campaign_databases_path, "catalog.json")

        default_work_path = os.path.join(work_dir, self.update_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.updater_service.create_dir(self.backups_path, False)
        self.updater_service.create_dir(self.downloads_path, False)

    def apply_update(
        self,               # type: CatalogUpdaterInteractor
        force=False,        # type: bool
    ):
        # 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()

        try:
            settings.set_update_control(self.update_name, "$STARTING_CATALOG_UPDATE")
            settings.set_update_control("updating", True)
            update_data = self.pos_repository.has_catalog_pending_update_to_apply()
            if not force and not update_data:
                return

            logger.info("Waiting Updating Catalog")
            with self.working_on_update:
                logger.info("Applying update")
                settings.set_update_control(self.update_name, "$GETTING_MENU_FROM_BACKOFFICE")
                self._create_backup(update_data)
                self._apply_update(update_data)
                self._copy_upsell_part_codes_json()

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

                StaticConfigurationService.update_static_databases()
                StaticConfigurationService.run_databases_script()
                logger.info("Update [{}] fully concluded with success!".format(update_data.update_id))

                logger.info("Skipping pending updates")
                self._skip_pending_updates()

                logger.info("Restarting Hypervisor to apply new Menu")
                settings.set_update_control(self.update_name, "$SUCCESS_UPDATE")
                self.pos_repository.common_pos_repository.restart_hv(
                    event_type="POS_UPDATER_CATALOG",
                )
                time.sleep(5)
        except NoUpdateFound:
            logger.info("No update founded to apply")
            raise
        except NotInsideUpdateWindow:
            logger.info("Sleeping until enter the update window")
            raise
        except ErrorRetrievingPendingUpdate:
            logger.exception("Error retrieving pending update")
            raise
        except ErrorCheckingGenesisDir:
            logger.error("Cannot find the server genesis databases directory. Please, verify and try again")
            raise
        except ErrorInsertingNewUpdateVersion:
            logger.exception("Error inserting new update")
            raise
        except ErrorIntegrityDatabase:
            settings.set_update_control(self.update_name, "$ERROR_INTEGRITY_DATABASE")
            raise
        except (Exception,):
            logger.exception("Not expected exception on catalog update")
            settings.set_update_control(self.update_name, "$UNKNOWN_ERROR")
            raise
        finally:
            settings.set_update_control("updating", False)

    def _apply_update(self, update_data):
        # type: (UpdateData) -> None
        try:
            if update_data.applied_date:
                return

            logger.info("Applying: get menus from catalog...")
            database_version = self.pos_repository.get_last_downloaded_version(UpdateType.catalog)
            database_version_name = database_version["updateName"]
            database_id = self.updater_service.get_database_id(database_version_name)

            self._copy_all_databases(database_id)
            self.pos_repository.update_step(update_data.update_id, UpdateStep.apply, UpdateType.catalog)
            logger.info("Menus successfully copied!")
        except ErrorIntegrityDatabase as error:
            self._restore_backup(update_data)
            raise error
        except Exception as ex:
            logger.exception("Error applying get menus from catalog")
            self._restore_backup(update_data)
            raise ErrorApplyingUpdate(ex)

    def _restore_backup(self, update_data):
        # type: (UpdateData) -> None

        logger.info("Restoring backup...")

        self.pos_repository.update_status(update_data.update_id, UpdateStatus.error)
        package_name = self.updater_service.get_package_name(update_data)
        backup_package = os.path.join(self.backups_path, package_name)

        self._perform_update(backup_package)

        logger.info("Backup successfully restored!")

    def _skip_pending_updates(self, type_id=None):
        # type: (UpdateType) -> None

        logger.info("Skipping old pending updates...")
        update_value = self.update_type.value if type_id is None else type_id
        self.pos_repository.skip_pending_updates(str(update_value))

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

        if os.path.exists(self.work_path):
            self._delete_work_dir()

        os.mkdir(self.work_path)

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

        shutil.rmtree(self.work_path)

    def _get_files_to_backup(self):
        # type: () -> List[str]

        self.updater_service.create_dir(self.genesis_databases_path, False)
        self.updater_service.create_dir(self.genesis_htdocs_pos_path, False)

        databases_list = os.listdir(os.path.join(self.genesis_databases_path))
        htdocs_list = os.listdir(os.path.join(self.genesis_htdocs_pos_path))

        return databases_list + htdocs_list

    def _copy_files_to_backup(self, files_to_backup):
        # type: (List[str]) -> None

        logger.info("Coping backup files to work dir...")
        for file_to_backup in files_to_backup:
            if file_to_backup.endswith(".db") or file_to_backup.endswith(".json"):
                file_path = os.path.join(self.genesis_databases_path, file_to_backup)
                databases_pos_path = os.path.join(self.work_path, "databases")
                self.updater_service.create_dir(databases_pos_path, False)
                shutil.copy(file_path, databases_pos_path)
            elif file_to_backup.endswith(".xml"):
                file_path = os.path.join(self.htdocs_pos_path, file_to_backup)
                if os.path.exists(file_path):
                    work_htdocs_pos_path = os.path.join(self.work_path, HT_DOCS_POS)
                    self.updater_service.create_dir(work_htdocs_pos_path, False)
                    shutil.copy(file_path, work_htdocs_pos_path)
            else:
                logger.error("Unknown file extension on backup: {}".format(file_to_backup))

        logger.info("Backup files successfully copied!")

    def _write_backup_zip(self, backup_package, files_to_backup):
        # type: (str, List[str]) -> None

        logger.info("Zipping backup files...")
        with zipfile.ZipFile(backup_package, "w") as zip_object:
            for file_to_backup in files_to_backup:
                if file_to_backup.endswith(".db") or file_to_backup.endswith(".json"):
                    path_to_store = os.path.join("databases", file_to_backup)
                elif file_to_backup.endswith(".xml"):
                    path_to_store = os.path.join(HT_DOCS_POS, file_to_backup)
                else:
                    logger.error("Unknown file extension on create zip: {}".format(file_to_backup))
                    continue

                file_path = os.path.join(self.work_path, path_to_store)
                if os.path.exists(file_path):
                    zip_object.write(file_path, path_to_store, zipfile.ZIP_DEFLATED)
        logger.info("Backup files successfully zipped!")

    def _perform_update(self, package):
        # type: (str) -> None

        self._create_work_dir()
        self._extract_package_on_work_dir(package)
        self._copy_working_files_to_genesis()
        self._delete_work_dir()

    def _extract_package_on_work_dir(self, package):
        # type: (str) -> None

        logger.info("Extracting package on work dir...")
        with zipfile.ZipFile(package, "r") as zip_object:
            zip_object.extractall(self.work_path)
        logger.info("Package successfully extracted!")

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

        logger.info("Coping files to genesis...")
        files_to_apply = os.listdir(self.work_path)
        for file_to_apply in files_to_apply:
            file_path = os.path.join(self.work_path, file_to_apply)
            if file_path.endswith(".db"):
                shutil.copy(file_path, self.genesis_databases_path)
            elif file_path.endswith(".xml"):
                shutil.copy(file_path, self.genesis_htdocs_pos_path)
            else:
                logger.error("Unknown file extension apply update: {}".format(file_to_apply))

        logger.info("Files copied with success!")

    def _create_backup(self, update_data):
        # type: (UpdateData) -> None

        if update_data.backup_date:
            return

        logger.info("Creating backup...")
        settings.set_update_control(self.update_name, "$CREATING_MENU_BACKUP")

        package_name = self.updater_service.get_package_name(update_data)
        backup_package = os.path.join(self.backups_path, package_name)

        self._create_work_dir()
        files_to_backup = self._get_files_to_backup()
        self._copy_files_to_backup(files_to_backup)
        self._write_backup_zip(backup_package, files_to_backup)
        self.updater_service.test_zip_file(backup_package)
        self._delete_work_dir()

        self.pos_repository.update_step(update_data.update_id, UpdateStep.backup, UpdateType.catalog)

        logger.info("Backup successfully created!")

    def _fix_catalog_directory(self):
        try:
            json_to_download = self.updater_service.retrieve_pending_update()
            self.updater_service.download_databases_campaign(json_to_download, False)
        except ErrorCheckingPackageIntegrity as error:
            raise error
        except (Exception,):
            raise NoCatalogDirectoryFoundException()

    @staticmethod
    def _check_database_integrity(
        db_path,    # type: str
    ):
        # type: (...) -> None

        con = sqlite3.connect(db_path)
        cur = con.cursor()
        try:
            status = cur.execute("PRAGMA integrity_check").fetchone()
            if status[0] != 'ok':
                logger.error("Error of integrity of database: {}. Status: {}".format(db_path, status[0]))
                raise ErrorIntegrityDatabase()
        except Exception as ex:
            logger.error("Error of integrity of database: {}. Exception: {}".format(db_path, ex.message))
            raise ErrorIntegrityDatabase()

    def _copy_all_databases(self, database_id):
        # type: (int) -> None

        logger.info("Applying: start copy databases campaign, database to apply: {}...".format(database_id))

        source_path = "{}/{}".format(self.genesis_campaign_databases_path, database_id)
        if not os.path.isdir(source_path):
            self._fix_catalog_directory()

        src_files = os.listdir(source_path)
        if len(src_files) == 0:
            raise DataBasesNotFound()

        for file_name in src_files:
            full_file_name = os.path.join(source_path, file_name)
            if os.path.isfile(full_file_name):
                shutil.copy(full_file_name, self.genesis_databases_path)

                if file_name.endswith('.db'):
                    db_path = os.path.join(self.genesis_databases_path, file_name)
                    self._check_database_integrity(db_path=db_path)

        logger.info("Applying: copy databases campaign finish...")

    def _copy_upsell_part_codes_json(
        self
    ):
        # type: (...) -> None

        try:
            origin_upsell_part_codes_json = os.path.join(self.genesis_databases_path, "upSellPartCodes.json")
            if not os.path.exists(origin_upsell_part_codes_json):
                logger.info("The file {} was not found to update".format(origin_upsell_part_codes_json))
                return

            internal_order_dir_exists = os.path.exists(
                os.path.join(
                    self.bundles_path,
                    "internalorder",
                )
            )
            if internal_order_dir_exists:
                destination_upsell_part_codes_json = os.path.join(
                    self.bundles_path,
                    "internalorder",
                    "upSellPartCodes.json"
                )
                self.updater_service.copy_file(
                    original_file_path=origin_upsell_part_codes_json,
                    destination_file_path=destination_upsell_part_codes_json,
                )
        except (Exception,):
            logger.exception("Error coping upSellPartCodes.json")
