#!/usr/bin/env python3

import atexit
import os
import random
import re
import shutil
import subprocess
import sys
import tarfile
import time
import urllib.parse

from eobuilder import VERSION, init, settings
from eobuilder.changelog import changelog_from_git
from eobuilder.cmdline import call, cat, error, output, parse_cmdline, setup_py, touch

# fix urljoin for git+ssh:// URLs
if 'git+ssh' not in urllib.parse.uses_relative:
    urllib.parse.uses_relative.append('git+ssh')


def rm_recursive(path):
    if os.path.exists(path):
        shutil.rmtree(path)


def smart_cleaning(files_path):
    now = time.time()
    project_files = {}
    for file_path in files_path:
        project_name = os.path.basename(file_path).split('_')[0]
        if project_name not in project_files:
            project_files[project_name] = []
        project_files[project_name].append(file_path)

    for project in project_files.iterkeys():
        nb_versions = len(project_files[project])
        if nb_versions > settings.MIN_PACKAGE_VERSIONS:
            project_files[project] = sorted(project_files[project], key=lambda x: os.stat(x).st_mtime)
            for filename in project_files[project]:
                if (
                    nb_versions > settings.MIN_PACKAGE_VERSIONS
                    and os.stat(filename).st_mtime < now - settings.MIN_AGE * 86400
                ):
                    os.remove(filename)
                    nb_versions -= 1


def clean(method):
    print('+ Cleanning %s' % method)
    if method == 'all':
        shutil.rmtree(settings.ORIGIN_PATH)
        shutil.rmtree(settings.PBUILDER_RESULT)
        shutil.rmtree(settings.GIT_PATH)
        shutil.rmtree(settings.LOCK_PATH)
    elif method == 'git':
        shutil.rmtree(settings.GIT_PATH)
    elif method == 'deb':
        shutil.rmtree(settings.PBUILDER_RESULT)
    elif method == 'archives':
        shutil.rmtree(settings.ORIGIN_PATH)
    elif method == 'locks':
        shutil.rmtree(settings.LOCK_PATH)
    elif method == 'smart':
        results_files = []
        origin_files = [os.path.join(settings.ORIGIN_PATH, f) for f in os.listdir(settings.ORIGIN_PATH)]
        for root, dirs, files in os.walk(settings.PBUILDER_RESULT):
            for fname in files:
                results_files.append(os.path.join(root, fname))
        smart_cleaning(results_files)
        smart_cleaning(origin_files)
        now = time.time()
        for root, dirs, files in os.walk(settings.PBUILDER_RESULT):
            for fname in files:
                fname = os.path.join(root, fname)
                ext = os.path.splitext(fname)
                if ext == 'build' and os.stat(fname).st_mtime < now - 365 * 86400:
                    os.remove(fname)
    else:
        error("Cleanning: unknow '%s' option" % method)


def get_project_infos(git_project_path, cmd_options):
    """return a dict with project informations"""
    os.chdir(git_project_path)
    results = {
        'name': '',
        'version': '',
        'fullname': '',
        'ac_version': '',
        'build_dir': '',
        'build_branch': cmd_options.branch,
        'commit_number': '',
        'git_path': git_project_path,
    }
    if os.path.exists('setup.py'):
        # Hack to support setup_requires
        setup_py('--help')
        results['name'] = setup_py('--name 2> /dev/null')[:-1]
        results['version'] = setup_py('--version 2> /dev/null')[:-1]
        results['fullname'] = setup_py('--fullname 2> /dev/null')[:-1]
    elif os.path.exists('configure.ac'):
        call('./autogen.sh')
        call('make all')
        results['name'] = output("./configure --version | head -n1 | sed 's/ configure.*//'")[:-1]
        results['ac_version'] = output("./configure --version | head -n1 | sed 's/.* configure //'")[:-1]
        results['version'] = results['ac_version'].replace('-', '.')
        results['fullname'] = results['name']
    elif os.path.exists('Makefile'):
        results['name'] = output('make name')[:-1]
        results['version'] = output('make version')[:-1]
        results['fullname'] = output('make fullname')[:-1]
    else:
        error('Unsupported project type', exit_code=2)

    results['build_dir'] = os.path.join(
        settings.EOBUILDER_TMP, '%s-%d' % (results['name'], random.randint(0, 1000000))
    )
    atexit.register(rm_recursive, results['build_dir'])
    results['commit_number'] = output('git rev-parse HEAD')[:-1]
    results['lock_path'] = os.path.join(settings.LOCK_PATH, results['name'])
    current_tag = output('git describe --abbrev=0 --tags --match=v*', exit_on_error=False)
    if current_tag:
        results['current_tag'] = current_tag[1:-1]
    else:
        results['current_tag'] = '0.0'

    return results


