# /*
#  * Copyright (C) 2020, KylinSoft Co., Ltd.
#  *
#  * This program is free software: you can redistribute it and/or modify
#  * it under the terms of the GNU General Public License as published by
#  * the Free Software Foundation, either version 3 of the License, or
#  * (at your option) any later version.
#  *
#  * This program is distributed in the hope that it will be useful,
#  * but WITHOUT ANY WARRANTY; without even the implied warranty of
#  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  * GNU General Public License for more details.
#  *
#  * You should have received a copy of the GNU General Public License
#  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
#  */

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
from SystemUpdater.modules.i18n import _
from multiprocessing import Process

from SystemUpdater.packageManager.pmUtils import *
from SystemUpdater.Core.UpdaterConfigParser import UpgradeConfig
from SystemUpdater.packageManager.UpdaterSDKInterface import UpdaterSDKPkgInstall

class TransactionManager:
    TRANSTYPE_NONE          = "TRANSTYPE_NONE"
    TRANSTYPE_PURGE         = "TRANSTYPE_PURGE"
    TRANSTYPE_INSTALL       = "TRANSTYPE_INSTALL"
    TRANSTYPE_BATCH_INSTALL = "TRANSTYPE_BATCH_INSTALL"
    TRANSTYPE_TAG_ONLINEINS = "TRANSTYPE_TAG_ONLINEINS"
    TRANSTYPE_TAG_OFFLINEINS= "TRANSTYPE_TAG_OFFLINEINS"

    TRANSTIMEOUT = {
        TRANSTYPE_NONE          : 1,
        TRANSTYPE_PURGE         : 20 * 60,
        TRANSTYPE_INSTALL       : 60 * 60 * 2,
        TRANSTYPE_BATCH_INSTALL : 60 * 60 * 2,
        TRANSTYPE_TAG_ONLINEINS : 60 * 60 * 2,
        TRANSTYPE_TAG_OFFLINEINS: 60 * 60 * 2
    }

    def __init__(self, updateDaemon):
        self.updateDaemon   = updateDaemon
        self.bus            = dbus.SystemBus()
        self.isRuning       = False
        self.transProcess   = None

        self.transInfo      = {
            "package"   : "",
            "type"      : self.TRANSTYPE_NONE,
            "timeout"   : self.TRANSTIMEOUT[self.TRANSTYPE_NONE]
        }

    def transaction_start(self):
        pass

    def transaction_timeout_checking(self):
        logging.info("Start transaction timedout, type: %s, package: %s", self.transInfo["type"], self.transInfo["package"])

        def _transaction_timeout():
            if self.transInfo["timeout"] % 60 == 0:
                logging.info("Checking for transaction timeout(%d), type: %s, package: %s", 
                                self.transInfo["timeout"],
                                self.transInfo["type"],
                                self.transInfo["package"])

            if not self.transProcess.is_alive():
                self.transProcess.join()
                logging.warning("Transaction finished, type: %s, package: %s, timeout check quit", 
                                self.transInfo["type"], self.transInfo["package"])
                self.transaction_finished()
                return False

            if (self.transInfo["timeout"] <= 0):
                logging.warning("Transaction timeout, type: %s, package: %s", 
                                self.transInfo["type"], self.transInfo["package"])
                self.transaction_finished()
                return False
            else:
                self.transInfo["timeout"] = self.transInfo["timeout"] - 1

            return True

        from gi.repository import GLib
        GLib.timeout_add_seconds(1, _transaction_timeout)

    def transaction_finished(self,):
        self.isRuning = False

        if self.transProcess:
            self.transProcess.terminate()
            self.transProcess.join()

        if self.transInfo["type"] in [self.TRANSTYPE_INSTALL,self.TRANSTYPE_BATCH_INSTALL,self.TRANSTYPE_TAG_ONLINEINS,self.TRANSTYPE_TAG_OFFLINEINS]:
            self.updateDaemon.dbus_send.InstalldebFinished (False, '', 'Installation timeout')
            self.updateDaemon.dbus_send.InstalldebFinishedWithErrCode (False, '', 'Installation timeout', '')
            self.updateDaemon.dbus_send.InstalldebFinishedWithPkgname (False, '', 'Installation timeout', '', str(self.transInfo["package"]))

        logging.info("Transaction '%s', finished", self.transInfo["package"])
        self.transInfo      = {
            "package"   : "",
            "type"      : self.TRANSTYPE_NONE,
            "timeout"   : self.TRANSTIMEOUT[self.TRANSTYPE_NONE]
        }


