// 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 "libdnf5/transaction/transaction.hpp"

#include "db/comps_environment.hpp"
#include "db/comps_group.hpp"
#include "db/db.hpp"
#include "db/rpm.hpp"
#include "db/trans.hpp"
#include "transaction/transaction_sr.hpp"

#include "libdnf5/base/base.hpp"
#include "libdnf5/transaction/comps_environment.hpp"
#include "libdnf5/transaction/comps_group.hpp"
#include "libdnf5/transaction/rpm_package.hpp"
#include "libdnf5/transaction/transaction_item.hpp"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"

namespace libdnf5::transaction {

class Transaction::Impl {
public:
    Impl(const BaseWeakPtr & base, int64_t id = 0);

private:
    friend Transaction;

    int64_t id{0};

    int64_t dt_begin = 0;
    int64_t dt_end = 0;
    std::string rpmdb_version_begin;
    std::string rpmdb_version_end;
    // TODO(dmach): move to a new "vars" table?
    std::string releasever;
    uint32_t user_id = 0;
    std::string description;
    std::string comment;
    TransactionState state = State::STARTED;

    std::optional<std::vector<std::pair<int, std::string>>> console_output;

    std::optional<std::vector<CompsEnvironment>> comps_environments;
    std::optional<std::vector<CompsGroup>> comps_groups;
    std::optional<std::vector<Package>> packages;

