#  pylint: disable=missing-module-docstring,invalid-name
#
# Copyright (c) 2008--2018 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#
#
# Package import process
#

import rpm
import sys
import os.path
from .importLib import (
    GenericPackageImport,
    IncompletePackage,
    Import,
    InvalidArchError,
    InvalidChannelError,
    IncompatibleArchError,
)
from .mpmSource import mpmBinaryPackage
from uyuni.common import rhn_pkg
from spacewalk.common.rhnConfig import cfg_component
from spacewalk.server import taskomatic
from spacewalk.server.rhnServer import server_packages


# pylint: disable-next=missing-class-docstring
class ChannelPackageSubscription(GenericPackageImport):
    def __init__(self, batch, backend, caller=None, strict=0, repogen=True):
        # If strict, the set of packages that was passed in will be the only
        # one in the channels - everything else will be unlinked
        GenericPackageImport.__init__(self, batch, backend)
        self.affected_channels = []
        # A hash keyed on the channel id, and with tuples
        # (added_packages, removed_packages) as values (packages are package
        # ids)
        self.affected_channel_packages = {}
        if not caller:
            self.caller = "backend.(unknown)"
        else:
            self.caller = caller
        self._strict_subscription = strict
        self.repogen = repogen
        self.package_type = None

    def preprocess(self):
        # Processes the package batch to a form more suitable for database
        # operations
        for package in self.batch:
            # if package object doesn't have multiple checksums (like satellite-sync objects)
            #   then let's fake it
            if "checksums" not in package:
                package["checksums"] = {package["checksum_type"]: package["checksum"]}
            if not isinstance(package, IncompletePackage):
                raise TypeError(
                    # pylint: disable-next=consider-using-f-string
                    "Expected an IncompletePackage instance, "
                    "got %s" % package.__class__.__name__
                )
            self._processPackage(package)

    def fix(self):
        # Look up arches and channels
        self.backend.lookupPackageArches(self.package_arches)
        self.backend.lookupChannels(self.channels)
        # Initialize self.channel_package_arch_compat
        self.channel_package_arch_compat = {}
        # pylint: disable-next=unused-variable
        for channel, channel_row in list(self.channels.items()):
            if not channel_row:
                # Unsupported channel
                continue
            self.channel_package_arch_compat[channel_row["channel_arch_id"]] = None
        # pylint: disable-next=unused-variable
        for label, aid in self.package_arches.items():
            self.package_type = self.backend.lookupPackageArchType(aid)
            if self.package_type:
                break
        self.backend.lookupChannelPackageArchCompat(self.channel_package_arch_compat)
        self.backend.lookupPackageNames(self.names)
        self.backend.lookupEVRs(self.evrs, self.package_type)
        self.backend.lookupChecksums(self.checksums)

        # Fix the package information up, and uniquify the packages too
        with cfg_component("server") as CFG:
            enable_nvrea = CFG.ENABLE_NVREA
        uniqdict = {}
        for package in self.batch:
            if package.ignored:
                continue
            self._postprocessPackageNEVRA(package)
            if not enable_nvrea:
                # nvrea disabled, skip checksum
                nevrao = (
                    package["name_id"],
                    package["evr_id"],
                    package["package_arch_id"],
                    package["org_id"],
                )
            else:
                # As nvrea is enabled uniquify based on checksum
                nevrao = (
                    package["name_id"],
                    package["evr_id"],
                    package["package_arch_id"],
                    package["org_id"],
                    package["checksum_id"],
                )

            if nevrao not in uniqdict:
                # Uniquify the channel names
                package["channels"] = {}
                # Initialize the channels
                # This is a handy way of checking arch compatibility for this
                # package with its channels
                self.__copyChannels(package, package)
                uniqdict[nevrao] = package
            else:
                # Package is found twice in the same batch
                # Are the packages the same?
                self._comparePackages(package, uniqdict[nevrao])
                # Invalidate it
                package.ignored = 1
                firstpackage = uniqdict[nevrao]
                # Copy any new channels
                self.__copyChannels(package, firstpackage)
                # Knowing the id of the referenced package
                package.first_package = firstpackage

    def _comparePackages(self, package1, package2):
        # XXX This should probably do a deep compare of the two packages
        pass

    def submit(self):
        self.backend.lookupPackages(self.batch, self.checksums)
        try:
            affected_channels = self.backend.subscribeToChannels(
                self.batch, strict=self._strict_subscription
            )
        except:
            self.backend.rollback()
            raise
        self.compute_affected_channels(affected_channels)

        if len(self.batch) < 10:
            # update small batch per package
            name_ids = [pkg["name_id"] for pkg in self.batch]
        else:
            # update bigger batch at once
            name_ids = []
        self.backend.update_newest_package_cache(
            caller=self.caller,
            affected_channels=self.affected_channel_packages,
            name_ids=name_ids,
        )
        # Now that channel is updated, schedule the repo generation
        if self.repogen:
            taskomatic.add_to_repodata_queue_for_channel_package_subscription(
                self.affected_channels, self.batch, self.caller
            )
        self.backend.commit()

    def compute_affected_channels(self, affected_channels):
        # Fill the list of affected channels
        self.affected_channel_packages.clear()
        self.affected_channel_packages.update(affected_channels)
        for channel_label, channel_row in list(self.channels.items()):
            channel_id = channel_row["id"]
            if channel_id in affected_channels:
                affected_channels[channel_id] = channel_label
        self.affected_channels = list(affected_channels.values())

    def _processPackage(self, package):
        GenericPackageImport._processPackage(self, package)

        # Process channels
        channels = []
        channelHash = {}
        for channel in package["channels"]:
            channelName = channel["label"]
            if channelName not in channelHash:
                channels.append(channelName)
                channelHash[channelName] = None
            self.channels[channelName] = None
        # Replace the channel list with the uniquified list
        package.channels = channels

    # Copies the channels from one package to the other
    def __copyChannels(self, sourcePackage, destPackage):
        dpHash = destPackage["channels"]
        for schannelName in sourcePackage.channels:
            # Check if the package is compatible with the channel
            channel = self.channels[schannelName]
            if not channel:
                # Unknown channel
                sourcePackage.ignored = 1
                raise InvalidChannelError(
                    channel,
                    # pylint: disable-next=consider-using-f-string
                    "Unsupported channel %s" % schannelName,
                )
            # Check channel-package compatibility
            charch = channel["channel_arch_id"]
            archCompat = self.channel_package_arch_compat[charch]
            if not archCompat:
                # Invalid architecture
                sourcePackage.ignored = 1
                raise InvalidArchError(
                    charch,
                    # pylint: disable-next=consider-using-f-string
                    "Invalid channel architecture %s" % charch,
                )

            # Now check if the source package's arch is compatible with the
            # current channel
            if sourcePackage["package_arch_id"] not in archCompat:
                sourcePackage.ignored = 1
                raise IncompatibleArchError(
                    sourcePackage.arch,
                    charch,
                    # pylint: disable-next=consider-using-f-string
                    "Package arch %s incompatible with channel %s"
                    % (sourcePackage.arch, schannelName),
                )

            dpHash[channel["id"]] = schannelName

        destPackage.channels = list(dpHash.values())


