// Copyright Contributors to the DNF5 project.
// Copyright (C) 2024 Red Hat, Inc.
// SPDX-License-Identifier: LGPL-2.0-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 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 "rpm/transaction.hpp"

#include "libdnf5/common/exception.hpp"

#include <libdnf5/base/base.hpp>
#include <libdnf5/transaction/offline.hpp>
#include <libdnf5/utils/bgettext/bgettext-mark-domain.h>
#include <libdnf5/utils/fs/file.hpp>
#include <toml.hpp>


const int STATE_VERSION = 2;
const std::string STATE_HEADER{"offline-transaction-state"};


struct OfflineTransactionStateTomlData {
    int state_version = STATE_VERSION;
    std::string status = libdnf5::offline::STATUS_DOWNLOAD_INCOMPLETE;
    std::string cachedir;
    std::string target_releasever;
    std::string system_releasever;
    std::string verb;
    std::string cmd_line;
    bool poweroff_after = false;
    std::string module_platform_id;
    std::string rpmdb_cookie;
};

TOML11_DEFINE_CONVERSION_NON_INTRUSIVE(
    OfflineTransactionStateTomlData,
    state_version,
    status,
    cachedir,
    target_releasever,
    system_releasever,
    verb,
    cmd_line,
    poweroff_after,
#ifdef WITH_MODULEMD
    module_platform_id,
#endif
    rpmdb_cookie)


