# /*
#  * 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
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.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.update_hooks import scan_hooks
from .backend.BackendOstreeNext import UpdateBackend,DownloadBackend,DeployBackend,RollbackBackend
from .UpdateManagerDbus import UpdateManagerDbusController,UPDATER_DBUS_INTERFACE,UPDATER_DBUS_PATH,UPDATER_DBUS_SERVICE
from SystemUpdater.Core.DataAcquisition import create_time_stamp
from SystemUpdater.self_upgrade.hooks import execute_hooks
from SystemUpdater.constants import OUTPUT_JSON_PATH,SYSTEM_UPDATE_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.bus)

            self.distro = get_dist()

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

    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):
        """Run the worker"""
        try:
            self.cancellable = trans._cancellable
            trans.download_limit = get_download_limit()

            # 在执行各种事务之前检查状态
            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:
                self.start_deploy(trans)
            
        except UpdateBaseError as excep:
            self._finished_signal(trans,excep.code)
        except Exception as e:
            logging.error(e)
            tbk = traceback.format_exc()
            logging.error(str(tbk))
            self._finished_signal(trans,ERROR_PROGRAM_EXCEPTION)
        finally:
            pass

    def _finished_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 _init_function(self):
        # 非麒麟系统不获取推送内容
        if KYLIN_OS_NAME != self.distro:
            G_settings["enable_push"] = False

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

    #进行更新的操作
    def start_update(self,trans):
        if G_settings.get("enable_push",True):
            def _update_important_success(push_list):
                trans.push_list = push_list
                trans.role = ROLE_UPDATE_CACHE_NEXT
                self.run_transaction(trans)

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

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

    #进行更新的操作
    def start_update_cache(self,trans):
        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

            if trans.success:
                if trans.from_revision and trans.new_revision and trans.from_revision != trans.new_revision:
                    # 检查是否为openkylin版本，openkylin版本只要commit不同就可以升级
                    logging.info("Status Info: distro:%s from_revision:%s to new_revision:%s deployed_refs:%s to available_refs:%s"\
                                                ,self.distro,trans.from_revision,trans.new_revision,trans.deployed_refs,trans.available_refs)
                    update_type = trans.update_metadata.get("update-type",SYSTEM_UPDATE_GROUPS)
                    if trans.is_kylin:
                        if trans.push_list or trans.available_refs != trans.deployed_refs:
                            self._make_meta_for_panel(trans.update_metadata,OUTPUT_JSON_PATH)
                            self.dbus_send.UpdateDetectFinishedNext(True,[update_type])
                        else:
                            self.dbus_send.UpdateDetectFinishedNext(trans.success,[])
                    else:
                        logging.info("Update available refs:%s old-revision:%s to new-revision:%s ",trans.available_refs,trans.from_revision,trans.new_revision)
                        self._make_meta_for_panel(trans.update_metadata,OUTPUT_JSON_PATH)
                        self.dbus_send.UpdateDetectFinishedNext(True,[update_type])
                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)
            
            trans.hooks = scan_hooks()

            # 配置写入到配置文件内
            if trans.sender == CONTROL_CONTER_SENDER or G_settings.get("replace",False):
                G_settings["current_update"] = trans.asdict()
            self.sqlite3_server.insert_into_display("check_time", create_time_stamp()[0:-4])
            self.emit("transaction-stage",trans)

        trans.is_kylin = KYLIN_OS_NAME == self.distro

        update_backend = UpdateBackend(self,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):
        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,[SYSTEM_UPDATE_GROUPS],trans.error,trans.error_message)
            # 配置写入到配置文件内
            G_settings["current_update"] = trans.asdict()
            self.emit("transaction-stage",trans)

        download_backend = DownloadBackend(self,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 start_deploy(self,trans):
        plymouth = PlymouthManager()
        def _deploy_progress(parent,progress):
            self.dbus_send.UpdateDloadAndInstStaChanged([SYSTEM_UPDATE_GROUPS],progress,"","")
            if not G_settings.get("prohibit_shutdown",False):
                plymouth.update(progress,SHUTDOWN_INSTALL_ENTRY)
                
        def _deploy_finished(parent,trans):
            self.dbus_send.DeployUpdatFinishedNext(trans.success,[SYSTEM_UPDATE_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]
            
            trans.hooks["post-deploy.d"] = execute_hooks(trans.hooks.get("post-deploy.d",[]))
            self.emit("transaction-stage",trans)
            if not G_settings.get("prohibit_shutdown",False):
                plymouth.show_countdown(trans.success,trans.reboot_required)
                system_shutdown(trans.reboot_required)
        
        trans.status = STATUS_DEPLOYING
        G_settings["current_update"] = trans.asdict()

        _deploy_progress(self,1)
        trans.hooks["pre-deploy.d"] = execute_hooks(trans.hooks.get("pre-deploy.d",[]))

        deploy_backend = DeployBackend(self,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_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,self.options.sysroot,self.options.os)
        rollback_backend.connect("transaction-done", _rollback_finished)
        rollback_backend.run(self.trans)

    def require_shutdown_install(self):
        current_update = G_settings.get("current_update",{})
        # 当前需要部署时，更新状态未下载完成
        if current_update and \
                current_update.get("role",ROLE_UNSET)  == ROLE_DOWNLOAD and current_update.get("success",False):
            return 1,0
        else:
            return 0,0

    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配置目录是否存在 不存在时进行创建
        VAR_CONFG_DIR = '/var/lib/kylin-system-updater'
        if not os.path.exists(VAR_CONFG_DIR):
            os.makedirs(VAR_CONFG_DIR)
        
        current_update = G_settings.get("current_update",{})
        # 此种情况标识 当前正在部署过程中异常关机
        if current_update and current_update.get("status",STATUS_FINISHED) == STATUS_DEPLOYING:
            # 补充历史数据
            current_update["success"] = False
            current_update["error"] = ERROR_INSTALL_POWER_OUTAGE
            self.emit("transaction-stage",current_update)
            
            # 同步更新历史
            history_list = [current_update] + G_settings.get("history",[])
            G_settings["history"] = history_list
            G_settings["current_update"] = {}

    def _setup_dbus(self,bus):
        # check if there is another g-a-i already and if not setup one
        # listening on dbus
        try:
            bus_name = dbus.service.BusName(UPDATER_DBUS_SERVICE,
                                            bus,do_not_queue=True)
            logging.info("Initiate dbus success ...")
            return  UpdateManagerDbusController(self,bus,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,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)