#!/usr/bin/python3
import os
import fcntl
#from re import sub
import subprocess
import sys
import datetime
import atexit
import signal
import logging
from threading import Event
from optparse import OptionParser
# for dbus signal handling
import dbus
#dbus mainloop 
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib

from typing import AbstractSet, cast, DefaultDict, Dict, Iterable, List
AbstractSet  # pyflakes
DefaultDict  # pyflakes
Dict  # pyflakes
Iterable  # pyflakes
List  # pyflakes
from typing import Set, Tuple, Union
Set  # pyflakes
Tuple  # pyflakes
Union  # pyflakes

# for apt stuff handling
import apt_pkg
import apt

#for configure file reading and editing
import configparser

from gettext import gettext as _
#log file path
LOG_FILE_PATH="/var/log/kylin-autoupdate/kylin-autoupdate-download.log"
#configure file path
UPGRADE_CONFIG_FILE_PATH="/var/lib/kylin-auto-upgrade/kylin-autoupgrade.conf"
PKGLIST_CONFIG_FILE_PATH="/var/lib/kylin-auto-upgrade/kylin-autoupgrade-pkglist.conf"
#update source template path
UPDATE_SOURCE_TEMPLATE_PATH ="/var/lib/kylin-software-properties/template/important.list"
# progress information is written here
PROGRESS_LOG = "/var/run/apt-download.progress"
PID_FILE = "/var/run/apt-download.pid"
LOCK_FILE = "/var/run/apt-download.lock"
#get app info wait period
WAIT_PERIOD_OF_APP_INFO = 1
#the lowest priority in policy
NEVER_PIN = -32768
#control panel file lock file
CONTROL_PANEL_LOCK_FILE = "/tmp/auto-upgrade/ukui-control-center.lock"

def WriteValueToFile(file,section,option,value):
    config=configparser.ConfigParser(allow_no_value=True)
    if os.path.exists(file):
        config.read(file)
        if config.has_section(section) and \
            config.has_option(section,option):
            config.set(section,option,value)
        config.write(open(file,"w"))

def signal_handler_term(signal, frame):
    # type: (int, object) -> None
    logging.warning("SIGTERM received, will stop")
    WriteValueToFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
    WriteValueToFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","false")
    if apt_pkg.pkgsystem_is_locked():
        apt_pkg.pkgsystem_unlock()
    try:
        os.close(lock)
    except:
      logging.warning("lock file release exception")   
    os._exit(1)
    
def signal_handler_int(signal,frame):
    # type: (int, object) -> None
    logging.warning("SIGINT received, will stop")
    WriteValueToFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
    WriteValueToFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","false")
    if apt_pkg.pkgsystem_is_locked():
        apt_pkg.pkgsystem_unlock()
    try:
        os.close(lock)
    except:
      logging.warning("lock file release exception") 
    os._exit(1)

def signal_handler_usr1(signal,frame):
    logging.info("SIGUSR1 received,will exit")
    WriteValueToFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
    WriteValueToFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","false")
    if apt_pkg.pkgsystem_is_locked():
        apt_pkg.pkgsystem_unlock()
    try:
        os.close(lock)
    except:
      logging.warning("lock file release exception") 
    os._exit(1)

def should_stop():
    return False

class LoggingDateTime:
    """The date/time representation for the dpkg log file timestamps"""
    LOG_DATE_TIME_FMT = "%Y-%m-%d  %H:%M:%S"

    @classmethod
    def as_string(cls):
        # type: () -> str
        """Return the current date and time as LOG_DATE_TIME_FMT string"""
        return datetime.datetime.now().strftime(cls.LOG_DATE_TIME_FMT)

    @classmethod
    def from_string(cls, logstr):
        # type: (str) -> datetime.datetime
        """Take a LOG_DATE_TIME_FMT string and return datetime object"""
        return datetime.datetime.strptime(logstr, cls.LOG_DATE_TIME_FMT)

class Options:
    def __init__(self):
        self.debug = False

class AptDownloadResult:
    def __init__(self,result,result_status,result_text) -> None:
        self.fetch_result = result
        self.result_status = result_status
        self.result_text = result_text
        pass

