#!/usr/bin/python3

try:
    import sys
    import os
    from   pathlib import Path
    import shutil
    import argparse
    import time
    import traceback
    import distutils.sysconfig
    import subprocess
    import platform
    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 BadReturnCode ( ErrorMessage ):

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


class UndefinedDistribDir ( ErrorMessage ):

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


class NonExistentRepoDir ( ErrorMessage ):

    def __init__ ( self, repoDir ):
        ErrorMessage.__init__( self, 1, 'Repository directory "{}" do not exist.'.format( repoDir ))


class UnknownHost ( ErrorMessage ):

    def __init__ ( self, hostname ):
        ErrorMessage.__init__( self, 1, 'Unsupported host "{}".'.format( hostname ))


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()
        workDir = Path.cwd()
        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 )
            child = subprocess.Popen( self.arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
            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 Repositories ( object ):

    distribDir = None
    repos      = []
    log        = None
    fdLog      = None

    @staticmethod
    def addRpm ( obsUser, distribution, comps=None ):
        Repositories.repos.append( RpmRepository( obsUser, distribution, comps ))

    @staticmethod
    def addDeb ( distribution, codename ):
        Repositories.repos.append( DebRepository( distribution, codename ))

    @staticmethod
    def sync ( distributions=[] ):
        Repositories.openLog()
        for repo in Repositories.repos:
            if not len(distributions) or repo.distribution in distributions:
                repo.sync()
        Repositories.closeLog()

    @staticmethod
    def openLog ():
        logDir = Repositories.distribDir / 'log'
        if not logDir.is_dir():
            logDir.mkdir( parents=True, exist_ok=True )
        index   = 0
        timeTag = time.strftime( '%Y.%m.%d' )
        while True:
            Repositories.log = logDir / 'updaterepo-{}-{:02}.log'.format(timeTag,index)
            if not Repositories.log.is_file():
                print( 'Report log: "{}"'.format(Repositories.log) )
                break
            index += 1
        Repositories.fdLog = Repositories.log.open( "w" )
        return

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


class RpmRepository ( object ):

    def __init__ ( self, obsUser, distribution, comps ):
        if not isinstance(Repositories.distribDir,Path):
            raise UndefinedDistribDir()
        self.obsUser      = obsUser
        self.distribution = distribution
        self.path         = Repositories.distribDir / distribution
        self.comps        = Repositories.distribDir / 'etc' / comps
        self.fdLog        = None
        if not self.comps.is_file():
            print( '[ERROR] Comps file not found, ignoring' )
            print( '        <{}>'.format( self.comps ))
            self.comps = None

    @property
    def repoid ( self ): return 'home_' + self.obsUser

    def resign ( self ):
        rpms    = []
        command = [ 'find', self.path.as_posix(), '-mtime', '-1', '-name', '*.rpm' ]
        child   = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
        while True:
            rpm = child.stdout.readline()
            if not rpm:
                break
            rpms.append( rpm.decode('utf-8')[:-1] )
        command = [ 'rpmsign', '--addsign' ] + rpms
        CommandArg( command, fdLog=Repositories.fdLog ).execute()

    def sync ( self ):
        sourcePath = self.path / 'source'
        x86_64Path = self.path / 'x86_64'
        if not sourcePath.is_dir():
            sourcePath.mkdir( parents=True, exist_ok=True )
        if not x86_64Path.is_dir():
            x86_64Path.mkdir( parents=True, exist_ok=True )
        dnfUpdate = [ 'dnf'
                    , 'reposync'
                    , '--norepopath'
                    , '--repofrompath'
                    , '{repoid},https://download.opensuse.org/repositories/home:/{obsUser}/{distribution}/' \
                      .format( repoid      =self.repoid
                             , obsUser     =self.obsUser
                             , distribution=self.distribution )
                    , '--repoid={}'.format( self.repoid )
                    ]

        print( 'UPDATE x86_64 repo "{}/{}" path="{}"'.format( self.obsUser, self.distribution, x86_64Path ))
        dnfUpdateX86_64 = dnfUpdate + [ '--arch', 'x86_64,noarch'
                                      , '--download-path={}'.format( x86_64Path ) ]
        CommandArg( dnfUpdateX86_64, fdLog=Repositories.fdLog ).execute()
        metalink = x86_64Path / 'metalink.xml'
        if metalink.exists():
            metalink.rename( metalink.with_suffix( '.DISABLED' ))
        self.resign()

        print( 'UPDATE x86_64 repo "{}/{}" path="{}"'.format( self.obsUser, self.distribution, sourcePath ))
        dnfUpdateSource = dnfUpdate + [ '--arch', 'src'
                                      , '--download-path={}'.format( sourcePath ) ]
        CommandArg( dnfUpdateSource, fdLog=Repositories.fdLog ).execute()
        metalink = x86_64Path / 'metalink.xml'
        if metalink.exists():
            metalink.rename( metalink.with_suffix( '.DISABLED' ))
        self.resign()
        
        createRepo = [ 'createrepo_c', '--skip-stat', '--update', '-d', x86_64Path.as_posix() ]
        if self.comps:
            createRepo += [ '--groupfile={}'.format( self.comps.as_posix() ) ]
        CommandArg( createRepo, fdLog=Repositories.fdLog ).execute()

        createRepo = [ 'createrepo_c', '--skip-stat', '--update', '-d', sourcePath.as_posix() ]
        CommandArg( createRepo, fdLog=Repositories.fdLog ).execute()


class DebRepository ( object ):

    distribDir = None
    repos      = []
    log        = None
    fdLog      = None

    def __init__ ( self, distribution, codename ):
        if not isinstance(Repositories.distribDir,Path):
            raise UndefinedDistribDir()
        self.distribution = distribution
        self.codename     = codename
        self.path         = Repositories.distribDir / self.repoid
        self.fdLog        = None

    @property
    def repoid ( self ): return self.distribution

    def sync ( self ):
        if not self.path.is_dir():
            raise NonExistentRepoDir( self.path )
        print( 'UPDATE repo "{}/{}" path="{}"'.format( self.distribution, self.codename, self.path ))
        os.chdir( self.path )
        repreproCommon = [ 'reprepro' , '--basedir', self.path.as_posix() ]
        repreproUpdate = repreproCommon + [ 'update' ]
        CommandArg( repreproUpdate, fdLog=Repositories.fdLog ).execute()
        index   = 0
        timeTag = time.strftime( '%Y.%m.%d' )
        while True:
            snapshot    = '{}-{:02}'.format(timeTag,index)
            snapshotDir = self.path / 'dists' / self.codename / 'snapshots' / snapshot
            if not snapshotDir.is_dir():
                print( 'Taking snaphot "{}"'.format( snapshot ))
                repreproSnapshot = repreproCommon + [ 'gensnapshot', self.codename, snapshot ]
                CommandArg( repreproSnapshot, fdLog=Repositories.fdLog ).execute()
                break
            index += 1


if __name__ == '__main__':
    parser = argparse.ArgumentParser( description="Synchronise/download packages from OBS." ) 
    #parser.add_argument(       "--usage"  , action="store_true"          , dest="usage"  , help="Print detailed usage/help.")
    #parser.add_argument( "-S", "--sync"   , action="store_true"          , dest="sync"   , help="Activate the effective transfert. By default we work in dry run mode.")
    #parser.add_argument( "-M", "--merge"  , action="store_true"          , dest="merge"  , help="Run in merge mode (do not delete extra files).")
    #parser.add_argument( "-R", "--reverse", action="store_true"          , dest="reverse", help="Pull the distribution from the target instead of pushing it.")
    #parser.add_argument(       "--all"    , action="store_true"          , dest="all"    , help="Synchronize all repositories.")
    #parser.add_argument(       "--target" , action="store"     , type=str, dest="target" , help="The target host on which to synchronize.")
    #parser.add_argument( "-c", "--conf"   , action="store"     , type=str, dest="conf"   , help="Rsync configuration file (default: ./rsync.conf)")
    parser.add_argument( 'distributions', nargs=argparse.REMAINDER )
    args = parser.parse_args()

    hostname = platform.node().split(".")[0]
    if hostname.startswith('lepka'):
        Repositories.distribDir = Path( "/dsk/l1/fossEDA" )
        print( 'Running on <lepka> ({}).'.format( hostname ))
    elif hostname.startswith('melon'):
        Repositories.distribDir = Path( "/dsk/l2/fossEDA" )
        print( 'Running on <melon> ({}).'.format( hostname ))
    else:
        raise UnknownHost( hostname )
    print( '  Using base directory "{}".'.format( Repositories.distribDir ))

    result = subprocess.run( [ 'tty' ], capture_output=True )
    tty    = result.stdout[:-1].decode()
    print( '  Using {} for GPG signing.'.format( tty ))
    os.environ[ 'GPG_TTY' ] = tty
    result = subprocess.run( [ 'gpg-connect-agent', 'updatestartuptty', '/bye' ] )

    Repositories.addRpm( "jpc-lip6"    , 'AlmaLinux_8' , 'comps-fossEDA-python3.11.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'AlmaLinux_9' , 'comps-fossEDA.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'AlmaLinux_10', 'comps-fossEDA.xml' )
   #Repositories.addRpm( "jpc-lip6"    , 'Fedora_40'   , 'comps-fossEDA.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'Fedora_41'   , 'comps-fossEDA.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'Fedora_42'   , 'comps-fossEDA.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'Fedora_43'   , 'comps-fossEDA.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'Mageia_9'    , 'comps-fossEDA.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'openSUSE_Leap_15.6_images', 'comps-fossEDA-python311.xml' )
    Repositories.addRpm( "jpc-lip6"    , '16.0'                     , 'comps-fossEDA.xml' )
    Repositories.addRpm( "jpc-lip6"    , 'openSUSE_Tumbleweed'      , 'comps-fossEDA.xml' )
    Repositories.addDeb( 'Debian_12'   , 'bookworm' )
    Repositories.addDeb( 'Debian_13'   , 'trixie' )
    Repositories.addDeb( 'Ubuntu_22_04', 'jammy' )
    Repositories.addDeb( 'Ubuntu_24_04', 'noble' )
    Repositories.addDeb( 'Ubuntu_24_10', 'oracular' )
    Repositories.sync( args.distributions )

    sys.exit( 0 )
    

