#!/usr/bin/python3
# pylint: disable=invalid-name  # https://github.com/PyCQA/pylint/issues/516

import os
import fnmatch
import tempfile
import subprocess
import logging

import apt
import apt_pkg
import debian.deb822
import debian.debian_support

import mini_buildd.cli
import mini_buildd.client
import mini_buildd.events


LOG = logging.getLogger("mini_buildd")


def get_source_version(src_pkg, version):
    try:
        # try to get the source version of the pkg, this differs
        # for some (e.g. libnspr4 on ubuntu)
        # this feature only works if the correct deb-src are in the
        # sources.list otherwise we fall back to the binary version number
        src_records = apt_pkg.SourceRecords()
    except SystemError:
        pass
    else:
        while src_records.lookup(src_pkg):
            print(src_records.version)
            if not src_records.version:
                continue
            if apt_pkg.version_compare(src_records.version, version) > 0:
                # The version is higher, it seems to match.
                return (src_records.version, src_records.section)


def _get_build_deps(dsc_url):
    def get_deps(key):
        value = dsc.get(key)
        print("D: {k}: {v}".format(k=key, v=value))
        return value.split(",") if value else []

    with tempfile.NamedTemporaryFile() as dsc_file:
        subprocess.check_call(["wget", "--quiet",
                               "--output-document={f}".format(f=dsc_file.name),
                               dsc_url])
        print("DSC {u} downloaded as {f}".format(u=dsc_url, f=dsc_file.name))
        dsc = debian.deb822.Dsc(dsc_file)
        return [b.lstrip() for b in get_deps("Build-Depends") + get_deps("Build-Depends-Indep")]


class BuildDep():
    """
    A BuildDep object.

    >>> p = BuildDep("package")
    >>> p.package, p.relation, p.version
    (u'package', u'', u'')

    >>> p = BuildDep("package (> 1.2.3)")
    >>> p.package, p.relation, p.version
    (u'package', u'>', u'1.2.3')

    >>> p = BuildDep("package (> 1.2.3) [!kfreebsd]")
    >>> p.package, p.relation, p.version
    (u'package', u'>', u'1.2.3')

    >>> p = BuildDep("package [kfreebsd]")
    >>> p.package, p.relation, p.version, p.arch_option, p.os_ignore
    (u'package', u'', u'', u'kfreebsd', True)

    >>> p = BuildDep("package [!kfreebsd]")
    >>> p.package, p.relation, p.version, p.arch_option, p.os_ignore
    (u'package', u'', u'', u'!kfreebsd', True)

    >>> p = BuildDep("package [linux]")
    >>> p.package, p.relation, p.version, p.arch_option, p.os_ignore
    (u'package', u'', u'', u'linux', False)
    """

    def __init__(self, dep):
        self.dep = dep
        p0 = dep.partition(" ")
        self.package = p0[0]

        self.relation = ""
        self.version = ""
        if dep.find("(") != -1:
            p1 = dep[dep.find("(") + 1:dep.find(")")].partition(" ")
            self.relation = p1[0]
            self.version = p1[2]

        self.arch_option = ""
        self.os_ignore = False
        if dep.find("[") != -1:
            self.arch_option = dep[dep.find("[") + 1:dep.find("]")]
            self.os_ignore = (self.arch_option[0] == "!" and self.arch_option[1:].startswith("linux")) or (not self.arch_option.startswith("linux") and not self.arch_option.startswith("any"))

    def __unicode__(self):
        return "{d}".format(d=self.dep)


def get_pool_path(src_pkg, section="main"):
    """Return the path in the pool where the files would be installed."""
    if src_pkg.startswith('lib'):
        subdir = src_pkg[:4]
    else:
        subdir = src_pkg[0]

    return 'pool/%s/%s/%s' % (section, subdir, src_pkg)


def make_dsc_url(package, version):
    return MBD_MIRROR + "/{pool_path}/{pkg}_{version}.dsc".format(pool_path=get_pool_path(package),
                                                                  pkg=package,
                                                                  version=version.rpartition(":")[2])


def lookup_pkg(pkg):
    if APT_CACHE.is_virtual_package(pkg):
        return APT_CACHE.get_providing_packages(pkg)[0]
    return APT_CACHE[pkg]


def satisfied_pkg(pkg, version):
    print("I: Checking: '{p}' needs version >= '{v}'".format(p=pkg.name, v=version))
    for v in pkg.versions:
        print("D: Found: {v}".format(v=v))
        if debian.debian_support.Version(v.version) >= debian.debian_support.Version(version):
            return True
    return False


def lookup_src_pkg(pkg):
    package = None
    version = debian.debian_support.Version("0")
    rec = apt_pkg.SourceRecords()
    while rec.lookup(pkg):
        print("I: lookup: {p} {v}".format(p=rec.package, v=rec.version))
        version = max(version, debian.debian_support.Version(rec.version))
        package = rec.package
    return (package, "{v}".format(v=version))


def _get_unsatisified_build_deps(result, seen, dsc_url):
    print("I: Get unsatisfied build deps for: {u}".format(u=dsc_url))

    # Avoids endless loops with ring dependencies
    if dsc_url in seen:
        return
    seen.append(dsc_url)

    for d in _get_build_deps(dsc_url):
        dep = BuildDep(d)
        if dep.os_ignore:
            print("I: Ignoring dep, not for linux: {d}".format(d=dep))
            break

        print("I: Checking dep: {d}".format(d=dep))

        satisfied = False
        pkg = None
        pkg_name = dep.package
        try:
            pkg = lookup_pkg(dep.package)
            pkg_name = pkg.name
            if not dep.version:
                satisfied = True
            else:
                satisfied = satisfied_pkg(pkg, dep.version)
        except:  # pylint: disable=bare-except  # noqa (pep8 E722)
            satisfied = False

        if not satisfied:
            package, version = lookup_src_pkg(pkg_name)
            if not package:
                print("ERROR: Can't find source package for: {p}".format(p=pkg_name))
            elif package and dep.version and debian.debian_support.Version(version) < debian.debian_support.Version(dep.version):
                print("ERROR: Can't satisfy build dep: {b}".format(b=dep))
            else:
                url = make_dsc_url(package, version)
                _get_unsatisified_build_deps(result, seen, url)
                result.append(url)
        else:
            print("I: Satisfied: {d}".format(d=dep))


