#!/usr/bin/python
# Copyright Cloud Linux Zug GmbH
__author__ = 'iseletsk'

import glob
import errno
import getopt
import shutil
import os
from os.path import join
import sys

BASE_DIR = '/usr/share/kcare-eportal/patches/'
LATEST_DIR = join(BASE_DIR, "release", "release-latest")
LATEST_FORMATS = ('latest.v1', 'latest.v2')


def link_file(name, dir, dest_dir):
    full_name = join(dest_dir, name)
    if os.path.exists(full_name) and not os.path.islink(full_name):
        os.unlink(full_name)
    if not os.path.exists(full_name):
        try:
            link_name = join(LATEST_DIR, dir, name)
            os.symlink(link_name, full_name)
        except OSError, e:
            if e.errno == errno.EEXIST:
                pass
            else:
                raise e


def dereference_link(link):
    fin = open(link, 'rb')
    os.unlink(link)
    fout = open(link, 'wb')
    shutil.copyfileobj(fin, fout)
    fout.close()
    fin.close()


def get_latest_format(directory):
    for latest in reversed(LATEST_FORMATS):
        path = join(directory, latest)
        if os.path.exists(path):
            return latest
    else:
        raise IOError('Unable to find release number. File %s not found.'
                      % join(directory, 'latest.vX'))


def cmp_latest_format(a, b):
    ind_a = LATEST_FORMATS.index(a)
    ind_b = LATEST_FORMATS.index(b)
    return cmp(ind_a, ind_b)


def find_latest_v1(latest, source, khash):
    return latest


def process_latest_file(latest, kernel_hash, destination, source):
    # a crutch to workaround bugs in previous deployments
    dest_v2 = join(destination, 'latest.v2')
    if os.path.islink(dest_v2) and not os.path.exists(dest_v2):
        print("Fixing previous release...")
        # there exist a broken link to latest.v2
        # it means that latest.v1 in release should be renamed to latest.v2
        link1 = join(destination, 'latest.v1')
        link2 = join(destination, 'latest.v2')
        link2_src = os.readlink(link2)

        if os.path.exists(os.path.dirname(link2_src)):
            if os.path.islink(link1):
                os.rename(os.readlink(link1), link2_src)
            elif os.path.isfile(link1):
                os.rename(link1, link2_src)

            # and latest.v1 should be restored to latest avaliable latest.v1
            shutil.copy(link2_src, link1)
        else:
            print("Unable to find the source directory of the kenrel patch {0}"
                  .format(destination))

    try:
        # detect latest file format used in 'directory'
        current_latest = get_latest_format(destination)
    except IOError:
        current_latest = LATEST_FORMATS[0]

    if cmp_latest_format(latest, current_latest) < 0:
        # release use an older version, we should fix it
        print "Fixing latest version to", current_latest
        os.rename(join(source, latest), join(source, current_latest))
        latest = current_latest

    link_file(latest, kernel_hash, destination)

    # dereference links to older latest files
    for latest in LATEST_FORMATS[:LATEST_FORMATS.index(latest)]:
        link = join(destination, latest)
        if os.path.exists(link) and os.path.islink(link):
            dereference_link(link)


def link_dir(dir, commit):
    print "Processing %s" % dir
    # /usr/share/kcare-eportal/patches/$hash
    dest_dir = join(BASE_DIR, dir)
    # /usr/share/kcare-eportal/patches/release/$release/$hash
    src_dir = join(SOURCE_DIR, dir)

    if not os.path.exists(dest_dir):
        os.makedirs(dest_dir)

    latest = get_latest_format(src_dir)
    with open(join(src_dir, latest)) as f:
        release = f.read()

    try:
        release_level = int(release)
    except (ValueError, TypeError) as e:
        raise Exception("Unable to parse release number: %s" % e)

    release_dir = join(dest_dir, release)
    if os.path.lexists(release_dir):
        try:
            os.unlink(release_dir)
        except OSError, e:
            if e.errno == errno.EISDIR:
                shutil.rmtree(release_dir)
            else:
                raise e

    os.symlink(join(src_dir, release), release_dir)

    # process fixups
    prev_release_level = release_level - 1
    prev_release_dir = join(dest_dir, str(prev_release_level))

    src_level = prev_release_level
    prev_src_dir = join(src_dir, str(prev_release_level))

    # find previous release directory
    while src_level > 0 and os.path.exists(prev_src_dir):
        while prev_release_level > 0:
            if os.path.exists(prev_release_dir):
                kpatch_fixups_mask = os.path.join(prev_src_dir, 'kpatch.*fixups')
                for kpatch_fixups_file in glob.glob(kpatch_fixups_mask):
                    if os.path.exists(kpatch_fixups_file):
                        fixups_file_basename = os.path.basename(kpatch_fixups_file)
                        kpatch_fixups_dest = join(prev_release_dir, fixups_file_basename)
                        shutil.copy(kpatch_fixups_file, kpatch_fixups_dest)
                        shutil.copy(kpatch_fixups_file + '.sig', kpatch_fixups_dest + '.sig')

                        with open(kpatch_fixups_file) as f:
                            for fixup_basename in f:
                                fixup_basename = fixup_basename.strip()
                                if not fixup_basename:
                                    continue

                                fixup_src_name = join(prev_src_dir, fixup_basename)
                                fixup_dest_name = join(prev_release_dir, fixup_basename)
                                shutil.copy(fixup_src_name, fixup_dest_name)
                                shutil.copy(fixup_src_name + '.sig', fixup_dest_name + '.sig')

                prev_release_level -= 1
                prev_release_dir = join(dest_dir, str(prev_release_level))

                break

            prev_release_level -= 1
            prev_release_dir = join(dest_dir, str(prev_release_level))

        src_level -= 1
        prev_src_dir = os.path.join(src_dir, str(src_level))

    link_file('kcare.ko', dir, dest_dir)
    link_file('kcare.ko.sig', dir, dest_dir)
    link_file('version', dir, dest_dir)

    if commit:
        process_latest_file(latest, dir, dest_dir, src_dir)

    # do release-latest
    rl_dir = join(LATEST_DIR, dir)
    if os.path.exists(rl_dir) and os.path.islink(rl_dir):
        os.unlink(rl_dir)
    print(src_dir, rl_dir)
    os.symlink(src_dir, rl_dir)


