/*
Copyright Contributors to the libdnf project.

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/>.
*/


#ifndef LIBDNF5_RPM_TRANSACTION_HPP
#define LIBDNF5_RPM_TRANSACTION_HPP

#include "rpm_log_guard.hpp"

#include "libdnf5/base/base_weak.hpp"
#include "libdnf5/base/transaction_package.hpp"
#include "libdnf5/common/exception.hpp"
#include "libdnf5/rpm/package.hpp"
#include "libdnf5/rpm/transaction_callbacks.hpp"

#include <rpm/header.h>
#include <rpm/rpmprob.h>
#include <rpm/rpmps.h>
#include <rpm/rpmts.h>

#include <memory>

// Required for building with fmt >= 10
// See: https://github.com/fmtlib/fmt/blob/10.0.0/ChangeLog.rst?plain=1#L68
inline uint32_t format_as(rpmCallbackType type) {
    return static_cast<uint32_t>(type);
}


namespace libdnf5::rpm {

class RpmProblemSet;


/// Class for access RPM header
class RpmHeader {
public:
    RpmHeader(const RpmHeader & src) : header(headerLink(src.header)) {}

    RpmHeader(RpmHeader && src) : header(src.header) { src.header = nullptr; }

    ~RpmHeader() { headerFree(header); }

    RpmHeader & operator=(const RpmHeader & src);
    RpmHeader & operator=(RpmHeader && src);

    std::string get_name() const { return headerGetString(header, RPMTAG_NAME); }

    uint64_t get_epoch() const noexcept { return headerGetNumber(header, RPMTAG_EPOCH); }

    std::string get_version() const { return headerGetString(header, RPMTAG_VERSION); }

    std::string get_release() const { return headerGetString(header, RPMTAG_RELEASE); }

    std::string get_arch() const { return headerGetString(header, RPMTAG_ARCH); }

    std::string get_evr() const;

    std::string get_nevra() const;

    std::string get_full_nevra() const;

    unsigned int get_rpmdbid() const noexcept { return headerGetInstance(header); }

private:
    friend class Transaction;
    explicit RpmHeader(Header hdr) : header(headerLink(hdr)) {}
    Header header;
};


class RpmProblem {
public:
    bool operator==(RpmProblem & other) const noexcept { return rpmProblemCompare(problem, other.problem) == 0; }

    /// Return package NEVR
    std::string get_pkg_nevr() const { return rpmProblemGetPkgNEVR(problem); }

    /// Return related (e.g. through a dependency) package NEVR
    std::string get_alt_nevr() const { return rpmProblemGetAltNEVR(problem); }

    /// Return type of problem (dependency, diskpace etc)
    rpmProblemType get_type() const { return rpmProblemGetType(problem); }

    /// Return pointer to transaction item associated to the problem or nullptr.
    const base::TransactionPackage * get_transaction_item() const noexcept {
        return static_cast<const base::TransactionPackage *>(rpmProblemGetKey(problem));
    }

    /// Return a generic data string from a problem
    std::string get_generic_string() const { return rpmProblemGetStr(problem); }

    /// Return disk requirement (needed disk space / number of inodes)
    /// depending on problem type. On problem types other than DISKSPACE
    /// and DISKNODES return value is undefined.
    uint64_t get_disk_need() const noexcept { return rpmProblemGetDiskNeed(problem); }

    /// Return formatted string representation of a problem.
    std::string to_string() const { return rpmProblemString(problem); }

private:
    friend RpmProblemSet;
    explicit RpmProblem(rpmProblem problem) : problem(problem) {}
    rpmProblem problem;
};


class RpmProblemSet {
public:
    class Iterator {
    public:
        // iterator traits
        using difference_type = int;
        using value_type = RpmProblem;
        using pointer = const RpmProblem *;
        using reference = const RpmProblem &;
        using iterator_category = std::forward_iterator_tag;

