// Copyright Contributors to the DNF5 project.
// Copyright Contributors to the libdnf project.
// SPDX-License-Identifier: LGPL-2.1-or-later
//
// This file is part of libdnf: https://github.com/rpm-software-management/libdnf/
//
// Libdnf is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
//
// Libdnf is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with libdnf.  If not, see <https://www.gnu.org/licenses/>.


#include "transaction.hpp"

#include "conf/config.h"

#include "libdnf5/base/transaction.hpp"
#include "libdnf5/common/exception.hpp"
#include "libdnf5/rpm/package_query.hpp"
#include "libdnf5/transaction/transaction_item_action.hpp"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"
#include "libdnf5/utils/to_underlying.hpp"

#include <fcntl.h>
#include <fmt/format.h>
#include <rpm/rpmbuild.h>
#include <rpm/rpmdb.h>
#include <rpm/rpmlib.h>
#include <rpm/rpmpgp.h>
#include <rpm/rpmtag.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <algorithm>
#include <map>


namespace libdnf5::rpm {

RpmHeader & RpmHeader::operator=(const RpmHeader & src) {
    if (&src != this) {
        headerFree(header);
        header = headerLink(src.header);
    }
    return *this;
}

RpmHeader & RpmHeader::operator=(RpmHeader && src) {
    if (&src != this) {
        headerFree(header);
        header = src.header;
        src.header = nullptr;
    }
    return *this;
}

std::string RpmHeader::get_evr() const {
    auto * tmp = headerGetAsString(header, RPMTAG_EVR);
    std::string ret = tmp;
    rfree(tmp);
    return ret;
}

std::string RpmHeader::get_nevra() const {
    auto * tmp = headerGetAsString(header, RPMTAG_NEVRA);
    std::string ret = tmp;
    rfree(tmp);
    return ret;
}

std::string RpmHeader::get_full_nevra() const {
    std::stringstream out;
    out << get_name() << '-' << get_epoch() << ':' << get_version() << '-' << get_release() << '.' << get_arch();
    return out.str();
}

RpmProblemSet::Iterator & RpmProblemSet::Iterator::operator++() {
    if (!rpmpsiNext(iter)) {
        free();
    }
    return *this;
};

Transaction::Transaction(const BaseWeakPtr & base) : base(base), rpm_log_guard(base) {
    ts = rpmtsCreate();
    auto & config = base->get_config();
    set_root_dir(config.get_installroot_option().get_value().c_str());
    auto vsflags = static_cast<rpmVSFlags>(rpmExpandNumeric("%{?__vsflags}"));
    set_signature_verify_flags(vsflags);
    rpmtsSetChangeCallback(ts, ts_change_callback, this);
}

Transaction::Transaction(Base & base) : Transaction(base.get_weak_ptr()) {}

Transaction::~Transaction() {
    rpmtsFree(ts);
    if (script_fd) {
        Fclose(script_fd);
    }
}


void Transaction::set_root_dir(const char * root_dir) {
    auto rc = rpmtsSetRootDir(ts, root_dir);
    if (rc != 0) {
        //TODO(jrohel): Why? Librpm does not provide this information.
        throw TransactionError(M_("Cannot set root directory \"{}\""), std::string(root_dir));
    }
}

std::string Transaction::get_db_cookie() const {
    std::string rpmdb_cookie;

    // Open rpm database if it is not already open
    if (!rpmtsGetRdb(ts)) {
        auto rc = rpmtsOpenDB(ts, rpmtsGetDBMode(ts));
        if (rc != 0) {
            throw TransactionError(M_("Error %i opening rpm database"), rc);
        }
    }

    std::unique_ptr<char, decltype(free) *> rpmdb_cookie_uptr{rpmdbCookie(rpmtsGetRdb(ts)), free};
    if (rpmdb_cookie_uptr) {
        rpmdb_cookie = rpmdb_cookie_uptr.get();
    }
    if (rpmdb_cookie.empty()) {
        throw TransactionError(M_("The rpmdbCookie() function did not return cookie of rpm database."));
    }

    return rpmdb_cookie;
}

void Transaction::fill(const base::Transaction & transaction) {
    transaction_items = transaction.get_transaction_packages();

    // If SOURCE_DATE_EPOCH is set, sort the package list for reproducibility
    if (std::getenv("SOURCE_DATE_EPOCH") != nullptr) {
        auto & logger = *base->get_logger();
        logger.debug("SOURCE_DATE_EPOCH detected; sorting {} packages for reproducibility", transaction_items.size());
        std::sort(transaction_items.begin(), transaction_items.end(), [](const auto & a, const auto & b) {
            return a.get_package().get_nevra() < b.get_package().get_nevra();
        });
    }

    // Auxiliary map name->package with the latest versions of currently
    // installed installonly packages.
    // Used to detect installation of a installonly package with lower version
    // that is currently installed.
    std::map<std::string, libdnf5::rpm::Package> installonly_versions;
    rpm::PackageQuery installonly_query(base);
    installonly_query.filter_installed();
    installonly_query.filter_latest_evr();
    installonly_query.filter_installonly();
    for (const auto & pkg : installonly_query) {
        installonly_versions.insert(std::make_pair(pkg.get_name(), pkg));
    }

    for (auto & tspkg : transaction_items) {
        switch (tspkg.get_action()) {
            case libdnf5::transaction::TransactionItemAction::INSTALL:
                install(tspkg);
                // Inbound installonly packages always have `INSTALL` action,
                // there is no `DOWNGRADE`. We need to detect downgrade
                // manually to correctly add RPMPROB_FILTER_OLDPACKAGE to rpm
                // transaction later during run() execution.
                if (!downgrade_requested) {
                    auto pkg = tspkg.get_package();
                    auto installonly_it = installonly_versions.find(pkg.get_name());
                    if (installonly_it != installonly_versions.end()) {
                        if (libdnf5::rpm::cmp_nevra(pkg, installonly_it->second) > 0) {
                            downgrade_requested = true;
                        }
                    }
                }
                break;
            case libdnf5::transaction::TransactionItemAction::UPGRADE:
                upgrade(tspkg);
                break;
            case libdnf5::transaction::TransactionItemAction::DOWNGRADE:
                downgrade(tspkg);
                break;
            case libdnf5::transaction::TransactionItemAction::REINSTALL:
                reinstall(tspkg);
                break;
            case libdnf5::transaction::TransactionItemAction::REMOVE:
            case libdnf5::transaction::TransactionItemAction::REPLACED:
                erase(tspkg);
                break;
            case libdnf5::transaction::TransactionItemAction::REASON_CHANGE:
            case libdnf5::transaction::TransactionItemAction::ENABLE:
            case libdnf5::transaction::TransactionItemAction::DISABLE:
            case libdnf5::transaction::TransactionItemAction::RESET:
            case libdnf5::transaction::TransactionItemAction::SWITCH:
                break;
        }
    }
    libdnf_assert(implicit_ts_elements.empty(), "The rpm transaction contains more elements than requested");

    // generate ordering for the rpm transaction
    if (rpmtsOrder(ts)) {
        throw TransactionError(M_("Failed to order the rpm transaction."));
    };
}

int Transaction::run() {
    rpmprobFilterFlags ignore_set = RPMPROB_FILTER_NONE;
    if (downgrade_requested) {
        ignore_set |= RPMPROB_FILTER_OLDPACKAGE;
    }
    if (base->get_config().get_ignorearch_option().get_value()) {
        ignore_set |= RPMPROB_FILTER_IGNOREARCH;
    }
    rpmtsSetNotifyStyle(ts, 1);
    rpmtsSetNotifyCallback(ts, ts_callback, &callbacks_holder);
    auto * const callbacks = callbacks_holder.callbacks.get();
    if (callbacks) {
        int nelements = rpmtsNElements(ts);
        libdnf_assert(nelements >= 0, "librpm rpmtsNElements() returned negative number of transaction elements.");
        callbacks->before_begin(static_cast<uint64_t>(nelements));
    }
    auto rc = rpmtsRun(ts, nullptr, ignore_set);
    if (callbacks) {
        callbacks->after_complete(rc == 0);
    }
    rpmtsSetNotifyCallback(ts, nullptr, nullptr);

    return rc;
}

void Transaction::set_script_out_fd(int fd) {
    FD_t script_fd;

    if (fd != -1) {
        script_fd = fdDup(fd);
        if (!script_fd) {
            throw TransactionError(
                M_("Failed to set scriptlet output file, cannot duplicate file descriptor: {}"),
                std::string(Fstrerror(script_fd)));
        }
    } else {
        script_fd = nullptr;
    }

    rpmtsSetScriptFd(ts, script_fd);

    if (this->script_fd) {
        Fclose(this->script_fd);
    }
    this->script_fd = script_fd;
}

void Transaction::set_script_out_file(const std::string & file_path) {
    auto * script_fd = Fopen(file_path.c_str(), "w+b");
    if (!script_fd) {
        throw TransactionError(
            M_("Failed to set scriptlet output file, cannot open file \"{}\": {}"),
            file_path,
            std::string(Fstrerror(script_fd)));
    }
    rpmtsSetScriptFd(ts, script_fd);

    if (this->script_fd) {
        Fclose(this->script_fd);
    }
    this->script_fd = script_fd;
}

BaseWeakPtr Transaction::get_base() const {
    return base;
}

Header Transaction::read_pkg_header(const std::string & file_path) const {
    FD_t fd = Fopen(file_path.c_str(), "r.ufdio");

    if (!fd) {
        throw TransactionError(
            M_("Failed to read package header, cannot open file \"{}\": {}"), file_path, std::string(Fstrerror(fd)));
    }

    const char * descr = file_path.c_str();
    Header h{};  // Initialization of h is not needed. It is output argument of rpmReadPackageFile().
    rpmRC rpmrc = rpmReadPackageFile(ts, fd, descr, &h);
    Fclose(fd);

    switch (rpmrc) {
        case RPMRC_NOTTRUSTED:
        case RPMRC_NOKEY:
        case RPMRC_OK:
            break;
        case RPMRC_NOTFOUND:
        case RPMRC_FAIL:
        default:
            h = headerFree(h);
            throw TransactionError(M_("Failed to read package header from file \"{}\""), file_path);
            break;
    }

    return h;
}

Header Transaction::get_header(unsigned int rec_offset) {
    Header hdr = nullptr;

    // rec_offset must be unsigned int
    auto * iter = rpmtsInitIterator(ts, RPMDBI_PACKAGES, &rec_offset, sizeof(rec_offset));
    if (!iter) {
        throw TransactionError(M_("Cannot init rpm database iterator"));
    }
    hdr = rpmdbNextIterator(iter);
    if (!hdr) {
        throw TransactionError(M_("Package was not found in rpm database"));
    }
    headerLink(hdr);
    rpmdbFreeIterator(iter);
    return hdr;
}

void Transaction::reinstall(base::TransactionPackage & item) {
    auto file_path = item.get_package().get_package_path();
    auto * header = read_pkg_header(file_path);
    last_added_item = &item;
    last_item_added_ts_element = false;
    auto rc = rpmtsAddReinstallElement(ts, header, &item);
    headerFree(header);
    if (rc != 0) {
        //TODO(jrohel): Why? Librpm does not provide this information.
        throw TransactionError(M_("Cannot reinstall package \"{}\""), item.get_package().get_full_nevra());
    }
    libdnf_assert(
        last_item_added_ts_element,
        "librpm has ignored explicit request to reinstall package \"{}\"",
        item.get_package().get_full_nevra());
}

void Transaction::erase(base::TransactionPackage & item) {
    auto rpmdb_id = static_cast<unsigned int>(item.get_package().get_rpmdbid());
    auto * header = get_header(rpmdb_id);
    int unused = -1;
    last_added_item = &item;
    last_item_added_ts_element = false;
    int rc = rpmtsAddEraseElement(ts, header, unused);
    headerFree(header);
    if (rc != 0) {
        //TODO(jrohel): Why? Librpm does not provide this information.
        throw TransactionError(M_("Cannot remove package \"{}\""), item.get_package().get_full_nevra());
    }
    if (!last_item_added_ts_element) {
        auto it = implicit_ts_elements.find(rpmdb_id);
        libdnf_assert(
            it != implicit_ts_elements.end(),
            "librpm has ignored explicit request to remove package \"{}\"",
            item.get_package().get_full_nevra());
        auto * te = it->second;
        rpmteSetUserdata(te, &item);
        implicit_ts_elements.erase(it);
    }
}

void Transaction::install_up_down(base::TransactionPackage & item, libdnf5::transaction::TransactionItemAction action) {
    BgettextMessage msg_error;
    std::string msg_action;
    bool upgrade{true};
    if (action == libdnf5::transaction::TransactionItemAction::UPGRADE) {
        msg_error = M_("Cannot upgrade package \"{}\"");
        msg_action = "upgrade";
    } else if (action == libdnf5::transaction::TransactionItemAction::DOWNGRADE) {
        downgrade_requested = true;
        msg_error = M_("Cannot downgrade package \"{}\"");
        msg_action = "downgrade";
    } else if (action == libdnf5::transaction::TransactionItemAction::INSTALL) {
        upgrade = false;
        msg_error = M_("Cannot install package \"{}\"");
        msg_action = "install";
    } else {
        libdnf_throw_assertion("Unsupported action: {}", utils::to_underlying(action));
    }
    auto file_path = item.get_package().get_package_path();
    auto * header = read_pkg_header(file_path);
    last_added_item = &item;
    last_item_added_ts_element = false;
    auto rc = rpmtsAddInstallElement(ts, header, &item, upgrade ? 1 : 0, nullptr);
    headerFree(header);
    if (rc != 0) {
        //TODO(jrohel): Why? Librpm does not provide this information.
        throw TransactionError(msg_error, item.get_package().get_full_nevra());
    }
    libdnf_assert(
        last_item_added_ts_element,
        "librpm has ignored explicit request to {} package \"{}\"",
        msg_action,
        item.get_package().get_full_nevra());
}

Nevra Transaction::trans_element_to_nevra(rpmte te) {
    Nevra nevra;
    nevra.set_name(rpmteN(te));
    const char * epoch = rpmteE(te);
    nevra.set_epoch(epoch ? epoch : "0");
    nevra.set_version(rpmteV(te));
    nevra.set_release(rpmteR(te));
    nevra.set_arch(rpmteA(te));
    return nevra;
};

std::vector<std::string> Transaction::extract_rpm_messages() {
    return rpm_log_guard.extract_rpm_logs_buffer();
}

int Transaction::ts_change_callback(int event, rpmte te, rpmte other, void * data) {
    auto * transaction = static_cast<Transaction *>(data);

    if (!other) {
        // explicit action caused by last_added_item
        rpmteSetUserdata(te, transaction->last_added_item);
        transaction->last_item_added_ts_element = true;
#if defined(HAVE_RPMTE_SETVFYLEVEL)
        if (!transaction->last_added_item->get_package().is_pkg_gpgcheck_enabled()) {
            // When nocrypto tsflag is set, skip digest verification as well
            auto & tsflags = transaction->base->get_config().get_tsflags_option().get_value();
            bool nocrypto = std::find(tsflags.begin(), tsflags.end(), "nocrypto") != tsflags.end();
            rpmteSetVfyLevel(te, nocrypto ? RPMSIG_NONE_TYPE : RPMSIG_DIGEST_TYPE);
        }
#endif
    } else {
        // action caused by librpm itself
        auto trigger_nevra = transaction->last_added_item->get_package().get_full_nevra();
        auto te_rpmdb_record_number = rpmteDBOffset(te);
        auto te_nevra = fmt::format(
            "{}-{}:{}-{}.{}", rpmteN(te), rpmteE(te) ? rpmteE(te) : "0", rpmteV(te), rpmteR(te), rpmteA(te));
        auto & logger = *transaction->base->get_logger();
        const char * te_type = "unknown";
        switch (rpmteType(te)) {
            case TR_ADDED:
                te_type = "install package";
                break;
            case TR_REMOVED:
                te_type = "remove package";
                break;
            case TR_RPMDB:
                te_type = "package from_rpmdb";
                break;
            //case TR_RESTORED:
            //TODO(jrohel): Newly added to librpm. What exactly does it do?
            //    te_type = "package will be restored";
            //    break;
            default:;
        }
        libdnf_assert(
            te_rpmdb_record_number != 0,
            "Implicit element {} type {} with zero record number (caused by {})",
            te_nevra,
            te_type,
            trigger_nevra);
        switch (event) {
            case RPMTS_EVENT_ADD: {
                transaction->implicit_ts_elements.insert({te_rpmdb_record_number, te});
                logger.debug("Implicitly added element {} type {} (caused by {})", te_nevra, te_type, trigger_nevra);
                break;
            }
            case RPMTS_EVENT_DEL: {
                libdnf_throw_assertion(
                    "Implicitly removed element {} type {} (caused by {})", te_nevra, te_type, trigger_nevra);
            }
        }
    }
    return 0;
}

TransactionCallbacks::ScriptType Transaction::rpm_tag_to_script_type(rpmTag_e tag) noexcept {
    switch (tag) {
        case RPMTAG_PREIN:
            return TransactionCallbacks::ScriptType::PRE_INSTALL;
        case RPMTAG_POSTIN:
            return TransactionCallbacks::ScriptType::POST_INSTALL;
        case RPMTAG_PREUN:
            return TransactionCallbacks::ScriptType::PRE_UNINSTALL;
        case RPMTAG_POSTUN:
            return TransactionCallbacks::ScriptType::POST_UNINSTALL;
        case RPMTAG_PRETRANS:
            return TransactionCallbacks::ScriptType::PRE_TRANSACTION;
        case RPMTAG_POSTTRANS:
            return TransactionCallbacks::ScriptType::POST_TRANSACTION;
        case RPMTAG_TRIGGERPREIN:
            return TransactionCallbacks::ScriptType::TRIGGER_PRE_INSTALL;
        case RPMTAG_TRIGGERIN:
            return TransactionCallbacks::ScriptType::TRIGGER_INSTALL;
        case RPMTAG_TRIGGERUN:
            return TransactionCallbacks::ScriptType::TRIGGER_UNINSTALL;
        case RPMTAG_TRIGGERPOSTUN:
            return TransactionCallbacks::ScriptType::TRIGGER_POST_UNINSTALL;
        case RPMTAG_SYSUSERS:
            return TransactionCallbacks::ScriptType::SYSUSERS;
        case RPMTAG_PREUNTRANS:
            return TransactionCallbacks::ScriptType::PREUN_TRANSACTION;
        case RPMTAG_POSTUNTRANS:
            return TransactionCallbacks::ScriptType::POSTUN_TRANSACTION;
        default:
            return TransactionCallbacks::ScriptType::UNKNOWN;
    }
}


#define libdnf_assert_transaction_item_set() libdnf_assert(item != nullptr, "TransactionItem is not set")

void * Transaction::ts_callback(
    const void * te,
    const rpmCallbackType what,
    const rpm_loff_t amount,
    const rpm_loff_t total,
    [[maybe_unused]] const void * pkg_key,
    rpmCallbackData data) {
    void * rc = nullptr;
    const auto & callbacks_holder = *static_cast<CallbacksHolder *>(data);
    auto & transaction = *callbacks_holder.transaction;
    auto & logger = *transaction.base->get_logger();
    auto * const callbacks = callbacks_holder.callbacks.get();
    auto * const trans_element = static_cast<rpmte>(const_cast<void *>(te));
    auto * const item = trans_element ? static_cast<base::TransactionPackage *>(rpmteUserdata(trans_element)) : nullptr;

    switch (what) {
        case RPMCALLBACK_INST_PROGRESS:
            libdnf_assert_transaction_item_set();
            if (callbacks) {
                callbacks->install_progress(*item, amount, total);
            }
            break;
        case RPMCALLBACK_INST_START:
            // Install? Maybe upgrade/downgrade/...obsolete?
            libdnf_assert_transaction_item_set();
            logger.info(
                "RPM callback install start \"{}\" total {}",
                to_full_nevra_string(trans_element_to_nevra(trans_element)),
                total);
            if (callbacks) {
                callbacks->install_start(*item, total);
            }
            break;
        case RPMCALLBACK_INST_OPEN_FILE: {
            libdnf_assert_transaction_item_set();
            auto file_path = item->get_package().get_package_path();
            logger.info("RPM callback open file \"{}\"", file_path);
            if (file_path.empty()) {
                return nullptr;
            }
            transaction.fd_in_cb = Fopen(file_path.c_str(), "r.ufdio");
            rc = transaction.fd_in_cb;
            break;
        }
        case RPMCALLBACK_INST_CLOSE_FILE:
            logger.info("RPM callback close file");
            if (transaction.fd_in_cb) {
                Fclose(transaction.fd_in_cb);
                transaction.fd_in_cb = nullptr;
            }
            break;
        case RPMCALLBACK_TRANS_PROGRESS:
            if (callbacks) {
                callbacks->transaction_progress(amount, total);
            }
            break;
        case RPMCALLBACK_TRANS_START:
            logger.info("RPM callback transaction start, total {}", total);
            if (callbacks) {
                callbacks->transaction_start(total);
            }
            break;
        case RPMCALLBACK_TRANS_STOP:
            logger.info("RPM callback transaction stop, total {}", total);
            if (callbacks_holder.base_transaction) {
                callbacks_holder.base_transaction->set_rpm_messages(transaction.extract_rpm_messages());
            }
            if (callbacks) {
                callbacks->transaction_stop(total);
            }
            break;
        case RPMCALLBACK_UNINST_PROGRESS:
            libdnf_assert_transaction_item_set();
            if (callbacks) {
                callbacks->uninstall_progress(*item, amount, total);
            }
            break;
        case RPMCALLBACK_UNINST_START:
            libdnf_assert_transaction_item_set();
            logger.info(
                "RPM callback uninstall start \"{}\" total {}",
                to_full_nevra_string(trans_element_to_nevra(trans_element)),
                total);
            if (callbacks) {
                callbacks->uninstall_start(*item, total);
            }
            break;
        case RPMCALLBACK_UNINST_STOP:
            libdnf_assert_transaction_item_set();
            if (callbacks_holder.base_transaction) {
                callbacks_holder.base_transaction->set_rpm_messages(transaction.extract_rpm_messages());
            }
            logger.info(
                "RPM callback uninstall stop \"{}\" amount {} total {}",
                to_full_nevra_string(trans_element_to_nevra(trans_element)),
                amount,
                total);
            if (callbacks) {
                callbacks->uninstall_stop(*item, amount, total);
            }
            break;
        case RPMCALLBACK_UNPACK_ERROR:
            libdnf_assert_transaction_item_set();
            if (callbacks_holder.base_transaction) {
                callbacks_holder.base_transaction->set_rpm_messages(transaction.extract_rpm_messages());
            }
            logger.error(
                "RPM callback unpack error \"{}\"", to_full_nevra_string(trans_element_to_nevra(trans_element)));
            if (callbacks) {
                callbacks->unpack_error(*item);
            }
            break;
        case RPMCALLBACK_CPIO_ERROR:
            // Not found usage in librpm.
            libdnf_assert_transaction_item_set();
            logger.error("RPM callback CPIO error \"{}\"", to_full_nevra_string(trans_element_to_nevra(trans_element)));
            if (callbacks) {
                callbacks->cpio_error(*item);
            }
            break;
        case RPMCALLBACK_SCRIPT_ERROR: {
            // amount is script tag
            auto script_type = rpm_tag_to_script_type(static_cast<rpmTag_e>(amount));
            auto nevra = trans_element_to_nevra(trans_element);
            // total is return code - if (!RPMSCRIPT_FLAG_CRITICAL) return_code = RPMRC_OK
            // therefore in case RPMRC_OK is passed, nothing critical happened
            // don't confuse the user by telling an error occurred, but exit code is "success"
            auto return_code = static_cast<rpmRC_e>(total);
            if (return_code != RPMRC_OK) {
                logger.error(
                    "RPM callback error in {} scriptlet \"{}\" return code {}",
                    TransactionCallbacks::script_type_to_string(script_type),
                    to_full_nevra_string(nevra),
                    total);
                if (callbacks) {
                    callbacks->script_error(item, nevra, script_type, return_code);
                }
            } else {
                logger.warning(
                    "RPM callback non-critical error in {} scriptlet \"{}\"",
                    TransactionCallbacks::script_type_to_string(script_type),
                    to_full_nevra_string(nevra));
            }
            break;
        }
        case RPMCALLBACK_SCRIPT_START: {
            // amount is script tag
            if (callbacks_holder.base_transaction) {
                // a new scriptlet is running, clear the stored output
                callbacks_holder.base_transaction->clear_last_script_output();
            }
            auto script_type = rpm_tag_to_script_type(static_cast<rpmTag_e>(amount));
            auto nevra = trans_element_to_nevra(trans_element);
            logger.info(
                "RPM callback start {} scriptlet \"{}\"",
                TransactionCallbacks::script_type_to_string(script_type),
                to_full_nevra_string(nevra));
            if (callbacks) {
                callbacks->script_start(item, nevra, script_type);
            }
            break;
        }
        case RPMCALLBACK_SCRIPT_STOP: {
            // amount is script tag
            // total is return code - if (error && !RPMSCRIPT_FLAG_CRITICAL) return_code = RPMRC_NOTFOUND
            if (callbacks_holder.base_transaction) {
                callbacks_holder.base_transaction->set_rpm_messages(transaction.extract_rpm_messages());
            }
            auto script_type = rpm_tag_to_script_type(static_cast<rpmTag_e>(amount));
            auto nevra = trans_element_to_nevra(trans_element);
            logger.info(
                "RPM callback stop {} scriptlet \"{}\" return code {}",
                TransactionCallbacks::script_type_to_string(script_type),
                to_full_nevra_string(nevra),
                total);
            if (callbacks) {
                callbacks->script_stop(item, nevra, script_type, total);
            }
            break;
        }
        case RPMCALLBACK_INST_STOP:
            libdnf_assert_transaction_item_set();
            if (callbacks_holder.base_transaction) {
                callbacks_holder.base_transaction->set_rpm_messages(transaction.extract_rpm_messages());
            }
            logger.info(
                "RPM callback install stop \"{}\" amount {} total {}",
                to_full_nevra_string(trans_element_to_nevra(trans_element)),
                amount,
                total);
            if (callbacks) {
                callbacks->install_stop(*item, amount, total);
            }
            break;
        case RPMCALLBACK_ELEM_PROGRESS:
            libdnf_assert_transaction_item_set();
            if (callbacks) {
                callbacks->elem_progress(*item, amount, total);
            }
            break;
        case RPMCALLBACK_VERIFY_PROGRESS:
            if (callbacks) {
                callbacks->verify_progress(amount, total);
            }
            break;
        case RPMCALLBACK_VERIFY_START:
            logger.info("RPM callback verify start, total {}", total);
            if (callbacks) {
                callbacks->verify_start(total);
            }
            break;
        case RPMCALLBACK_VERIFY_STOP:
            logger.info("RPM callback verify stop, total {}", total);
            if (callbacks) {
                callbacks->verify_stop(total);
            }
            break;
        default:
            logger.warning("Unknown RPM Transaction callback type {}", what);
            break;
    }

    return rc;
}

#undef libdnf_assert_transaction_item_set

}  // namespace libdnf5::rpm