def link_json():
    patches_filename = 'patches.json'
    lpatches = join(LATEST_DIR, patches_filename)
    if os.path.exists(lpatches):
        os.unlink(lpatches)
    os.symlink(join(SOURCE_DIR, 'patches.json'), lpatches)
    base_patches_json = join(BASE_DIR, patches_filename)
    if not os.path.exists(base_patches_json):
        os.symlink(lpatches, base_patches_json)


def process_latest(commit):
    for dir in os.listdir(SOURCE_DIR):
        dirname = join(SOURCE_DIR, dir)
        if os.path.isdir(dirname):
            link_dir(dir, commit)
        if commit:
            link_json()
    print "All Done"


def usage():
    print 'Usage: %s [OPTIONS] release_tag' % (sys.argv[0])
    print
    print 'deploys release RELEASE_TAG as latest release'
    print 'patches for the RELEASE_TAG should be located in ' \
          '/usr/share/kcare-eportal/patches/release/RELEASE_TAG'
    print '-p | --production         : deploy to production, otherwise test assumed'
    print '-h | --help               : show this help'
    print '--24h                     : deploy to 24h folder'
    print '--48h                     : deploy to 48h folder'
    print '--prefix                  : specify a custom deploy folder'
    print '--commit [Yes/no]         : apply all changes as stable'


def main():
    global BASE_DIR, SOURCE_DIR, LATEST_DIR
    try:
        opts, args = getopt.getopt(
            sys.argv[1:],
            'h:p', ['production', 'help', '24h', '48h', 'prefix=', "commit="])
    except getopt.GetoptError, e:
        print "ERROR: %s" %(str(e),)
        usage()
        sys.exit(1)

    prefix = 'test/'
    commit = True
    for o, a in opts:
        if o in ['-h', '--help']:
            usage()
            sys.exit(0)
        elif o in ['-p', '--production']:
            prefix = ''
        elif o in ['--24h']:
            prefix = '24h/'
        elif o in ['--48h']:
            prefix = '48h/'
        elif o in ['--prefix']:
            if not a:
                print "--prefix argument is required"
                sys.exit(1)
            prefix = a
        elif o in ["--commit"]:
            if not a:
                print "--commit argument is required"
                sys.exit(1)
            commit = a.lower() in [1, 'y', 'yes', 'true']

    if len(args) != 1:
        print "RELEASE TAG required"
        usage()
        sys.exit(1)

    BASE_DIR = os.path.abspath(join(BASE_DIR, prefix))
    if not os.path.exists(BASE_DIR):
        print "Creating directory: %s" % BASE_DIR
        os.makedirs(BASE_DIR)

    SOURCE_DIR = join(BASE_DIR, 'release', args[0])
    LATEST_DIR = join(BASE_DIR, 'release', 'release-latest')

    if not os.path.exists(SOURCE_DIR):
        print "%s doesn't exist" % SOURCE_DIR
        sys.exit(1)
    if not os.path.exists(LATEST_DIR):
        try:
            os.makedirs(LATEST_DIR)
        except OSError as e:
            print "Unable to create %s: %s" % (LATEST_DIR, e)
            sys.exit(1)
    if not prefix:
        deploy_type = "PRODUCTION"
    else:
        deploy_type = prefix[:-1].upper()

    print "Deploying to %s, release %s" % (deploy_type, args[0])
    process_latest(commit)


if __name__ == '__main__':
    main()