namespace libdnf5::offline {

class OfflineTransactionStateData::Impl {
public:
    friend OfflineTransactionStateData;
    OfflineTransactionStateTomlData data;
};


OfflineTransactionStateData::~OfflineTransactionStateData() = default;

OfflineTransactionStateData::OfflineTransactionStateData() : p_impl(std::make_unique<Impl>()) {};
OfflineTransactionStateData::OfflineTransactionStateData(const OfflineTransactionStateData & src)
    : p_impl(new Impl(*src.p_impl)) {}
OfflineTransactionStateData::OfflineTransactionStateData(OfflineTransactionStateData && src) noexcept = default;

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

OfflineTransactionStateData & OfflineTransactionStateData::operator=(OfflineTransactionStateData && src) noexcept =
    default;

void OfflineTransactionStateData::set_state_version(int state_version) {
    p_impl->data.state_version = state_version;
}
int OfflineTransactionStateData::get_state_version() const {
    return p_impl->data.state_version;
}

void OfflineTransactionStateData::set_status(const std::string & status) {
    p_impl->data.status = status;
}
const std::string & OfflineTransactionStateData::get_status() const {
    return p_impl->data.status;
}

void OfflineTransactionStateData::set_cachedir(const std::string & cachedir) {
    p_impl->data.cachedir = cachedir;
}
const std::string & OfflineTransactionStateData::get_cachedir() const {
    return p_impl->data.cachedir;
}

void OfflineTransactionStateData::set_target_releasever(const std::string & target_releasever) {
    p_impl->data.target_releasever = target_releasever;
}
const std::string & OfflineTransactionStateData::get_target_releasever() const {
    return p_impl->data.target_releasever;
}

void OfflineTransactionStateData::set_system_releasever(const std::string & system_releasever) {
    p_impl->data.system_releasever = system_releasever;
}
const std::string & OfflineTransactionStateData::get_system_releasever() const {
    return p_impl->data.system_releasever;
}

void OfflineTransactionStateData::set_verb(const std::string & verb) {
    p_impl->data.verb = verb;
}
const std::string & OfflineTransactionStateData::get_verb() const {
    return p_impl->data.verb;
}

void OfflineTransactionStateData::set_cmd_line(const std::string & cmd_line) {
    p_impl->data.cmd_line = cmd_line;
}
const std::string & OfflineTransactionStateData::get_cmd_line() const {
    return p_impl->data.cmd_line;
}

void OfflineTransactionStateData::set_poweroff_after(bool poweroff_after) {
    p_impl->data.poweroff_after = poweroff_after;
}
bool OfflineTransactionStateData::get_poweroff_after() const {
    return p_impl->data.poweroff_after;
}

void OfflineTransactionStateData::set_module_platform_id([[maybe_unused]] const std::string & module_platform_id) {
#ifdef WITH_MODULEMD
    p_impl->data.module_platform_id = module_platform_id;
#endif
}
const std::string & OfflineTransactionStateData::get_module_platform_id() const {
    return p_impl->data.module_platform_id;
}

void OfflineTransactionStateData::set_rpmdb_cookie(const std::string & rpmdb_cookie) {
    p_impl->data.rpmdb_cookie = rpmdb_cookie;
}
std::string OfflineTransactionStateData::get_rpmdb_cookie() const {
    return p_impl->data.rpmdb_cookie;
}


class OfflineTransactionState::Impl {
    friend OfflineTransactionState;
    std::exception_ptr read_exception;
    std::filesystem::path path;
    OfflineTransactionStateData data;
};

OfflineTransactionState::~OfflineTransactionState() = default;

OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : p_impl(std::make_unique<Impl>()) {
    p_impl->path = std::move(path);
    read();
}
OfflineTransactionState::OfflineTransactionState(const OfflineTransactionState & src) : p_impl(new Impl(*src.p_impl)) {}
OfflineTransactionState::OfflineTransactionState(OfflineTransactionState && src) noexcept = default;

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

OfflineTransactionStateData & OfflineTransactionState::get_data() {
    return p_impl->data;
}

const std::exception_ptr & OfflineTransactionState::get_read_exception() const {
    return p_impl->read_exception;
}

std::filesystem::path OfflineTransactionState::get_path() const {
    return p_impl->path;
}


void OfflineTransactionState::read() {
    try {
        const std::ifstream file{p_impl->path};
        if (!file.good()) {
            throw libdnf5::FileSystemError(errno, p_impl->path, M_("error reading offline state file"));
        }
        const auto & value = toml::parse(p_impl->path);
        p_impl->data.p_impl->data = toml::find<OfflineTransactionStateTomlData>(value, STATE_HEADER);
        if (p_impl->data.get_state_version() != STATE_VERSION) {
            throw libdnf5::RuntimeError(M_("incompatible version of state data"));
        }
    } catch (const std::exception & ex) {
        p_impl->read_exception = std::current_exception();
        p_impl->data = OfflineTransactionStateData{};
    }
}

void OfflineTransactionState::write() {
    auto file = libdnf5::utils::fs::File(p_impl->path, "w");
#ifdef TOML11_COMPAT
    file.write(toml::format(toml::value{{STATE_HEADER, p_impl->data.p_impl->data}}));
#else
    file.write(toml::format(toml::value{toml::table{{STATE_HEADER, p_impl->data.p_impl->data}}}));
#endif
    file.close();
}

OfflineTransactionState OfflineTransactionState::from_base(const Base & base) {
    return OfflineTransactionState(
        base.get_config().get_installroot_option().get_value() / DEFAULT_DATADIR.relative_path() /
        TRANSACTION_STATE_FILENAME);
}

bool OfflineTransactionState::is_pending() const {
    if (p_impl->read_exception) {
        return false;
    }
    const auto & status = p_impl->data.get_status();
    return status == STATUS_DOWNLOAD_COMPLETE || status == STATUS_READY;
}

void OfflineTransactionState::invalidate() {
    const auto datadir = p_impl->path.parent_path();
    std::error_code ec;
    auto target = std::filesystem::read_symlink(MAGIC_SYMLINK, ec);
    if (ec) {
        return;
    }
    std::error_code ec_target, ec_datadir;
    auto canonical_target = std::filesystem::weakly_canonical(target, ec_target);
    auto canonical_datadir = std::filesystem::weakly_canonical(datadir, ec_datadir);
    if (!ec_target && !ec_datadir && canonical_target == canonical_datadir) {
        std::filesystem::remove(MAGIC_SYMLINK, ec);
    }
}

void OfflineTransactionState::capture_rpmdb_cookie(Base & base) {
    rpm::Transaction rpm_ts(base);
    p_impl->data.set_rpmdb_cookie(rpm_ts.get_db_cookie());
}

bool OfflineTransactionState::check_rpmdb_cookie(Base & base) const {
    const auto & stored_cookie = p_impl->data.get_rpmdb_cookie();
    if (stored_cookie.empty()) {
        return true;
    }
    rpm::Transaction rpm_ts(base);
    return rpm_ts.get_db_cookie() == stored_cookie;
}

}  // namespace libdnf5::offline
