#!/usr/bin/env python3

try:
    import sys
    import os.path
    import shutil
    import optparse
    import time
    import traceback
    import distutils.sysconfig
    import subprocess
    import socket
    import re
    import bz2
    import smtplib
    from io                     import IOBase
except ImportError as e:
    module = str(e).split()[-1]


class ErrorMessage ( Exception ):

    def __init__ ( self, code, *arguments ):
        self._code   = code
        self._errors = [ 'Malformed call to ErrorMessage()', '{}'.format(arguments) ]
        text = None
        if len(arguments) == 1:
            if isinstance(arguments[0],Exception): text = str(arguments[0]).split('\n')
            else:
                self._errors = arguments[0]
        elif len(arguments) > 1:
            text = list(arguments)
        if text:
            self._errors = []
            while len(text[0]) == 0: del text[0]
            lstrip = 0
            if text[0].startswith('[ERROR]'): lstrip = 8
            for line in text:
                if line[0:lstrip  ] == ' '*lstrip or \
                   line[0:lstrip-1] == '[ERROR]':
                    self._errors += [ line[lstrip:] ]
                else:
                    self._errors += [ line.lstrip() ]
        return

    def __str__ ( self ):
        if not isinstance(self._errors,list):
            return "[ERROR] {}".format(self._errors)
        formatted = "\n"
        for i in range(len(self._errors)):
            if i == 0: formatted += "[ERROR] {}".format(self._errors[i])
            else:      formatted += "        {}".format(self._errors[i])
            if i+1 < len(self._errors): formatted += "\n"
        return formatted

    def addMessage ( self, message ):
        if not isinstance(self._errors,list):
            self._errors = [ self._errors ]
        if isinstance(message,list):
            for line in message:
                  self._errors += [ line ]
        else:
            self._errors += [ message ]
        return

    def terminate ( self ):
        print( self )
        sys.exit(self._code)

    @property
    def code ( self ): return self._code


class BadBinary ( ErrorMessage ):

    def __init__ ( self, binary ):
        ErrorMessage.__init__( self, 1, 'Binary not found: "{}".'.format(binary) )
        return


class BadReturnCode ( ErrorMessage ):

    def __init__ ( self, status ):
        ErrorMessage.__init__( self, 1, 'Command returned status:{}.'.format(status) )
        return


class InstallFail ( ErrorMessage ):

    def __init__ ( self, packages ):
        ErrorMessage.__init__( self, 1, 'Failed to install packages:{}.'.format(packages) )


class UndefinedDistribDir ( ErrorMessage ):

    def __init__ ( self ):
        ErrorMessage.__init__( self, 1, 'Repository.distribDir has not been defined.' )


