#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4
#
# Copyright (C) 2010, 2011, 2012, 2013, 2014 Intel, Inc.
#
#    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; version 2
#    of the License.
#
#    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, write to the Free Software
#    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
#
"""This script will sync a merged change in gerrit to OBS corresponding project.
"""

import os
import sys
import tempfile
import shutil
import re
import atexit
import ast
from time import sleep
from urllib2 import HTTPError

# internal module
from common import utils
from common.upload_service import upload_obs_service, UploadError
from common.git import Git, clone_gitproject
from common import obspkg
from common.mapping import git_obs_map
from common.gerrit import Gerrit, get_gerrit_event, GerritError, is_ref_deleted
from common.send_mail import prepare_mail
from common.buildservice import BuildService

from gbp.rpm import SpecFile
from gbp.git.repository import GitRepositoryError
from gbp.errors import GbpError

WRONG_DATE_MSG = '- The date %s in tag does NOT follow correct format. You can'\
                 ' use shell command "date --utc +%%Y%%m%%d.%%H%%M%%S" to '\
                 'generate it, like 20120801.083113.'

UNDER_REVIEW_MSG = '- Submission %s has been rejected because tagged commit %s'\
                   ' is still under review in gerrit. Please re-trigger '\
                   'submission after your change is accepted.'

WRONG_COMMIT_MSG = '- The commit %s tag attached does NOT exist in git tree or'\
                   ' gerrit open change. Please make sure the commit has been '\
                   'pushed to gerrit and correct magical ref refs/for/branch, '\
                   'then re-submit tag.'

UNKNOWN_FORMAT_MSG = '- Unknown tag format, please follow the format '\
                     'submit/{version}/{date.time}.'

NOT_ANNOTATED_MSG = '- Tag should be annotated tag.'

SUGGESTION = 'Suggest to use "gbs submit" to trigger submission, or use '\
             'following command to create tag "git tag '\
             'submit/${version}/$(date --utc +%%Y%%m%%d.%%H%%M%%S) -m "****" ".'

TITLE_FAILED = '[Submit Request Failed]: tag: %s in %s'

EMAIL_FOOTER = '\n\n--------------------------------------------------------\n'\
               'Automatically generated by backend service.\n'\
               'Please DO NOT Reply!'

def send_mail(title, msg, receiver):
    """ post message back to gerrit and send mail to tag owner """

    if 'author' in receiver and 'email' in receiver:
        msg = 'Hi, %s,\n\n' % receiver['author'] + msg + EMAIL_FOOTER
        prepare_mail("%s.env" % os.getenv('BUILD_TAG'), title, msg,
                     os.getenv('NOREPLY_EMAIL_SENDER'), receiver['email'])

def parse_submit_tag(tag):
    """parse info from submit tag name"""

    branch = None
    date = None

    if tag.startswith('submit/'):
        pos = tag.rfind('/', len('submit/'))
        if pos != -1:
            branch = tag[len('submit/'):pos]
            if branch == 'trunk':
                branch = 'master'
            date = tag[pos+1:]

    return branch, date

def find_submit_tag(event, mygit):
    """find the corresponding submit tag for this event"""

    if event['event_type'] == 'ref-updated':
        tag = event['refname'][len('refs/tags/'):]
        event['branch'] = parse_submit_tag(tag)[0]
        # Since patchset_revision is used in gerrit feedback, real tag check
        # is needed; and the key point is parse_submit_tag can not ensure the
        # tag exsisting too.
        try:
            event['patchset_revision'] = mygit.rev_parse('%s^{commit}' % tag)
        except GitRepositoryError:
            tag = None
    elif event['event_type'] == 'change-merged':
        # for chanage-merged, search submit tag on this commit
        branch = event['branch']
        if event['branch'] == 'master':
            branch = 'trunk'
        try:
            tag = mygit.describe(event['patchset_revision'],
                                 pattern='submit/%s/*' % branch,
                                 exact_match=True)
        except GitRepositoryError:
            # don'n find submit tag on this commit, return None
            tag = None

    return tag

