#!/usr/bin/python3.6
import argparse
import contextlib
import errno
import grp
import logging
import os
import pwd
import shutil
import sys
import tempfile

from functools import cmp_to_key
from pathlib import Path
from subprocess import run, DEVNULL, STDOUT
from types import ModuleType
from typing import Iterator, Optional, TextIO

import rpm
import dnf

from retrace.architecture import get_canon_arch
from retrace.retrace import (log_debug,
                             log_error,
                             log_info,
                             log_warn)

from retrace.config import Config, CREATEREPO_BIN
from retrace.plugins import Plugins
from retrace.util import lock, unlock, parse_rpm_name

CONFIG = Config()
plugins = Plugins()

TARGET_USER = "retrace"
TARGET_GROUP = CONFIG["AuthGroup"]

BUFSIZE = 1 << 22 # 4 MB


@contextlib.contextmanager
def redirect_stderr(path: Path) -> Iterator[TextIO]:
    with path.open(mode="a") as dnflog:
        _stderr = sys.stderr
        sys.stderr = dnflog

        try:
            yield _stderr
        finally:
            sys.stderr = _stderr


def sync_using_dnf(target_id: str, repo_url: str, global_dnf_cfg: str = "",
                   local_dnf_cfg: str = "") -> int:
    with tempfile.NamedTemporaryFile(mode="w", delete=False,
            prefix="repo", suffix=".conf") as dnftmp:
        dnftmp.write(global_dnf_cfg)
        dnftmp.write(local_dnf_cfg)

    with Path(dnftmp.name).open("r") as file:
        log_debug("Using dnf config from %s\n%s" % (dnftmp.name, file.read()))

    pkg_dir = Path(CONFIG["RepoDir"], target_id, "Packages")
    cachedir = Path(CONFIG["RepoDir"], "temp", target_id)

    if cachedir.is_dir():
        shutil.rmtree(cachedir)

    cachedir.mkdir(parents=True, exist_ok=True)

    with redirect_stderr(Path(CONFIG["LogDir"], "reposync_dnf.log")) as old_stderr:
        # BEGIN DNF
        dnfbase = dnf.Base()
        dnfbase.conf.read(filename=dnftmp.name)

        dnfbase.conf.cachedir = cachedir

        dnfbase.repos.add_new_repo(target_id, dnfbase.conf, baseurl=[repo_url])

        try:
            # Fill the sack with repository packages
            dnfbase.fill_sack(load_system_repo=False)
        except Exception as ex:
            sys.stderr = old_stderr
            log_error(str(ex))
            return -1

        download = []

        for pkg in dnfbase.sack.query():
            rpmname = Path(pkg.location).name
            pkgpath = Path(pkg_dir) / rpmname
            if not pkgpath.is_file() or pkgpath.stat().st_size != pkg.size:
                log_info("%s will be downloaded" % rpmname)
                download.append(pkg)
            else:
                log_debug("%s is already downloaded, skipping" % rpmname)

        try:
            dnfbase.download_packages(download)
        except dnf.exceptions.DownloadError as ex:
            print("Some errors occurred during download:\n%s" % ex, file=old_stderr)

        for pkg in download:
            downloadpath = Path(pkg.localPkg())
            targetpath = pkg_dir / downloadpath.name

            if not downloadpath.is_file():
                # error message should be listed in errors
                continue

            if targetpath.is_file():
                targetpath.unlink()

            try:
                downloadpath.rename(targetpath)
            except OSError as ex:
                if ex.errno != errno.EXDEV:
                    raise

                shutil.copy2(downloadpath, targetpath)
                downloadpath.unlink()
        # END DNF

    try:
        os.unlink(dnftmp.name)
        shutil.rmtree(cachedir)
    except Exception as ex:
        log_error("Unable to clean up: %s." % ex)
        return -1

    return 0


def vercmp(ver1: str, ver2: str) -> int:
    # ToDo: Involve epoch?
    ver, rel = ver1.split("-", 1)
    first = (None, ver, rel)
    ver, rel = ver2.split("-", 1)
    second = (None, ver, rel)

    return rpm.labelCompare(first, second)


def clean_rawhide_repo(release: str) -> None:
    packages = {}
    pkg_dir = Path(CONFIG["RepoDir"], release, "Packages")
    for f in pkg_dir.iterdir():
        if f.suffix != ".rpm":
            continue

        pkgdata = parse_rpm_name(f.name)
        if not pkgdata["name"]:
            continue

        ver = "%s-%s" % (pkgdata["version"], pkgdata["release"])

        if not pkgdata["name"] in packages:
            packages[pkgdata["name"]] = {ver: [f.name]}
            continue

        if ver in packages[pkgdata["name"]]:
            packages[pkgdata["name"]][ver].append(f.name)
            continue

        packages[pkgdata["name"]][ver] = [f.name]

    for pkg in packages:
        pkgcnt = len(packages[pkg])
        if pkgcnt > CONFIG["KeepRawhideLatest"]:
            vers = sorted(packages[package].keys(), key=cmp_to_key(vercmp))
            for n, ver in enumerate(vers):
                for filename in packages[pkg][ver]:
                    if n < pkgcnt - CONFIG["KeepRawhideLatest"]:
                        log_info("Removing %s" % filename)
                        (pkg_dir / filename).unlink()