class Command ( object ):

    def __init__ ( self, arguments, fdLog=None, fdFilter=None ):
        self.arguments = arguments
        self.fdLog     = fdLog
        self.fdFilter  = fdFilter
        self.output    = []
        if self.fdLog != None and not isinstance(self.fdLog,IOBase):
            print( '[WARNING] Command.__init__(): "fdLog" is neither None nor a file.' )
        return

    def _argumentsToStr ( self, arguments ):
        s = ''
        for argument in arguments:
            if argument.find(' ') >= 0: s += ' "' + argument + '"'
            else:                       s += ' '  + argument
        return s

    def log ( self, text, toStdout=False ):
        line = None
        if isinstance(text,bytes):
            line = text.decode('utf-8')
        elif isinstance(text,str):
            line = text
        else:
            print( '[ERROR] Command.log(): "text" is neither bytes or str.' )
            print( '        {}'.format(text) )
        if line is not None:
            if isinstance(self.fdLog,IOBase):
                if self.fdFilter:
                    self.fdFilter.logFilter( line )
                self.fdLog.write( line )
                self.fdLog.flush()
            if toStdout or not isinstance(self.fdLog,IOBase):
                print( line[:-1] )
            self.output.append( line[:-1] )
        sys.stdout.flush()
        sys.stderr.flush()
        return

    def execute ( self ):
        global conf
        sys.stdout.flush()
        sys.stderr.flush()
        
        homeDir = os.environ['HOME']
        workDir = os.getcwd()
        if homeDir.startswith(workDir):
            workDir = '~' + workDir[ len(homeDir) : ]
        user = 'root'
        if 'USER' in os.environ: user = os.environ['USER']
        prompt = '{}@{}:{}$'.format(user,'melon',workDir)
        
        try:
            self.log( '{}{}\n'.format(prompt,self._argumentsToStr(self.arguments)), toStdout=True )
            #print( self.arguments )
            child = subprocess.Popen( self.arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
            while True:
                line = child.stdout.readline()
                if not line: break
                self.log( line )
        except OSError as e:
            raise BadBinary( self.arguments[0] )
        
        (pid,status) = os.waitpid( child.pid, 0 )
        status >>= 8
        return status


class CommandArg ( object ):

    def __init__ ( self, command, wd=None, fdLog=None ):
        self.command = command
        self.wd      = wd
        self.fdLog   = fdLog
        self.output  = []
        return

    def __str__ ( self ):
        s = ''
        if self.wd: s = 'cd {} && '.format(self.wd)
        for i in range(len(self.command)):
            if i: s += ' '
            s += self.command[i]
        return s

    def logFilter ( self, line ):
        pass

    def getArgs ( self ):
        return self.command

    def execute ( self ):
        if self.wd: os.chdir( self.wd )
        command = Command( self.getArgs(), self.fdLog, fdFilter=self )
        command.execute()
        self.output = command.output


class Repository ( object ):

    UPDATE     = 0x0001
    MERGE      = 0x0002
    distribDir = None
    repos      = []
    log        = None
    fdLog      = None

    @staticmethod
    def add ( repoId, path, comps=None, packages=[], flags=UPDATE ):
        Repository.repos.append( Repository( repoId, path, comps, packages, flags ))

    @staticmethod
    def sync ( repoIds=[] ):
        Repository.openLog()
        for repo in Repository.repos:
            if not len(repoIds) or repo.id in repoIds:
                repo._sync()
        Repository.closeLog()


    @staticmethod
    def openLog ():
        logDir = os.path.join( Repository.distribDir, 'soc/9-alma/dim/log' )
        if not os.path.isdir(logDir):
            os.makedirs( logDir )
        index   = 0
        timeTag = time.strftime( "%Y.%m.%d" )
        while True:
            Repository.log = os.path.join(logDir,"updaterepo-{}-{:02}.log".format(timeTag,index))
            if not os.path.isfile(Repository.log):
                print( 'Report log: "{}"'.format(Repository.log) )
                break
            index += 1
        Repository.fdLog = open( Repository.log, "w" )
        return

    @staticmethod
    def closeLog ():
        if Repository.fdLog:
            Repository.fdLog.close()

    def __init__ ( self, repoId, path, comps, packages, flags ):
        if not Repository.distribDir:
            raise UndefinedDistribDir()
        self.id       = repoId
        self.path     = os.path.join( Repository.distribDir, path )
        self.comps    = os.path.join( Repository.distribDir, comps ) if comps else None
        self.packages = packages
        self.flags    = flags
        self.fdLog    = None

    def _sync ( self ):
        if not len(self.packages):
            dnfUpdate = [ 'dnf'
                        , 'reposync'
                        , '--repoid={}'.format( self.id )
                        , '--download-path={}'.format( self.path )
                        , '--norepopath'
                        , '--newest-only'
                        ]
            if self.flags & Repository.UPDATE:
                print( 'UPDATE repo "{}" path="{}"'.format( self.id, self.path ))
                dnfUpdate += [ '--download-metadata' ]
                CommandArg( dnfUpdate, fdLog=Repository.fdLog ).execute()
            else:
                print( 'MERGE repo "{}" path="{}"'.format( self.id, self.path ))
                dnfUpdate += [ '--arch=x86_64'
                             , '--arch=noarch'
                             ]
                CommandArg( dnfUpdate, fdLog=Repository.fdLog ).execute()
            metalink = self.path + '/metalink.xml'
            if os.path.exists(metalink):
                os.rename( metalink, metalink+'.DISABLED' )
        else:
            dnfUpdate = [ 'sudo'
                        , 'dnf'
                        , 'install'
                        , '--assumeyes'
                        , '--enablerepo={}'.format( self.id )
                        , '--downloaddir={}'.format( self.path )
                        , '--downloadonly'
                        ] + self.packages
            CommandArg( dnfUpdate, fdLog=Repository.fdLog ).execute()
            chown = [ 'sudo', 'chown', '-R', 'jpc:ita-iatos', self.path ]
            CommandArg( chown, fdLog=Repository.fdLog ).execute()
        createRepo = [ 'createrepo_c', '-d', self.path ]
        CommandArg( createRepo, fdLog=Repository.fdLog ).execute()
        self.keepLatests( 2 )
        createRepo = [ 'createrepo_c', '-d', self.path ]
        if self.comps:
            createRepo += [ '--groupfile={}'.format(self.comps) ]
        CommandArg( createRepo, fdLog=Repository.fdLog ).execute()

    def keepLatests ( self, depth ):
        dnfManage = CommandArg( [ 'dnf'
                                , 'repomanage'
                                , '--keep', str(depth)
                                , self.path
                                ]
                              , fdLog=Repository.fdLog )
        dnfManage.execute()
        latestRPMS = set( dnfManage.output[2:] )
        allRPMS    = []
        for root, dirs, files in os.walk( self.path ):
            for file in files:
                if file.endswith('.rpm'):
                    allRPMS.append( root + '/' + file )
        oldRPMS = [ rpm for rpm in allRPMS if rpm not in latestRPMS ]
        print( 'Removing {} outdated RPMS.'.format(len(oldRPMS)), file=Repository.fdLog )
        for rpm in oldRPMS:
            print( 'R', rpm, file=Repository.fdLog )
            os.unlink( rpm )


if __name__ == '__main__':
    Repository.distribDir = "/dsk/l2/distributions"
    Repository.add( "lip6-baseos"               , 'al/9/BaseOS/x86_64/os'
                                                , comps='soc/9-alma/dim/etc/comps-BaseOS.x86_64.xml' )
    Repository.add( "baseos-debuginfo"          , 'al/9/BaseOS/debug/x86_64' )
    Repository.add( "baseos-source"             , 'al/9/BaseOS/Source' )
    Repository.add( "lip6-appstream"            , 'al/9/AppStream/x86_64/os'
                                                , comps='soc/9-alma/dim/etc/comps-AppStream.x86_64.xml' )
    Repository.add( "appstream-debuginfo"       , 'al/9/AppStream/debug/x86_64' )
    Repository.add( "appstream-source"          , 'al/9/AppStream/Source' )
    Repository.add( "lip6-crb"                  , 'al/9/CRB/x86_64/os'
                                                , comps='soc/9-alma/dim/etc/comps-CRB.x86_64.xml' )
    Repository.add( "crb-debuginfo"             , 'al/9/CRB/debug/x86_64' )
    Repository.add( "crb-source"                , 'al/9/CRB/Source' )
    Repository.add( "lip6-highavailability"     , 'al/9/HighAvailability/x86_64/os' )
    Repository.add( "highavailability-debuginfo", 'al/9/HighAvailability/debug/x86_64' )
    Repository.add( "highavailability-source"   , 'al/9/HighAvailability/Source' )
    Repository.add( "lip6-nfv"                  , 'al/9/NFV/x86_64/os' )
    Repository.add( "nfv-debuginfo"             , 'al/9/NFV/debug/x86_64' )
    Repository.add( "nfv-source"                , 'al/9/NFV/Source' )
    Repository.add( "lip6-rt"                   , 'al/9/RT/x86_64/os' )
    Repository.add( "rt-debuginfo"              , 'al/9/RT/debug/x86_64' )
    Repository.add( "rt-source"                 , 'al/9/RT/Source' )
    Repository.add( "lip6-resilientstorage"     , 'al/9/ResilientStorage/x86_64/os' )
    Repository.add( "resilientstorage-debuginfo", 'al/9/ResilientStorage/debug/x86_64' )
    Repository.add( "resilientstorage-source"   , 'al/9/ResilientStorage/Source' )
    Repository.add( "epel"                      , 'epel/9/Everything/x86_64'
                                                , comps='soc/9-alma/dim/etc/comps-EPEL-Everything.x86_64.xml' )
    Repository.add( "epel-testing"              , 'epel/testing/9/Everything/x86_64' )
   #Repository.add( "epel-next"                 , 'epel/next/9/Everything/x86_64'
   #                                            , comps='soc/9/dim/etc/comps-EPEL-next-Everything.x86_64.xml' )
    Repository.add( "google-chrome"             , 'soc/9-alma/vendors/x86_64/os', flags=Repository.MERGE )
    Repository.add( "vscode"                    , 'soc/9-alma/vendors/x86_64/os', flags=Repository.MERGE )
   #Repository.add( "virtualbox"                , 'soc/9-alma/vendors/x86_64/os', flags=Repository.MERGE )


   #Repository.add( "sublime-text"           , 'soc/9/vendors/x86_64/os'
   #              , packages=[ 'sublime-text' ] )
   #Repository.sync( [ 'appstream' ] )
   #Repository.sync( [ 'google-chrome' ] )
   #Repository.sync( [ 'baseos' ] )
   #Repository.sync( [ 'baseos-source', 'baseos-debug' ] )
   #Repository.sync( [ 'epel', 'epel-next' ] )
   #Repository.sync( [ 'vscode' ] )
    Repository.sync()