class PmWorker(TransactionManager):
    def __init__(self, parent = None):
        super().__init__(parent)

        self.parent             = parent
        self.dbus_send          = parent.dbus_send
        self.sqlite3_server     = parent.sqlite3_server
        self.updater_package    = UpdaterSDKPkgInstall ( parent )

        if not hasattr(parent, 'configs_uncover') or parent.configs_uncover is None:
            self.configs_uncover    = UpgradeConfig (
                                        defaults_conf = "/etc/kylin-system-updater/system-updater-defaults.conf" )
        else:
            self.configs_uncover    = parent.configs_uncover

    def sdk_package_manage(self):
        _success = True

        if self.transInfo["type"] == self.TRANSTYPE_INSTALL:
            for pl in self.transInfo["package"]:
                _success = self.updater_package.package_interface_install(pl)
                if not _success:
                    break

            if self.updater_package.installFinished:
                self.updater_package.installFinished = False
            else:
                self.dbus_send.InstalldebFinished(_success, '', '')
                self.dbus_send.InstalldebFinishedWithErrCode(_success, '', '', '')
                self.dbus_send.InstalldebFinishedWithPkgname(_success, '', '', '', self.updater_package.installPkgName)

        elif self.transInfo["type"] == self.TRANSTYPE_BATCH_INSTALL:
            _success = self.updater_package.package_interface_batch_install(self.transInfo["package"])
            if self.updater_package.batchInstFinished:
                self.updater_package.batchInstFinished = False
            else:
                self.dbus_send.UpdateInstallFinished(_success, self.transInfo["package"], '', '')
                self.updater_package.package_interface_install_clear()

        elif self.transInfo["type"] == self.TRANSTYPE_TAG_ONLINEINS:
            for pl in self.transInfo["package"]:
                _name = pl
                _version = ''
                _type = self.transInfo["kare_type"]
                _compat = self.transInfo["_compat"]

                if "=" in pl:
                    _name, _version = pl.split("=")
                else:
                    _version = None

                _success = self.updater_package.package_interface_tag_online_install(_name, _version, _type, _compat)
                if not _success:
                    break

            if self.updater_package.installFinished:
                self.updater_package.installFinished = False
            else:
                self.dbus_send.InstalldebFinished(_success, '', '')
                self.dbus_send.InstalldebFinishedWithErrCode(_success, '', '', '')
                self.dbus_send.InstalldebFinishedWithPkgname(_success, '', '', '', self.updater_package.installPkgName)

        elif self.transInfo["type"] == self.TRANSTYPE_TAG_OFFLINEINS:
            for pl in self.transInfo["package"]:
                _name = pl
                _type = self.transInfo["kare_type"]
                _compat = self.transInfo["_compat"]

                _success = self.updater_package.package_interface_tag_offline_install(_name, _type, _compat)
                if not _success:
                    break

            if self.updater_package.installFinished:
                self.updater_package.installFinished = False
            else:
                self.dbus_send.InstalldebFinished(_success, '', '')
                self.dbus_send.InstalldebFinishedWithErrCode(_success, '', '', '')
                self.dbus_send.InstalldebFinishedWithPkgname(_success, '', '', '', self.updater_package.installPkgName)

        elif self.transInfo["type"] == self.TRANSTYPE_PURGE:
            for pl in self.transInfo["package"]:
                _success = self.updater_package.package_interface_remove(pl)
                if not _success:
                    break

            if self.updater_package.purgeFinished:
                self.updater_package.purgeFinished = False
            else:
                self.dbus_send.PurgePackagesFinished (_success, self.updater_package.purgeLoginfo, '')
                self.dbus_send.PurgePackagesFinishedWithPkgname (_success, self.updater_package.purgeLoginfo, '', ",".join(self.transInfo["package"]))
                self.dbus_send.PurgePackagesFinishedWithErrCode (_success, self.updater_package.purgeLoginfo, '', ",".join(self.transInfo["package"]), '')

    def transaction_start(self, package = [], kare_type = '', kare_compat = '', trans_type = None):
        if self.isRuning:
            logging.warning("PmWorker transaction is running, please wait...")
            return

        logging.info("Transaction %s running: '%s', timeout: %s s",
                     trans_type, package, self.TRANSTIMEOUT[trans_type])

        try:
            self.isRuning               = True
            self.transInfo["package"]   = package
            self.transInfo["type"]      = trans_type
            self.transInfo["kare_type"] = kare_type
            self.transInfo["_compat"]   = kare_compat
            self.transInfo["timeout"]   = self.TRANSTIMEOUT[trans_type]
            self.transProcess           = Process(target=self.sdk_package_manage)

            self.transaction_timeout_checking()
            self.transProcess.start()

        except Exception as e:
            logging.error("transaction_start err: %s", e)

    def start_deb_install(self, deb_uri = "", _check_local_dep = False, _auto_satisfy = False, source = '', sender=None):
        sender_name = get_proc_from_dbus_name(sender)
        caller_trans = get_source_name_from_enum(sender_name)

        try:
            def _run_install_handler():
                self.transaction_start([str(deb_uri)], trans_type = self.TRANSTYPE_INSTALL)

            def on_authorization_success(result):
                logging.info("Authorization success ...")

                self.sqlite3_server.deb_policy_keep = True
                def _check_deb_policy():
                    if self.sqlite3_server.deb_policy_timestamp % 10 == 0:
                        logging.info("Checking for deb policy timeout(%d)...",self.sqlite3_server.deb_policy_timestamp)
                    if (self.sqlite3_server.deb_policy_timestamp <= 0):
                        logging.warning("Deb policy timeout")
                        self.sqlite3_server.deb_policy_keep = False
                        return False
                    else:
                        self.sqlite3_server.deb_policy_timestamp = self.sqlite3_server.deb_policy_timestamp - 1
                    return True

                from gi.repository import GLib
                self.sqlite3_server.deb_policy_timestamp = 60 * 5
                GLib.timeout_add_seconds(1,_check_deb_policy)

                _run_install_handler()

            def on_authorization_failure(error):
                try:
                    logging.info("Authorization failed ...")

                    self.sqlite3_server.deb_policy_keep = False
                    self.sqlite3_server.deb_policy_timestamp = 0

                    error.raise_exception()
                except Exception as error:
                    logging.error(error)
                    self.dbus_send.InstalldebFinished(False,_("Cancel authentication."),'')
                    self.dbus_send.InstalldebFinishedWithErrCode(False,_("Cancel authentication."),'','')
                    self.dbus_send.InstalldebFinishedWithPkgname(False,_("Cancel authentication."),'','',self.installPkgName)

            def _policykit_authority_handler(install_handler = None, authority_failed_handler = None):
                if not self.sqlite3_server.deb_policy_keep:
                    deferred = PolicyKit_Authority(caller_trans+_(" requires authentication to install software packages."),
                                                sender = sender,
                                                authentication = self.configs_uncover.getWithDefault(
                                                                                        "InstallAndPurge",
                                                                                        "install_authority", False),
                                                source=source)

                    deferred.add_callback(on_authorization_success)
                    deferred.add_errback(on_authorization_failure)
                else:
                    self.sqlite3_server.deb_policy_timestamp = 60 * 5
                    logging.info("Deb policy keep, ignore...")

                    _run_install_handler()

            _run_install_handler()

        except Exception as e:
            logging.info("start_deb_install error: %s", e)
            self.dbus_send.InstalldebFinished(False, str(e), '')
            self.dbus_send.InstalldebFinishedWithErrCode(False, str(e), '', '')
            self.dbus_send.InstalldebFinishedWithPkgname(False, str(e), '', '', self.installPkgName)

    def start_tag_online_install(self, _name, _type, _compat, sender=None):
        try:
            self.transaction_start([str(_name)], _type, _compat, self.TRANSTYPE_TAG_ONLINEINS)
        except Exception as e:
            logging.error("start_tag_online_install err: %s", e)

    def start_tag_offline_install(self, _name, _type, _compat, sender=None):
        try:
            self.transaction_start([str(_name)], _type, _compat, self.TRANSTYPE_TAG_OFFLINEINS)
        except Exception as e:
            logging.error("start_tag_offline_install err: %s", e)

    def start_purge_pkgs(self,pkgs_list,sender=None):
        try:
            sender_name = get_proc_from_dbus_name(sender)

            def _run_purge_handler():
                self.transaction_start(pkgs_list, trans_type = self.TRANSTYPE_PURGE)

            def on_authorization_success(result):
                logging.info("Authorization success ...")

                self.sqlite3_server.purge_policy_keep = True
                def _check_purge_policy():
                    if self.sqlite3_server.purge_policy_timestamp % 10 == 0:
                        logging.info("Checking for deb policy timeout(%d)...",self.sqlite3_server.purge_policy_timestamp)
                    if (self.sqlite3_server.purge_policy_timestamp <= 0):
                        logging.warning("Deb policy timeout")
                        self.sqlite3_server.purge_policy_keep = False
                        return False
                    else:
                        self.sqlite3_server.purge_policy_timestamp = self.sqlite3_server.purge_policy_timestamp - 1
                    return True

                from gi.repository import GLib
                self.sqlite3_server.purge_policy_timestamp = 60 * 5
                GLib.timeout_add_seconds(1,_check_purge_policy)

                _run_purge_handler()

            def on_authorization_failure(error):
                """Convert the DeferredException to a normal exception."""
                try:
                    logging.info("Authorization failed ...")

                    self.sqlite3_server.purge_policy_keep = False
                    self.sqlite3_server.purge_policy_timestamp = 0

                    error.raise_exception()
                except Exception as error:
                    logging.error(error)
                    self.dbus_send.PurgePackagesFinished(False,_("Cancel authentication."),'')
                    self.dbus_send.PurgePackagesFinishedWithPkgname(False,_("Cancel authentication."),'',",".join(pkgs_list))
                    self.dbus_send.PurgePackagesFinishedWithErrCode(False,_("Cancel authentication."),'',",".join(pkgs_list),get_error_num_from_enum(ERROR_PURGE_PKG_VERIFY))

            def _policykit_authority_handler(install_handler = None, authority_failed_handler = None):
                if not self.sqlite3_server.purge_policy_keep:
                    deferred = PolicyKit_Authority(
                                    get_source_name_from_enum(sender_name)+_(" requires authentication to uninstall software packages."),
                                    sender = sender,
                                    authentication = self.configs_uncover.getWithDefault("InstallAndPurge","purge_authority",False), 
                                    source=sender_name)

                    deferred.add_callback(on_authorization_success)
                    deferred.add_errback(on_authorization_failure)
                else:
                    self.sqlite3_server.purge_policy_timestamp = 60 * 5
                    logging.info("Purge policy keep, ignore...")

                    _run_purge_handler()

            _policykit_authority_handler()

        except Exception as e:
            logging.error("start_purge_pkgs error: %s", e)

    def start_back_upgrade(self, pkgs_list, sender=None):
        try:
            batchInstPkgs = [str(item).strip() for item in pkgs_list if item and str(item).strip()]

            self.transaction_start(batchInstPkgs, trans_type = self.TRANSTYPE_BATCH_INSTALL)
        except Exception as e:
            logging.error("start_back_upgrade err: %s", e)

    def check_package_status(self, _name, _version = '', sender = None):
        install_status = -100

        try:
            if not _version:
                _version = 'NULL'

            install_status = self.updater_package.package_interface_check_status(_name, _version)

        except Exception as e:
            logging.error("check_package_status err: %s", e)

        return install_status

    def get_package_version(self, _name, sender = None):
        install_version = ""

        try:
            install_version = self.updater_package.package_interface_get_version(_name)

        except Exception as e:
            logging.error("get_package_version err: %s", e)

        return install_version

    def is_package_online_installable(self, _name, _version = '', sender = None):
        _is_online_installable = False

        try:
            _is_online_installable = self.updater_package.package_interface_is_online_installable(_name, _version)

        except Exception as e:
            logging.error("is_package_online_installable err: %s", e)

        return _is_online_installable
