DEFAULTS = dict(
    # PDC URL
    pdc_url="https://pdc.fedoraproject.org",
    # dist-git URL
    dist_git_url="https://src.fedoraproject.org",
    # mdapi URL
    mdapi_url="https://apps.fedoraproject.org/mdapi",
    # hotness issue tracker URL
    hotness_issue_tracker="https://github.com/fedora-infra/the-new-hotness/issues",
    # Repository id
    repoid="rawhide",
    # Distribution
    distro="Fedora",
    # The time in seconds the-new-hotness should wait for a socket to connect
    # before giving up.
    connect_timeout=15,
    # The time in seconds the-new-hotness should wait for a read from a socket
    # before giving up.
    read_timeout=15,
    # The number of times the-new-hotness should retry a network request
    # that failed for any reason (e.g. read timeout, DNS error, etc)
    requests_retries=3,
    # Bugzilla configuration
    bugzilla=dict(
        enabled=True,
        url="https://partner-bugzilla.redhat.com",
        api_key="",
        product="Fedora",
        version="rawhide",
        keywords="FutureFeature, Triaged",
        bug_status="NEW",
        explanation_url="https://fedoraproject.org/wiki/upstream_release_monitoring",
        reporter="Upstream Release Monitoring",
        reporter_email="upstream-release-monitoring@fedoraproject.org",
        short_desc_template="%(name)s-%(latest_upstream)s is available",
        description_template="""
Latest upstream release: %(latest_upstream)s

Current version/release in %(repo_name)s: %(repo_version)s-%(repo_release)s

URL: %(url)s


    Please consult the package updates policy before you
issue an update to a stable branch:
    https://docs.fedoraproject.org/en-US/fesco/Updates_Policy


More information about the service that created this bug can be found at:

    %(explanation_url)s


    Please keep in mind that with any upstream change, there may also be packaging
    changes that need to be made. Specifically, please remember that it is your
    responsibility to review the new version to ensure that the licensing is still
    correct and that no non-free or legally problematic items have been added
    upstream.

Based on the information from anitya: https://release-monitoring.org/project/%(projectid)s/
""",
    ),
    # Koji configuration
    koji=dict(
        server="https://koji.fedoraproject.org/kojihub",
        weburl="https://koji.fedoraproject.org/koji",
        # Kerberos configuration to authenticate with Koji. In development
        # environments, use `kinit <fas-name>@FEDORAPROJECT.ORG` to get a
        # Kerberos ticket and use the default settings below.
        krb_principal="",
        krb_keytab="",
        krb_ccache="",
        krb_proxyuser="",
        krb_sessionopts=dict(timeout=3600, krb_rdns=False),
        git_url="https://src.fedoraproject.org/cgit/rpms/{package}.git",
        user_email=[
            "Upstream Monitor",
            "<upstream-release-monitoring@fedoraproject.org>",
        ],
        opts=dict(scratch=True),
        priority=30,
        target_tag="rawhide",
    ),
)
def _load_dict(config_dict: dict, defaults: dict) -> dict:
def load(config_dict: dict) -> dict: class Package:
    """
    Internal representation of package object.

    Attributes:
        name (str): Name of the package
        version (str): Latest version of the package
        distro (str): Distribution assigned
    """

    def __init__(self, name: str, version: str, distro: str) -> None:
        """
        Class constructor
        """
        self.name = name
        self.version = version
        self.distro = distro

    def to_dict(self) -> dict:
        """
        Convert object to dictionary.

        Return:
            Dictionary created from package object.
        """
        return {"name": self.name, "version": self.version, "distro": self.distro}

    @classmethod
    def from_dict(cls, package_dict: dict) -> object:
        """
        Convert dictionary to object.

        Params:
            package_dict: Dictionary to create package from.

        Return:
            Package object.
        """
        package = Package(
            name=package_dict["name"],
            version=package_dict["version"],
            distro=package_dict["distro"],
        )
        return package

    def __eq__(self, other: object) -> bool:
        """
        Compare method for package object.

        Params:
            other: object to compare with

        Return:
            Compare result.
        """
        return self.to_dict() == other.to_dict()  # type: ignore class Koji(Builder):
    """
    Wrapper class around koji (https://koji.fedoraproject.org/koji/) builder.
    It starts the scratch builds for the-new-hotness in Fedora.

    Attributes:
       server_url: URL of the Koji server
       web_url: Web interface URL for Koji
       krb_principal: Principal for Kerberos domain
       krb_keytab: Kerberos keytab
       krb_ccache: Kerberos CCache
       krb_proxyuser: Kerberos proxy user
       krb_sessionopts: Additional Kerberos session options
       git_url: URL of dist git where to look for the package repository
       user_email: Tuple containing user name and e-mail
       opts: Any additional opts for koji
       priority: Priority of builds submitted by this wrapper
       target_tag: Tag under which builds will be submitted
    """

    def __init__(
        self,
        server_url: str,
        web_url: str,
        kerberos_args: dict,
        git_url: str,
        user_email: tuple,
        opts: dict,
        priority: int,
        target_tag: str,
    ) -> None:
        """
        Class constructor.
        """
        super(Koji, self).__init__()
        self.server_url = server_url
        self.web_url = web_url
        self.krb_principal = kerberos_args["krb_principal"]
        self.krb_keytab = kerberos_args["krb_keytab"]
        self.krb_ccache = kerberos_args["krb_ccache"]
        self.krb_proxyuser = kerberos_args["krb_proxyuser"]
        self.krb_sessionopts = kerberos_args["krb_sessionopts"]
        self.git_url = git_url
        self.user_email = user_email
        self.opts = opts
        self.priority = priority
        self.target_tag = target_tag

    def build(self, package: Package, opts: dict) -> dict:
        """
        Clones the package dist git repository, bumps version, downloads sources,
        starts scratch build and prepares patch.

        Params:
            package: Package to build
            opts: Contains bugzilla issue to reference in build description and commit message
            Example:
            {
                "bz_id": 100
            }

        Returns:
            Dictionary containing output from builder.
            Example:
            {
                "build_id": 1000, # Build id of the started build
                "patch": "", # String containing the patch created by version bump
                "patch_filename": "", # Name of the patch file
                "message": "", # Any info we want to share in notifier later
            }
        """
        output = {"build_id": 0, "patch": "", "patch_filename": "", "message": ""}
        # Get bugzilla id from opts
        bz_id = opts["bz_id"]
        # Write file to temporary directory
        # and stop bandit from complaining about a hardcoded temporary directory
        # because it's needed for OpenShift
        with TemporaryDirectory(prefix="thn-", dir="/var/tmp") as tmp:  # nosec
            dist_git_url = self.git_url.format(package=package.name)
            _logger.info("Cloning %r to %r" % (dist_git_url, tmp))
            try:
                sp.check_output(["git", "clone", dist_git_url, tmp], stderr=sp.STDOUT)
            except sp.CalledProcessError as exc:
                std_out = ""
                std_err = ""
                if exc.stdout:
                    std_out = exc.stdout.decode()
                if exc.stderr:
                    std_err = exc.stderr.decode()
                raise BuilderException(str(exc), std_out=std_out, std_err=std_err)

            specfile = os.path.join(tmp, package.name + ".spec")

            comment = "Update to %s (#%d)" % (package.version, bz_id)

            # This requires rpmdevtools-8.5 or greater
            cmd = [
                "/usr/bin/rpmdev-bumpspec",
                "--new",
                package.version,
                "-c",
                comment,
                "-u",
                " ".join(self.user_email),
                specfile,
            ]
            try:
                sp.check_output(cmd, stderr=sp.STDOUT)
            except sp.CalledProcessError as exc:
                std_out = ""
                std_err = ""
                if exc.stdout:
                    std_out = exc.stdout.decode()
                if exc.stderr:
                    std_err = exc.stderr.decode()
                raise BuilderException(str(exc), std_out=std_out, std_err=std_err)

            # We compare the old sources to the new ones to make sure we download
            # new sources from bumping the specfile version. Some packages don't
            # use macros in the source URL(s). We want to detect these and notify
            # the packager on the bug we filed about the new version.
            old_sources = self._dist_git_sources(tmp)
            new_sources = self._spec_sources(specfile, tmp)
            output["message"] = self._compare_sources(old_sources, new_sources)

            try:
                cmd_output = sp.check_output(
                    [
                        "rpmbuild",
                        "-D",
                        "_sourcedir .",
                        "-D",
                        "_topdir .",
                        "-bs",
                        specfile,
                    ],
                    cwd=tmp,
                    stderr=sp.STDOUT,
                )
            except sp.CalledProcessError as exc:
                std_out = ""
                std_err = ""
                if exc.stdout:
                    std_out = exc.stdout.decode()
                if exc.stderr:
                    std_err = exc.stderr.decode()
                raise BuilderException(
                    str(exc), value=output, std_out=std_out, std_err=std_err
                )

            srpm = os.path.join(tmp, cmd_output.decode("utf-8").strip().split()[-1])
            _logger.debug("Got srpm %r" % srpm)

            session = self._session_maker()
            if not session:
                raise BuilderException("Can't authenticate with Koji!")
            output["build_id"] = self._scratch_build(session, package.name, srpm)

            # Now, craft a patch to attach to the ticket
            try:
                sp.check_output(
                    ["git", "config", "user.name", self.user_email[0]],
                    cwd=tmp,
                    stderr=sp.STDOUT,
                )
                sp.check_output(
                    ["git", "config", "user.email", self.user_email[1]],
                    cwd=tmp,
                    stderr=sp.STDOUT,
                )
                sp.check_output(
                    ["git", "commit", "-a", "-m", comment], cwd=tmp, stderr=sp.STDOUT
                )
                filename = sp.check_output(
                    ["git", "format-patch", "HEAD^"], cwd=tmp, stderr=sp.STDOUT
                )
                filename_str = filename.decode("utf-8").strip()
            except sp.CalledProcessError as exc:
                std_out = ""
                std_err = ""
                if exc.stdout:
                    std_out = exc.stdout.decode()
                if exc.stderr:
                    std_err = exc.stderr.decode()
                raise BuilderException(
                    str(exc), value=output, std_out=std_out, std_err=std_err
                )

            output["patch_filename"] = filename_str

            # Copy the content of file to output
            patch = os.path.join(tmp, filename_str)
            with open(patch) as f:
                output["patch"] = f.read()

            return output

    def _dist_git_sources(self, dist_git_path: str) -> list:
        """
        Retrieve sources from dist-git.

        Example:
            >>> dist_git_sources('/path/to/repo')
            ['/path/to/repo/source0.tar.gz', '/path/to/repo/source1.tar.gz']

        Params:
            dist_git_path: The filesystem path to the dist-git repository

        Returns:
            A list of absolute paths to source files downloaded

        Raises:
            subprocess.CalledProcessError: When downloading the sources fails.
        """
        files = []
        # The output format is:
        # Downloading requests-2.12.4.tar.gz
        # ####################################################################### 100.0%
        # Downloading requests-2.12.4-tests.tar.gz
        # ####################################################################### 100.0%
        output = sp.check_output(
            ["fedpkg", "--user", "hotness", "sources"], cwd=dist_git_path
        )
        for line in output.decode("utf-8").splitlines():
            if line.startswith("Downloading"):
                files.append(os.path.join(dist_git_path, line.split()[-1]))

        return files

    def _spec_sources(self, specfile_path: str, target_dir: str) -> list:
        """
        Retrieve a specfile's sources and store them in the given target directory.

        Example:
            >>> spec_sources('/path/to/specfile', '/tmp/dir')
            ['/tmp/dir/source0.tar.gz', '/tmp/dir/source1.tar.gz']

        Params:
            specfile_path: The filesystem path to the specfile
            target_dir: The directory is where the file(s) will be saved.

        Returns:
            A list of absolute paths to source files downloaded

        Raises:
            exceptions.DownloadException: If a networking-related error occurs while
                downloading the specfile sources. This includes hostname resolution,
                non-200 HTTP status codes, SSL errors, etc.
        """
        files = []
        try:
            output = sp.check_output(["spectool", "-g", specfile_path], cwd=target_dir)
            for line in output.decode("utf-8").splitlines():
                if line.startswith("Getting"):
                    files.append(
                        os.path.realpath(os.path.join(target_dir, line.split()[-1]))
                    )
        except sp.CalledProcessError as e:
            # spectool passes the cURL exit codes back so see its manpage for the full list
            if e.returncode == 1:
                # Unknown protocol (e.g. not ftp, http, or https)
                msg = (
                    "The specfile contains a Source URL with an unknown protocol; it should"
                    'be "https", "http", or "ftp".'
                )
            elif e.returncode in (5, 6):
                msg = "Unable to resolve the hostname for one of the package's Source URLs"
            elif e.returncode == 7:
                # Failed to connect to the host
                msg = (
                    "Unable to connect to the host for one of the package's Source URLs"
                )
            elif e.returncode == 22:
                # cURL uses 22 for 400+ HTTP errors; the final line contains the specific code
                msg = (
                    "An HTTP error occurred downloading the package's new Source URLs: "
                    + e.output.decode().splitlines()[-1]
                )
            elif e.returncode == 60:
                msg = (
                    "Unable to validate the TLS certificate for one of the package's "
                    "Source URLs"
                )
            else:
                msg = (
                    "An unexpected error occurred while downloading the new package sources; "
                    "please report this as a bug on the-new-hotness issue tracker.\n"
                    "Error output:\n"
                    "{}".format(e.stderr)
                )
                _logger.error(
                    "{cmd} failed (exit {code}): {msg}".format(
                        cmd=e.cmd, code=e.returncode, msg=e.output.decode()
                    )
                )
            raise DownloadException(msg)

        return files

    def _compare_sources(self, old_sources: list, new_sources: list) -> str:
        """
        Compare two sets of files via checksum and raise an exception if both sets
        contain the same file.

        Params:
            old_sources: A list of filesystem paths to source tarballs.
            new_sources: A list of filesystem paths to source tarballs.

        Returns:
            String containing information message, it is returned when identical files
            are found, otherwise it's empty.
        """
        old_checksums: typing.Set[str] = set()
        new_checksums: typing.Set[str] = set()
        source_checksum: typing.Dict = {}
        for sources, checksums in ((old_sources, old_checksums),):
            for file_path in sources:
                with open(file_path, "rb") as fd:
                    h = hashlib.sha512()
                    h.update(fd.read())
                    checksum = h.hexdigest()
                    checksums.add(checksum)
                    if checksum not in source_checksum:
                        source_checksum[checksum] = {
                            "old_sources": [],
                            "new_sources": [],
                        }
                    source_checksum[checksum]["old_sources"].append(
                        os.path.basename(file_path)
                    )

        for sources, checksums in ((new_sources, new_checksums),):
            for file_path in sources:
                with open(file_path, "rb") as fd:
                    h = hashlib.sha512()
                    h.update(fd.read())
                    checksum = h.hexdigest()
                    checksums.add(checksum)
                    if checksum not in source_checksum:
                        source_checksum[checksum] = {
                            "old_sources": [],
                            "new_sources": [],
                        }
                    source_checksum[checksum]["new_sources"].append(
                        os.path.basename(file_path)
                    )

        intersection_checksums = old_checksums.intersection(new_checksums)
        if intersection_checksums:
            files_string = ""
            for checksum in intersection_checksums:
                source_dict = source_checksum[checksum]
                old_sources = source_dict["old_sources"]
                new_sources = source_dict["new_sources"]
                files_string = (
                    files_string
                    + f"Old: {old_sources} -> New: {new_sources} ({checksum})\n"
                )
            return (
                "One or more of the new sources for this package are identical to "
                "the old sources. This is most likely caused either by identical source files "
                "between releases, for example service files, or the specfile does not use "
                "version macro in its source URLs. If this is the second case, then please "
                "update the specfile to use version macro in its source URLs.\n"
                "Here is the list of the files with SHA512 checksums:\n"
                f"{files_string}"
            )

        return ""

    def _session_maker(self) -> typing.Optional[koji.ClientSession]:
        """
        Creates a new koji session for the build.

        Returns:
            New koji session or None if authentication fails.
        """
        _logger.info("Creating a new Koji session to %s", self.server_url)
        with _koji_session_lock:
            koji_session = koji.ClientSession(self.server_url, self.krb_sessionopts)
            result = koji_session.krb_login(
                principal=self.krb_principal,
                keytab=self.krb_keytab,
                ccache=self.krb_ccache,
                proxyuser=self.krb_proxyuser,
            )
            if not result:
                _logger.error("Koji kerberos authentication failed")
                return None

            return koji_session

    def _scratch_build(
        self, session: koji.ClientSession, name: str, source: str
    ) -> int:
        """
        Uploads source RPM and starts scratch build of package in Koji.

        Params:
            session: Koji session to use for starting build.
            name: Name of the package to build
            source: Path to SRPM.

        Returns:
            Build id.
        """
        _logger.info("Uploading {source} to koji".format(source=source))
        suffix = "".join([random.choice(string.ascii_letters) for i in range(8)])
        serverdir = "%s/%r.%s" % ("cli-build", time.time(), suffix)
        session.uploadWrapper(source, serverdir)

        remote = "%s/%s" % (serverdir, os.path.basename(source))
        _logger.info(
            "Intiating koji build for %r"
            % dict(name=name, target=self.target_tag, source=remote, opts=self.opts)
        )
        task_id = session.build(
            remote, self.target_tag, self.opts, priority=self.priority
        )
        _logger.info(
            "Scratch build created for {name}: {url}".format(
                name=name, url=self.web_url + "/taskinfo?taskID={}".format(task_id)
            )
        )

        return task_id class Cache(Database):
    """
    Wrapper for the cache for the-new-hotness.
    It is represented by python dictionary.

    Attributes:
        cache (dict): Dictionary to hold key/value pairs
    """

    def __init__(self) -> None:
        """
        Class constructor.
        """
        super(Cache, self).__init__()
        self.cache: Dict = {}

    def insert(self, key: str, value: str) -> dict:
        """
        It insert key/value pair to cache. If key is already in cache
        it will change the value.

        Params:
            key: Key to insert to database
            value: Value for the key to add

        Returns:
            Dictionary containing key/value pairs.
            Example:
            {
              "key": "key", # Key we added/edited in database
              "value": "value", # New value added to the key
              "old_value": "old_value" # Old value for the key, empty if the key is new
            }
        """
        output = {"key": key, "value": value, "old_value": self.cache.get(key, "")}

        self.cache[key] = value

        return output

    def retrieve(self, key: str) -> dict:
        """
        Retrieve value for a key in database. If the key is not available
        it returns empty value represented by "".

        Params:
            key: Key to retrieve from database

        Returns:
            Dictionary containing key/value pairs.
            Example:
            {
              "key": "key", # Key we retrieved from database
              "value": "value" # Retrieved value for the key
            }
        """
        output = {"key": key, "value": self.cache.get(key, "")}

        return output class Bugzilla(Notifier):
    """
    This class is a wrapper for https://bugzilla.redhat.com/
    It creates or updates the ticket in Bugzilla and also handles Bugzilla
    session.

    Attributes:
        reporter (str): Reporter e-mail to use
        bugzilla (bugzilla.Bugzilla): Bugzilla session
        base_query (dict): Base query to use when looking for ticket in bugzilla
        bug_status_early (list): Statuses assigned to ticket in early stages
        bug_status_open (list): `bug_status_early` + statuses when the ticket is still considered
                                open
        bug_status_closed (list): List of statuses in which the ticket is considered closed
        new_bug (dict): Parameters for creating a new_bug in Bugzilla
    """

    base_query = {
        "query_format": "advanced",
        "emailreporter1": "1",
        "emailtype1": "exact",
    }

    bug_status_early = ["NEW", "ASSIGNED"]
    bug_status_open = bug_status_early + [
        "MODIFIED",
        "ON_DEV",
        "ON_QA",
        "VERIFIED",
        "FAILS_QA",
        "RELEASE_PENDING",
        "POST",
    ]

    bug_status_closed = ["CLOSED"]

    new_bug = {
        "op_sys": "Unspecified",
        "platform": "Unspecified",
        "bug_severity": "unspecified",
    }

    def __init__(
        self,
        server_url: str,
        reporter: str,
        email: str,
        api_key: str,
        product: str,
        keywords: str,
        version: str,
        status: str,
    ) -> None:
        """
        Class constructor.

        It initializes bugzilla session using the provided credentials.
        If the `api_key` is not provided it raises an `NotifierException`.

        Params:
            server_url: URL of the bugzilla server
            reporter: User that is reporting the issues
            email: E-mail of the reporter user
            api_key: API key to use for authentication
            product: Product to assign the ticket to
            keywords: Keywords for the new ticket
            version: Version of product to assign to new ticket
            status: Status of the new bug

        Raises:
            NotifierException: When the bugzilla session can't be established
        """
        super(Bugzilla, self).__init__()
        if api_key:
            self.bugzilla = bugzilla.Bugzilla(
                url=server_url, api_key=api_key, cookiefile=None, tokenfile=None
            )
        else:
            raise NotifierException(
                "Authentication info not provided! Provide API key."
            )
        self.bugzilla.bug_autorefresh = True

        self.reporter = reporter

        self.base_query["product"] = product
        self.base_query["email1"] = email

        self.new_bug["product"] = product
        if keywords:
            self.new_bug["keywords"] = keywords
        self.new_bug["version"] = version
        self.new_bug["status"] = status

    def notify(self, package: Package, message: str, opts: dict) -> dict:
        """
        This method is inherited from `hotness.notifiers.Notifier`.

        It either follows up on existing bug if bug id is provided or creates
        a new ticket if bug id is not provided and no ticket is found.

        Params:
            package: Package to create notification for
            message: Message to post
            opts: Additional options for bugzilla. Example:
                {
                    "bz_id": 100, # Bugzilla ticket id, if provided the
                                  # new message will be added as new comment to this ticket
                    "bz_short_desc": "test-1.0 is available" # Short description (title) to use
                                                             # when searching for the ticket
                                                             # If not found the new bug will be
                                                             # created with this short description
                                                             # containing `message`
                }

        Returns:
            Dictionary containing ticket bugzilla id
            Example:
            {
                "bz_id": 100
            }

        Raises:
            NotifierException: When the required `opts` parameters are missing.
        """
        output = {}
        bug_id = opts.get("bz_id", 0)
        short_desc = opts.get("bz_short_desc", "")
        # If bugzilla ticket id is provided, just follow up on the bug
        if not bug_id == 0:
            _logger.info("Bug id provided. Updating %s" % bug_id)
            self._update_bug(bug_id, message)
            output["bz_id"] = bug_id
            return output

        if not short_desc:
            raise NotifierException(
                "Additional parameters are missing! "
                "Please provide either `bz_id` or `bz_short_desc`."
            )

        # If bugzilla ticket id is not provided, try to find the bug using
        # the short description
        bug = self._exact_bug(package, short_desc)
        if bug:
            _logger.info("Found exact bug '%r'. Skipping notify action." % bug.weburl)
            output["bz_id"] = bug.bug_id
            return output

        # If none is found, try to look for any bug previously reported by the-new-hotness
        # that is still open and update that one
        bug = self._inexact_bug(package)
        if bug:
            _logger.info(
                "There is already bug opened for previous version '%r'. "
                "Let's update this one." % bug.weburl
            )
            self._update_bug(bug.bug_id, message, short_desc)
            output["bz_id"] = bug.bug_id
            return output

        # If no bug exists create one
        bug = self._create_bug(package, message, short_desc)
        _logger.info("Filled a new bug %r" % bug.weburl)
        output["bz_id"] = bug.bug_id
        return output

    def _update_bug(self, bug_id: int, message: str, summary: str = "") -> None:
        """
        Updates bug in bugzilla specified by bug id.

        Params:
            bug_id: Id of the bug in bugzilla
            message: Message to add as comment
            summary: Summary of the bug, only needs to be provided if we want
                     to update it.
        """
        update = {
            "comment": {
                "body": message,
                "is_private": False,
            },
            "ids": [bug_id],
        }
        if summary:
            update["summary"] = summary
        _logger.debug("Updating bug %r with %r" % (bug_id, update))
        res = self.bugzilla._proxy.Bug.update(update)
        _logger.debug("Result from bug update: %r" % res)
        _logger.info("Updated bug: %s" % bug_id)

    def _exact_bug(self, package: Package, short_desc: str) -> "bugzilla.Bug":
        """
        Look for exact bug for the specified package.

        Params:
            package: Search for bug for this package
            short_desc: Short description to search for

        Returns:
            `bugzilla.Bug` if the bug is found or None if not.
        """
        query = {
            "component": package.name,
            "bug_status": self.bug_status_open + self.bug_status_closed,
            "short_desc": short_desc,
            "short_desc_type": "substring",
        }

        query.update(self.base_query)
        bugs = self.bugzilla.query(query)
        if bugs:
            return bugs[0]
        return None

    def _inexact_bug(self, package: Package) -> "bugzilla.Bug":
        """
        Search for tickets in bugzilla filled by `self.reporter` and still
        in early states.

        Params:
            package: Search for bug for this package

        Returns:
            `bugzilla.Bug` if the bug is found or None if not.
        """
        # We'll match bugs in the NEW or ASSIGNED state
        # https://github.com/fedora-infra/the-new-hotness/issues/58
        possible_statuses = list(set(self.bug_status_early + [self.new_bug["status"]]))
        possible_statuses.sort()
        query = {
            "component": package.name,
            "bug_status": possible_statuses,
            "creator": self.reporter,
        }

        query.update(self.base_query)
        bugs = self.bugzilla.query(query)
        if bugs:
            return bugs[0]
        return None

    def _create_bug(
        self, package: Package, message: str, short_desc: str
    ) -> "bugzilla.Bug":
        """
        Create a new bug in bugzilla.

        Params:
            package: Create bug for this package
            message: Message to add to bug
            short_desc: Summary of the bug to create

        Returns:
            Created `bugzilla.Bug`.
        """
        bug_dict = {
            "component": package.name,
            "short_desc": short_desc,
            "description": message,
        }
        bug_dict.update(self.new_bug)
        new_bug = self.bugzilla.createbug(**bug_dict)
        _logger.info("Created bug: %s" % new_bug)

        return new_bug class FedoraMessaging(Notifier):
    """
    This class is a wrapper for https://github.com/fedora-infra/fedora-messaging publisher.
    It publishes a message to fedora messaging broker using hotness_schema messages.

    Attributes:
        prefix (str): String used as prefix for the topic
    """

    def __init__(self, prefix: str) -> None:
        """
        Class constructor.

        It initializes class attributes.

        Params:
            prefix: Prefix to use for topic
        """
        super(FedoraMessaging, self).__init__()
        self.prefix = prefix

    def notify(self, package: Package, message: str, opts: dict) -> dict:
        """
        This method is inherited from `hotness.notifiers.Notifier`.

        It publishes messages to Fedora messaging broker.

        Params:
            package: Package to create notification for
            message: Topic for the message
            opts: Additional options for fedora message. Example:
                {
                    "body": {} # Body of the message we want to sent. Look at the
                               # hotness_schema for more info
                }

        Returns:
            Dictionary containing message id
            Example:
            {
                "msg_id": "ae68f"
            }

        Raises:
            NotifierException: When the required `opts` parameters are missing or
                               error happens during publishing.
        """
        topic = self.prefix + "." + message
        body = opts.get("body", {})
        output = {}

        if not body:
            raise NotifierException(
                "Additional parameters are missing! Please provide `body` for the message."
            )

        message_class = fm_message.get_class(topic)
        if not message_class:
            raise NotifierException("Unknown topic provided '{}'".format(topic))

        _logger.info("publishing topic %r" % topic)
        try:
            msg = message_class(topic=topic, body=body)
            api.publish(msg)
        except PublishException as e:
            raise NotifierException(
                "Fedora messaging broker rejected message {}:{}".format(msg.id, e)
            )
        except ConnectionException as e:
            raise NotifierException("Error sending the message {}:{}".format(msg.id, e))
        finally:
            output["msg_id"] = msg.id

        return output class Bugzilla(Patcher):
    """
    This class is a wrapper for https://bugzilla.redhat.com/
    It submit patches to Bugzilla.

    Attributes:
        bugzilla (bugzilla.Bugzilla): Bugzilla session
    """

    def __init__(
        self,
        server_url: str,
        api_key: str,
    ) -> None:
        """
        Class constructor.

        It initializes bugzilla session using the provided credentials.
        If the `api_key` is not provided it raises an `PatcherException`.

        Params:
            server_url: URL of the bugzilla server
            api_key: API key to use for authentication

        Raises:
            PatcherException: When the bugzilla session can't be established
        """
        super(Bugzilla, self).__init__()
        if api_key:
            self.bugzilla = bugzilla.Bugzilla(
                url=server_url, api_key=api_key, cookiefile=None, tokenfile=None
            )
        else:
            raise PatcherException("Authentication info not provided! Provide API key.")
        self.bugzilla.bug_autorefresh = True

    def submit_patch(self, package: Package, patch: str, opts: dict) -> dict:
        """
        This method is inherited from `hotness.patchers.Patcher`.

        It attaches file to existing ticket in bugzilla.

        Params:
            package: Package to create notification for
            patch: Patch to attach, it needs to be converted to file first
            opts: Additional options for bugzilla. Example:
                {
                    "bz_id": 100, # Bugzilla ticket id, if provided the
                                  # new message will be added as new comment to this ticket
                    "patch_filename": "patch" # Name of the file, that should be attached
                }

        Returns:
            Dictionary containing ticket bugzilla id
            Example:
            {
                "bz_id": 100
            }
        """
        output = {}
        bug_id = opts.get("bz_id", 0)
        filename = opts.get("patch_filename", "")
        if bug_id == 0 or not filename:
            raise PatcherException(
                "Additional parameters are missing! "
                "Please provide `bz_id` and `patch_filename`."
            )

        # Write file to temporary directory
        # and stop bandit from complaining about a hardcoded temporary directory
        # because it's needed for OpenShift
        with TemporaryDirectory(prefix="thn-", dir="/var/tmp") as tmp:  # nosec
            filepath = os.path.join(tmp, filename)
            with open(filepath, "w") as f:
                f.write(patch)

            _logger.debug("Attaching file to bug %r" % bug_id)
            description = "Update to {} (#{})".format(package.version, bug_id)
            self.bugzilla.attachfile(bug_id, filepath, description, is_patch=True)
            _logger.info("Attached file to bug: %r" % bug_id)

        output["bz_id"] = bug_id
        return output class MDApi(Validator):
    """
    Wrapper around the mdapi https://mdapi.fedoraproject.org/

    It inherits the Validator abstract class and implements the validate method.

    The mdapi will be used to check for the last available package version available in
    Fedora and compare if this is newer than the provided package.

    Attributes:
        url: URL of the mdapi server
        requests_session: Session object which will be used for HTTP request
        timeout: Timeouts to HTTP request in seconds (connect timeout, read timeout)
        __rc_release_regex: Regex for parsing release field obtained from mdapi
    """

    __rc_release_regex = re.compile(r"0\.[0-9]+\.(rc|pre|beta|alpha|dev)([0-9]*)", re.I)

    def __init__(self, url: str, requests_session: Session, timeout: tuple) -> None:
        """
        Class constructor.
        """
        super(MDApi, self).__init__()
        self.url = url
        self.requests_session = requests_session
        self.timeout = timeout

    def validate(self, package: Package) -> dict:
        """
        Implementation of `Validator.validate` method. It calls the mdapi, compares
        retrieved version with package version and returns the output.

        Params:
            package: Package to check against mdapi

        Returns:
            Dictionary containing output of response and validation.
            Example:
            {
                "newer": True,
                "version": "1.0",
                "release": 1
            }

        Raises:
            HTTPException: Is raised when HTTP status code of response isn't 200.
        """
        output = {}
        mdapi_url = "{0}/koji/srcpkg/{1}".format(self.url, package.name)
        _logger.debug("Getting pkg info from %r" % mdapi_url)

        response = self.requests_session.get(mdapi_url, timeout=self.timeout)
        # If error is encountered raise exception
        if response.status_code != 200:
            raise HTTPException(
                response.status_code,
                "Error encountered on request {}".format(mdapi_url),
            )

        js = response.json()
        output["version"] = js["version"]
        output["release"] = js["release"]

        _logger.info(
            "Comparing upstream %s against repo %s-%s"
            % (package.version, output["version"], output["release"])
        )

        rc_tuple = self._get_rc(output["release"])

        mdapi_version = "{}{}{}".format(output["version"], rc_tuple[0], rc_tuple[1])

        diff = RPM.compare(package.version, mdapi_version)

        if diff == 1:
            _logger.info(
                "%s is newer than %s-%s"
                % (package.version, output["version"], output["release"])
            )
            output["newer"] = True
        else:
            _logger.info(
                "%s is not newer than %s-%s"
                % (package.version, output["version"], output["release"])
            )
            output["newer"] = False

        return output

    def _get_rc(self, release: str) -> tuple:
        """
        Get the release candidate value of a package's release

        Params:
            release: Release to parse

        Returns:
            Tuple containing string part of release and numeric part of release.
        """
        match = self.__rc_release_regex.match(release)

        if match:
            return (match.group(1), match.group(2))
        else:
            return ("", "") class Pagure(Validator):
    """
    Wrapper around Pagure dist-git https://src.fedoraproject.org

    It inherits the Validator abstract class and implements the validate method.

    The Pagure wrapper is used to check if the package owner in Fedora wants to be notified
    about new version and optionally run a scratch build.

    Attributes:
        url: URL of the PDC server
        requests_session: Session object which will be used for HTTP request
        timeout: Timeouts to HTTP request in seconds (connect timeout, read timeout)
    """

    def __init__(
        self,
        url: str,
        requests_session: Session,
        timeout: tuple,
    ) -> None:
        """
        Class constructor.
        """
        self.url = url
        self.requests_session = requests_session
        self.timeout = timeout

    def validate(self, package: Package) -> dict:
        """
        Implementation of `Validator.validate` method. It calls the Pagure dist-git, checks if the
        package owner wants to be monitored and returns the output.

        Params:
            package: Package to check in Pagure

        Returns:
            Dictionary containing output of response and validation.
            Example:
            {
                "monitoring": True,  # If the packager wants to be notified or not
                "scratch_build": False  # If the packager wants to run a scratch build or not
            }

        Raises:
            HTTPException: Is raised when HTTP status code of response isn't 200.
        """
        output = {"monitoring": False, "scratch_build": False}
        dist_git_url = "{0}/_dg/anitya/rpms/{1}".format(self.url, package.name)
        _logger.debug(
            "Checking {} to see if {} is monitored.".format(dist_git_url, package.name)
        )
        response = self.requests_session.get(dist_git_url, timeout=self.timeout)

        if not response.status_code == 200:
            raise HTTPException(
                response.status_code,
                "Error encountered on request {}".format(dist_git_url),
            )

        data = response.json()
        monitoring_value = data.get("monitoring", "no-monitoring")

        # Fill the output based on the monitoring value
        # Invalid = monitoring: False; scrach_build: False
        # monitoring-with-scratch = monitoring: True; scrach_build: True
        # monitoring = monitoring: True; scrach_build: False
        # no-monitoring = monitoring: False; scrach_build: False
        if monitoring_value not in MONITORING_STATUSES:
            _logger.info(
                "Unknown status '{}' recovered from {}".format(
                    dist_git_url, monitoring_value
                )
            )

        if monitoring_value == "monitoring-with-scratch":
            output["monitoring"] = True
            output["scratch_build"] = True

        if monitoring_value == "monitoring":
            output["monitoring"] = True

        return output class PDC(Validator):
    """
    Wrapper around PDC (Product definition center) https://pdc.fedoraproject.org

    It inherits the Validator abstract class and implements the validate method.

    The PDC wrapper is used to check if the package is retired in Fedora.

    Attributes:
        url: URL of the PDC server
        requests_session: Session object which will be used for HTTP request
        timeout: Timeouts to HTTP request in seconds (connect timeout, read timeout)
        branch: Branch to check (rawhide, f34, f33...)
        package_type: Type of package to check (rpm, module...)
    """

    def __init__(
        self,
        url: str,
        requests_session: Session,
        timeout: tuple,
        branch: str,
        package_type: str,
    ) -> None:
        """
        Class constructor.
        """
        self.url = url
        self.requests_session = requests_session
        self.timeout = timeout
        self.branch = branch
        self.package_type = package_type

    def validate(self, package: Package) -> dict:
        """
        Implementation of `Validator.validate` method. It calls the PDC, checks the response
        and returns the output.

        Params:
            package: Package to check in PDC

        Returns:
            Dictionary containing output of response and validation.
            Example:
            {
                "retired": True,
                "count": "0",  # Number of active branches found
            }

        Raises:
            HTTPException: Is raised when HTTP status code of response isn't 200.
        """
        output = {}
        pdc_url = "{0}/rest_api/v1/component-branches/".format(self.url)
        params = (
            ("name", self.branch),
            ("global_component", package.name),
            ("type", self.package_type),
            ("active", True),
        )
        _logger.debug(
            "Checking {} to see if {} is retired, {}".format(pdc_url, package, params)
        )
        response = self.requests_session.get(
            pdc_url, params=params, timeout=self.timeout
        )

        if not response.status_code == 200:
            raise HTTPException(
                response.status_code, "Error encountered on request {}".format(pdc_url)
            )

        js = response.json()
        # If there are zero active rawhide branches for this package, then it is
        # retired.
        output["retired"] = js["count"] == 0
        output["active_branches"] = js["count"]

        return output class ResponseFailure(Response):
    """
    Class that represents failure response returned from use case.
    It is send when some exception happen during the use case.
    Defines constants for error types.

    Attributes:
        type: Type of the failure.
        message: Error message.
        traceback: Exception traceback as string.
        use_case_value: Partial use case output from Exception, if provided.
    """

    VALIDATOR_ERROR = "ValidatorError"
    BUILDER_ERROR = "BuilderError"
    DATABASE_ERROR = "DatabaseError"
    NOTIFIER_ERROR = "NotifierError"
    PATCHER_ERROR = "PatcherError"
    INVALID_REQUEST_ERROR = "InvalidRequestError"

    def __init__(self, type: str, message: Any) -> None:
        """
        Class constructor.
        """
        self.type = type
        self.message = self._format_message(message)
        self.traceback = self._get_stack_trace(message)
        self.use_case_value = self._get_value(message)

    def _get_value(self, message: Any) -> Optional[dict]:
        """
        Retrieves the use case value information from Exception,
        otherwise just returns empty dict.

        Params:
            message: Input to retrieve use case value from

        Returns:
            Value if message is Exception, otherwise None
        """
        value = None

        if isinstance(message, BaseHotnessException):
            value = message.value

        return value

    def _get_stack_trace(self, message: Any) -> List[str]:
        """
        Retrieves the stack trace information from Exception,
        otherwise just returns empty list.

        Params:
            message: Input to retrieve stack trace from

        Returns:
            Stack trace if message is Exception, otherwise empty string
        """
        stack_trace = []

        if isinstance(message, Exception):
            stack_trace = traceback.format_tb(message.__traceback__)

        return stack_trace

    def _format_message(self, message: Any) -> Any:
        """
        Formats the input message if the message inherits from Exception,
        otherwise just return it back.

        Params:
            message: Input message to format

        Returns:
            String if exception, otherwise return the same object we received.
        """
        if isinstance(message, Exception):
            return "{}: {}".format(message.__class__.__name__, "{}".format(message))

        return message

    @property
    def value(self):
        """
        Returns the dict representation of the failure response.
        """
        return {
            "type": self.type,
            "message": self.message,
            "use_case_value": self.use_case_value,
        }

    def __bool__(self) -> bool:
        """
        Boolean representation of response.
        """
        return False

    @classmethod
    def validator_error(cls, message: Any) -> "ResponseFailure":
        """
        Creates response for validator failure.

        Params:
            message: Message to add to this error

        Returns:
            ResponseFailure object
        """
        response = ResponseFailure(
            type=ResponseFailure.VALIDATOR_ERROR, message=message
        )

        return response

    @classmethod
    def builder_error(cls, message: Any) -> "ResponseFailure":
        """
        Creates response for builder failure.

        Params:
            message: Message to add to this error

        Returns:
            ResponseFailure object
        """
        response = ResponseFailure(type=ResponseFailure.BUILDER_ERROR, message=message)

        return response

    @classmethod
    def database_error(cls, message: Any) -> "ResponseFailure":
        """
        Creates response for database failure.

        Params:
            message: Message to add to this error

        Returns:
            ResponseFailure object
        """
        response = ResponseFailure(type=ResponseFailure.DATABASE_ERROR, message=message)

        return response

    @classmethod
    def notifier_error(cls, message: Any) -> "ResponseFailure":
        """
        Creates response for notifier failure.

        Params:
            message: Message to add to this error

        Returns:
            ResponseFailure object
        """
        response = ResponseFailure(type=ResponseFailure.NOTIFIER_ERROR, message=message)

        return response

    @classmethod
    def patcher_error(cls, message: Any) -> "ResponseFailure":
        """
        Creates response for patcher failure.

        Params:
            message: Message to add to this error

        Returns:
            ResponseFailure object
        """
        response = ResponseFailure(type=ResponseFailure.PATCHER_ERROR, message=message)

        return response

    @classmethod
    def invalid_request_error(cls, request: Request) -> "ResponseFailure":
        """
        Creates response for invalid request failure.

        Params:
            request: Invalid request to add to this error

        Returns:
            ResponseFailure object
        """
        response = ResponseFailure(
            type=ResponseFailure.INVALID_REQUEST_ERROR, message=str(request.errors)
        )

        return response class PackageRequest(Request):
    """
    This class represents request, which will be sent to use cases working with package,
    like validators.

    Attributes:
        package: Package sent with request.
    """

    def __init__(self, package: Package) -> None:
        """
        Class constructor.
        """
        super(PackageRequest, self).__init__()
        self.package = package class BuildRequest(PackageRequest):
    """
    This class represents request, which will be sent to scratch build use case.

    Attributes:
        package: Package sent with request.
        opts: Any additional options for specific builder.
    """

    def __init__(self, package: Package, opts: dict) -> None:
        """
        Class constructor.
        """
        super(BuildRequest, self).__init__(package)
        self.opts = opts class NotifyRequest(PackageRequest):
    """
    This class represents request, which will be sent to use cases sending notifications,
    like notifiers.

    Attributes:
        package: Package sent with request.
        message: Message to sent.
        opts: Additional options for specific notifier.
    """

    def __init__(self, package: Package, message: str, opts: dict) -> None:
        """
        Class constructor.
        """
        super(NotifyRequest, self).__init__(package)
        self.message = message
        self.opts = opts class SubmitPatchRequest(PackageRequest):
    """
    This class represents request, which will be sent to use cases submitting patches,
    like patchers.

    Attributes:
        package: Package sent with request.
        patch: Patch to submit.
        opts: Options for specific external patcher.
    """

    def __init__(self, package: Package, patch: str, opts: dict) -> None:
        """
        Class constructor.
        """
        super(SubmitPatchRequest, self).__init__(package)
        self.patch = patch
        self.opts = opts class InsertDataRequest(Request):
    """
    This class represents request, which contain key/value pair to store in database for
    later use.

    Attributes:
        key: Key for the key/value pair.
        value: Value for the key/value pair.
    """

    def __init__(self, key: str, value: str) -> None:
        """
        Class constructor.
        """
        super(InsertDataRequest, self).__init__()
        self.key = key
        self.value = value class RetrieveDataRequest(Request):
    """
    This class represents request, which contain key to retrieve from database.

    Attributes:
        key: Key for the key/value pair.
    """

    def __init__(self, key: str) -> None:
        """
        Class constructor.
        """
        super(RetrieveDataRequest, self).__init__()
        self.key = key class InsertDataUseCase:
    """
    This class represents use case for inserting key/value pairs using databases.

    Attributes:
        database: Database to use.
    """

    def __init__(self, database: Database):
        """
        Class constructor.
        """
        self.database = database

    def insert(self, request: InsertDataRequest) -> responses.Response:
        """
        Call the insert method on the database.
        This method will handle any error that happens when inserting data.

        Params:
            request: Request to handle.

        Return:
           Output of the data insertion.
        """
        if not request:
            return responses.ResponseFailure.invalid_request_error(request)
        try:
            result = self.database.insert(request.key, request.value)
            return responses.ResponseSuccess(result)
        except Exception as exc:
            logger.exception("Insert data use case failure", exc_info=True)
            return responses.ResponseFailure.database_error(exc) class RetrieveDataUseCase:
    """
    This class represents use case for retrieving value for key using databases.

    Attributes:
        database: Database to use.
    """

    def __init__(self, database: Database):
        """
        Class constructor.
        """
        self.database = database

    def retrieve(self, request: RetrieveDataRequest) -> responses.Response:
        """
        Call the retrieve method on the database.
        This method will handle any error that happens when retrieving data.

        Params:
            request: Request to handle.

        Return:
           Output of the data insertion.
        """
        if not request:
            return responses.ResponseFailure.invalid_request_error(request)
        try:
            result = self.database.retrieve(request.key)
            return responses.ResponseSuccess(result)
        except Exception as exc:
            logger.exception("Retrieve data use case failure", exc_info=True)
            return responses.ResponseFailure.database_error(exc) class PackageCheckUseCase:
    """
    This class represents use case for validating the package with provided validator.

    Attributes:
        validator: Validator to use.
    """

    def __init__(self, validator: Validator):
        """
        Class constructor.
        """
        self.validator = validator

    def validate(self, request: PackageRequest) -> responses.Response:
        """
        Call the validate method on the validator.
        This method will handle any error that happens during validation.

        Params:
            request: Request to handle.

        Return:
           Output of the validation.
        """
        if not request:
            return responses.ResponseFailure.invalid_request_error(request)
        try:
            result = self.validator.validate(request.package)
            return responses.ResponseSuccess(result)
        except Exception as exc:
            logger.exception("Package check use case failure", exc_info=True)
            return responses.ResponseFailure.validator_error(exc) class PackageScratchBuildUseCase:
    """
    This class represents use case for building the package with provided builder.

    Attributes:
        builder: Builder to use.
    """

    def __init__(self, builder: Builder):
        """
        Class constructor.
        """
        self.builder = builder

    def build(self, request: BuildRequest) -> responses.Response:
        """
        Call the build method on the builder.
        This method will handle any error that happens when starting build.

        Params:
            request: Request to handle.

        Return:
           Output of the build.
        """
        if not request:
            return responses.ResponseFailure.invalid_request_error(request)
        try:
            result = self.builder.build(request.package, request.opts)
            return responses.ResponseSuccess(result)
        except Exception as exc:
            logger.exception("Package scratch build use case failure", exc_info=True)
            return responses.ResponseFailure.builder_error(exc) class NotifyUserUseCase:
    """
    This class represents use case for sending notification using notifiers.

    Attributes:
        notifier: Notifier to use.
    """

    def __init__(self, notifier: Notifier):
        """
        Class constructor.
        """
        self.notifier = notifier

    def notify(self, request: NotifyRequest) -> responses.Response:
        """
        Call the notify method on the notifier.
        This method will handle any error that happens when sending notification.

        Params:
            request: Request to handle.

        Return:
           Output of the notification.
        """
        if not request:
            return responses.ResponseFailure.invalid_request_error(request)
        try:
            result = self.notifier.notify(
                request.package, request.message, request.opts
            )
            return responses.ResponseSuccess(result)
        except Exception as exc:
            logger.exception("Notify user use case failure", exc_info=True)
            return responses.ResponseFailure.notifier_error(exc) class SubmitPatchUseCase:
    """
    This class represents use case for submitting the patch to package with provided patcher.

    Attributes:
        patcher: Patcher to use.
    """

    def __init__(self, patcher: Patcher):
        """
        Class constructor.
        """
        self.patcher = patcher

    def submit_patch(self, request: SubmitPatchRequest) -> responses.Response:
        """
        Call the submit_patch method on the patcher.
        This method will handle any error that happens during submit of the patch.

        Params:
            request: Request to handle.

        Return:
           Output of the build.
        """
        if not request:
            return responses.ResponseFailure.invalid_request_error(request)
        try:
            result = self.patcher.submit_patch(
                request.package, request.patch, request.opts
            )
            return responses.ResponseSuccess(result)
        except Exception as exc:
            logger.exception("Submit patch use case failure", exc_info=True)
            return responses.ResponseFailure.patcher_error(exc)
