/*
 * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 *
 * This program 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see .
 */
#include "DBUpdater.h"
#include "BuiltInConfig.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "DatabaseLoader.h"
#include "GitRevision.h"
#include "Log.h"
#include "QueryResult.h"
#include "StartProcess.h"
#include "UpdateFetcher.h"
#include 
#include 
#include 
std::string DBUpdaterUtil::GetCorrectedMySQLExecutable()
{
    if (!corrected_path().empty())
        return corrected_path();
    else
        return BuiltInConfig::GetMySQLExecutable();
}
bool DBUpdaterUtil::CheckExecutable()
{
    boost::filesystem::path exe(GetCorrectedMySQLExecutable());
    if (!is_regular_file(exe))
    {
        exe = Trinity::SearchExecutableInPath("mysql");
        if (!exe.empty() && is_regular_file(exe))
        {
            // Correct the path to the cli
            corrected_path() = absolute(exe).generic_string();
            return true;
        }
        TC_LOG_FATAL("sql.updates", "Didn't find any executable MySQL binary at \'{}\' or in path, correct the path in the *.conf (\"MySQLExecutable\").",
            absolute(exe).generic_string());
        return false;
    }
    return true;
}
std::string& DBUpdaterUtil::corrected_path()
{
    static std::string path;
    return path;
}
// Auth Database
template<>
std::string DBUpdater::GetConfigEntry()
{
    return "Updates.Auth";
}
template<>
std::string DBUpdater::GetTableName()
{
    return "Auth";
}
template<>
std::string DBUpdater::GetBaseFile()
{
    return BuiltInConfig::GetSourceDirectory() +
        "/sql/base/auth_database.sql";
}
template<>
bool DBUpdater::IsEnabled(uint32 const updateMask)
{
    // This way silences warnings under msvc
    return (updateMask & DatabaseLoader::DATABASE_LOGIN) ? true : false;
}
// World Database
template<>
std::string DBUpdater::GetConfigEntry()
{
    return "Updates.World";
}
template<>
std::string DBUpdater::GetTableName()
{
    return "World";
}
template<>
std::string DBUpdater::GetBaseFile()
{
    return GitRevision::GetFullDatabase();
}
template<>
bool DBUpdater::IsEnabled(uint32 const updateMask)
{
    // This way silences warnings under msvc
    return (updateMask & DatabaseLoader::DATABASE_WORLD) ? true : false;
}
template<>
BaseLocation DBUpdater::GetBaseLocationType()
{
    return LOCATION_DOWNLOAD;
}
// Character Database
template<>
std::string DBUpdater::GetConfigEntry()
{
    return "Updates.Character";
}
template<>
std::string DBUpdater::GetTableName()
{
    return "Character";
}
template<>
std::string DBUpdater::GetBaseFile()
{
    return BuiltInConfig::GetSourceDirectory() +
        "/sql/base/characters_database.sql";
}
template<>
bool DBUpdater::IsEnabled(uint32 const updateMask)
{
    // This way silences warnings under msvc
    return (updateMask & DatabaseLoader::DATABASE_CHARACTER) ? true : false;
}
// Hotfix Database
template<>
std::string DBUpdater::GetConfigEntry()
{
    return "Updates.Hotfix";
}
template<>
std::string DBUpdater::GetTableName()
{
    return "Hotfixes";
}
template<>
std::string DBUpdater::GetBaseFile()
{
    return GitRevision::GetHotfixesDatabase();
}
template<>
bool DBUpdater::IsEnabled(uint32 const updateMask)
{
    // This way silences warnings under msvc
    return (updateMask & DatabaseLoader::DATABASE_HOTFIX) ? true : false;
}
template<>
BaseLocation DBUpdater::GetBaseLocationType()
{
    return LOCATION_DOWNLOAD;
}
// All
template
BaseLocation DBUpdater::GetBaseLocationType()
{
    return LOCATION_REPOSITORY;
}
template
bool DBUpdater::Create(DatabaseWorkerPool& pool)
{
    TC_LOG_INFO("sql.updates", "Database \"{}\" does not exist, do you want to create it? [yes (default) / no]: ",
        pool.GetConnectionInfo()->database);
    std::string answer;
    std::getline(std::cin, answer);
    if (!answer.empty() && !(answer.substr(0, 1) == "y"))
        return false;
    TC_LOG_INFO("sql.updates", "Creating database \"{}\"...", pool.GetConnectionInfo()->database);
    // Path of temp file
    static Path const temp("create_table.sql");
    // Create temporary query to use external MySQL CLi
    std::ofstream file(temp.generic_string());
    if (!file.is_open())
    {
        TC_LOG_FATAL("sql.updates", "Failed to create temporary query file \"{}\"!", temp.generic_string());
        return false;
    }
    file << "CREATE DATABASE `" << pool.GetConnectionInfo()->database << "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci\n\n";
    file.close();
    try
    {
        DBUpdater::ApplyFile(pool, pool.GetConnectionInfo()->host, pool.GetConnectionInfo()->user, pool.GetConnectionInfo()->password,
            pool.GetConnectionInfo()->port_or_socket, "", pool.GetConnectionInfo()->ssl, temp);
    }
    catch (UpdateException&)
    {
        TC_LOG_FATAL("sql.updates", "Failed to create database {}! Does the user (named in *.conf) have `CREATE`, `ALTER`, `DROP`, `INSERT` and `DELETE` privileges on the MySQL server?", pool.GetConnectionInfo()->database);
        boost::filesystem::remove(temp);
        return false;
    }
    TC_LOG_INFO("sql.updates", "Done.");
    boost::filesystem::remove(temp);
    return true;
}
template
bool DBUpdater::Update(DatabaseWorkerPool& pool)
{
    if (!DBUpdaterUtil::CheckExecutable())
        return false;
    TC_LOG_INFO("sql.updates", "Updating {} database...", DBUpdater::GetTableName());
    Path const sourceDirectory(BuiltInConfig::GetSourceDirectory());
    if (!is_directory(sourceDirectory))
    {
        TC_LOG_ERROR("sql.updates", "DBUpdater: The given source directory {} does not exist, change the path to the directory where your sql directory exists (for example c:\\source\\trinitycore). Shutting down.", sourceDirectory.generic_string());
        return false;
    }
    UpdateFetcher updateFetcher(sourceDirectory, [&](std::string const& query) { DBUpdater::Apply(pool, query); },
        [&](Path const& file) { DBUpdater::ApplyFile(pool, file); },
            [&](std::string const& query) -> QueryResult { return DBUpdater::Retrieve(pool, query); });
    UpdateResult result;
    try
    {
        result = updateFetcher.Update(
            sConfigMgr->GetBoolDefault("Updates.Redundancy", true),
            sConfigMgr->GetBoolDefault("Updates.AllowRehash", true),
            sConfigMgr->GetBoolDefault("Updates.ArchivedRedundancy", false),
            sConfigMgr->GetIntDefault("Updates.CleanDeadRefMaxCount", 3));
    }
    catch (UpdateException&)
    {
        return false;
    }
    std::string const info = Trinity::StringFormat("Containing {} new and {} archived updates.",
        result.recent, result.archived);
    if (!result.updated)
        TC_LOG_INFO("sql.updates", ">> {} database is up-to-date! {}", DBUpdater::GetTableName(), info);
    else
        TC_LOG_INFO("sql.updates", ">> Applied {} {}. {}", result.updated, result.updated == 1 ? "query" : "queries", info);
    return true;
}
template
bool DBUpdater::Populate(DatabaseWorkerPool& pool)
{
    {
        QueryResult const result = Retrieve(pool, "SHOW TABLES");
        if (result && (result->GetRowCount() > 0))
            return true;
    }
    if (!DBUpdaterUtil::CheckExecutable())
        return false;
    TC_LOG_INFO("sql.updates", "Database {} is empty, auto populating it...", DBUpdater::GetTableName());
    std::string const p = DBUpdater::GetBaseFile();
    if (p.empty())
    {
        TC_LOG_INFO("sql.updates", ">> No base file provided, skipped!");
        return true;
    }
    Path const base(p);
    if (!exists(base))
    {
        switch (DBUpdater::GetBaseLocationType())
        {
            case LOCATION_REPOSITORY:
            {
                TC_LOG_ERROR("sql.updates", ">> Base file \"{}\" is missing. Try fixing it by cloning the source again.",
                    base.generic_string());
                break;
            }
            case LOCATION_DOWNLOAD:
            {
                std::string const filename = base.filename().generic_string();
                std::string const workdir = boost::filesystem::current_path().generic_string();
                TC_LOG_ERROR("sql.updates", ">> File \"{}\" is missing, download it from \"https://github.com/TrinityCore/TrinityCore/releases\"" \
                    " uncompress it and place the file \"{}\" in the directory \"{}\".", filename, filename, workdir);
                break;
            }
        }
        return false;
    }
    // Update database
    TC_LOG_INFO("sql.updates", ">> Applying \'{}\'...", base.generic_string());
    try
    {
        ApplyFile(pool, base);
    }
    catch (UpdateException&)
    {
        return false;
    }
    TC_LOG_INFO("sql.updates", ">> Done!");
    return true;
}
template
QueryResult DBUpdater::Retrieve(DatabaseWorkerPool& pool, std::string const& query)
{
    return pool.Query(query.c_str());
}
template
void DBUpdater::Apply(DatabaseWorkerPool& pool, std::string const& query)
{
    pool.DirectExecute(query.c_str());
}
template
void DBUpdater::ApplyFile(DatabaseWorkerPool& pool, Path const& path)
{
    DBUpdater::ApplyFile(pool, pool.GetConnectionInfo()->host, pool.GetConnectionInfo()->user, pool.GetConnectionInfo()->password,
        pool.GetConnectionInfo()->port_or_socket, pool.GetConnectionInfo()->database, pool.GetConnectionInfo()->ssl, path);
}
template
void DBUpdater::ApplyFile(DatabaseWorkerPool& pool, std::string const& host, std::string const& user,
    std::string const& password, std::string const& port_or_socket, std::string const& database, std::string const& ssl,
    Path const& path)
{
    std::vector args;
    args.reserve(9);
    // CLI Client connection info
    args.emplace_back("-h" + host);
    args.emplace_back("-u" + user);
    if (!password.empty())
        args.emplace_back("-p" + password);
    // Check if we want to connect through ip or socket (Unix only)
#ifdef _WIN32
    if (host == ".")
        args.emplace_back("--protocol=PIPE");
    else
        args.emplace_back("-P" + port_or_socket);
#else
    if (!std::isdigit(port_or_socket[0]))
    {
        // We can't check if host == "." here, because it is named localhost if socket option is enabled
        args.emplace_back("-P0");
        args.emplace_back("--protocol=SOCKET");
        args.emplace_back("-S" + port_or_socket);
    }
    else
        // generic case
        args.emplace_back("-P" + port_or_socket);
#endif
    // Set the default charset to utf8
    args.emplace_back("--default-character-set=utf8mb4");
    // Set max allowed packet to 1 GB
    args.emplace_back("--max-allowed-packet=1GB");
#if !defined(MARIADB_VERSION_ID) && MYSQL_VERSION_ID >= 80000
    if (ssl == "ssl")
        args.emplace_back("--ssl-mode=REQUIRED");
#else
    if (ssl == "ssl")
        args.emplace_back("--ssl");
#endif
    // Execute sql file
    args.emplace_back("-e");
    args.emplace_back(Trinity::StringFormat("BEGIN; SOURCE {}; COMMIT;", path.generic_string()));
    // Database
    if (!database.empty())
        args.emplace_back(database);
    // Invokes a mysql process which doesn't leak credentials to logs
    int const ret = Trinity::StartProcess(DBUpdaterUtil::GetCorrectedMySQLExecutable(), std::move(args),
                                 "sql.updates", "", true);
    if (ret != EXIT_SUCCESS)
    {
        TC_LOG_FATAL("sql.updates", "Applying of file \'{}\' to database \'{}\' failed!" \
            " If you are a user, please pull the latest revision from the repository. "
            "Also make sure you have not applied any of the databases with your sql client. "
            "You cannot use auto-update system and import sql files from TrinityCore repository with your sql client. "
            "If you are a developer, please fix your sql query.",
            path.generic_string(), pool.GetConnectionInfo()->database);
        throw UpdateException("update failed");
    }
}
template class TC_DATABASE_API DBUpdater;
template class TC_DATABASE_API DBUpdater;
template class TC_DATABASE_API DBUpdater;
template class TC_DATABASE_API DBUpdater;