#!/usr/bin/python3 -s

import subprocess
import sys
import argparse
import os
import re
import time
import shutil

parser = argparse.ArgumentParser(description='Remove old packages from rpm-md repository')

parser.add_argument('path', action='store',
                   help='local path to a yum repository')
parser.add_argument('--days', type=int, action='store', default=0,
                   help='only remove packages (and build directories when --cleancopr is used)\
                   that are DAYS old or older (for packages by their build date, for directories\
                   the last modification time is considered')
parser.add_argument('--cleancopr', action='store_true',
                   help='additionaly remove whole copr build dirs and logs if the associated package gets deleted')
parser.add_argument('--alwayscreaterepo', action='store_true',
                   help='Recreate repository even when there was no change in data.')
parser.add_argument('--nocreaterepo', action='store_true',
                   help='repository is not automatically recreated (not even after data deletion). Supresses --alwayscreaterepo.')
parser.add_argument('--verbose', action='store_true',
                   help='print all deleted items to stdout')
parser.add_argument('--quiet', action='store_true',
                   help='do not print any info messages, just do your job')
parser.add_argument('--dry-run', action='store_true',
                    help='do not remove anything from the repository and print the actions instead')
parser.add_argument('-v', '--version', action='version', version='1.5',
                   help='print program version and exit')

args = parser.parse_args()
if args.dry_run:
    args.verbose = True


get_all_packages_cmd = [
    'dnf',
    'repoquery',
    '--repofrompath=prunerepo_query,'+os.path.abspath(args.path),
    '--repo=prunerepo_query',
    '--refresh',
    '--queryformat=%{location}',
    '--quiet',
    '--setopt=skip_if_unavailable=False',
]

get_latest_packages_cmd = get_all_packages_cmd + [ '--latest-limit=1' ]


def is_srpm(package):
    return package.endswith(".src.rpm")


def rm_file(path):
    """
    Remove file given its absolute path
    """
    if args.verbose:
        log_info("Removing: "+path)
    if args.dry_run:
        return
    if os.path.exists(path) and os.path.isfile(path):
        os.remove(path)


def log_info(msg):
    if not args.quiet:
        print(msg)


def run_cmd(cmd, silent=False, dry_run=False):
    """
    Run given command in a subprocess
    """
    if not silent:
        log_info("Executing: "+' '.join(cmd))
    if dry_run:
        return
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (stdout, stderr) = process.communicate()
    sys.stderr.write(stderr.decode(encoding='utf-8'))
    if process.returncode != 0:
        sys.exit(1)
    return stdout.decode(encoding='utf-8').splitlines()


def get_package_build_time(package_path):
    """
    Get build time by reading package metadata
    """
    query_cmd = ['/usr/bin/rpm', '-qp', '--queryformat', '%{BUILDTIME}'] + [ package_path ]
    stdout = run_cmd(query_cmd, silent=True)
    return int(stdout[0])


def get_rpms(repoquery_cmd):
    """
    Get paths to rpm packages in the repository according to given repoquery_cmd
    """
    stdout = run_cmd(repoquery_cmd) # returns srpms as well
    rel_rpms_paths = [relpath for relpath in stdout if not is_srpm(relpath)]
    abs_rpms_paths = [os.path.abspath(os.path.join(args.path, relpath)) for relpath in rel_rpms_paths]
    return abs_rpms_paths


def rm_srpm(rpm):
    """
    If there is matching srpm in the same directory as given rpm (described by its absolute path), delete it
    """
    get_srpm_cmd = get_all_packages_cmd + [ '--srpm', os.path.splitext(os.path.basename(rpm))[0] ]
    output = run_cmd(get_srpm_cmd, silent=True)
    if not output:
        return

    srpm_name = os.path.basename(output[0])
    srpm_path = os.path.abspath(os.path.join(os.path.dirname(rpm), srpm_name))

    if args.verbose:
        log_info("Removing: " + srpm_path)
    if args.dry_run:
        return
    rm_file(srpm_path)


def prune_packages():
    """
    Remove obsoleted packages
    """
    log_info('Removing obsoleted packages...')
    was_deletion = False
    latest_rpms = get_rpms(get_latest_packages_cmd)
    if not latest_rpms:
        log_info("No RPMs available")
        return was_deletion
    all_rpms = get_rpms(get_all_packages_cmd)
    to_remove_rpms = set(all_rpms) - set(latest_rpms)
    for rpm in to_remove_rpms:
        if time.time() - get_package_build_time(rpm) > args.days * 24 * 3600:
            rm_srpm(rpm)
            rm_file(rpm)
            was_deletion = True
    return was_deletion


def recreate_repo():
    """
    Recreate the repository by using createrepo_c
    """
    log_info("Recreating repository...")
    createrepo_cmd = ['/usr/bin/createrepo_c', '--database', '--update', '--local-sqlite',
                      '--cachedir', '/tmp/', '--workers', '8'] + [ args.path ]
    return run_cmd(createrepo_cmd, dry_run=args.dry_run)


def clean_copr():
    """
    Remove whole copr build dirs if they no longer contain a srpm/rpm file
    """
    log_info("This feature is deprecated and will be removed in a future release. Please, use a custom solution instead.")
    log_info("Cleaning COPR repository...")
    for dir_name in os.listdir(args.path):
        dir_path = os.path.abspath(os.path.join(args.path, dir_name))

        if not os.path.isdir(dir_path):
            continue
        if not os.path.isfile(os.path.join(dir_path, 'build.info')):
            continue
        if [item for item in os.listdir(dir_path) if re.match(r'.*\.rpm$', item)]:
            continue
        if time.time() - os.stat(dir_path).st_mtime <= args.days * 24 * 3600:
            continue

        if args.verbose:
            log_info('Removing: '+dir_path)
        shutil.rmtree(dir_path)

        # also remove the associated log in the main dir
        build_id = os.path.basename(dir_path).split('-')[0]
        buildlog_name = 'build-' + build_id + '.log'
        buildlog_path = os.path.abspath(os.path.join(args.path, buildlog_name))
        rm_file(os.path.join(args.path, buildlog_path))


if __name__ == '__main__':
    was_deletion = prune_packages()
    if (was_deletion or args.alwayscreaterepo) \
            and not args.nocreaterepo:
        recreate_repo()

    if args.cleancopr:
        clean_copr()
