"""
remote binpkg support

Currently this primarily just holds the Packages cache used for remote, and
local binpkg repositories
"""

__all__ = ("PackagesCacheV0", "PackagesCacheV1")

import os
from operator import itemgetter
from time import time

from snakeoil.chksum import get_chksums
from snakeoil.containers import RefCountingSet
from snakeoil.fileutils import AtomicWriteFile, readlines
from snakeoil.mappings import ImmutableDict, StackedDict

from .. import cache
from ..log import logger
from ..restrictions import packages


def _iter_till_empty_newline(data):
    for x in data:
        if not x:
            return
        k, v = x.split(":", 1)
        yield k, v.strip()


class CacheEntry(StackedDict):
    """Customized version of StackedDict blocking pop from modifying the target.

    Note that this pop doesn't through KeyError if something is missing- just
    returns None instead. This is likely to be changed.
    """

    __slots__ = ()

    def pop(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default


def find_best_savings(stream, line_prefix):
    rcs = RefCountingSet(stream)
    line_overhead = len(line_prefix)
    stream = ((k, v) for k, v in rcs.items() if v != 1)
    return max(stream, key=lambda x: (len(x[0]) + line_overhead) * x[1])[0]


class PackagesCacheV0(cache.bulk):
    """Cache backend for writing binpkg Packages caches

    Note this follows version 0 semantics- not the most efficient, and
    doesn't bundle certain useful keys like RESTRICT
    """

    _header_mangling_map = ImmutableDict(
        {
            "FEATURES": "UPSTREAM_FEATURES",
            "ACCEPT_KEYWORDS": "KEYWORDS",
        }
    )

    # this maps from literal keys in the cache to .data[key] expected forms
    _deserialize_map = {
        "DESC": "DESCRIPTION",
        "MTIME": "mtime",
        "repo": "REPO",
    }
    # this maps from .attr to data items.
    _serialize_map = {
        "DESCRIPTION": "DESC",
        "mtime": "MTIME",
        "source_repository": "REPO",
    }
    deserialized_inheritable = frozenset(("CBUILD", "CHOST", "source_repository"))
    _pkg_attr_sequences = ("use", "keywords", "iuse")
    _deserialized_defaults = dict.fromkeys(
        (
            "BDEPEND",
            "DEPEND",
            "RDEPEND",
            "PDEPEND",
            "IDEPEND",
            "BUILD_TIME",
            "IUSE",
            "KEYWORDS",
            "LICENSE",
            "PATH",
            "PROPERTIES",
            "USE",
            "DEFINED_PHASES",
            "CHOST",
            "CBUILD",
            "DESC",
            "REPO",
            "DESCRIPTION",
        ),
        "",
    )
    _deserialized_defaults.update({"EAPI": "0", "SLOT": "0"})
    _deserialized_defaults = ImmutableDict(_deserialized_defaults)

    _stored_chfs = ("size", "sha1", "md5", "mtime")

    version = 0

    def __init__(self, location, *args, **kwds):
        self._location = location
        vkeys = {"CPV"}
        vkeys.update(self._deserialized_defaults)
        vkeys.update(x.upper() for x in self._stored_chfs)
        kwds["auxdbkeys"] = vkeys
        super().__init__(*args, **kwds)

    def _handle(self):
        return readlines(self._location, True, False, False)

    def read_preamble(self, handle):
        return ImmutableDict(
            (self._header_mangling_map.get(k, k), v)
            for k, v in _iter_till_empty_newline(handle)
        )

    def _read_data(self):
        try:
            handle = self._handle()
        except FileNotFoundError:
            return {}
        self.preamble = self.read_preamble(handle)

        defaults = dict(self._deserialized_defaults.items())
        defaults.update(
            (k, v)
            for k, v in self.preamble.items()
            if k in self.deserialized_inheritable
        )
        defaults = ImmutableDict(defaults)

        pkgs = {}
        count = 0
        vkeys = self._known_keys
        while True:
            raw_d = dict(_iter_till_empty_newline(handle))

            d = {k: v for k, v in raw_d.items() if k in vkeys}
            if not d:
                break
            count += 1
            cpv = d.pop("CPV", None)
            if cpv is None:
                cpv = f"{d.pop('CATEGORY')}/{d.pop('PF')}"

            if "USE" in d:
                d.setdefault("IUSE", d.get("USE", ""))
            for src, dst in self._deserialize_map.items():
                if src in d:
                    d.setdefault(dst, d.pop(src))

            pkgs[cpv] = CacheEntry(d, defaults)
        assert count == int(self.preamble.get("PACKAGES", count))
        return pkgs

    @classmethod
    def _assemble_preamble_dict(cls, target_dicts):
        preamble = {
            "VERSION": cls.version,
            "PACKAGES": len(target_dicts),
            "TIMESTAMP": str(int(time())),
        }
        for key in cls.deserialized_inheritable:
            try:
                preamble[key] = find_best_savings(
                    (d[1].get(key, "") for d in target_dicts), key
                )
            except ValueError:
                # empty iterable handed to max
                pass
        return preamble

    @classmethod
    def _assemble_pkg_dict(cls, pkg):
        d = {}
        sequences = cls._pkg_attr_sequences
        for key in cls._stored_attrs:
            value = getattr(pkg, key)
            if key in sequences:
                value = " ".join(sorted(value))
            else:
                value = str(getattr(pkg, key)).strip()
            key = key.upper()
            d[cls._serialize_map.get(key, key)] = value

        for key, value in zip(
            cls._stored_chfs, get_chksums(pkg.path, *cls._stored_chfs)
        ):
            if key != "size":
                value = "%x" % (value,)
            d[key.upper()] = value
        d["MTIME"] = str(os.stat(pkg.path).st_mtime)
        return d

    def _write_data(self):
        handler = None
        try:
            try:
                handler = AtomicWriteFile(self._location)
                self._serialize_to_handle(list(self.data.items()), handler)
                handler.close()
            except PermissionError as e:
                logger.error(f"failed writing binpkg cache to {self._location!r}: {e}")
        finally:
            if handler is not None:
                handler.discard()

    def _serialize_to_handle(self, data, handler):
        preamble = self._assemble_preamble_dict(data)

        convert_key = self._serialize_map.get

        for key in sorted(preamble):
            handler.write(f"{convert_key(key, key)}: {preamble[key]}\n")
        handler.write("\n")

        spacer = " "
        if self.version != 0:
            spacer = ""

        vkeys = self._known_keys
        for cpv, pkg_data in sorted(data, key=itemgetter(0)):
            handler.write(f"CPV:{spacer}{cpv}\n")
            data = [(convert_key(key, key), value) for key, value in pkg_data.items()]
            for write_key, value in sorted(data):
                if write_key not in vkeys:
                    continue
                value = str(value).strip()
                if write_key in preamble:
                    if value != preamble[write_key]:
                        if value:
                            handler.write(f"{write_key}:{spacer}{value}\n")
                        else:
                            handler.write(f"{write_key}:\n")
                elif value:
                    handler.write(f"{write_key}:{spacer}{value}\n")
            handler.write("\n")

    def update_from_xpak(self, pkg, xpak):
        # invert the lookups here; if you do .items() on an xpak,
        # it'll load up the contents in full.
        new_dict = {k: xpak[k] for k in self._known_keys if k in xpak}
        new_dict["_chf_"] = xpak._chf_
        chfs = [x for x in self._stored_chfs if x != "mtime"]
        for key, value in zip(chfs, get_chksums(pkg.path, *chfs)):
            if key != "size":
                value = "%x" % (value,)
            new_dict[key.upper()] = value
        self[pkg.cpvstr] = new_dict
        return new_dict

    def update_from_repo(self, repo):
        # try to collapse certain keys down to the profile preamble
        targets = repo.match(packages.AlwaysTrue, sorter=sorted)

        if not targets:
            # just open/trunc the target instead, and bail
            open(self._location, "wb").close()
            return


class PackagesCacheV1(PackagesCacheV0):
    """Cache backend for writing binpkg Packages caches in format version 1.

    See :py:class:`PackagesCacheV0` for usage information; this just writes
    a better ondisk format.
    """

    deserialized_inheritable = PackagesCacheV0.deserialized_inheritable.union(
        ("SLOT", "EAPI", "LICENSE", "KEYWORDS", "USE", "RESTRICT")
    )

    _deserialized_defaults = ImmutableDict(
        list(PackagesCacheV0._deserialized_defaults.items()) + [("RESTRICT", "")]
    )

    @classmethod
    def _assemble_pkg_dict(cls, pkg):
        # not the most efficient...
        d = PackagesCacheV0._assemble_pkg_dict(pkg)
        use = set(pkg.use).intersection(pkg.iuse_stripped)
        d.pop("IUSE", None)
        iuse_bits = [f"-{x}" for x in pkg.iuse_stripped if x not in use]
        use.update(iuse_bits)
        d["USE"] = " ".join(sorted(use))
        return d

    version = 1


def get_cache_kls(version):
    version = str(version)
    if version == "0":
        return PackagesCacheV0
    elif version in ("1", "-1"):
        return PackagesCacheV1
    raise KeyError(f"cache version {version} unsupported")