    BaseWeakPtr base;
};

Transaction::Impl::Impl(const BaseWeakPtr & base, int64_t id) : id(id), base(base) {}

std::string transaction_state_to_string(TransactionState state) {
    switch (state) {
        case TransactionState::STARTED:
            return "Started";
        case TransactionState::OK:
            return "Ok";
        case TransactionState::ERROR:
            return "Error";
    }
    return "";
}


TransactionState transaction_state_from_string(const std::string & state) {
    if (state == "Started") {
        return TransactionState::STARTED;
    } else if (state == "Ok") {
        return TransactionState::OK;
    } else if (state == "Error") {
        return TransactionState::ERROR;
    }

    throw InvalidTransactionState(state);
}


InvalidTransactionState::InvalidTransactionState(const std::string & state)
    : libdnf5::Error(M_("Invalid transaction state: {}"), state) {}


Transaction::Transaction(const BaseWeakPtr & base, int64_t id) : p_impl(std::make_unique<Impl>(base, id)) {}


Transaction::Transaction(const BaseWeakPtr & base) : p_impl(std::make_unique<Impl>(base)) {}

Transaction::~Transaction() = default;

Transaction::Transaction(const Transaction & src) : p_impl(new Impl(*src.p_impl)) {}
Transaction::Transaction(Transaction && src) noexcept = default;

Transaction & Transaction::operator=(const Transaction & src) {
    if (this != &src) {
        if (p_impl) {
            *p_impl = *src.p_impl;
        } else {
            p_impl = std::make_unique<Impl>(*src.p_impl);
        }
    }

    return *this;
}
Transaction & Transaction::operator=(Transaction && src) noexcept = default;

bool Transaction::operator==(const Transaction & other) const {
    return get_id() == other.get_id();
}


bool Transaction::operator<(const Transaction & other) const {
    return get_id() > other.get_id();
}


bool Transaction::operator>(const Transaction & other) const {
    return get_id() < other.get_id();
}


std::vector<CompsEnvironment> & Transaction::get_comps_environments() {
    if (p_impl->comps_environments) {
        return *p_impl->comps_environments;
    }

    p_impl->comps_environments =
        CompsEnvironmentDbUtils::get_transaction_comps_environments(*transaction_db_connect(*p_impl->base), *this);
    return *p_impl->comps_environments;
}


CompsEnvironment & Transaction::new_comps_environment() {
    if (!p_impl->comps_environments) {
        p_impl->comps_environments.emplace();
    }

    CompsEnvironment comps_env(*this);
    return p_impl->comps_environments->emplace_back(std::move(comps_env));
}


std::vector<CompsGroup> & Transaction::get_comps_groups() {
    if (p_impl->comps_groups) {
        return *p_impl->comps_groups;
    }

    p_impl->comps_groups =
        CompsGroupDbUtils::get_transaction_comps_groups(*transaction_db_connect(*p_impl->base), *this);
    return *p_impl->comps_groups;
}


CompsGroup & Transaction::new_comps_group() {
    if (!p_impl->comps_groups) {
        p_impl->comps_groups.emplace();
    }

    CompsGroup comps_group(*this);
    return p_impl->comps_groups->emplace_back(comps_group);
}


std::vector<Package> & Transaction::get_packages() {
    if (p_impl->packages) {
        return *p_impl->packages;
    }

    p_impl->packages = RpmDbUtils::get_transaction_packages(*transaction_db_connect(*p_impl->base), *this);
    return *p_impl->packages;
}


Package & Transaction::new_package() {
    if (!p_impl->packages) {
        p_impl->packages.emplace();
    }

    Package const pkg(*this);
    return p_impl->packages->emplace_back(pkg);
}


void Transaction::fill_transaction_packages(
    const std::vector<libdnf5::base::TransactionPackage> & transaction_packages) {
    for (auto & tspkg : transaction_packages) {
        auto & new_pkg = new_package();
        auto source_pkg = tspkg.get_package();
        new_pkg.set_name(source_pkg.get_name());
        new_pkg.set_epoch(source_pkg.get_epoch());
        new_pkg.set_version(source_pkg.get_version());
        new_pkg.set_release(source_pkg.get_release());
        new_pkg.set_arch(source_pkg.get_arch());
        new_pkg.set_repoid(tspkg.get_package().get_repo_id());
        new_pkg.set_action(tspkg.get_action());
        new_pkg.set_reason(tspkg.get_reason());
    }

    // If SOURCE_DATE_EPOCH is set, sort the package list for reproducibility.
    // TODO(jlebon): This duplicates the RPM transaction code. Ideally we'd sort
    // the package list once, but currently it's ordered a specific way for the
    // system state update logic.
    if (std::getenv("SOURCE_DATE_EPOCH") != nullptr && p_impl->packages) {
        auto & logger = *p_impl->base->get_logger();
        logger.debug(
            "SOURCE_DATE_EPOCH detected; sorting {} packages for history DB reproducibility", p_impl->packages->size());
        std::sort(p_impl->packages->begin(), p_impl->packages->end(), [](const auto & a, const auto & b) {
            // we need to compare across different packages, so just do a stringy NEVRA cmp
            return a.to_string() < b.to_string();
        });
    }
}


void Transaction::fill_transaction_environments(
    const std::vector<libdnf5::base::TransactionEnvironment> & transaction_environments,
    const std::set<std::string> & installed_group_ids) {
    for (auto & tsenv : transaction_environments) {
        auto & new_env = new_comps_environment();
        auto environment = tsenv.get_environment();
        new_env.set_environment_id(environment.get_environmentid());
        new_env.set_name(environment.get_name());
        new_env.set_translated_name(environment.get_translated_name());
        libdnf5::comps::PackageType package_types{libdnf5::comps::PackageType::MANDATORY};
        if (tsenv.get_with_optional()) {
            package_types |= libdnf5::comps::PackageType::OPTIONAL;
        }
        new_env.set_package_types(package_types);
        if (!tsenv.get_environment().get_repos().empty()) {
            new_env.set_repoid(*tsenv.get_environment().get_repos().begin());
        }
        for (const auto & group_id : environment.get_groups()) {
            auto & new_env_grp = new_env.new_group();
            new_env_grp.set_group_id(group_id);
            new_env_grp.set_group_type(libdnf5::comps::PackageType::MANDATORY);
            new_env_grp.set_installed(installed_group_ids.contains(group_id));
        }
        for (const auto & group_id : environment.get_default_groups()) {
            auto & new_env_grp = new_env.new_group();
            new_env_grp.set_group_id(group_id);
            new_env_grp.set_group_type(libdnf5::comps::PackageType::DEFAULT);
            new_env_grp.set_installed(installed_group_ids.contains(group_id));
        }
        for (const auto & group_id : environment.get_optional_groups()) {
            auto & new_env_grp = new_env.new_group();
            new_env_grp.set_group_id(group_id);
            new_env_grp.set_group_type(libdnf5::comps::PackageType::OPTIONAL);
            new_env_grp.set_installed(installed_group_ids.contains(group_id));
        }
        new_env.set_action(tsenv.get_action());
        new_env.set_reason(tsenv.get_reason());
    }
}


void Transaction::fill_transaction_groups(
    const std::vector<libdnf5::base::TransactionGroup> & transaction_groups,
    const std::set<std::string> & installed_names) {
    for (auto & tsgrp : transaction_groups) {
        auto & new_grp = new_comps_group();
        auto group = tsgrp.get_group();
        new_grp.set_group_id(group.get_groupid());
        new_grp.set_name(group.get_name());
        new_grp.set_action(tsgrp.get_action());
        new_grp.set_reason(tsgrp.get_reason());
        new_grp.set_translated_name(group.get_translated_name());
        if (!tsgrp.get_group().get_repos().empty()) {
            new_grp.set_repoid(*tsgrp.get_group().get_repos().begin());
        }

        for (const auto & group_package : group.get_packages()) {
            auto & new_grp_pkg = new_grp.new_package();
            auto name = group_package.get_name();
            new_grp_pkg.set_name(name);
            new_grp_pkg.set_package_type(group_package.get_type());
            new_grp_pkg.set_installed(installed_names.contains(name) ? true : false);
        }
        new_grp.set_package_types(tsgrp.get_package_types());
    }
}

void Transaction::start() {
    if (p_impl->id != 0) {
        throw RuntimeError(M_("Transaction has already started!"));
    }

    auto conn = transaction_db_connect(*p_impl->base);
    conn->exec("BEGIN");
    try {
        auto query = TransactionDbUtils::trans_insert_new_query(*conn);
        TransactionDbUtils::trans_insert(*query, *this);

        CompsEnvironmentDbUtils::insert_transaction_comps_environments(*conn, *this);
        CompsGroupDbUtils::insert_transaction_comps_groups(*conn, *this);
        RpmDbUtils::insert_transaction_packages(*conn, *this);
        conn->exec("COMMIT");
    } catch (...) {
        conn->exec("ROLLBACK");
        throw;
    }
}


void Transaction::finish(TransactionState state) {
    // TODO(dmach): save item states
    /*
    // save states to the database before checking for UNKNOWN state
    for (auto i : getItems()) {
        i->saveState();
    }

    for (auto i : getItems()) {
        if (i->get_state() == TransactionItemState::STARTED) {
            throw std::runtime_error(
                fmt::format(_("TransactionItem state is not set: {}"), i->getItem()->toStr()));
        }
    }
    */

    auto conn = transaction_db_connect(*p_impl->base);
    conn->exec("BEGIN");
    try {
        set_state(state);
        auto query = TransactionDbUtils::trans_update_new_query(*conn);
        TransactionDbUtils::trans_update(*query, *this);
        conn->exec("COMMIT");
    } catch (...) {
        conn->exec("ROLLBACK");
        throw;
    }
}

std::string Transaction::serialize() {
    return json_serialize(to_replay(*this));
}


// Getters
int64_t Transaction::get_id() const noexcept {
    return p_impl->id;
}
int64_t Transaction::get_dt_start() const noexcept {
    return p_impl->dt_begin;
}
int64_t Transaction::get_dt_end() const noexcept {
    return p_impl->dt_end;
}
const std::string & Transaction::get_rpmdb_version_begin() const noexcept {
    return p_impl->rpmdb_version_begin;
}
const std::string & Transaction::get_rpmdb_version_end() const noexcept {
    return p_impl->rpmdb_version_end;
}
const std::string & Transaction::get_releasever() const noexcept {
    return p_impl->releasever;
}
uint32_t Transaction::get_user_id() const noexcept {
    return p_impl->user_id;
}
const std::string & Transaction::get_description() const noexcept {
    return p_impl->description;
}
const std::string & Transaction::get_comment() const noexcept {
    return p_impl->comment;
}
TransactionState Transaction::get_state() const noexcept {
    return p_impl->state;
}

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


// Setters
void Transaction::set_id(int64_t value) {
    p_impl->id = value;
}
void Transaction::set_comment(const std::string & value) {
    p_impl->comment = value;
}
void Transaction::set_dt_start(int64_t value) {
    p_impl->dt_begin = value;
}
void Transaction::set_dt_end(int64_t value) {
    p_impl->dt_end = value;
}
void Transaction::set_description(const std::string & value) {
    p_impl->description = value;
}
void Transaction::set_user_id(uint32_t value) {
    p_impl->user_id = value;
}
void Transaction::set_releasever(const std::string & value) {
    p_impl->releasever = value;
}
void Transaction::set_rpmdb_version_end(const std::string & value) {
    p_impl->rpmdb_version_end = value;
}
void Transaction::set_rpmdb_version_begin(const std::string & value) {
    p_impl->rpmdb_version_begin = value;
}
void Transaction::set_state(State value) {
    p_impl->state = value;
}

}  // namespace libdnf5::transaction
