#!/usr/bin/python
# Authors Dominig ar Foll (Intel OTC) (first versions in Bash)
#         Florent Vennetier (Intel OTC) (third version in Python)
# Date 16 May 2012
# Version 3.3
# License GPLv2

import os
import sys
import time

from urllib2 import URLError, HTTPError
from xml.etree import ElementTree

from osc import conf, core

DEBUG_MODE = False
MAX_RETRIES = 5

def help_(progName="obstag"):
    print """HELP
Function : Tag a project in an OBS.
        It will create a tag file which contains the MD5 to represent
        the package revision. The tag file can be used as direct input
        for obs2obscopy.

Usage:   """ + progName + """ <obs-alias|obs-api-url> <project-name> <md5-file-name>

        <obs-alias>     the alias of an OBS server as defined by OSC in ~/.oscrc.
        <obs-api-url>   a URL to an OBS API. Note that if you do not have a
                        configured account in ~/.oscrc for this API, you should
                        try public access, by appending "/public" to the URL.
        <project-name>  the name of the project you want to tag.
        <md5-file-name> the name of the file where to write results.

Example: """ + progName + """ http://myObs.mynetwork:81 home:me:my_project my_project.tag

Return code:
          0 success
          1 some packages not tagged
          2 wrong (number of) arguments
          3 problem writing MD5 file
          4 network error
          5 interrupted by user
          
Environment:
       If the DEBUG environment variable is non-empty, all URLs will
       be printed to stderr before being called.

Version 3.3   License GPLv2"""

def printerror(message):
    print >> sys.stderr, message

def printdebug(message):
    if DEBUG_MODE:
        print >> sys.stderr, "DEBUG:", message

def getApiUrlFromAlias(alias):
    """
    Return the full API URL corresponding to `alias`.
    Return `alias` if it is an URL.
    """
    if not alias.startswith("http"):
        apiUrl = conf.config["apiurl_aliases"].get(alias, None)
        if apiUrl is None:
            message = "Wrong API URL or alias: '%s'" % alias
            printerror(message)
            sys.exit(2)
        printdebug("%s is an alias for %s" % (alias, apiUrl))
        return apiUrl
    else:
        return alias


class PackageRevision(object):
    """
    Reflects the content of a <revision> tag as returned by a 
    'GET /source/<project>/<package>/_history' call on an OBS project package.
    """

    def __init__(self, packageName, rev, vrev):
        self.name = packageName
        self.rev = rev
        self.vrev = vrev
        self.srcmd5 = ""
        self.time = "0"
        self.version = ""
        self.user = ""
        self.comment = "<no message>"

    def __str__(self):
        """
        Simply calls self.__repr__()
        """
        return self.__repr__()

    def __repr__(self):
        """
        Returns a string suitable for obs2obscopy input
        """
        if isinstance(self.comment, basestring):
            self.comment = self.comment.replace("\n", "\t")
            self.comment = self.comment.strip()
        date = time.localtime(int(self.time))
        s = "%(srcmd5)40s | %(rev)6s | " % self.__dict__
        s += "%20s | " % time.strftime("%Y-%m-%d %H:%M:%S", date)
        s += "%(name)s | %(version)s |%(user)s |%(comment)s " % self.__dict__
        return s.encode("utf8")

    def __cmp__(self, other):
        if self.name == other.name:
            return cmp(int(self.rev) or 0, int(other.rev) or 0)
        else:
            return cmp(self.name, other.name)


class ObsProject(object):

    def __init__(self, apiUrlOrAlias, projectName):
        self.name = projectName
        conf.get_config()
        tmpApiUrl = getApiUrlFromAlias(apiUrlOrAlias)
        self.apiUrl = tmpApiUrl if not tmpApiUrl.endswith("/") else tmpApiUrl[:-1]

    def getPackageList(self):
        """
        Returns the list of all package names of this project.
        Can raise `urllib2.HTTPError`, `urllib2.URLError` or `ElementTree.ParseError`.
        """
        # The following method does not work on public repositories :
        #   core.meta_get_packagelist(self.apiUrl, self.name)
        # This is why we have to use the WEB API and parse XML ourselves.
        url = self.apiUrl + "/source/" + self.name
        printdebug("Calling %s" % url)
        xmlResult = core.http_request("GET", url).read()
        packageList = list()
        xmlPackageDir = ElementTree.fromstring(xmlResult)
        for packageEntry in xmlPackageDir.iter("entry"):
            packageList.append(packageEntry.get("name"))
        return packageList

    def getPackageRevisions(self, packageName):
        """
        Returns a list of `PackageRevision` of packageName.
        Can raise `urllib2.HTTPError`, `urllib2.URLError` or `ElementTree.ParseError`.
        """
        revisionsList = list()
        url = self.apiUrl + "/source/" + self.name
        url += "/" + packageName + "/_history"
        printdebug("Calling %s" % url)
        try:

            res=core.http_request("GET", url)
            
            xmlResult = res.read()

        except HTTPError as e:
            if e.code == 404:
                printdebug("History not available for %s, trying to get undated last revision"
                           % packageName)
                return self.getPackageLastRevisions(packageName)
            else:
                raise

        xmlRevisionLists = ElementTree.fromstring(xmlResult)
        for xmlRevisionList in xmlRevisionLists.iter("revisionlist"):
            for xmlRevision in xmlRevisionList.iter("revision"):
                revision = PackageRevision(packageName,
                                           xmlRevision.get("rev", 1),
                                           xmlRevision.get("vrev", 1))
                for field in xmlRevision.iter():
                    if field.text is not None:
                        revision.__dict__[field.tag] = field.text
                revisionsList.append(revision)

        revisionsList.sort(reverse=True)
        return revisionsList

    def getPackageLastRevisions(self, packageName):
        """
        Returns a list of `PackageRevision` of packageName (probably
        only one).
        Can raise `urllib2.HTTPError`, `urllib2.URLError` or `ElementTree.ParseError`.
        """
        revisionsList = list()
        url = "%s/source/%s/%s" % (self.apiUrl, self.name, packageName)
        printdebug("Calling %s" % url)
        res=core.http_request("GET", url)
        xmlResult = res.read()
        directoryList = ElementTree.fromstring(xmlResult)
        for directory in directoryList.iter("directory"):
            revision = PackageRevision(packageName,
                                       directory.get("rev", 1),
                                       directory.get("vrev", 1))
            srcmd5 = directory.get("srcmd5")
            if srcmd5 is not None:
                revision.srcmd5 = srcmd5
                revisionsList.append(revision)
        return revisionsList