def prepare_build(dist, project, cmd_options, new):
    """
    Create origin archive, update git and Debian changelog
    """
    package = {
        'repository': settings.DEFAULT_UNSTABLE_REPOSITORIES[dist],
        'version': '',
        'source_name': '',
        'names': [],
    }
    if cmd_options.hotfix:
        package['repository'] = settings.HOTFIX_REPOSITORIES[dist]
    os.chdir(project['git_path'])
    build_branch = cmd_options.branch
    if cmd_options.hotfix and not build_branch.startswith('hotfix/'):
        return error('Invalid name for hotfix branch (must start with hotfix/)', exit_code=2)
    debian_folder = cmd_options.debian_folder
    if os.path.isdir('debian-' + dist) and debian_folder == 'debian':
        debian_folder = 'debian-' + dist
    debian_branch = None
    if not os.path.isdir(debian_folder):
        debian_branch = 'debian'
        debian_folder = 'debian'
        branches = output('git branch -r -l')
        if debian_branch == 'debian' and 'debian-%s' % dist in branches:
            debian_branch = 'debian-' + dist
        if not 'origin/%s' % debian_branch in output('git branch -r -l'):
            print('!!! WARNING: cannot build for dist %s, no debian directory found' % dist)
            return
        print('!!! WARNING obsolete: using a branch for debian/ packaging')
        print('+ Updating Debian branch for %s' % dist)
        call('git checkout --quiet %s' % debian_branch)
        call('git pull')
    else:
        print('+ Building from %s debian folder' % debian_folder)
    for r in cmd_options.repositories:
        repo = r.split(':')
        if repo[0] == dist:
            package['repository'] = repo[1]

    # get package source name
    control_file = os.path.join(debian_folder, 'control')
    package['names'] = re.findall(r'Package\s*:\s*(.*?)\n', cat(control_file))
    package['source_name'] = re.search(r'^Source\s*:\s*(.*?)\n', cat(control_file), re.MULTILINE).group(1)

    # build tarball
    origin_archive = os.path.join(
        settings.ORIGIN_PATH, '%s_%s.orig.tar.bz2' % (package['source_name'], project['version'])
    )
    if not os.path.exists(origin_archive):
        print('+ Generating origin tarball ...')
        os.chdir(project['git_path'])
        call('git checkout --quiet %s' % build_branch)
        if os.path.exists('setup.py'):
            setup_py('clean --all')
            setup_py('sdist --formats=bztar')
            shutil.move('dist/%s.tar.bz2' % project['fullname'], origin_archive)
        elif os.path.exists('./configure.ac'):
            call('make dist-bzip2')
            shutil.move('%s-%s.tar.bz2' % (project['name'], project['ac_version']), origin_archive)
        elif os.path.exists('Makefile'):
            call('make dist-bzip2')
            shutil.move('sdist/%s.tar.bz2' % project['fullname'], origin_archive)
        else:
            error('Unsupported project type', project['build_dir'], exit_code=2)

    last_version_file = os.path.join(
        project['lock_path'],
        '%s_%s_%s.last_version' % (project['name'], package['repository'], build_branch.replace('/', '_')),
    )
    debian_changelog = os.path.join(debian_folder, 'changelog')
    if os.path.exists(last_version_file):
        last_debian_package_version = cat(last_version_file)
    else:
        last_debian_package_version = re.search(
            r'^Version:\s(.*?)$', output('dpkg-parsechangelog -l%s' % debian_changelog), re.MULTILINE
        ).group(1)
    last_version = last_debian_package_version.split('-')[0]
    package['version'] = last_debian_package_version

    if cmd_options.native:
        debian_revision_number = ''
    else:
        debian_revision_number = '-1'

    if last_version == project['version'] and new and '~eob' in last_debian_package_version:
        new_inc = int(last_debian_package_version.rsplit('+', 1)[-1]) + 1
        version_suffix = '%s~eob%s+%s' % (debian_revision_number, settings.DEBIAN_VERSIONS[dist], new_inc)
    else:
        version_suffix = '%s~eob%s+1' % (debian_revision_number, settings.DEBIAN_VERSIONS[dist])
    call('git checkout --quiet %s' % build_branch)
    changelog = '\n'.join(
        changelog_from_git(
            package['source_name'],
            version_suffix,
            project['git_path'],
            package['repository'],
            epoch=cmd_options.epoch,
        )
    )
    if changelog:
        if not os.path.isdir(debian_folder):
            call('git checkout --quiet %s' % debian_branch)
        debian_generated_changelog_filename = debian_changelog + '.generated'
        with open(debian_generated_changelog_filename, 'w+') as f:
            f.write(changelog)
        package['version'] = re.search(
            r'^Version:\s(.*?)$',
            output('dpkg-parsechangelog -l%s' % debian_generated_changelog_filename),
            re.MULTILINE,
        ).group(1)
        os.unlink(debian_generated_changelog_filename)
    else:
        # changelog couldn't be generated, this happens with checkouts from
        # dgit, at least.
        package['version'] = ''

    if os.path.exists(project['build_dir']):
        shutil.rmtree(project['build_dir'])
    os.makedirs(project['build_dir'], 0o755)

    if package['version'].split('-')[0].split(':')[-1] == project['version']:
        # the generated changelog has the right version number, use it.
        good_changelog_contents = changelog
    else:
        # wrong version number, in that case we add an arbitrary new entry
        # to the existing changelog
        package['version'] = '%s%s' % (project['version'], version_suffix)
        call(
            'dch "Eobuilder version" -v %s --distribution %s \
                 --force-bad-version --force-distribution --changelog %s'
            % (
                (
                    '%s:%s' % (cmd_options.epoch, package['version'])
                    if cmd_options.epoch
                    else package['version']
                ),
                package['repository'],
                debian_changelog,
            )
        )
        good_changelog_contents = open(debian_changelog).read()

    if cmd_options.hotfix:
        version_part = build_branch.split('/', 1)[1].lstrip('v')
        if not project['version'].startswith(version_part):
            return error('Invalid name for hotfix branch (must start with version number)', exit_code=2)

    build_file = os.path.join(
        project['lock_path'],
        '%s_%s_%s_%s.build'
        % (project['name'], package['version'], package['repository'], build_branch.replace('/', '_')),
    )
    if os.path.exists(build_file):
        print('+ Already built for %s !' % dist)
        return package

    print('+ Preparing Debian build (%s %s) ...' % (package['source_name'], package['version']))
    if debian_branch:
        call('git checkout --quiet %s' % debian_branch)
    os.chdir(project['build_dir'])
    project_build_path = os.path.join(project['build_dir'], '%s-%s' % (project['name'], project['version']))
    shutil.copy(origin_archive, project['build_dir'])
    tar = tarfile.open('%s_%s.orig.tar.bz2' % (package['source_name'], project['version']), 'r:bz2')
    tar.extractall()
    tar.close()
    if os.path.exists('%s/debian' % project_build_path):
        shutil.rmtree('%s/debian' % project_build_path)

    shutil.copytree(os.path.join(project['git_path'], debian_folder), '%s/debian' % project_build_path)
    with open(os.path.join(project_build_path, 'debian', 'changelog'), 'w') as f:
        f.write(good_changelog_contents)
    return package