# pylint: disable-next=missing-class-docstring
class PackageImport(ChannelPackageSubscription):
    def __init__(
        self,
        batch,
        backend,
        caller=None,
        update_last_modified=0,
        import_signatures=True,
    ):
        ChannelPackageSubscription.__init__(self, batch, backend, caller=caller)
        self.ignoreUploaded = 1
        self._update_last_modified = update_last_modified
        self.capabilities = {}
        self.groups = {}
        self.sourceRPMs = {}
        self.changelog_data = {}
        self.suseProdfile_data = {}
        self.suseEula_data = {}
        self.extraTags = {}
        self.import_signatures = import_signatures

    def _skip_tag(self, package, tag):
        # Allow all tags in case of DEB packages
        if package["arch"] and package["arch"].endswith("deb"):
            return False
        # See if the installed version of RPM understands a given tag
        # Assumed attr-format in RPM is 'RPMTAG_<UPPERCASETAG>'
        return not hasattr(rpm, "RPMTAG_" + tag.upper())

    def _processPackage(self, package):
        ChannelPackageSubscription._processPackage(self, package)

        # Process package groups
        group = self._fix_encoding(package["package_group"]).strip()
        if group not in self.groups:
            self.groups[group] = None
        sourceRPM = package["source_rpm"]
        if (sourceRPM is not None) and (sourceRPM not in self.sourceRPMs):
            self.sourceRPMs[sourceRPM] = None
        # Change copyright to license
        # XXX
        package["copyright"] = self._fix_encoding(package["license"])

        for tag in (
            "recommends",
            "suggests",
            "supplements",
            "enhances",
            "breaks",
            "predepends",
        ):
            if (
                self._skip_tag(package, tag)
                or tag not in package
                # pylint: disable-next=unidiomatic-typecheck
                or type(package[tag]) != type([])
            ):
                # older spacewalk server do not export weak deps.
                # and older RPM doesn't know about them either
                # lets create an empty list
                package[tag] = []

        # Creates all the data structures needed to insert capabilities
        for tag in (
            "provides",
            "requires",
            "conflicts",
            "obsoletes",
            "recommends",
            "suggests",
            "supplements",
            "enhances",
            "breaks",
            "predepends",
        ):
            depList = package[tag]
            # pylint: disable-next=unidiomatic-typecheck
            if type(depList) != type([]):
                sys.stderr.write(
                    # pylint: disable-next=consider-using-f-string
                    "!!! packageImport.PackageImport._processPackage: "
                    "erronous depList for '%s', converting to []\n" % tag
                )
                depList = []

            for dep in depList:
                nv = []
                for f in ("name", "version"):
                    nv.append(self._fix_encoding(dep[f]))
                    del dep[f]
                nv = tuple(nv)
                dep["capability"] = nv
                if nv not in self.capabilities:
                    self.capabilities[nv] = None

        # Check whether package is a PTF or part of PTF
        package["is_ptf"] = False
        package["is_part_of_ptf"] = False
        for cap in package["provides"]:
            if cap["capability"][0] == "ptf()":
                package["is_ptf"] = True
            elif cap["capability"][0] == "ptf-package()":
                package["is_part_of_ptf"] = True

        # Process files too
        fileList = package["files"]
        for f in fileList:
            filename = self._fix_encoding(f["name"])
            nv = (filename, "")
            del f["name"]
            f["capability"] = nv
            if nv not in self.capabilities:
                self.capabilities[nv] = None
            f["checksum"] = self._fix_encoding(f["checksum"])
            fchecksumTuple = (f["checksum_type"], f["checksum"])
            if fchecksumTuple not in self.checksums:
                self.checksums[fchecksumTuple] = None

        # Uniquify changelog entries
        unique_package_changelog_hash = set()
        unique_package_changelog = []
        for changelog in package["changelog"]:
            changelog_name = self._fix_encoding(changelog["name"][:128])
            changelog_time = self._fix_encoding(changelog["time"])
            changelog_text = self._fix_encoding(changelog["text"])[:3000]
            key = (changelog_name, changelog_time, changelog_text)
            if key not in unique_package_changelog_hash:
                self.changelog_data[key] = None
                changelog["name"] = changelog_name
                changelog["text"] = changelog_text
                changelog["time"] = changelog_time
                unique_package_changelog.append(changelog)
                unique_package_changelog_hash.add(key)
        package["changelog"] = unique_package_changelog

        # fix encoding issues in package summary and description
        package["description"] = self._fix_encoding(package["description"])
        package["summary"] = self._fix_encoding(package["summary"])
        package["summary"] = package["summary"].rstrip() if package["summary"] else ""

        if package["product_files"] is not None:
            for prodFile in package["product_files"]:
                evrtuple = (prodFile["epoch"], prodFile["version"], prodFile["release"])
                evr = {evrtuple: None}
                archhash = {prodFile["arch"]: None}
                self.backend.lookupEVRs(evr, "rpm")
                self.backend.lookupPackageArches(archhash)
                prodFile["evr"] = evr[evrtuple]
                prodFile["package_arch_id"] = archhash[prodFile["arch"]]
                key = (
                    prodFile["name"],
                    prodFile["evr"],
                    prodFile["package_arch_id"],
                    prodFile["vendor"],
                    prodFile["summary"],
                    prodFile["description"],
                )
                self.suseProdfile_data[key] = None

        if package["eulas"] is not None:
            for eula in package["eulas"]:
                key = (eula["text"], eula["checksum"])
                self.suseEula_data[key] = None

        if "extra_tags" in package and package["extra_tags"] is not None:
            for tag in package["extra_tags"]:
                self.extraTags[tag["name"]] = None

    def fix(self):
        # If capabilities are available, process them
        if self.capabilities:
            try:
                self.backend.processCapabilities(self.capabilities)
            except:
                # Oops
                self.backend.rollback()
                raise
            # Since this is the bulk of the work, commit
            self.backend.commit()

        self.backend.processChangeLog(self.changelog_data)
        self.backend.processSuseProductFiles(self.suseProdfile_data)
        self.backend.processSuseEulas(self.suseEula_data)

        ChannelPackageSubscription.fix(self)

        self.backend.lookupSourceRPMs(self.sourceRPMs)
        self.backend.lookupPackageGroups(self.groups)
        self.backend.processExtraTags(self.extraTags)
        # Postprocess the gathered information
        self.__postprocess()

    def submit(self):
        upload_force = self.uploadForce
        if not upload_force and self._update_last_modified:
            # # Force it just a little bit - kind of hacky
            upload_force = 0.5
        try:
            self.backend.processPackages(
                self.batch,
                uploadForce=upload_force,
                forceVerify=self.forceVerify,
                ignoreUploaded=self.ignoreUploaded,
                transactional=self.transactional,
            )
            if self.import_signatures:
                self._import_signatures()
        except:
            # Oops
            self.backend.rollback()
            raise
        self.backend.commit()
        if not self._update_last_modified:
            # Go though the list of objects and clear out the ones that have a
            # force of 0.5
            for p in self.batch:
                if p.diff and p.diff.level == 0.5:
                    # Ignore this difference completely
                    p.diff = None
                    # Leave p.diff_result in place

    def subscribeToChannels(self):
        affected_channels = self.backend.subscribeToChannels(self.batch)
        # Fill the list of affected channels
        self.compute_affected_channels(affected_channels)

        name_ids = [pkg["name_id"] for pkg in self.batch]
        self.backend.update_newest_package_cache(
            caller=self.caller,
            affected_channels=self.affected_channel_packages,
            name_ids=name_ids,
        )
        taskomatic.add_to_repodata_queue_for_channel_package_subscription(
            self.affected_channels, self.batch, self.caller
        )
        self.backend.commit()

    def __postprocess(self):
        # Gather the IDs we've found

        for package in self.batch:
            if package.ignored:
                # Skip it
                continue
            # Only deal with packages
            self.__postprocessPackage(package)

    def __postprocessPackage(self, package):
        """populate the columns foo_id with id numbers from appropriate hashes"""
        package["package_group"] = self.groups[
            self._fix_encoding(package["package_group"]).strip()
        ]
        source_rpm = package["source_rpm"]
        if source_rpm is not None:
            source_rpm = self.sourceRPMs[source_rpm]
        else:
            source_rpm = ""
        package["source_rpm_id"] = source_rpm
        package["checksum_id"] = self.checksums[
            (package["checksum_type"], package["checksum"])
        ]

        # Postprocess the dependency information
        for tag in (
            "provides",
            "requires",
            "conflicts",
            "obsoletes",
            "files",
            "recommends",
            "suggests",
            "supplements",
            "enhances",
            "breaks",
            "predepends",
        ):
            for entry in package[tag]:
                nv = entry["capability"]
                entry["capability_id"] = self.capabilities[nv]
        for c in package["changelog"]:
            c["changelog_data_id"] = self.changelog_data[
                (c["name"], c["time"], c["text"])
            ]
        if package["product_files"] is not None:
            for p in package["product_files"]:
                p["prodfile_id"] = self.suseProdfile_data[
                    (
                        p["name"],
                        p["evr"],
                        p["package_arch_id"],
                        p["vendor"],
                        p["summary"],
                        p["description"],
                    )
                ]
        if package["eulas"] is not None:
            for e in package["eulas"]:
                e["eula_id"] = self.suseEula_data[(e["text"], e["checksum"])]
        fileList = package["files"]
        for f in fileList:
            f["checksum_id"] = self.checksums[(f["checksum_type"], f["checksum"])]
        if "extra_tags" in package and package["extra_tags"] is not None:
            for t in package["extra_tags"]:
                t["key_id"] = self.extraTags[t["name"]]

    def _comparePackages(self, package1, package2):
        if (
            package1["checksum_type"] == package2["checksum_type"]
            and package1["checksum"] == package2["checksum"]
        ):
            return
        # XXX Handle this better
        # pylint: disable-next=broad-exception-raised
        raise Exception("Different packages in the same batch")

    # pylint: disable-next=redefined-builtin
    def _cleanup_object(self, object):
        ChannelPackageSubscription._cleanup_object(self, object)
        if object.ignored:
            object.id = object.first_package.id

    def _import_signatures(self):
        with cfg_component("server.satellite") as CFG:
            mount_point = CFG.MOUNT_POINT
        for package in self.batch:
            # skip missing files and mpm packages
            if package["path"] and not isinstance(package, mpmBinaryPackage):
                full_path = os.path.join(mount_point, package["path"])
                if os.path.exists(full_path):
                    header = rhn_pkg.get_package_header(filename=full_path)
                    server_packages.processPackageKeyAssociations(
                        header, package["checksum_type"], package["checksum"]
                    )


