#include "./config.h"

#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <c++utilities/io/inifile.h>

#include <algorithm>
#include <fstream>
#include <iostream>
#include <unordered_map>

#include <sys/utsname.h> // for uname

using namespace std;
using namespace CppUtilities;
using namespace CppUtilities::EscapeCodes;

namespace LibPkg {

static void moveLastValue(string &target, multimap<string, string> &multimap, const string &key)
{
    const auto it = find_if(multimap.rbegin(), multimap.rend(), [&key](const pair<string, string> &i) { return i.first == key; });
    if (it != multimap.rend()) {
        target = move(it->second);
    }
}

static void moveValues(vector<string> &target, multimap<string, string> &multimap, const string &key)
{
    for (auto range = multimap.equal_range(key); range.first != range.second; ++range.first) {
        target.emplace_back(move(range.first->second));
    }
}

void Config::loadPacmanConfig(const char *pacmanConfigPath)
{
    // open and parse ini
    auto configIni = IniFile();
    auto includedInis = std::unordered_map<std::string, IniFile>();
    {
        auto configFile = ifstream();
        configFile.exceptions(ios_base::failbit | ios_base::badbit);
        configFile.open(pacmanConfigPath, ios_base::in);
        configIni.parse(configFile);
    }
    auto &configData = configIni.data();
    auto architecture = std::string_view{};

    // read options and create Database object for each db
    for (auto &scope : configData) {
        if (scope.first == "options") {
            // read global options or assume defaults
            auto &options = scope.second;
            for (auto range = options.equal_range("Architecture"); range.first != range.second; ++range.first) {
                if (range.first->second != "auto") {
                    architecture = *architectures.emplace(move(range.first->second)).first;
                } else {
                    struct utsname un;
                    uname(&un);
                    architecture = *architectures.emplace(un.machine).first;
                }
            }
            moveLastValue(pacmanDatabasePath, options, "DBPath");
            if (pacmanDatabasePath.empty()) {
                pacmanDatabasePath = "/var/lib/pacman/";
            }
            moveValues(packageCacheDirs, options, "CacheDir");
            if (packageCacheDirs.empty()) {
                packageCacheDirs.emplace_back("/var/cache/pacman/pkg/");
            }
            auto sigLevel = std::string();
            moveLastValue(sigLevel, options, "SigLevel");
            signatureLevel = SignatureLevelConfig::fromString(sigLevel);
            if (!signatureLevel.isValid()) {
                signatureLevel = SignatureLevelConfig();
                cerr << Phrases::WarningMessage << "The global/default signature level \"" << sigLevel << "\" is invalid and will be ignored."
                     << Phrases::End << Phrases::SubWarning << "Assuming default \"" << signatureLevel.toString() << "\" instead"
                     << Phrases::EndFlush;
            }
        } else {
            // read sync database
            auto *const db = findOrCreateDatabase(std::move(scope.first), architecture);
            // read sig level
            auto sigLevel = std::string();
            moveLastValue(sigLevel, scope.second, "SigLevel");
            const auto dbSpecificSignatureLevelConfig = SignatureLevelConfig::fromString(sigLevel);
            if (dbSpecificSignatureLevelConfig.databaseScope != SignatureLevel::Invalid) {
                db->signatureLevel = dbSpecificSignatureLevelConfig.databaseScope;
            } else {
                cerr << Phrases::WarningMessage << "The signature level \"" << sigLevel << "\" specified for DB \"" << db->name
                     << "\" is invalid and will be ignored." << Phrases::End << Phrases::SubWarning << "Assuming global default \""
                     << signatureLevelToString(signatureLevel.databaseScope) << "\" instead" << Phrases::EndFlush;
                db->signatureLevel = signatureLevel.databaseScope;
            }
            // add mirrors
            for (auto range = scope.second.equal_range("Server"); range.first != range.second; ++range.first) {
                for (const auto &arch : architectures) {
                    auto url = range.first->second;
                    findAndReplace<std::string>(url, "$repo", db->name);
                    findAndReplace<std::string>(url, "$arch", arch);
                    db->mirrors.emplace_back(move(url));
                }
            }
            // add included mirrors
            for (auto includeRange = scope.second.equal_range("Include"); includeRange.first != includeRange.second; ++includeRange.first) {
                const auto &path = includeRange.first->second;
                auto &includedIni = includedInis[path];
                if (includedIni.data().empty()) {
                    try {
                        auto includedFile = std::ifstream();
                        includedFile.exceptions(ios_base::failbit | ios_base::badbit);
                        includedFile.open(path, ios_base::in);
                        includedIni.parse(includedFile);
                    } catch (const ios_base::failure &) {
                        cerr << Phrases::WarningMessage << "An IO error occurred when parsing the included file \"" << path << "\"."
                             << Phrases::EndFlush;
                    }
                }
                for (auto &nestedScope : includedIni.data()) {
                    if (!nestedScope.first.empty()) {
                        continue;
                    }
                    for (auto serverRange = nestedScope.second.equal_range("Server"); serverRange.first != serverRange.second; ++serverRange.first) {
                        for (const auto &arch : architectures) {
                            string url = serverRange.first->second;
                            findAndReplace<string>(url, "$repo", db->name);
                            findAndReplace<string>(url, "$arch", arch);
                            db->mirrors.emplace_back(move(url));
                        }
                    }
                }
            }
            // set database file paths
            if (db->localDbDir.empty()) {
                db->localDbDir = pacmanDatabasePath + "sync";
            }
            if (db->localPkgDir.empty() && !packageCacheDirs.empty()) {
                db->localPkgDir = packageCacheDirs.front();
            }
            // ensure the database is not being discarded
            db->toBeDiscarded = false;
        }
    }
}

void Config::loadAllPackages(bool withFiles, bool force)
{
    for (auto &db : databases) {
        try {
            db.loadPackagesFromConfiguredPaths(withFiles, force);
        } catch (const runtime_error &e) {
            cerr << Phrases::ErrorMessage << "Unable to load database \"" << db.name << "\": " << e.what() << Phrases::EndFlush;
        }
    }
}

std::uint64_t Config::restoreFromCache()
{
    fstream cacheFile;
    cacheFile.exceptions(ios_base::failbit | ios_base::badbit);
    cacheFile.open("cache.bin", ios_base::in | ios_base::binary);
    restoreFromBinary(cacheFile);
    return static_cast<std::uint64_t>(cacheFile.tellg());
}

std::uint64_t Config::dumpCacheFile()
{
    fstream cacheFile;
    cacheFile.exceptions(ios_base::failbit | ios_base::badbit);
    cacheFile.open("cache.bin", ios_base::out | ios_base::trunc | ios_base::binary);
    toBinary(cacheFile);
    const auto size = static_cast<std::uint64_t>(cacheFile.tellp());
    cacheFile.close();
    return size;
}

std::pair<std::string_view, std::string_view> Config::parseDatabaseDenotation(std::string_view databaseDenotation)
{
    const auto archStart = databaseDenotation.rfind('@');
    if (archStart == std::string_view::npos) {
        return std::make_pair(databaseDenotation, "x86_64");
    } else {
        return std::make_pair(databaseDenotation.substr(0, archStart), databaseDenotation.substr(archStart + 1));
    }
}

std::tuple<std::string_view, std::string_view, std::string_view> Config::parsePackageDenotation(std::string_view packageDenotation)
{
    const char *const end = packageDenotation.data() + packageDenotation.size();
    const char *packageName = packageDenotation.data();
    for (; packageName != end && *packageName != '/'; ++packageName)
        ;
    if (packageName == end) {
        return std::make_tuple(std::string_view(), std::string_view(), packageDenotation);
    } else {
        const auto &[dbName, dbArch]
            = parseDatabaseDenotation(std::string_view(packageDenotation.data(), static_cast<std::size_t>(packageName - packageDenotation.data())));
        return std::make_tuple(dbName, dbArch, std::string_view(packageName + 1, static_cast<std::size_t>(end - packageName - 1)));
    }
}

} // namespace LibPkg

#include "reflection/config.h"