def build_project(dist, arch, project, package, new):
    pbuilder_project_result = os.path.join(settings.PBUILDER_RESULT, '%s-%s' % (dist, arch))
    project_build_path = os.path.join(project['build_dir'], '%s-%s' % (project['name'], project['version']))
    if not os.path.exists(pbuilder_project_result):
        os.makedirs(pbuilder_project_result, 0o755)
    os.chdir(project['lock_path'])
    source_build = os.path.join(
        project['lock_path'],
        '%s_%s_%s_%s_source.build'
        % (
            project['name'],
            project['version'],
            package['repository'],
            project['build_branch'].replace('/', '_'),
        ),
    )
    bin_build = os.path.join(
        project['lock_path'],
        '%s_%s_%s_%s_%s.build'
        % (
            project['name'],
            package['version'],
            package['repository'],
            project['build_branch'].replace('/', '_'),
            arch,
        ),
    )
    print('SOURCE_BUILD:', source_build)
    if os.path.exists(source_build):
        source_opt = '-b'
    else:
        source_opt = '-sa'
    if new == 0 and os.path.exists(bin_build):
        print('+ Already build !')
        return
    os.chdir(project_build_path)
    print('+ Building %s %s %s %s' % (project['name'], project['version'], dist, arch))
    call(
        'DIST=%s ARCH=%s pdebuild --use-pdebuild-internal --architecture %s --debbuildopts "%s"'
        % (dist, arch, arch, source_opt)
    )

    print('+ Lock build')
    touch(bin_build)
    if not os.path.exists(source_build):
        touch(source_build)