# pylint: disable-next=missing-class-docstring
class SourcePackageImport(Import):
    # pylint: disable-next=unused-argument
    def __init__(self, batch, backend, caller=None, update_last_modified=0):
        Import.__init__(self, batch, backend)
        self._update_last_modified = update_last_modified
        self.ignoreUploaded = 1
        self.sourceRPMs = {}
        self.groups = {}
        self.checksums = {}

    def preprocess(self):
        for package in self.batch:
            self._processPackage(package)

    def fix(self):
        self.backend.lookupSourceRPMs(self.sourceRPMs)
        self.backend.lookupPackageGroups(self.groups)
        self.backend.lookupChecksums(self.checksums)
        self.__postprocess()
        # Uniquify the packages
        uniqdict = {}
        for package in self.batch:
            # Unique key
            key = (package["org_id"], package["source_rpm_id"])
            if key not in uniqdict:
                uniqdict[key] = package
                continue
            else:
                self._comparePackages(package, uniqdict[key])
                # And invalidate it
                package.ignored = 1
                package.first_package = uniqdict[key]

    def submit(self):
        upload_force = self.uploadForce
        if not upload_force and self._update_last_modified:
            # # Force it just a little bit - kind of hacky
            upload_force = 0.5
        try:
            self.backend.processSourcePackages(
                self.batch,
                uploadForce=upload_force,
                forceVerify=self.forceVerify,
                ignoreUploaded=self.ignoreUploaded,
                transactional=self.transactional,
            )
        except:
            # Oops
            self.backend.rollback()
            raise
        self.backend.commit()
        if not self._update_last_modified:
            # Go though the list of objects and clear out the ones that have a
            # force of 0.5
            for p in self.batch:
                if p.diff and p.diff.level == 0.5:
                    # Ignore this difference completely
                    p.diff = None
                    # Leave p.diff_result in place

    def _comparePackages(self, package1, package2):
        if (
            package1["checksum_type"] == package2["checksum_type"]
            and package1["checksum"] == package2["checksum"]
        ):
            return
        # XXX Handle this better
        # pylint: disable-next=broad-exception-raised
        raise Exception("Different packages in the same batch")

    def _processPackage(self, package):
        Import._processPackage(self, package)
        # Fix the arch
        package.arch = "src"
        package.source_rpm = package["source_rpm"]
        group = self._fix_encoding(package["package_group"]).strip()
        if group not in self.groups:
            self.groups[group] = None
        sourceRPM = package["source_rpm"]
        if not sourceRPM:
            # Should not happen
            # pylint: disable-next=broad-exception-raised
            raise Exception("Source RPM %s does not exist")
        self.sourceRPMs[sourceRPM] = None

        checksumTuple = (package["checksum_type"], package["checksum"])
        if checksumTuple not in self.checksums:
            self.checksums[checksumTuple] = None

        sigchecksumTuple = (package["sigchecksum_type"], package["sigchecksum"])
        if sigchecksumTuple not in self.checksums:
            self.checksums[sigchecksumTuple] = None

    def __postprocess(self):
        # Gather the IDs we've found

        for package in self.batch:
            if package.ignored:
                # Skip it
                continue
            # Only deal with packages
            self.__postprocessPackage(package)

    def __postprocessPackage(self, package):
        # Set the ids
        package["package_group"] = self.groups[
            self._fix_encoding(package["package_group"]).strip()
        ]
        package["source_rpm_id"] = self.sourceRPMs[package["source_rpm"]]
        package["checksum_id"] = self.checksums[
            (package["checksum_type"], package["checksum"])
        ]
        package["sigchecksum_id"] = self.checksums[
            (package["sigchecksum_type"], package["sigchecksum"])
        ]

    # pylint: disable-next=redefined-builtin
    def _cleanup_object(self, object):
        Import._cleanup_object(self, object)
        if object.ignored:
            object.id = object.first_package.id


def packageImporter(batch, backend, source=0, caller=None):
    if source:
        return SourcePackageImport(batch, backend, caller=caller)
    return PackageImport(batch, backend, caller=caller)