        Iterator() = default;
        Iterator(const Iterator & other) = delete;
        ~Iterator() { rpmpsFreeIterator(iter); }

        Iterator & operator=(const Iterator & other) = delete;

        Iterator & operator++();

        // TODO(jrohel): postfix operator ++, Bad iterator support in the librpm
        // iterator operator++(int);

        bool operator!=(Iterator & other) const { return rpmpsGetProblem(iter) != rpmpsGetProblem(other.iter); }

        RpmProblem operator*() { return RpmProblem(rpmpsGetProblem(iter)); }

    private:
        friend RpmProblemSet;
        explicit Iterator(rpmps problem_set) : iter(rpmpsInitIterator(problem_set)) { rpmpsiNext(iter); }

        void free() {
            rpmpsFreeIterator(iter);
            iter = nullptr;
        }

        rpmpsi iter{nullptr};
    };

    Iterator begin() { return Iterator(problem_set); }

    Iterator end() { return Iterator(); }

    RpmProblemSet(const RpmProblemSet & lhs) : problem_set(rpmpsLink(lhs.problem_set)) {}

    ~RpmProblemSet() { rpmpsFree(problem_set); }

    /// Return number of problems in set.
    /// @return  number of problems
    int size() noexcept { return rpmpsNumProblems(problem_set); }

    bool empty() noexcept { return size() == 0; }

private:
    friend class Transaction;
    explicit RpmProblemSet(rpmps problem_set) : problem_set(problem_set) {}
    rpmps problem_set;
};


class TransactionError : public Error {
public:
    using Error::Error;
    const char * get_domain_name() const noexcept override { return "libdnf5::rpm"; }
    const char * get_name() const noexcept override { return "TransactionError"; }
};


class Transaction {
public:
    explicit Transaction(Base & base);
    explicit Transaction(const BaseWeakPtr & base);
    Transaction(const Transaction &) = delete;
    Transaction(Transaction &&) = delete;
    ~Transaction();

    Transaction & operator=(const Transaction &) = delete;
    Transaction & operator=(Transaction &&) = delete;

    /// Set transaction root directory, i.e. path to chroot(2).
    /// @param root_dir  new transaction root directory (or NULL)
    /// @return  0 on success, -1 on error (invalid root directory)
    void set_root_dir(const char * root_dir);

    /// Get transaction root directory, i.e. path to chroot(2).
    /// @return  transaction root directory
    const char * get_root_dir() const { return rpmtsRootDir(ts); }

    /// Retrieve color bits of transaction set.
    /// @return  color bits
    rpm_color_t get_color() const { return rpmtsColor(ts); }

    /// Set color bits of transaction set.
    /// @param color  new color bits
    /// @return  previous color bits
    rpm_color_t set_color(rpm_color_t color) { return rpmtsSetColor(ts, color); }

    /// Retrieve preferred file color
    /// @return  color bits
    rpm_color_t get_pref_color() const { return rpmtsPrefColor(ts); }

    /// Set preferred file color
    /// @param color  new color bits
    /// @return  previous color bits
    rpm_color_t set_pref_color(rpm_color_t color) { return rpmtsSetPrefColor(ts, color); }

    /// Get transaction flags, i.e. bits that control rpmtsRun().
    /// @return  transaction flags
    rpmtransFlags get_flags() const { return rpmtsFlags(ts); }

    /// Set transaction flags, i.e. bits that control rpmtsRun().
    /// @param flags  new transaction flags
    /// @return  previous transaction flags
    rpmtransFlags set_flags(rpmtransFlags flags) { return rpmtsSetFlags(ts, flags); }

    /// Get verify signatures flag(s).
    /// @return  verify signatures flags
    rpmVSFlags get_signature_verify_flags() const { return rpmtsVSFlags(ts); }

    /// Set verify signatures flag(s).
    /// @param verify_flags  new verify signatures flags
    /// @return         previous value
    rpmVSFlags set_signature_verify_flags(rpmVSFlags verify_flags) { return rpmtsSetVSFlags(ts, verify_flags); }