class FILE_LOCK(object):
    def __init__(self,name):
        self.fobj = open(name,'w')
        self.fd = self.fobj.fileno()

    def get_lock(self):
        try:
            fcntl.flock(self.fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
            return True
        except:
            return False

    def unlock(self):
        self.fobj.close()


class ConfigManager:
    def __init__(self) -> None:
        self.config = configparser.ConfigParser(allow_no_value=True)
        pass

    def ReadFromFile(self,file):
        if os.path.exists(file):
            self.config.read(file)
        else:
            logging.warning("file %s does not exist"%file)

    def WriteToFile(self,file):
        if os.path.exists(file):
            self.config.write(open(file,"w"))
            return True
        else:
            logging.error("config file path does not exist!")
            return False  

    def GetKeyValue(self,section,option):
        if not self.config.has_section(section):
            logging.warning("no such section %s"%section)
            return
        if not self.config.has_option(section,option):
            logging.warning("no such option %s in section %s"%(option,section))
            return
        return self.config.get(section,option)

    def SetKeyValue(self,section,option,value):
        if not self.config.has_section(section):
            self.config.add_section(section)
        self.config.set(section,option,value)
        return

    def WriteListToEmptyFile(self,file,section,option,list):
        config = configparser.ConfigParser(allow_no_value=True)
        config.add_section(section)
        liststring = " ".join(list)
        config.set(section,option,liststring) 
        config.write(open(file,"w+"))
        return

    def WriteValuetoFile(self,file,section,option,value):
        config = configparser.ConfigParser(allow_no_value=True)
        config.read(file)
        if config.has_section(section) and config.has_option(section,option):
            config.set(section,option,value)
        config.write(open(file,"w"))
        return


class DownloadProgress(apt.progress.base.AcquireProgress):
    def __init__(self,progress_log) -> None:
        self.progress_log = progress_log
        super().__init__()

class AptDownload:
    def __init__(self,**kwargs) -> None:
        #set default main loop
        DBusGMainLoop(set_as_default=True)
        #get loop object
        self.loop=GLib.MainLoop()
        #get the system bus object
        self.system_bus = dbus.SystemBus()
        #get the proxy object for software properties 
        self.software_proxy = self.system_bus.get_object('com.kylin.software.properties',
            '/com/kylin/software/properties')
        #get the proxy object for update manager 
        self.update_proxy =  self.system_bus.get_object('cn.kylinos.KylinUpdateManager',
            '/cn/kylinos/KylinUpdateManager')
        #get the proxy object of login1  
        self.login1_proxy = self.system_bus.get_object('org.freedesktop.login1', 
            '/org/freedesktop/login1')               
        #get software properties interface
        self.software_interface =  dbus.Interface(self.software_proxy,
            dbus_interface='com.kylin.software.properties.interface')
        #get interface for update manager
        self.update_interface = dbus.Interface(self.update_proxy,
            dbus_interface='cn.kylinos.KylinUpdateManager')
        #get interface for login1 
        self.login1_iface = dbus.Interface(self.login1_proxy,
            dbus_interface='org.freedesktop.login1')
        #app info signal revieve count
        self.app_message_signal_count = 0

        self.update_source_template_path = UPDATE_SOURCE_TEMPLATE_PATH
        self.updatelist = []
        self.downloadlist = []
        self.installlist = []
        self.downloadset = set()
        self.config_manager = ConfigManager()
        self.download_max_try_times = kwargs['times']
        self.quit_main_loop = kwargs['quit']

    def UpdateSourceTemplate(self):
        #get the latest source list template
        #self.software_interface.updateSourceTemplate()
        ret = self.software_interface.updateSourceTemplate()
        if ret:
            logging.info("successfully got latest update list")
        else:
            logging.warning("failed to get latest update list")
        return ret

    def GetUpdateList(self):
        #read the update list
        if (os.path.exists(self.update_source_template_path)):
            with open(self.update_source_template_path) as f:
                templist = f.read()
                self.updatelist = templist.split()
            return True
        else:
            logging.warning("update list file dose not exist!")
            return False

    def GetAppMessage(self):    
        #send the update list to update manager
        #self.update_interface.get_app_message(self.updatelist)
        self.system_bus.call_async('cn.kylinos.KylinUpdateManager','/cn/kylinos/KylinUpdateManager',\
                          'cn.kylinos.KylinUpdateManager','get_app_message','as',\
                           (self.updatelist,),reply_handler=None,error_handler=None,timeout=-1)
        return

    def ConnectToAppInfoSignal(self):
        def important_app_message_signal_handler(map,urllist,namelist,fullnamelist,sizelist,allsize,dependstate):
            self.app_message_signal_count+=1            
            if dependstate :
                self.downloadlist.append(map['appname'])
                self.downloadlist += namelist
                self.installlist.append(map['appname'])
                self.installlist += namelist  
            return
        #connect to important app list signal
        self.update_proxy.connect_to_signal('important_app_message_signal',important_app_message_signal_handler,
                                dbus_interface='cn.kylinos.KylinUpdateManager')
        return

    def ConnectToAppInfoFinishSignal(self):
        def get_message_finished_signal_handler(state):
            logging.info("app info recieving finished,%d messages recieved"%self.app_message_signal_count)
            self.config_manager.WriteListToEmptyFile(PKGLIST_CONFIG_FILE_PATH,"DOWNLOAD","pkgname",self.installlist)
            logging.debug("update list:%s","\n".join(self.updatelist))
            logging.debug("download list:%s","\n".join(self.downloadlist))
            logging.debug("install list:%s","\n".join(self.installlist))
            ret = subprocess.run(['apt','update'])
            if ret.returncode == 0 :
                logging.info("apt update success")
                pass
            else:
                logging.warning("apt update process failed with exit code %d"%ret.returncode)

            # check and get pkg system lock
            try:
                apt_pkg.pkgsystem_lock()
            except SystemError:
                logging.error("Lock could not be acquired ")
                self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
                self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","false")
                #relese the lock file
                try:
                    os.close(lock)
                except:
                    logging.warning("lock file release exception") 
                os._exit(1)
            remote_item_count = 0
            local_item_count =  0
            ret = AptDownloadResult(result= False,result_status=1,result_text="apt download result initialization")
            while (self.download_max_try_times > 0):
                self.download_max_try_times-=1
                #subprocess.run(['apt','update'])
                self.InitAptObjects()
                self.ReadSourceList()
                self.MarkInstallFromUpdateManager()
                self.GetArchives()
                remote_item_count = self.GetRemoteItemCount()
                local_item_count = self.GetLocalItemCount()
                ret = self.RunFetcher()           
                logging.info("%d try count,%d remote items left to be fetch,%d local items"\
                        %(self.download_max_try_times,remote_item_count,local_item_count))
                if remote_item_count == 0:
                    logging.debug("all packages downloaded")
                    break
            if ret.result_status == 0 and remote_item_count == 0:
                self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","true")
            else:
                self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","false")
            self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle") 
            
            # unlock the apt pkg system
            try:
                apt_pkg.pkgsystem_unlock()
            except SystemError:
                logging.error("Lock could not be released ") 
            #relese the lock file
            try:
                os.close(lock)
            except:
                logging.warning("lock file release exception") 
            os._exit(0)
            #quit main loop           

        #connect to get message finished signal
        self.update_proxy.connect_to_signal('get_message_finished_signal',get_message_finished_signal_handler,
                                dbus_interface='cn.kylinos.KylinUpdateManager')  
        return 

    def ConnectToKillAutoDownloadSignal(self):
        def kill_auto_download_signal_handler():
            logging.warning("kill signal from control center,now quiting...")
            self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
            self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","false")
            if apt_pkg.pkgsystem_is_locked():
                apt_pkg.pkgsystem_unlock()
            try:
                os.close(lock)
            except:
                logging.warning("lock file release exception")             
            os._exit(1)
        self.update_proxy.connect_to_signal('KillAutoDownload',kill_auto_download_signal_handler,
                                dbus_interface='cn.kylinos.KylinUpdateManager')
    
    def ConnectToPrepareForShutdownSignal(self):
        def prepare_for_shutdown_handler(active):
            logging.warning("prepare for shutdown signal recieved,now quiting...")
            if not active:
                logging.warning("PrepareForShutdown(false) received, "
                                    "this should not happen")
            self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
            self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"DOWNLOAD","finish_status","false")
            if apt_pkg.pkgsystem_is_locked():
                apt_pkg.pkgsystem_unlock()
            os.close(lock)             
            os._exit(1)
        self.login1_proxy.connect_to_signal("PrepareForShutdown", prepare_for_shutdown_handler,
                                dbus_interface='org.freedesktop.login1.Manager')
      

    def RunMainLoop(self): 
        self.loop.run()
        return

    #you should use the following functiongs after the cache has been updated
    def InitAptObjects(self):
        #initialize the apt_pkg envirion
        apt_pkg.init()
        #set the apt cache
        self.cache = apt_pkg.Cache()
        #set the apt depcache
        self.depcache = apt_pkg.DepCache(self.cache)
        #set the package records
        self.packagerecords = apt_pkg.PackageRecords(self.cache)
        #set the package manager
        self.packagemanager = apt_pkg.PackageManager(self.depcache)
        #set sourcelist object
        self.sourcelist = apt_pkg.SourceList()
        #set progress object
        #self.fetch_progress = apt.progress.base.AcquireProgress()
        #set acquire object
        self.fetcher = apt_pkg.Acquire(apt.progress.text.AcquireProgress())

    def MarkInstallFromUpdateList(self):
        for pkgname in self.updatelist:
            pkg = self.cache[pkgname]
            if self.depcache.is_upgradable(pkg):
                self.depcache.mark_install(pkg)
            
    def MarkInstallFromUpdateManager(self):
        for pkgname in self.downloadlist:
            if pkgname in self.cache:
                pkg = self.cache[pkgname]
                self.depcache.mark_install(pkg)
            else:
                logging.warning("package %s not in local cache"%pkgname)

    def GetDownloadList(self):
        return self.downloadlist

    def GetInstallList(self):
        return self.installlist

    def GetInstCount(self):
       return self.depcache.inst_count

    def ReadSourceList(self):
        self.sourcelist.read_main_list()

    def GetArchives(self):
        self.packagemanager.get_archives(self.fetcher,self.sourcelist,self.packagerecords)

    def RunFetcher(self):
        result = False
        result_status = 1
        result_text = ""
        try:
            ret = self.fetcher.run()
            logging.debug("fetch.run() result: %s", ret)
        except SystemError as e:
            logging.error("fetch.run() result: %s", e)
        if ret == apt_pkg.Acquire.RESULT_FAILED:
            result_text = "fetch process failed"
            logging.error("fetching process failed")
        elif ret == apt_pkg.Acquire.RESULT_CONTINUE:
            result = True
            result_status = 0
            result_text = "fetch process success"
            logging.info("fetching process success")
        elif ret == apt_pkg.Acquire.RESULT_CANCELLED:
            result_text = "fetch process cancelled"
            logging.error("fetching process abort")
        else:
            logging.debug("not recognized return value fromg Acquire object")
        return AptDownloadResult(result,result_status,result_text)

    def GetLocalItemCount(self):
        local_item_count = 0
        for item in self.fetcher.items:
            if item.local:
                local_item_count+=1
        return local_item_count

    def GetRemoteItemCount(self): 
        remote_item_count = 0
        for item in self.fetcher.items:
            if not item.local:
                remote_item_count+=1     
        return remote_item_count