def main(apiUrl, projectName, outFilePath):
    try:
        outFile = open(outFilePath, "wu+", 1)
    except IOError as err:
        print >> sys.stderr, "Cannot write", outFilePath, ":", err.strerror
        return 3

    print >> outFile, "# This is a revision Tag file from an OBS project created by obstag."
    print >> outFile, "# That MD5 Tag is directly usable by obs2obscopy script."
    print >> outFile, "#"
    print >> outFile, "# Created :", time.strftime("%a, %b %d %H:%M:%S %Z %Y")
    print >> outFile, "# Project name : %s" % projectName
    print >> outFile, "#"
    print >> outFile, "# %38s | %6s | %20s | %s | %s |%s |%s " \
                  % ("pkgMd5", "pkgRev", "pkgDate", "pkgName", "pkgVersion",
                     "pkgOwner", "pkgComment")
    print >> outFile, "#"
    obsProject = ObsProject(apiUrl, projectName)

    packageList = list()
    try:
        packageList = obsProject.getPackageList()
    except HTTPError as e:
        print >> sys.stderr, "An error occured while retrieving package list: ", str(e)
        return 4
    except URLError:
        print >> sys.stderr, "The URL you gave seems to be invalid or unreachable:", apiUrl
        return 4
    except ElementTree.ParseError as pe:
        print >> sys.stdout, "An error occured while parsing package list:", str(pe)
        return 4

    if len(packageList) < 1:
        print "No package found in project %s !", projectName
        return 0

    print "Creating a revision tag of source packages as present in ", projectName
    packagesTaggedNumber = 0
    packageCounter = 0
    failedPackageList = []
    for packageName in packageList:
        packageCounter += 1
        nbError = 0
        while (nbError < MAX_RETRIES):
            nbError += 1
            try:
                print "(%d/%d) %s" % (packageCounter, len(packageList), packageName),
                if nbError > 1:
                    print "retry %d" % (nbError - 1)
                else:
                    print
                revisions = obsProject.getPackageRevisions(packageName)

                if len(revisions) > 0:
                    print >> outFile, revisions[0]
                else:
                    rev = PackageRevision(packageName, "", "")
                    print >> outFile, rev

                packagesTaggedNumber += 1
                nbError = 0
                break
            except ElementTree.ParseError as pe:
                print >> sys.stderr, "An error occured while parsing revision"\
                    + " list of package '%s': %s" % (packageName, str(pe))
            except HTTPError as e:
                print >> sys.stderr, "An error occured while retrieving package revision:",
                print >> sys.stderr, str(e)
        if nbError >= MAX_RETRIES:
            failedPackageList.append(packageName)

    outFile.close()

    print "\nFinal reports"
    print "   Total number packages tagged = ", packagesTaggedNumber
    print "   %s created" % outFilePath
    if len(failedPackageList) > 0:
        print "   Tag failed for packages: %s" % (", ".join(failedPackageList))
        return 1
    return 0


if __name__ == '__main__':
    if os.environ.has_key("DEBUG") and os.environ["DEBUG"] is not None:
        DEBUG_MODE = True
    if len(sys.argv) != 4:
        help_(sys.argv[0])
        sys.exit(2)
    apiUrl = sys.argv[1]
    projectName = sys.argv[2]
    outFilePath = sys.argv[3]
    try:
        sys.exit(main(apiUrl, projectName, outFilePath))
    except KeyboardInterrupt:
        print >> sys.stderr, "Interrupted by user..."
        sys.exit(5)
