# /*
#  * 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/>.
#  */

# UpdateManager.py
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
import os
import json
import dbus,time,sys
import traceback
import logging
import apt_pkg
apt_pkg.init_config()
apt_pkg.init_system()
from .Core.errors import *
from .Core.enums import *
from gi.repository import GObject
from .Core.loop import mainloop
from SystemUpdater.modules.i18n import _
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
from .Core.Database import Sqlite3Server
from gi.repository import GLib
from SystemUpdater.UpdatesAvailable import Transaction
from SystemUpdater.modules.system import get_dist,system_shutdown
from .push_update import update_important,get_push_content
from SystemUpdater.Core.PlymouthManager import PlymouthManager
from SystemUpdater.checks.registry import run_checks
from SystemUpdater.modules.rate_limiter import get_download_limit
from SystemUpdater.configs import G_settings
from SystemUpdater.Core.dbus_helper import get_bus
from SystemUpdater.update_hooks import scan_hooks
from .backend.BackendBackup import BackUpBackend 
from .backend.BackendOstreeNext import UpdateBackend,DownloadBackend,DeployBackend,RollbackBackend
from .UpdateManagerDbus import UpdateManagerDbusController,UPDATER_DBUS_INTERFACE,UPDATER_DBUS_PATH,UPDATER_DBUS_SERVICE
from SystemUpdater.self_upgrade.hooks import execute_hooks
from SystemUpdater.constants import OUTPUT_JSON_PATH,SYSTEM_GROUPS,KYLIN_OS_NAME,UPDATER_PACKAGE_NAME,HISTORY_LIST_LEN,CONTROL_CONTER_SENDER