def check_tag_format(git, mygerrit, event, tag):
    """check whether tag follow proper format"""

    branch, date = parse_submit_tag(tag)
    message = []
    psr = event['patchset_revision']

    # check tag name format
    if branch and date:
        # check date format
        pattern = re.compile(r'^[0-9]{8}\.[0-9]{6}$')
        if not pattern.match(date):
            message.append(WRONG_DATE_MSG % date)

        if not git.branch_contains(tag):
            # Check if change is still under review
            cmd = '--current-patch-set status: open project: %s commit: %s' % \
                      (event['project'], psr)
            gerritinfo = mygerrit.query(cmd)
            if len(gerritinfo) == 1 and 'number' in gerritinfo[0] \
                    and 'currentPatchSet' in gerritinfo[0]:
                if gerritinfo[0]['branch'] == branch:
                    # the tagged commit still open, abort submit this time
                    message.append(UNDER_REVIEW_MSG % (tag, psr))
            else:
                # cannot find tagged commit in git tree or gerrit open change
                message.append(WRONG_COMMIT_MSG % psr)
    else:
        # wrong tag format
        message.append(UNKNOWN_FORMAT_MSG)

    # check whether tag is an annotated tag
    tagger = git.get_tag(tag)
    if 'author' not in tagger or 'email' not in tagger:
        message.append(NOT_ANNOTATED_MSG)

    # post comment to gerrit and send email if check failed
    if message:
        print message

        msg = 'The tag %s was pushed, but it was not completed because of '\
                'the following reason(s):\n\n' % tag + '\n'.join(message)

        if len(message) != 1 or (message[0] != UNDER_REVIEW_MSG % (tag, psr) \
           and message[0] != WRONG_COMMIT_MSG % psr):
            msg += '\n\n' + SUGGESTION

        try:
            mygerrit.review(commit=psr, message=msg)
        except GerritError, err:
            print >> sys.stderr, 'Error posting review comment '\
                                 'back to Gerrit: %s' % str(err)
            # return 1 if this exception is not caused by invalid commit
            if 'no such patch set' not in str(err):
                return False

        send_mail(TITLE_FAILED % (tag, event['project']), msg, tagger)

        return False

    return True

def find_specfile(prj_dir, packaging_dir, tag, event, tagger, pkg_name=None, debug_prj_name=None):
    """search specfile under packaging directory"""

    msg = ''

    if True:
        specs = utils.find_spec('%s/%s' % (prj_dir, packaging_dir))
        if not specs:
            # no spec exist under packaging, use default name
            msg = "The tag %s pushed, but packaging directory doesn't "\
                    "contain any spec file. Please create one and re-submit "\
                    "it." % tag
        elif len(specs) == 1:
            # only one spec exist under packaging
            spec = specs[0]
            print 'Single spec. Use %s' % os.path.basename(spec)
        else:
            # multiple specs exist under packaging, use default name
            spec = None
            if pkg_name:
                spec = '%s/%s/%s.spec' % (prj_dir, packaging_dir, pkg_name)
                print 'Multiple specs. Try %s' % os.path.basename(spec)
            if spec is None or not os.path.isfile(spec):
                spec = '%s/%s/%s.spec' % (prj_dir, packaging_dir, \
                        os.path.basename(event['project']))
                print 'Multiple specs with no pkg_name.spec. Try %s' % os.path.basename(spec)
            if not os.path.isfile(spec):
                spec = sorted(specs)[0]
                print 'Multiple sorted(specs)[0] %s' % os.path.basename(spec)
            if not os.path.isfile(spec):
                msg = "The tag %s pushed, but packaging directory contains "\
                        "multiply spec files, backend service can not decide "\
                        "which spec file to use. Please use OBS_PACKAGE "\
                        "parameter in scm/git-obs-mapping project to specify "\
                        "the target spec file or contact system "\
                        "administrator for more details." % tag

    if msg:
        if debug_prj_name is not None:
            msg = "TARGET_PROJECT: %s\n%s" % (debug_prj_name, msg)
        print msg
        send_mail(TITLE_FAILED % (tag, event['project']), msg, tagger)
        return None
    else:
        print 'specfile %s' % spec
        return spec