def send_packages(dist, arch, project, package, last_tag, dput=True):
    stamp_file = os.path.join(
        project['lock_path'],
        '%s_%s_%s_%s_%s.upload'
        % (
            project['name'],
            package['version'],
            package['repository'],
            arch,
            project['build_branch'].replace('/', '_'),
        ),
    )
    if os.path.exists(stamp_file):
        print('+ Already uploaded')
        return

    pbuilder_project_result = os.path.join(settings.PBUILDER_RESULT, '%s-%s' % (dist, arch))

    print('+ Updating local repository...')
    subprocess.check_call(
        'apt-ftparchive packages . | gzip > Packages.gz', cwd=pbuilder_project_result, shell=True
    )

    if dput:
        print('+ Sending package...')
        os.chdir(pbuilder_project_result)
        call(
            'dput -u %s %s_%s_%s.changes'
            % (package['repository'], package['source_name'], package['version'].split(':', 1)[-1], arch)
        )
    else:
        print('+ Package not sent to repository (--no-dput used).')
        return

    open(stamp_file, 'w').close()


def clean_git_on_exit(git_project_path):
    if not os.path.exists(git_project_path):
        return
    os.chdir(git_project_path)
    call('git stash --quiet')
    changelog_tmp = os.path.join(git_project_path, 'debian', 'changelog.git')
    if os.path.exists(changelog_tmp):
        os.remove(changelog_tmp)


def get_git_project_name(project_reference):
    project_name = os.path.basename(project_reference)
    if project_name.endswith('.git'):
        project_name = project_name[:-4]
    return project_name


def get_git_project_path(project_reference):
    return os.path.join(settings.GIT_PATH, get_git_project_name(project_reference))


def get_git_branch_name(project_reference):
    git_project_path = get_git_project_path(project_reference)
    for branch_name in ('main', 'master'):
        try:
            subprocess.check_call(
                ['git', 'rev-parse', branch_name],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                cwd=git_project_path,
            )
        except subprocess.CalledProcessError:
            continue
        return branch_name
    else:
        raise Exception('failed to determine branch')