import logging
import requests
from typing import cast, List
from requests.packages.urllib3.util import retry  # type: ignore
from anitya_schema.project_messages import ProjectVersionUpdated  # type: ignore
from fedora_messaging.message import Message  # type: ignore
from hotness.config import config
from hotness.domain import Package
from hotness.builders import Koji
from hotness.databases import Cache
from hotness.notifiers import Bugzilla as bz_notifier, FedoraMessaging
from hotness.patchers import Bugzilla as bz_patcher
from hotness.validators import MDApi, Pagure, PDC
from hotness.responses import ResponseFailure
from hotness.requests import (
    BuildRequest,
    InsertDataRequest,
    NotifyRequest,
    PackageRequest,
    RetrieveDataRequest,
    SubmitPatchRequest,
)
from hotness.use_cases import (
    InsertDataUseCase,
    NotifyUserUseCase,
    PackageScratchBuildUseCase,
    PackageCheckUseCase,
    RetrieveDataUseCase,
    SubmitPatchUseCase,
)
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2, or (at your option) any later
# version.  This program is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY expressed or implied, including the
# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.  You
# should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Any Red Hat trademarks that are incorporated in the source
# code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission
# of Red Hat, Inc.



_logger = logging.getLogger(__name__)

# Prefix used for the topic of published messages
PREFIX = "hotness"