def parse_specfile(specfile, tag, event, tagger):
    """parse specfile"""

    spec = None

    try:
        # use gbp to parse specfile
        spec = SpecFile(specfile)
    except GbpError, err:
        print 'gbp parse spec failed. %s' % err
        msg = 'The tag %s pushed, but backend service failed to parse %s. '\
                'Please try "gbs export" on this tag and make sure it can '\
                'work.\n\n'\
                'Error message:\n'\
                '%s' % (tag, os.path.basename(specfile), err)
        send_mail(TITLE_FAILED % (tag, event['project']), msg, tagger)
        return None

    return spec

def check_obs_project(apiurl, apiuser, apipasswd, obs_prjs):
    """check whether the specific project exist in obs
       return the list of non-exist project
    """

    obs = BuildService(apiurl, apiuser, apipasswd)

    return [prj for prj in set(obs_prjs) if not obs.exists(prj)]

def check_sync_pattern(event, pattern_str):
    """
    Check whether need sync this ref.
    Return: True if need, False otherwise.
    """
    pattern = ast.literal_eval(pattern_str)
    for item in pattern:
        if re.match(item['project'], event['project']) and \
            re.match(item['refname'], event['refname']):
            return True

    return False

def sync_ref(event, git, user, host, port):
    """
    Push ref to gerrit. Create project if necessary
    Return: True if sync was successful, False otherwise.
    """

    try:
        mygerrit = Gerrit(host, user, port)
        mygerrit.create_project(event['project'], '-p',
                                os.getenv('GERRIT_PARENT_PROJECT'))
    except GerritError, err:
        print >> sys.stderr, "Error creating gerrit project %s: %s" % \
                             (event['project'], str(err))

    repo = 'ssh://%s@%s:%s/%s' % (user, host, port, event['project'])
    refname = event['refname']
    try:
        if event['newrev'] == '0000000000000000000000000000000000000000':
            git.push(repo=repo, src=':%s' % event['refname'])
        elif event['refname'].startswith("refs/tags/"):
            git.push(repo=repo, src=refname, tags=True, force=True)
        else:
            git.push(repo=repo, src='origin/%s' % refname,
                     dst='refs/heads/%s' % refname, tags=True, force=True)
    except GitRepositoryError, err:
        print >> sys.stderr, "Error pushing %s to %s: %s" % (refname, repo, err)
        return False

    return True