    /// Get package verify flag(s).
    /// @return  verify signatures flags
    rpmVSFlags get_pkg_verify_flags() const { return rpmtsVfyFlags(ts); }

    /// Set package verify flag(s).
    /// @param verify_flags  new package verify flags
    /// @return  old package verify flags
    rpmVSFlags set_pkg_verify_flags(rpmVSFlags verify_flags) { return rpmtsSetVfyFlags(ts, verify_flags); }

    /// Get enforced package verify level
    /// @return  package verify level
    int get_pkg_verify_level() const { return rpmtsVfyLevel(ts); }

    /// Set enforced package verify level
    /// @param verify_level  new package verify level
    /// @return  old package verify level
    int set_pkg_verify_level(int verify_level) { return rpmtsSetVfyLevel(ts, verify_level); }

    /// Retrieve rpm database cookie.
    /// Useful for eg. determining cache validity.
    /// @return rpm database cookie
    std::string get_db_cookie() const;

    /// Get transaction id, i.e. transaction time stamp.
    /// @return  transaction id
    rpm_tid_t get_id() const { return rpmtsGetTid(ts); }

    /// Set transaction callbacks.
    /// @param callbacks a `unique_ptr` containing the callbacks object
    void set_callbacks(std::unique_ptr<TransactionCallbacks> && callbacks) {
        callbacks_holder.callbacks = std::move(callbacks);
    }

    /// Set pointer to the running libdnf5::Base::Transaction.
    /// @param base_transaction a pointer to the transaction
    void set_base_transaction(libdnf5::base::Transaction * base_transaction) {
        callbacks_holder.base_transaction = base_transaction;
    }

    /// Fill the RPM transaction from base::Transaction.
    /// @param transcation The base::Transaction object.
    void fill(const base::Transaction & transaction);

    /// Perform a dependency check on the transaction set.
    /// After headers have been added to a transaction set,
    /// a dependency check can be performed to make sure that all package dependencies are satisfied.
    /// Any found problems can be examined by retrieving the problem set with rpmtsProblems().
    /// @return  true on dependencies are ok
    bool check() { return rpmtsCheck(ts) == 0; }

    /// Process all package elements in a transaction set.
    /// Before calling run() be sure to have:
    ///
    ///    - setup the rpm root dir via set_root_dir().
    ///    - setup the rpm notify callback via register_cb().
    ///    - setup the rpm transaction flags via set_flags().
    ///
    /// Additionally, though not required you may want to:
    ///
    ///    - setup the rpm verify signature flags via set_signature_verify_flags().
    ///
    /// @return		0 on success, -1 on error, >0 with newProbs set
    int run();

    /// Get current transaction set problems.
    RpmProblemSet get_problems() { return RpmProblemSet(rpmtsProblems(ts)); }

    /// Set transaction script file descriptor, i.e. stdout/stderr on scriptlet execution.
    /// The file descriptor is copied using dup().
    /// @param fd  new script file descriptor (or -1)
    void set_script_out_fd(int fd);

    /// Set transaction script file, i.e. stdout/stderr on scriptlet execution.
    /// The set_script_fd() can be used to pass file handle.
    /// @param file_path  new file path
    void set_script_out_file(const std::string & file_path);

    /// Retrieve recently emitted rpm log messages. After the method is called,
    /// the stored rpm log messages are cleared.
    std::vector<std::string> extract_rpm_messages();

    /// @return A `Base` object to which the transaction belongs.
    /// @since 5.0
    BaseWeakPtr get_base() const;

private:
    struct CallbacksHolder {
        std::unique_ptr<TransactionCallbacks> callbacks;
        Transaction * transaction;
        libdnf5::base::Transaction * base_transaction;
    };

    BaseWeakPtr base;
    rpmts ts;
    FD_t script_fd{nullptr};
    CallbacksHolder callbacks_holder{nullptr, this, nullptr};
    FD_t fd_in_cb{nullptr};  // file descriptor used by transaction in callback (install/reinstall package)