if __name__ == "__main__":
    # parse arguments
    argparser = argparse.ArgumentParser(description="Retrace Server repository downloader")
    argparser.add_argument("distribution", type=str, help="Distribution name")
    argparser.add_argument("version", type=str, help="Release version")
    argparser.add_argument("architecture", type=str, help="CPU architecture")
    argparser.add_argument("-v", "--verbose", action="count", default=0)
    args = argparser.parse_args()

    if args.verbose == 0:
        logging.basicConfig(level=logging.INFO)
    else:
        logging.basicConfig(level=logging.DEBUG)

    distribution = args.distribution
    version = args.version
    arch = get_canon_arch(args.architecture)

    if CONFIG["UseFafPackages"]:
        log_info("Creating repository from faf database")
        releaseid = f"{distribution}-{version}-{arch}"
        targetdir = Path(CONFIG["RepoDir"], releaseid)

        if not targetdir.is_dir():
            targetdir.mkdir(parents=True)

        cmd_line = ["retrace-server-reposync-faf", distribution, version, arch,
                    "--outputdir", targetdir]
        child = run(cmd_line, stdout=sys.stderr, stderr=STDOUT, encoding="utf-8",
                    check=False)

        if child.returncode:
            log_error("Could not create repository")
            log_debug(child.stdout)
            log_debug(child.stderr)
            sys.exit(child.returncode)

        sys.exit(0)

    # drop privilegies if possible
    try:
        gr = grp.getgrnam(TARGET_GROUP)
        os.setgid(gr.gr_gid)
        pw = pwd.getpwnam(TARGET_USER)
        os.setuid(pw.pw_uid)
        log_info("Privileges set to '%s:%s'." % (TARGET_USER, TARGET_GROUP))
    except Exception as ex:
        log_error("Unable to change privileges to '%s:%s'" % (TARGET_USER, TARGET_GROUP))
        log_error(str(ex))
        sys.exit(6)

    # load plugin
    plugin: Optional[ModuleType] = None
    for iplugin in plugins.all():
        if iplugin.distribution == distribution:
            plugin = iplugin
            break

    if not plugin:
        log_error("Unknown distribution: '%s'" % distribution)
        sys.exit(1)

    releaseid = f"{distribution}-{version}-{arch}"
    lockfile = Path(f"/tmp/retrace-reposync-lock-{releaseid}")

    if lockfile.is_file():
        log_error(f"Repository for {releaseid} is already being created by a different process")
        sys.exit(2)

    # set lock
    if not lock(lockfile):
        log_error("Unable to set lock")
        sys.exit(3)

    null: Optional[int] = None
    if args.verbose < 2:
        null = DEVNULL

    try:
        targetdir = Path(CONFIG["RepoDir"], releaseid)
        pkgdir = targetdir / "Packages"

        if not pkgdir.is_dir():
            pkgdir.mkdir(parents=True)

        for filepath in targetdir.iterdir():
            if filepath.suffix == ".rpm":
                filepath.rename(pkgdir / filepath.name)

        globaldnfcfg = ""
        if hasattr(plugin, "dnfcfg"):
            globaldnfcfg = plugin.dnfcfg.replace("$ARCH", arch).replace("$VER", version)

        i = 0
        # run rsync
        for repo in plugin.repos:
            i += 1
            retcode = -1
            localdnfcfg = ""
            if isinstance(repo, tuple):
                repo, localdnfcfg = repo
                localdnfcfg = (
                    localdnfcfg.replace("$ARCH", arch)
                               .replace("$VER", version)
                )

            for mirror in repo:
                repourl = mirror.replace("$ARCH", arch).replace("$VER", version)
                log_info("[%d / %d] Repo: %s" % (i, len(plugin.repos), repourl))
                log_info("Downloading packages")
                sys.stdout.flush()

                if repourl.startswith("http://") or \
                   repourl.startswith("https://") or \
                   repourl.startswith("ftp://"):
                    retcode = sync_using_dnf(releaseid, repourl, globaldnfcfg, localdnfcfg)
                else:
                    if repourl.startswith("rsync://"):
                        files = [repourl]
                    else:
                        # folder in FS
                        files = []
                        try:
                            for package in Path(repourl).iterdir():
                                files.append(package)
                        except Exception as ex:
                            log_warn("Download failed: %s" % ex)
                            log_info("Trying another mirror")
                            continue

                    retcode = run(["rsync", "-t"] + files + [pkgdir],
                                  stdout=null, stderr=null, check=False).returncode

                if retcode == 0:
                    log_info("Download succeeded")
                    break

                log_warn("Download failed, trying another mirror")

            if retcode != 0:
                log_error("Download failed, no more mirrors to try")

        if version.lower() == "rawhide":
            log_info("Cleaning rawhide repo...")
            clean_rawhide_repo(releaseid)

        # run createrepo
        log_info("Running createrepo on '%s'..." % targetdir)
        sys.stdout.flush()

        cmd = [CREATEREPO_BIN, targetdir]
        if CONFIG["UseCreaterepoUpdate"]:
            cmd.append("--update")

        retcode = run(cmd, stdout=null, stderr=null, check=False).returncode
    finally:
        unlock(lockfile)

    if retcode != 0:
        log_error("Failed")
        sys.exit(4)

    log_info("Repository synchronization finished successfully")