def main():
    """script entry point"""

    print '---[JOB STARTED]----------------------------------------'

    # prepare related global variables
    workspace = os.getenv('WORKSPACE')
    apiurl = os.getenv('OBS_API_URL')
    apiuser = os.getenv('OBS_API_USERNAME')
    apipasswd = os.getenv('OBS_API_PASSWD')

    event = get_gerrit_event()

    if event['event_type'] != "ref-updated":
        # This is just a sanity check as ref-updated is the only event we
        # react on and it's configured in the job configuraion
        print >> sys.stderr, "Configuration error: This job can't process"\
                             "event %s! Only ref-updated events are allowed" % \
                             event['event_type']
        return 1

    # check whether need to sync this project
    pattern = os.getenv('SYNC_GERRIT_PROJECT_PATTERN')
    if pattern and not check_sync_pattern(event, pattern):
        print 'Do not need to sync project %s, exit now' % event['project']
        return 0

    if event['refname'].startswith('refs/changes/'):
        print 'Do not need to process refname %s, exit now' % event['refname']
        return 0

    # prepare separate temp directory for each build
    tmpdir = tempfile.mkdtemp(prefix=workspace+'/')
    atexit.register(shutil.rmtree, tmpdir)
    prjdir = os.path.join(tmpdir, event['project'])

    # clone gerrit project to local dir
    if not clone_gitproject(event['project'], prjdir):
        print >> sys.stderr, 'Error cloning %s' % event['project']
        return 1
    mygit = Git(prjdir)

    # sync refs if it's sync instance
    sync_user = os.getenv('SYNC_GERRIT_USERNAME')
    sync_host = os.getenv('SYNC_GERRIT_HOSTNAME')
    sync_port = os.getenv('SYNC_GERRIT_SSHPORT')
    if sync_user and sync_host:
        if not sync_ref(event, mygit, sync_user, sync_host, sync_port):
            return 1

    # check whether tag name is start with 'submit/'
    if not event['refname'].startswith('refs/tags/submit/'):
        print '\nREFNAME "%s" isn\'t start with refs/tags/submit, exit now'\
              % event['refname']
        return 0
    elif is_ref_deleted(event['oldrev'], event['newrev']):
        print '\nREFNAME "%s" is deleted, exit now' % event['refname']
        return 0

    # quit directly if project do not map to any OBS project
    if not git_obs_map(event['project']):
        print '\nThis project do not map to any OBS project, exit now'
        return 0

    mygerrit = Gerrit(event['hostname'], event['username'], \
            event['sshport'], int(os.getenv('GERRIT_SILENT_MODE')))

    tag = find_submit_tag(event, mygit)
    if not tag:
        print '\nThis commit don\'t contain submit/*/* tag, exit now'
        return 0

    # check whether tag meet format
    if not check_tag_format(mygit, mygerrit, event, tag):
        return 0

    tagger = mygit.get_tag(tag)
    commitinfo = mygit.get_commit_info(event['patchset_revision'])

    # parse git-obs-mapping to get OBS target project
    obstargets = git_obs_map(event['project'], event['branch'])
    print 'git-obs-mapping:', event['project'], event['branch'], obstargets

    packagingdir = utils.parse_link('%s/%s' % (prjdir, 'packaging'))
    print 'packaging dir is %s/%s' % (prjdir, packagingdir)

    # checkout submit tag
    mygit.checkout(tag)

    #connect OBS-service
    build = BuildService(apiurl, apiuser, apipasswd)

    # generate tarball and submit to obs
    for target in obstargets:
        mygit.clean(directories=True, force=True)
        obs_dst_prj = target['OBS_project']
        obs_stg_prj = target['OBS_staging_project']
        obs_pkg = target['OBS_package']
        if not obs_dst_prj:
            continue

        # skip projects using prerelease or abs workflow
        if obs_stg_prj == 'prerelease' or obs_stg_prj == 'abs':
            print "%s has been switched to prerelease or abs workflow."\
                  " Skipping ..." % obs_dst_prj
            continue

        print "\n...Processing target %s" % target
        if not obs_stg_prj:
            obs_stg_prj = obs_dst_prj

        # check whether these projects specified in git-obs-mapping exist in obs
        non_exist_prjs = check_obs_project(apiurl, apiuser, apipasswd, \
                [obs_dst_prj, obs_stg_prj])
        if non_exist_prjs:
            # SYNC_GERRIT_HOSTNAME exists only on sync instances
            if os.getenv('SYNC_GERRIT_HOSTNAME'):
                # for sync instances we don't need to sync this obs project
                continue
            else:
                # for normal instance it means configuration mistake in git->obs
                # mapping
                for prj in non_exist_prjs:
                    print "Project %s doesn't exist in OBS" % prj
                return 1

        if 'OBS_use_specname' in target and target['OBS_use_specname'] == 'yes':
            # search specfile under packaging directory
            specfile = find_specfile(prjdir, packagingdir, tag, event, tagger, \
                    obs_pkg, debug_prj_name=obs_dst_prj)
            if not specfile:
                return 0
            # parse specfile
            spec = parse_specfile(specfile, tag, event, tagger)
            if not spec:
                return 0
            package = spec.name
            print 'spec name = %s' % spec.name
        else:
            # get package name from xml files.
            if obs_pkg:
                package = obs_pkg
            else:
                #package name from base name of git path.
                package = os.path.basename(event['project'])
        print 'package name = %s' % package

        retry_count = 3
        while retry_count > 0:
            try:
                if obs_stg_prj != obs_dst_prj:
                    tmppkg = obspkg.ObsPackage(tmpdir, obs_stg_prj, 'tmp', \
                            apiurl, apiuser, apipasswd)
                    if tmppkg.is_new_pkg():
                        tmppkg.commit("Leave an empty package in this "\
                                "project to prevent OBS delete it "\
                                "automatically when all request from "\
                                "here are accepted.")

                print '\n Upload _service file to replace update package to OBS'
                url = 'ssh://%s:%s' % (os.getenv('GERRIT_HOSTNAME_EXTERNAL'),
                                    os.getenv('GERRIT_SSHPORT'))
                gerrit_project = os.getenv('GERRIT_PROJECT')

                # create local package
                print '\nCheckout %s/%s to local' % (obs_stg_prj, package)
                localpkg = obspkg.ObsPackage(tmpdir, obs_stg_prj, package, \
                        apiurl, apiuser, apipasswd)
                commit_msg = 'Submitter: %s <%s>\nComments: %s\nGit project: '\
                        '%s\nTag: %s\nCommit: %s %s' % (tagger['author'], \
                        tagger['email'], tagger['message'], event['project'], \
                        tag, commitinfo['id'], commitinfo['subject'])
                # upload _service to obs
                try:
                    upload_obs_service(url, gerrit_project, tag,
                            event['newrev'], obs_stg_prj, build, package)
                except UploadError, err:
                    print err
                    return 1

                # create SR
                status = True
                if obs_stg_prj != obs_dst_prj:
                    sr_retry_count = 10
                    while sr_retry_count > 0:
                        try:
                            sleep(30)
                            newreq = localpkg.submit_req(obs_dst_prj,
                                                    msg=commit_msg)
                            print 'New request %s is created' % newreq
                            status = True
                            break
                        except HTTPError, err:
                            print 'New request failed to be created'
                            print err
                            status = False
                            if err.msg != "Bad Request":
                                break
                            sr_retry_count -= 1
                if status:
                    break
                retry_count -= 1
            except Exception, err:
                print 'obs operation failed, retrying...'
                print err
                sleep(1)
                retry_count -= 1

        if not retry_count:
            return 1

        # post sr info back to gerrit
        comment = '- Submitter: %s <%s>\n- Comments: %s\n- Git project: '\
                '%s\n- Tag: %s\n- Commit: %s %s' % (tagger['author'], \
                tagger['email'], tagger['message'], event['project'], tag, \
                commitinfo['id'], commitinfo['subject'])
        if obs_stg_prj != obs_dst_prj:
            # submit to :build project and create a SR
            requrl = ' %s/request/show/%s' % (os.getenv('OBS_URL_EXTERNAL'), newreq)
            comment = 'A SR (Submit Request) has been triggered to submit '\
                    'the commit to OBS %s project.\n' % obs_dst_prj + \
                      comment + '\n' + '- Request URL:%s' % requrl
        else:
            # submit to final target project directly
            comment = 'This commit has been submitted to OBS %s project.\n' \
                    % obs_dst_prj + comment
        try:
            mygerrit.review(commit=event['patchset_revision'],
                            message=comment)
        except GerritError, err:
            print >> sys.stderr, 'Error posting review comment '\
                                 'back to Gerrit: %s' % str(err)
            # return 1 if this exception is not caused by invalid commit
            if 'no such patch set' not in str(err):
                return 1

    return 0

if __name__ == '__main__':
    sys.exit(main())