    base::TransactionPackage * last_added_item{nullptr};  // item added by last install/reinstall/remove/...
    bool last_item_added_ts_element{false};               // Did the last item add the element ts?

    std::map<unsigned int, rpmte> implicit_ts_elements;  // elements added to the librpm transaction by librpm itself
    bool downgrade_requested{false};
    std::vector<base::TransactionPackage> transaction_items;

    RpmLogGuard rpm_log_guard;


    /// Return header from package.
    /// @param path  file path
    /// @retval hdrp  header (NULL on failure)
    /// @return  package header
    Header read_pkg_header(const std::string & file_path) const;

    /// Get header of package at offset in the rpmdbi database
    Header get_header(unsigned int rec_offset);

    /// Add package to be installed to transaction set.
    /// The transaction set is checked for duplicate package names.
    /// If found, the package with the "newest" EVR will be replaced.
    /// @param item  item to be installed
    void install(base::TransactionPackage & item) {
        install_up_down(item, libdnf5::transaction::TransactionItemAction::INSTALL);
    }

    /// Add package to be upgraded to transaction set.
    /// The transaction set is checked for duplicate package names.
    /// If found, the package with the "newest" EVR will be replaced.
    /// @param item  item to be upgraded
    void upgrade(base::TransactionPackage & item) {
        install_up_down(item, libdnf5::transaction::TransactionItemAction::UPGRADE);
    }

    /// Add package to be upgraded to transaction set.
    /// The transaction set is checked for duplicate package names.
    /// If found, the package with the "newest" EVR will be replaced.
    /// @param item  item to be upgraded
    void downgrade(base::TransactionPackage & item) {
        install_up_down(item, libdnf5::transaction::TransactionItemAction::DOWNGRADE);
    }

    /// Add package to be reinstalled to transaction set.
    /// @param item  item to be reinstalled
    void reinstall(base::TransactionPackage & item);

    /// Add package to be removed to transaction set.
    /// @param item  item to be erased
    void erase(base::TransactionPackage & item);

    /// Add package to be installed to transaction set.
    /// The transaction set is checked for duplicate package names.
    /// If found, the package with the "newest" EVR will be replaced.
    /// @param item  item to be removed
    /// @param action  one of TransactionItemAction::UPGRADE,
    ///     TransactionItemAction::DOWNGRADE, TransactionItemAction::INSTALL
    void install_up_down(base::TransactionPackage & item, libdnf5::transaction::TransactionItemAction action);

    static Nevra trans_element_to_nevra(rpmte te);

    /// Function triggered by rpmtsNotifyChange()
    ///
    /// On explicit install/remove add events, "other" is NULL, on implicit
    /// add events (erasures due to obsolete/upgrade/reinstall, replaced by newer)
    /// it points to the replacing package.
    ///
    /// @param event  Change event (see rpmtsEvent enum)
    /// @param te  Transaction element
    /// @param other  Related transaction element (or NULL)
    /// @param data  Application private data from rpmtsSetChangeCallback()
    static int ts_change_callback(int event, rpmte te, rpmte other, void * data);

    static TransactionCallbacks::ScriptType rpm_tag_to_script_type(rpmTag_e tag) noexcept;

    /// Function triggered by rpmtsNotify()
    ///
    /// @param te  related transaction element or NULL
    /// @param what  kind of notification
    /// @param amount  number of bytes/packages already processed or tag of the scriptlet involved
    ///                or 0 or some other number
    /// @param total  total number of bytes/packages to be processed or return code of the scriptlet or 0
    /// @param key  result of rpmteKey() of related rpmte or 0
    /// @param data  user data as passed to rpmtsSetNotifyCallback()
    static void * 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);
};

}  // namespace libdnf5::rpm

#endif  // LIBDNF5_RPM_TRANSACTION_HPP