def main(options):
    apt_download = AptDownload(quit = options.quit,times=options.times)
    ret = apt_download.UpdateSourceTemplate()
    if ret:
        pass
    else:
        logging.error("fail to get update list")
        WriteValueToFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
        sys.exit(1)
    apt_download.GetUpdateList()
    apt_download.ConnectToAppInfoSignal()
    apt_download.ConnectToAppInfoFinishSignal()
    apt_download.ConnectToKillAutoDownloadSignal()
    apt_download.ConnectToPrepareForShutdownSignal()
    apt_download.GetAppMessage()    
    apt_download.RunMainLoop()
    return 0

if __name__ == "__main__":
    # init the options
    parser = OptionParser()
    parser.add_option("-d", "--debug",
                      action="store_true",
                      default=True,
                      help="print debug messages")
    parser.add_option("-q", "--quit",
                      action="store_true",
                      default=True,
                      help="quit main loop after each download")
    parser.add_option("-t", "--times",
                      action="store_true",
                      default=10,
                      help="quit main loop after each download")                                            
    options = cast(Options, (parser.parse_args())[0])
    #set up logging
    logging.basicConfig(level=logging.INFO,format='%(asctime)s %(levelname)s %(message)s',filename=LOG_FILE_PATH)                                           
    logger = logging.getLogger()
    if options.debug:
        logger.setLevel(logging.DEBUG)
        stdout_handler = logging.StreamHandler(sys.stdout)
        logger.addHandler(stdout_handler)
        logging.debug("apt-download debug enabled")    
    if os.getuid() != 0:
        sys.exit(1)
    # ensure that we are not killed when the terminal goes away e.g. on
    # shutdown
    signal.signal(signal.SIGHUP, signal.SIG_IGN) 
    signal.signal(signal.SIGINT,signal_handler_int)
    # setup signal handler for graceful stopping
    signal.signal(signal.SIGTERM, signal_handler_term)
    signal.signal(signal.SIGUSR1,signal_handler_usr1)
    # write pid to let other processes find this one
    pidf = PID_FILE
    # clean up pid file on exit
    with open(pidf, "w") as fp:
        fp.write("%s" % os.getpid())
    atexit.register(os.remove, pidf)
    #get lock file to block another download program
    lock = apt_pkg.get_lock(LOCK_FILE)
    if lock < 0:
        logging.error("Lock file is already taken, exiting")
        #self.config_manager.WriteValuetoFile(UPGRADE_CONFIG_FILE_PATH,"CONTROL_CENTER","autoupdate_run_status","idle")
        sys.exit(1)
    #check control center lock
    if os.path.exists(CONTROL_PANEL_LOCK_FILE):
        file_lock = FILE_LOCK(CONTROL_PANEL_LOCK_FILE)
        if file_lock.get_lock():
            logging.debug("control center not running")
            file_lock.unlock()
        else:
            logging.warning("control center running ,exiting...")
            sys.exit(1)
    #get run status configuration
    run_stat_config = configparser.ConfigParser(allow_no_value=True)
    run_stat_config.read(UPGRADE_CONFIG_FILE_PATH)
    auto_update_allowed = run_stat_config.get("CONTROL_CENTER","autoupdate_allow")
    if auto_update_allowed == "false":
        logging.warning("auto update cannot run at this moment,quiting ....\
            auto update allow:%s"%(auto_update_allowed))
        sys.exit(1)    
    '''
    switch_status = run_stat_config.get("CONTROL_CENTER","swicth_status")
    if switch_status == "true" or auto_update_allowed == "false":
        logging.warning("auto update cannot run at this moment,quiting ....\
            switch status:%s,auto update allow:%s"%(switch_status,auto_update_allowed))
        sys.exit(1)    
    '''
    if run_stat_config.has_section("CONTROL_CENTER") and \
        run_stat_config.has_option("CONTROL_CENTER","autoupdate_run_status"):
        run_stat_config.set("CONTROL_CENTER","autoupdate_run_status","download")
    else:
        logging.warning("auto update run status file configuration incorrect")
        sys.exit(1)
    run_stat_config.write(open(UPGRADE_CONFIG_FILE_PATH,"w"))
    # run the main code
    ret = main(options)
    logging.debug("main func returned %d"%ret)
    '''
    run_stat_config.read(UPGRADE_CONFIG_FILE_PATH)
    run_stat_config.set("CONTROL_CENTER","autoupdate_run_status","idle")
    if ret == 0:
        run_stat_config.set("DOWNLOAD","finish_status","true")    
    else:
        run_stat_config.set("DOWNLOAD","finish_status","false") 
    run_stat_config.write(UPGRADE_CONFIG_FILE_PATH,"w")
    '''     
    sys.exit(ret)