class UpdateManager(GObject.GObject):
    __gsignals__ = {
                    "transaction-stage": (GObject.SignalFlags.RUN_FIRST,
                                         None,
                                         (GObject.TYPE_PYOBJECT,)),
                    }
    def __init__(self,options):
        try:
            GObject.GObject.__init__(self)
            self.cancellable = None
            self.options = options
            self.sqlite3_server = Sqlite3Server(self)

            self._check_exception_status()
            
            self.bus = dbus.SystemBus()
            self.dbus_send = self._setup_dbus()

            self.distro = get_dist()

            self._init_function()
            
        except Exception as e:
            logging.error(e)

    def _init_function(self):
        # 非麒麟系统不获取推送内容
        if KYLIN_OS_NAME != self.distro:
            G_settings["enable_push"] = False

    def run(self):
        """Start the daemon and listen for calls."""
        logging.info("Waiting for calls...")
        try:
            mainloop.run()
        except KeyboardInterrupt:
            self.dbus_send.Quit(None)

    def run_transaction(self, trans: Transaction):
        """Run the worker"""
        try:
            self.cancellable = trans._cancellable
            trans.download_limit = get_download_limit()
            trans.is_kylin = KYLIN_OS_NAME == self.distro
            
            # 在执行各种事务之前检查状态
            run_checks(trans, G_settings)

            if trans.role == ROLE_UPDATE_CACHE:
                self.start_update(trans)
            if trans.role == ROLE_UPDATE_CACHE_NEXT:
                self.start_update_cache(trans)
            elif trans.role == ROLE_DOWNLOAD:
                self.start_download(trans)
            elif trans.role == ROLE_DEPLOY:
                trans.status = STATUS_DEPLOYING
                G_settings["current_update"] = trans.asdict()
                func_feedback = self.start_deploy if self.options.prohibit_backup else self.start_backup
                func_feedback(trans)

        except UpdateBaseError as excep:
            trans.success = False
            trans.error = excep.code
            self._failed_signal(trans,excep.code)
        except Exception as e:
            logging.error(e)
            tbk = traceback.format_exc()
            logging.error(str(tbk))
            self._failed_signal(trans,ERROR_PROGRAM_EXCEPTION)
        finally:
            pass

    def _failed_signal(self,trans,error_code):
        if trans.role in (ROLE_UPDATE_CACHE,ROLE_UPDATE_CACHE_NEXT):
            self.dbus_send.UpdateDetectFinishedNext(False,[''],error_code)
        elif trans.role == ROLE_DOWNLOAD:
            self.dbus_send.UpdateDownloadFinishedNext(False,[''],error_code)
        elif trans.role == ROLE_DEPLOY:
            self.dbus_send.DeployUpdatFinishedNext(False,[''],error_code)   

        self.emit("transaction-stage", trans)

    def start_cancel(self):
        self.cancellable.cancel()

    #进行更新的操作
    def start_update(self, trans: Transaction):
        if G_settings.get("enable_push",True):
            def _update_important_success(push_list):
                if not push_list and trans.is_kylin:
                    logging.info("No important updates available, exiting update process...")
                    self.dbus_send.UpdateDetectFinishedNext(True,[])
                    return
                
                trans.push_list = push_list
                trans.role = ROLE_UPDATE_CACHE_NEXT
                self.run_transaction(trans)

            update_important(trans,reply_handler=_update_important_success,error_handler=self._failed_signal)
            return

        trans.push_list = get_push_content()
        self.start_update_cache(trans)

    #进行更新的操作
    def start_update_cache(self,trans: Transaction):
        def _download_info(parent,fetched,requested,bytes_transferred,download_all,bytes_sec):
            self.dbus_send.UpdateDownloadInfo(fetched,requested,bytes_transferred,download_all,bytes_sec)

        def _force_kill_self():
            logging.warning("Forcefully terminating current process using os._exit(0)")
            os._exit(0)

        def _update_finished(parent,trans):
            if trans.error == PRIORITY_UPGRADE_SUCCCESSED:
                self.dbus_send.UpdateDetectFinishedNext(trans.success,[UPDATER_PACKAGE_NAME],trans.error,trans.error_message)
                GLib.timeout_add_seconds(3,_force_kill_self)
                self.dbus_send.Quit()
                return
            
            trans.hooks = scan_hooks()
            trans.hooks["post-check.d"] = execute_hooks(trans.hooks.get("post-check.d",[]))
            if trans.success:
                logging.info(f"Status: distro:{self.distro} from_revision:{trans.from_revision} new_revision:{trans.new_revision} deployed_refs:{trans.deployed_refs} to available_refs:{trans.available_refs}")
                if trans.from_revision and trans.new_revision and trans.from_revision != trans.new_revision:
                    if trans.push_list or trans.available_refs != trans.deployed_refs or not trans.is_kylin:
                        self._make_meta_for_panel(trans.update_metadata,OUTPUT_JSON_PATH)
                        self.dbus_send.UpdateDetectFinishedNext(True,[trans.update_metadata.get("update-type",SYSTEM_GROUPS)])
                    else:
                        self.dbus_send.UpdateDetectFinishedNext(trans.success,[])
                else:
                    logging.info("System is up-to-date from_revision:%s == new_revision:%s",trans.from_revision,trans.new_revision)
                    self.dbus_send.UpdateDetectFinishedNext(trans.success,[])
            else:
                self.dbus_send.UpdateDetectFinishedNext(trans.success,[""],trans.error,trans.error_message)
            if trans.sender == CONTROL_CONTER_SENDER or G_settings.get("replace",False):
                G_settings["current_update"] = trans.asdict()
            self.emit("transaction-stage",trans)

        update_backend = UpdateBackend(self.options.sysroot,self.options.os)
        update_backend.connect("transaction-done", _update_finished)
        update_backend.connect("download-info", _download_info)
        update_backend.run(trans)

    def start_download(self,trans: Transaction):
        def _download_info(parent,fetched,requested,bytes_transferred,download_all,bytes_sec):
            self.dbus_send.UpdateDownloadInfo(fetched,requested,bytes_transferred,download_all,bytes_sec)

        def _download_finished(parent,trans):
            self.dbus_send.UpdateDownloadFinishedNext(trans.success,[trans.update_metadata.get("update-type",SYSTEM_GROUPS)],trans.error,trans.error_message)
            if trans.success:
                G_settings["current_update"] = trans.asdict()
            self.emit("transaction-stage",trans)

        download_backend = DownloadBackend(self.options.sysroot,self.options.os)
        download_backend.connect("transaction-done", _download_finished)
        download_backend.connect("download-info", _download_info)
        download_backend.run(trans)

    def deploy_finished(self,trans: Transaction):
        self.dbus_send.DeployUpdatFinishedNext(trans.success,[trans.update_metadata.get("update-type",SYSTEM_GROUPS)],trans.error,trans.error_message)            
        trans.status = STATUS_FINISHED
        G_settings["current_update"] = {}
        G_settings["history"] = ([trans.asdict()] + G_settings.get("history", []))[:HISTORY_LIST_LEN]
        self.emit("transaction-stage",trans)

        PlymouthManager.show_countdown(trans.success,trans.reboot_required)
        system_shutdown(trans.reboot_required)

    def start_deploy(self,trans: Transaction, plymouth=None):
        plymouth = plymouth or PlymouthManager()
        def _deploy_progress(parent, progress):
            self.dbus_send.UpdateDloadAndInstStaChanged([trans.update_metadata.get("update-type",SYSTEM_GROUPS)],progress,"","")
            plymouth.update(progress,_("The system is installing updates. Please do not turn off your computer."),10,100)
        
        def _deploy_finished(parent, trans):
            trans.hooks["post-deploy.d"] = execute_hooks(trans.hooks.get("post-deploy.d",[]))
            self.deploy_finished(trans)

        _deploy_progress(self,1)
        trans.hooks["pre-deploy.d"] = execute_hooks(trans.hooks.get("pre-deploy.d",[]))
        deploy_backend = DeployBackend(self.options.sysroot,self.options.os)
        deploy_backend.connect("transaction-done", _deploy_finished)
        deploy_backend.connect("deploy-progress", _deploy_progress)
        deploy_backend.run(trans)

    def start_backup(self,trans: Transaction):
        plymouth = PlymouthManager()
        def _progress_info(parent,progress):
            logging.info("Backup progress: %d%%", progress)
            plymouth.update(progress,_("The system is installing updates. Please do not turn off your computer."),1,10)

        def _backup_finished(parent,result):
            logging.info("Backup finished with result: %s", str(result))
            trans.backup_success = bool(result)
            self.start_deploy(trans, plymouth)
        
        backup_backend = BackUpBackend(self)
        backup_backend.connect("transaction-done", _backup_finished)
        backup_backend.connect("backup-progress", _progress_info)
        backup_backend.run(trans)

    def start_rollback(self):
        def _rollback_finished(parent,trans):
            self.dbus_send.RollbackSysFinishedNext(parent.exit,trans.available_refs,trans.error,trans.error_message)

            if not G_settings.get("prohibit_shutdown",False):
                system_shutdown()

        rollback_backend = RollbackBackend(self.options.sysroot,self.options.os)
        rollback_backend.connect("transaction-done", _rollback_finished)
        rollback_backend.run(self.trans)

    def _make_meta_for_panel(self,metadata,output_path):
        try: 
            if not os.path.exists(output_path):
                os.makedirs(output_path)
                logging.info('making the ConfigPath(%s) is complete...',output_path)

            update_info = {}
            update_info.update(metadata)
            
            # 适配旧版本，在没有更新name
            if "update-name" not in update_info:
                update_name = {"zh_CN":update_info.get("update-name_zh_CN","系统更新"),"en_US":update_info.get("update-name_en_US","System update")}
                update_info.update({"update-name":update_name})

            # 适配旧版本以及新版本元数据的制定
            if isinstance(update_info.get('version',{}), str):
                update_info.update({"version":update_info.get('version',"")})
            elif isinstance(update_info.get('version',{}), dict):
                update_info.update({"version":update_info.get('version',{}).get("value","")})

            # 当description是字符串的时候 转化为字典类型
            if isinstance(update_info.setdefault('description',{"zh_CN":"","en_US":""}), str):
                temp_dict = {"zh_CN":update_info.get("description",""),"en_US":update_info.get("description","")}
                update_info.update({"description":temp_dict})

            if isinstance(update_info.setdefault('changelog',{"zh_CN":"","en_US":""}), str):
                temp_dict = {"zh_CN":update_info.get("changelog",""),"en_US":update_info.get("changelog","")}
                update_info.update({"changelog":temp_dict})
            
            update_info.update({"total_download_size":int(update_info.get("download-size",0))})
            update_info.update({"total_install_size":int(update_info.get("install-size",0))})

            update_info.update({"icon":update_info.setdefault("icon","/usr/share/upgrade/icon/kylin-update-desktop-system.png")})

            output_config_name = os.path.join(output_path, update_info.get("update-type") + '.json')
            with open(output_config_name, 'w', encoding='utf-8') as f:
                json.dump(update_info, f, ensure_ascii=False, indent=4)                   
            logging.info("Generate Jsonfile(%s) to complete... ",output_config_name)
        except Exception as e:
            logging.error(e)

    def _check_exception_status(self):
        """检查异常状态，例如部署过程中异常关机。"""
        VAR_CONFIG_DIR = '/var/lib/kylin-system-updater'
        os.makedirs(VAR_CONFIG_DIR, exist_ok=True)

        current_update = G_settings.get("current_update")
        if not current_update:
            return

        trans = Transaction(**current_update)

        # 标识：部署过程中异常关机
        if trans.status == STATUS_DEPLOYING:
            trans.success = False
            trans.error = ERROR_INSTALL_POWER_OUTAGE
            self.emit("transaction-stage", trans)

            # 更新历史记录并清理当前事务
            history = G_settings.get("history", [])
            G_settings["history"] = [trans.asdict(), *history]
            G_settings["current_update"] = {}

    def _setup_dbus(self):
        # check if there is another g-a-i already and if not setup one
        # listening on dbus
        try:
            bus = get_bus()
            bus_name = dbus.service.BusName(UPDATER_DBUS_SERVICE,
                                            bus,do_not_queue=True)
            logging.info("Initiate dbus success ...")
            return  UpdateManagerDbusController(self,bus_name)
        except dbus.exceptions.NameExistsException:
            if not G_settings.get("replace",False):
                logging.critical("Another daemon is already running")
                sys.exit(1)
            logging.warning("Replacing already running daemon")
            
            retry_reboot_times = 0
            the_other_guy = bus.get_object(UPDATER_DBUS_SERVICE,
                                            UPDATER_DBUS_PATH)
            the_other_guy.Quit(dbus_interface=UPDATER_DBUS_INTERFACE,
                                timeout=300)
            time.sleep(1)
            while True:
                retry_reboot_times = retry_reboot_times + 1
                #当重试次数超过5次时退出程序
                if retry_reboot_times > 5:
                    logging.critical("Reboot backend is Failed...")
                    sys.exit(1)
                try:
                    bus_name = dbus.service.BusName(UPDATER_DBUS_SERVICE,
                                                    bus,
                                                    do_not_queue=True)
                    logging.warning("Replacing already running daemon to Success...")
                    return  UpdateManagerDbusController(self,bus_name)
                except dbus.exceptions.NameExistsException:
                    the_other_guy = bus.get_object(UPDATER_DBUS_SERVICE,
                                                    UPDATER_DBUS_PATH)
                    the_other_guy.Quit(dbus_interface=UPDATER_DBUS_INTERFACE,
                                        timeout=300)
                    logging.warning("Dbus has not withdrawn and retry reboot times:%d...",retry_reboot_times)
                    time.sleep(1)

    # 获取当前deploy的系统hash
    def get_cur_sys_hash(self):
        update_backend = UpdateBackend(self.options.sysroot,self.options.os)
        return update_backend.cur_deployment.get_csum()