def setup_git_tree(project_reference, options):
    git_project_path = get_git_project_path(project_reference)
    if options.branch and options.branch.startswith('wip/'):
        if os.path.exists(git_project_path):
            shutil.rmtree(git_project_path)
    if os.path.exists(git_project_path):
        existing_tree = True
        branch_name = options.branch or get_git_branch_name(project_reference)
        try:
            subprocess.check_call(['git', 'fetch'], cwd=git_project_path)
            subprocess.check_call(['git', 'checkout', '--quiet', branch_name], cwd=git_project_path)
            subprocess.check_call(['git', 'reset', '--hard', 'origin/%s' % branch_name], cwd=git_project_path)
        except subprocess.CalledProcessError as e:
            print(e, file=sys.stderr)
            shutil.rmtree(git_project_path)
            return setup_git_tree(project_reference, options)
    else:
        existing_tree = False
        os.chdir(settings.GIT_PATH)
        if project_reference.startswith('/'):
            call('git clone %s' % project_reference)
        else:
            parsed = urllib.parse.urlparse(project_reference)
            if not parsed.netloc:
                project_url = urllib.parse.urljoin(settings.GIT_REPOSITORY_URL, project_reference) + '.git'
            else:
                project_url = project_reference
            call('git clone %s' % project_url)
        if options.branch:
            subprocess.check_call(['git', 'checkout', '--quiet', options.branch], cwd=git_project_path)
        branch_name = get_git_branch_name(project_reference)

    if not options.branch:
        options.branch = branch_name

    os.chdir(git_project_path)
    try:
        subprocess.check_call(['git', 'checkout', '--quiet', branch_name])
        subprocess.check_call(['git', 'pull'])
        subprocess.check_call(['git', 'submodule', 'init'])
        subprocess.check_call(['git', 'submodule', 'update'])
    except subprocess.CalledProcessError as e:
        if existing_tree:
            print(e, file=sys.stderr)
            shutil.rmtree(git_project_path)
            return setup_git_tree(project_reference, options)
        raise


def main():
    options, args = parse_cmdline()
    for method in options.cleaning:
        clean(method)

    if options.cleaning:
        sys.exit(0)

    init()
    project_reference = args[0]
    git_project_path = get_git_project_path(project_reference)
    atexit.register(clean_git_on_exit, git_project_path)
    if options.branch and options.branch.startswith('origin/'):
        # normalize without origin/
        options.branch = options.branch[len('origin/') :]
    existing_tree = os.path.exists(git_project_path)
    if existing_tree:
        os.chdir(git_project_path)
        last_tag = output('git describe --abbrev=0 --tags --match=v*', exit_on_error=False)
        if last_tag:
            last_tag = last_tag[1:-1]
        else:
            last_tag = '0.0'
    else:
        last_tag = '0.0'
    setup_git_tree(project_reference, options)
    project = get_project_infos(git_project_path, options)

    if not os.path.exists(project['lock_path']):
        os.mkdir(project['lock_path'], 0o755)

    # compare revision between last build and now to determine if something is really new
    new = 1
    current_revision = output('git rev-parse HEAD', True).strip()
    branch_name = get_git_branch_name(project_reference)
    last_branch_revision_file_path = os.path.join(
        project['lock_path'], '%s_%s.last_revision' % (project['name'], branch_name.replace('/', '_'))
    )
    try:
        with open(last_branch_revision_file_path) as f:
            last_branch_revision = f.read().strip()
    except OSError:
        pass
    else:
        if current_revision == last_branch_revision:
            new = 0

    if options.force and not new:
        print('+ Warning force a new build')
        new = 1

    for dist in options.distrib:
        os.chdir(git_project_path)
        call('git checkout --quiet %s' % branch_name)
        package = prepare_build(dist, project, options, new)
        if package:
            for arch in options.architectures:
                build_project(dist, arch, project, package, new)
                send_packages(dist, arch, project, package, last_tag, dput=options.dput)
            print('+ Add a build file to lock new build for %s' % dist)
            touch(
                os.path.join(
                    project['lock_path'],
                    '%s_%s_%s_%s.build'
                    % (
                        project['name'],
                        package['version'],
                        package['repository'],
                        branch_name.replace('/', '_'),
                    ),
                )
            )

            last_version_file = os.path.join(
                project['lock_path'],
                '%s_%s_%s.last_version'
                % (project['name'], package['repository'], branch_name.replace('/', '_')),
            )
            with open(last_version_file, 'w+') as f:
                f.write(package['version'])

    # keep current revision for next build
    with open(last_branch_revision_file_path, 'w+') as f:
        f.write(current_revision)


if __name__ == '__main__':
    main()