class HotnessConsumer(object):
    """
    A fedora-messaging consumer that is the heart of the-new-hotness.

    This consumer subscribes to the following topics:

    * 'org.fedoraproject.prod.buildsys.task.state.change'
      handled by :method:`BugzillaTicketFiler.handle_buildsys_scratch`

    * 'org.release-monitoring.prod.anitya.project.version.update'
      handled by :method:`BugzillaTicketFiler.handle_anitya_version_update`

    * 'org.release-monitoring.prod.anitya.project.map.new'
      handled by :method:`BugzillaTicketFiler.handle_anitya_map_new`

    Attributes:
        short_desc_template (str): Short description template for notifier
        description_template (str): Template for the message content
        distro (str): Distro to watch the updates for
        builder_koji (`Koji`): Koji builder to use for scratch builds
        database_cache (`Cache`): Database that will be used for holding key/value
                                  for build id/bug id
        notifier_bugzilla (`bz_notifier`): Bugzilla notifier for creating and updating
                                           tickets in Bugzilla
        notifier_fedora_messaging (`FedoraMessaging`): Fedora messaging notifier to send
                                                       fedora messages to broker
        patcher_bugzilla (`bz_patcher`): Bugzilla patcher for attaching patcher to tickets
                                         in Bugzilla
        validator_mdapi (`MDApi`): MDApi validator to retrieve the metadata for package
        validator_pagure (`Pagure`): Pagure dist git for retrieval of notification settings
        validator_pdc (`PDC`): PDC validator to check if package is retired
    """

    def __init__(self):
        """
        Consumer initialization.

        It loads the configuration and then initializes all external systems
        use cases will call.
        """
        # Prepare requests session
        requests_session = requests.Session()
        timeout = (
            config["connect_timeout"],
            config["read_timeout"],
        )
        retries = config["requests_retries"]
        retry_conf = retry.Retry(
            total=retries, connect=retries, read=retries, backoff_factor=1
        )
        retry_conf.BACKOFF_MAX = 5
        requests_session.mount(
            "http://", requests.adapters.HTTPAdapter(max_retries=retry_conf)
        )
        requests_session.mount(
            "https://", requests.adapters.HTTPAdapter(max_retries=retry_conf)
        )

        # Initialize attributes
        self.short_desc_template = config["bugzilla"]["short_desc_template"]
        self.description_template = config["bugzilla"]["description_template"]
        self.explanation_url = config["bugzilla"]["explanation_url"]
        self.distro = config["distro"]
        self.repoid = config["repoid"]
        self.hotness_issue_tracker = config["hotness_issue_tracker"]
        self.builder_koji = Koji(
            server_url=config["koji"]["server"],
            web_url=config["koji"]["weburl"],
            kerberos_args={
                "krb_principal": config["koji"]["krb_principal"],
                "krb_keytab": config["koji"]["krb_keytab"],
                "krb_ccache": config["koji"]["krb_ccache"],
                "krb_proxyuser": config["koji"]["krb_proxyuser"],
                "krb_sessionopts": config["koji"]["krb_sessionopts"],
            },
            git_url=config["koji"]["git_url"],
            user_email=tuple(config["koji"]["user_email"]),
            opts=config["koji"]["opts"],
            priority=config["koji"]["priority"],
            target_tag=config["koji"]["target_tag"],
        )
        self.database_cache = Cache()
        self.notifier_bugzilla = bz_notifier(
            server_url=config["bugzilla"]["url"],
            reporter=config["bugzilla"]["reporter"],
            email=config["bugzilla"]["reporter_email"],
            api_key=config["bugzilla"]["api_key"],
            product=config["bugzilla"]["product"],
            keywords=config["bugzilla"]["keywords"],
            version=config["bugzilla"]["version"],
            status=config["bugzilla"]["bug_status"],
        )
        self.notifier_fedora_messaging = FedoraMessaging(prefix=PREFIX)
        self.patcher_bugzilla = bz_patcher(
            server_url=config["bugzilla"]["url"],
            api_key=config["bugzilla"]["api_key"],
        )

        self.validator_mdapi = MDApi(