"""Run reprepro commands."""

import os
import shutil
import threading

import logging

import mini_buildd.dist
import mini_buildd.call

LOG = logging.getLogger(__name__)

_LOCKS = {}


class Reprepro():
    r"""
    Abstraction to reprepro repository commands.

    *Locking*

    This implicitly provides a locking mechanism to avoid
    parallel calls to the same repository from mini-buildd
    itself. This rules out any failed call due to reprepro
    locking errors in the first place.

    For the case that someone else is using reprepro
    manually, we also always run it with '--waitforlock'.

    *Ignoring 'unusedarch' check*

    Known broken use case is linux' 'make deb-pkg' up to version 4.13.

    linux' native 'make deb-pkg' is the recommended and documented way to
    produce custom kernels on Debian systems.

    Up to linux version 4.13 (see [l1]_, [l2]_), this would also produce
    firmware packages, flagged "arch=all" in the control file, but
    actually producing "arch=any" firmware \*.deb. The changes file
    produced however would still list "all" in the Architecture field,
    making the reprepro "unsusedarch" check fail (and thusly, installation
    on mini-buildd will fail).

    While this is definitely a bug in 'make deb-pkg' (and also not an
    issue 4.14 onwards or when you use it w/o producing a firmware
    package), the check is documented as "safe to ignore" in reprepro, so
    I think we should allow these cases to work.

    .. [l1] https://github.com/torvalds/linux/commit/cc18abbe449aafc013831a8e0440afc336ae1cba
    .. [l2] https://github.com/torvalds/linux/commit/5620a0d1aacd554ebebcff373e31107bb1ef7769
    """

    def __init__(self, basedir):
        self._basedir = basedir
        self._cmd = ["reprepro", "--verbose", "--waitforlock", "10", "--ignore", "unusedarch", "--basedir", f"{basedir}"]
        self._lock = _LOCKS.setdefault(self._basedir, threading.Lock())

    def __str__(self):
        return f"Reprepro repository at {self._basedir}"

    def _call(self, args):
        return mini_buildd.call.Call(self._cmd + args).check().log().stdout

    def _call_locked(self, args):
        with self._lock:
            return self._call(args)

    @classmethod
    def clean_exportable_indices(cls, dist_dir):
        LOG.info(f"Reprepro: Cleaning exportable indices from {dist_dir}...")
        for item in os.listdir(dist_dir):
            if item != "snapshots":
                rm_path = os.path.join(dist_dir, item)
                LOG.debug(f"PURGING: {rm_path}")
                if os.path.isdir(rm_path) and not os.path.islink(rm_path):
                    shutil.rmtree(rm_path, ignore_errors=True)
                else:
                    os.remove(rm_path)

    def reindex(self, distributions):
        """
        (Re-)index repository.

        Remove all files from repository that can be re-indexed via
        "reprepro export", and remove all files that would not (i.e.,
        stale files from removed distributions).

        Historically, this would just remove 'dists/' completely. With
        the support of reprepro snapshots however, extra care must be
        taken that valid snapshots are *not removed* (as they are not
        reprepro-exportable) and stale snapshots (from removed
        distributions) are *properly unreferenced* in the database
        prior to removing. See doc for 'gen_snapshot' in reprepro's
        manual.

        """
        LOG.info(f"(Re)indexing {self}")
        with self._lock:
            dists_dir = os.path.join(self._basedir, "dists")
            os.makedirs(dists_dir, exist_ok=True)

            for dist_str in os.listdir(dists_dir):
                dist_dir = os.path.join(dists_dir, dist_str)
                self.clean_exportable_indices(dist_dir)

                dist = mini_buildd.dist.Dist(dist_str)
                if dist.get(rollback=False) not in distributions:
                    LOG.debug(f"Dist has been removed from repo: {dist_dir}")
                    for s in self.get_snapshots(dist_str):
                        self._del_snapshot(dist_str, s)  # Call w/o locking!
                    LOG.debug(f"PURGING: {dist_dir}")
                    shutil.rmtree(dist_dir, ignore_errors=True)

            # Update reprepro dbs, and delete any packages no longer in dists.
            self._call(["--delete", "clearvanished"])

            # Finally, rebuild all exportable indices
            self._call(["export"])

    def check(self):
        return self._call_locked(["check"])

    def list(self, pattern, distribution, list_max=50):
        result = []
        for item in self._call_locked(["--list-format", "${package}|${$type}|${architecture}|${version}|${$source}|${$codename}|${$component};",
                                       "--list-max", f"{list_max}"]
                                      + ["listmatched",
                                         distribution,
                                         pattern]).split(";"):
            if item:
                item_split = item.split("|")
                result.append({"package": item_split[0],
                               "type": item_split[1],
                               "architecture": item_split[2],
                               "version": item_split[3],
                               "source": item_split[4],
                               "distribution": item_split[5],
                               "component": item_split[6],
                               })
        return result

    def ils(self, package, type_="dsc"):
        # reprepro lsbycomponent format: "<source> | <version> | <distribution> | <component> | <architecture>"
        for item in self._call_locked(["--type", type_, "lsbycomponent", package]).split("\n"):
            if item:
                item_split = item.split("|")
                yield {
                    "distribution": item_split[2].strip(),
                    "source": item_split[0].strip(),
                    "version": item_split[1].strip(),
                    "component": item_split[3].strip(),
                    "architecture": item_split[4].strip(),
                }

    @classmethod
    def find_in(cls, ls, distribution=None, version=None):
        for r in ls:
            if (distribution is None or r["distribution"] == distribution) and (version is None or r["version"] == version):
                return r
        return None

    def find(self, package, distribution=None, version=None):
        p = self.find_in(self.ils(package), distribution, version)
        if p is None:
            raise mini_buildd.HTTPBadRequest(f"Package '{package}' ({version}) not found in '{distribution}'")
        return p

    def migrate(self, package, src_distribution, dst_distribution, version=None):
        return self._call_locked(["copysrc", dst_distribution, src_distribution, package] + ([version] if version else []))

    def remove(self, package, distribution, version=None):
        return self._call_locked(["removesrc", distribution, package] + ([version] if version else []))

    def install(self, changes, distribution):
        return self._call_locked(["include", distribution, changes])

    def install_dsc(self, dsc, distribution):
        return self._call_locked(["includedsc", distribution, dsc])

    #
    # Reprepro Snapshots
    #
    def snapshots_dir(self, distribution):
        if not distribution:
            raise mini_buildd.HTTPBadRequest("Empty reprepro snapshots distribution name")
        return os.path.join(self._basedir, "dists", distribution, "snapshots")

    def snapshot_dir(self, distribution, name):
        sd = self.snapshots_dir(distribution)
        if not name:
            raise mini_buildd.HTTPBadRequest("Empty reprepro snapshot name")
        return os.path.join(sd, name)

    def get_snapshots(self, distribution, prefix=""):
        sd = self.snapshots_dir(distribution)
        return [s for s in os.listdir(sd) if s.startswith(prefix)] if os.path.exists(sd) else []

    def gen_snapshot(self, distribution, name):
        sd = self.snapshot_dir(distribution, name)
        if os.path.exists(sd):
            raise mini_buildd.HTTPBadRequest(f"Reprepro snapshot '{name}' for '{distribution}' already exists")

        with self._lock:
            self._call(["gensnapshot", distribution, name])

    def _del_snapshot(self, distribution, name):
        sd = self.snapshot_dir(distribution, name)
        if not os.path.exists(sd):
            raise mini_buildd.HTTPBadRequest(f"Reprepro snapshot '{name}' for '{distribution}' does not exist")

        LOG.info(f"Deleting reprepro snapshot '{sd}'...")
        self._call(["unreferencesnapshot", distribution, name])
        shutil.rmtree(sd, ignore_errors=True)
        self._call(["deleteunreferenced"])

    def del_snapshot(self, distribution, name):
        with self._lock:
            self._del_snapshot(distribution, name)