def get_unsatisified_build_deps(dsc_url):
    result, seen = [], []
    _get_unsatisified_build_deps(result, seen, dsc_url)
    return result


def uniq(seq):
    seen = set()
    seen_add = seen.add
    return [x for x in seq if x not in seen and not seen_add(x)]


class DscUrl():
    def __init__(self, url):
        self.url = url
        p0 = os.path.basename(url).partition("_")
        self.package = p0[0]
        # XXX: We won't get epochs here
        self.version = p0[2][:-4]

    def is_satisfied(self):
        try:
            # XXX: This should rather look up for the source package name, and compare stable/testing source versions.
            pkg = lookup_pkg(self.package)
            return satisfied_pkg(pkg, self.version)
        except:  # pylint: disable=bare-except  # noqa (pep8 E722)
            return False

    def is_in_repo(self):
        try:
            MBD_CLIENT.api("find", {"package": self.package, "version": self.version, "distributions": MBD_DISTRIBUTION})
            return True
        except:  # pylint: disable=bare-except  # noqa (pep8 E722)
            return False

    def wait_for_repo(self):
        for event in MBD_CLIENT.events(types=[mini_buildd.events.Type.INSTALLED, mini_buildd.events.Type.FAILED], package=self.package, minimal_version=f"{self.version}~", distribution=MBD_DISTRIBUTION, stop=True):
            print(f"I: Keyring package result: {event}")
            if event.type == mini_buildd.events.Type.INSTALLED:
                return True
            return False


def apt_get_update():
    print("I: Running apt-get update...")
    subprocess.check_call("sudo apt-get update >/dev/null", shell=True)


def run_url(dsc_url):
    apt_get_update()
    print("\n\nI: RUN_URL", dsc_url)

    dsc = DscUrl(dsc_url)
    if dsc.is_satisfied():
        print("I: Already satisfied, skipping", dsc.url)
        return

    if dsc.is_in_repo():
        print("I: Already in repo, skipping", dsc.url)
        return

    needs_build = uniq(get_unsatisified_build_deps(dsc.url))

    print("N:---------------------\nN: WILL PORTEXT: ")
    print("\n".join(needs_build + [dsc_url]))
    print("N:---------------------")

    if MBD_DRY_RUN:
        print("W: DRY RUN, skipping builds")
        for url in needs_build + [dsc.url]:
            LOG.info("{p}".format(p=DscUrl(url).package))
    else:
        for url in needs_build + [dsc.url]:
            MBD_CLIENT.api("portext", {"dsc": url, "distributions": MBD_DISTRIBUTION})
            DscUrl(url).wait_for_repo()
            apt_get_update()


def run_pattern(pattern):
    class Filter(apt.cache.Filter):
        def apply(self, pkg):
            return fnmatch.fnmatch(pkg.name, pattern)

    filtered = apt.cache.FilteredCache(APT_CACHE)
    filtered.set_filter(Filter())

    src_packages = uniq([p.candidate.source_name for p in filtered])
    for p in src_packages:
        package, version = lookup_src_pkg(p)
        if package:
            run_url(make_dsc_url(package, version))
        else:
            print("W: No source package found: {p}".format(p=p))


#: Needed for man page hack in setup.py
DESCRIPTION = "Portext a package including all dependencies."


class CLI(mini_buildd.cli.CLI):
    def __init__(self):
        self.dputcf = mini_buildd.cli.DputCf()

        super().__init__("mbu-super-portext", DESCRIPTION)
        self._add_endpoint(self.parser)
        self.parser.add_argument("package", action="store", metavar="PACKAGE", help=f"Pattern or DSC URL")
        self.parser.add_argument("distribution", action="store", metavar="DISTRIBUTION", help=f"distribution to portext to")
        self.parser.add_argument("-M", "--mirror", action="store", metavar="MIRROR", default="http://localhost:3142/debian/", help=f"Debian mirror to use")
        self.parser.add_argument("-U", "--upload", action="store_true", help="Non-dry run: Actually upload packages")

    def runcli(self):
        global APT_CACHE, MBD_MIRROR, MBD_TARGET, MBD_DISTRIBUTION, MBD_CLIENT, MBD_DRY_RUN  # pylint: disable=global-statement

        #
        # Run in the system you want to port to (be sure sources.list is set up fine)
        #
        apt_pkg.init()
        APT_CACHE = apt.Cache()

        # local setup
        MBD_MIRROR = self.args.mirror
        MBD_TARGET = self.args.endpoint
        MBD_DISTRIBUTION = self.args.distribution
        MBD_CLIENT = mini_buildd.client.Client(MBD_TARGET)
        MBD_DRY_RUN = not self.args.upload

        if self.args.package.find("://") == -1:
            run_pattern(self.args.package)
        else:
            run_url(self.args.package)


APT_CACHE = None
MBD_MIRROR = None
MBD_TARGET = None
MBD_DISTRIBUTION = None
MBD_CLIENT = None
MBD_DRY_RUN = None


CLI().run()
