#!/usr/bin/python3
import fnmatch
import logging
import os
import urllib.request

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

from mini_buildd import api, cli, client, events

LOG = logging.getLogger("mini_buildd")

#: Needed for man page hack in setup.py
DESCRIPTION = "EXPERIMENTAL: Portext a source package including all dependencies."
EPILOG = f"""
{DESCRIPTION}

Intended to help porting larger 'projects' (f.e. desktop environments) with
a lot of source packages and inter-package build dependencies.

This still has a lot of rough edges. Use with care.

This tool uses the apt cache of the system it is run on. This
needs to be set up pior to running. For example:

1. Run this in on system you want to PORT TO (f.e..: buster, 'deb' apt lines for buster).
2. Add 'deb-src' apt lines for the system you want to PORT FROM (f.e.: bullseye, 'deb-src' apt lines for bullseye).
3. sudo apt-get update
4. mini-buildd-super-portext -v http://admin@localhost:8066 http://localhost:3142/debian//pool/main/k/kguiaddons/kguiaddons_5.70.0-2.dsc buster-test-unstable

Per default, this will just print a list of DSC URLs it would portext
(in this order). Use --upload to actually run 'the portexts'.
"""


class UniqueList(list):
    def append(self, item):
        if item not in self:
            super().append(item)


class CLI(cli.CLI):
    @classmethod
    def _get_build_deps(cls, dsc_url):
        def get_deps(key):
            value = dsc.get(key)
            LOG.debug("%s: %s", key, value)
            return value.split(",") if value else []

        with urllib.request.urlopen(dsc_url) as response:
            dsc = debian.deb822.Dsc(response)
            return [b.lstrip() for b in get_deps("Build-Depends") + get_deps("Build-Depends-Indep")]

    class BuildDep():
        """
        A BuildDep object

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

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

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

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

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

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

        def __init__(self, dep):
            self.dep = dep
            p0 = dep.partition(" ")
            self.source = 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 __str__(self):
            return f"{self.dep}"

    @classmethod
    def get_pool_path(cls, 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 f"pool/{section}/{subdir}/{src_pkg}"

    def make_dsc_url(self, source, version):
        return self.mbd_mirror + f"/{self.get_pool_path(source)}/{source}_{version.rpartition(':')[2]}.dsc"

    def lookup_pkg(self, pkg):
        if self.apt_cache.is_virtual_package(pkg):
            return self.apt_cache.get_providing_packages(pkg)[0]
        return self.apt_cache[pkg]

    @classmethod
    def satisfied_pkg(cls, pkg, version):
        LOG.debug("Checking: '%s' needs version >= '%s'", pkg.name, version)
        for v in pkg.versions:
            LOG.debug("Found: %s", v)
            if debian.debian_support.Version(v.version) >= debian.debian_support.Version(version):
                return True
        return False

    @classmethod
    def lookup_src_pkg(cls, pkg):
        package = None
        version = None
        rec = apt_pkg.SourceRecords()
        while rec.lookup(pkg):
            LOG.debug("lookup: %s %s", rec.package, rec.version)
            v = debian.debian_support.Version(rec.version)
            version = v if not version else max(version, v)
            package = rec.package
        return (package, f"{version}")

    def get_unsatisified_build_deps(self, dsc_url):
        LOG.info("Get unsatisfied build deps for: %s", dsc_url)

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

        for d in self._get_build_deps(dsc_url):
            LOG.info("Checking dep: %s", d)

            dep = self.BuildDep(d)
            if dep.os_ignore:
                LOG.info("Ignoring dep, not for linux: %s", dep)
                break

            satisfied = False
            pkg = None
            pkg_name = dep.source
            try:
                pkg = self.lookup_pkg(dep.source)
                pkg_name = pkg.name
                if not dep.version:
                    satisfied = True
                else:
                    satisfied = self.satisfied_pkg(pkg, dep.version)
            except BaseException:
                satisfied = False

            if not satisfied:
                source, version = self.lookup_src_pkg(pkg_name)
                if not source:
                    LOG.error("Can't find source package for: %s", pkg_name)
                elif source and dep.version and debian.debian_support.Version(version) < debian.debian_support.Version(dep.version):
                    LOG.error("Can't satisfy build dep: %s", dep)
                else:
                    url = self.make_dsc_url(source, version)
                    self.get_unsatisified_build_deps(url)
                    self.portext.append(url)
            else:
                LOG.debug("Satisfied: %s", dep)

    @classmethod
    def uniq(cls, 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.source = p0[0]
            #: FIXME: We won't get epochs here
            self.version = p0[2][:-4]

        def is_satisfied(self, lookup_pkg):
            try:
                #: FIXME: This should rather look up for the source package name, and compare stable/testing source versions
                pkg = lookup_pkg(self.source)
                return CLI.satisfied_pkg(pkg, self.version)
            except BaseException:
                return False

        def is_in_repo(self, clnt, distribution):
            try:
                clnt.api("find", {"source": self.source, "minimal_version": f"{self.version}~", "distributions": distribution})
                return True
            except BaseException:
                return False

        def wait_for_repo(self, clnt, distribution):
            event = clnt.event(types=[events.Type.INSTALLED, events.Type.FAILED], source=self.source, minimal_version=f"{self.version}~", distribution=distribution)
            if event.type == events.Type.INSTALLED:
                LOG.info("Package result: %s", event)
            LOG.error("Package result: %s", event)

    def run_url(self, dsc_url):
        LOG.info("RUN_URL: %s", dsc_url)

        dsc = self.DscUrl(dsc_url)
        if dsc.is_satisfied(self.lookup_pkg):
            LOG.info("Already satisfied, skipping: %s", dsc.url)
            return

        if dsc.is_in_repo(self.mbd_client, self.mbd_distribution):
            LOG.info("Already in repo, skipping: %s", dsc.url)
            return

        self.get_unsatisified_build_deps(dsc.url)
        self.portext.append(dsc_url)

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

        filtered = apt.cache.FilteredCache(self.apt_cache)
        filtered.set_filter(Filter())

        src_packages = self.uniq([p.candidate.source_name for p in filtered])
        for p in src_packages:
            package, version = self.lookup_src_pkg(p)
            if package:
                self.run_url(self.make_dsc_url(package, version))
            else:
                LOG.warning("No source package found: %s", p)

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

        apt_pkg.init()
        self.apt_cache = apt.Cache()
        self.mbd_mirror = None
        self.mbd_distribution = None
        self.mbd_client = None

        self.already_checked = UniqueList()
        self.portext = UniqueList()

    def runcli(self):
        # local setup
        self.mbd_mirror = self.args.mirror
        self.mbd_distribution = self.args.distribution
        self.mbd_client = client.Client(self.args.endpoint)

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

        if self.args.upload:
            for url in self.portext:
                self.mbd_client.api(api.PortExt(dsc=url, distributions=self.mbd_distribution))
                self.DscUrl(url).wait_for_repo(self.mbd_client, self.mbd_distribution)
        else:
            LOG.info("Ordered portext list:")
            for url in self.portext:
                print(url)


if __name__ == "__main__":
    CLI().run()
