#include "db/internal.hpp"

#include <assert.h>

// Miscellanious in-game database interactions

static int getAccountIDFromPlayerID(int playerId, int *accountLevel=nullptr) {
    const char *sql = R"(
        SELECT Players.AccountID, AccountLevel
        FROM Players
        JOIN Accounts ON Players.AccountID = Accounts.AccountID
        WHERE PlayerID = ?;
        )";
    sqlite3_stmt *stmt;

    // get AccountID from PlayerID
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, playerId);

    if (sqlite3_step(stmt) != SQLITE_ROW) {
        std::cout << "[WARN] Database: failed to get AccountID from PlayerID: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_finalize(stmt);
        return -1;
    }

    int accountId = sqlite3_column_int(stmt, 0);

    // optional secondary return value, for checking GM status
    if (accountLevel != nullptr)
        *accountLevel = sqlite3_column_int(stmt, 1);

    sqlite3_finalize(stmt);

    return accountId;
}

static bool banAccount(int accountId, int days, std::string& reason) {
    const char* sql = R"(
        UPDATE Accounts SET
            BannedSince = (strftime('%s', 'now')),
            BannedUntil = (strftime('%s', 'now')) + ?,
            BanReason = ?
        WHERE AccountID = ?;
        )";
    sqlite3_stmt* stmt;

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    sqlite3_bind_int(stmt, 1, days * 86400); // convert days to seconds
    sqlite3_bind_text(stmt, 2, reason.c_str(), -1, NULL);
    sqlite3_bind_int(stmt, 3, accountId);

    if (sqlite3_step(stmt) != SQLITE_DONE) {
        std::cout << "[WARN] Database: failed to ban account: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_finalize(stmt);
        return false;
    }

    sqlite3_finalize(stmt);
    return true;
}

static bool unbanAccount(int accountId) {
    const char* sql = R"(
        UPDATE Accounts SET
            BannedSince = 0,
            BannedUntil = 0,
            BanReason = ''
        WHERE AccountID = ?;
        )";
    sqlite3_stmt* stmt;

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    sqlite3_bind_int(stmt, 1, accountId);

    if (sqlite3_step(stmt) != SQLITE_DONE) {
        std::cout << "[WARN] Database: failed to unban account: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_finalize(stmt);
        return false;
    }

    sqlite3_finalize(stmt);
    return true;
}

// expressed in days
#define THIRTY_YEARS 10957

bool Database::banPlayer(int playerId, std::string& reason) {
    std::lock_guard<std::mutex> lock(dbCrit);

    int accountLevel;
    int accountId = getAccountIDFromPlayerID(playerId, &accountLevel);
    if (accountId < 0) {
        return false;
    }

    if (accountLevel <= 30) {
        std::cout << "[WARN] Cannot ban a GM." << std::endl;
        return false;
    }

    // do the ban
    if (!banAccount(accountId, THIRTY_YEARS, reason)) {
        return false;
    }

    return true;
}

bool Database::unbanPlayer(int playerId) {
    std::lock_guard<std::mutex> lock(dbCrit);

    int accountId = getAccountIDFromPlayerID(playerId);
    if (accountId < 0)
        return false;

    return unbanAccount(accountId);
}

// buddies
// returns num of buddies + blocked players
int Database::getNumBuddies(Player* player) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        SELECT COUNT(*)
        FROM Buddyships
        WHERE PlayerAID = ? OR PlayerBID = ?;
        )";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, player->iID);
    sqlite3_bind_int(stmt, 2, player->iID);
    sqlite3_step(stmt);
    int result = sqlite3_column_int(stmt, 0);

    sqlite3_finalize(stmt);

    sql = R"(
        SELECT COUNT(*)
        FROM Blocks
        WHERE PlayerID = ?;
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, player->iID);
    sqlite3_step(stmt);
    result += sqlite3_column_int(stmt, 0);

    sqlite3_finalize(stmt);

    // again, for peace of mind
    return result > 50 ? 50 : result;
}

void Database::addBuddyship(int playerA, int playerB) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        INSERT INTO Buddyships (PlayerAID, PlayerBID)
        VALUES (?, ?);
        )";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, playerA);
    sqlite3_bind_int(stmt, 2, playerB);

    if (sqlite3_step(stmt) != SQLITE_DONE)
        std::cout << "[WARN] Database: failed to add buddyship: " << sqlite3_errmsg(db) << std::endl;
    sqlite3_finalize(stmt);
}

void Database::removeBuddyship(int playerA, int playerB) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        DELETE FROM Buddyships
        WHERE (PlayerAID = ? AND PlayerBID = ?) OR (PlayerAID = ? AND PlayerBID = ?);
        )";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, playerA);
    sqlite3_bind_int(stmt, 2, playerB);
    sqlite3_bind_int(stmt, 3, playerB);
    sqlite3_bind_int(stmt, 4, playerA);

    sqlite3_step(stmt);
    sqlite3_finalize(stmt);
}

// blocking
void Database::addBlock(int playerId, int blockedPlayerId) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        INSERT INTO Blocks (PlayerID, BlockedPlayerID)
        VALUES (?, ?);
        )";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, playerId);
    sqlite3_bind_int(stmt, 2, blockedPlayerId);

    if (sqlite3_step(stmt) != SQLITE_DONE)
        std::cout << "[WARN] Database: failed to block player: " << sqlite3_errmsg(db) << std::endl;
    sqlite3_finalize(stmt);
}

void Database::removeBlock(int playerId, int blockedPlayerId) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        DELETE FROM Blocks
        WHERE PlayerID = ? AND BlockedPlayerID = ?;
        )";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, playerId);
    sqlite3_bind_int(stmt, 2, blockedPlayerId);

    sqlite3_step(stmt);
    sqlite3_finalize(stmt);
}

RaceRanking Database::getTopRaceRanking(int epID, int playerID) {
    std::lock_guard<std::mutex> lock(dbCrit);
    std::string sql(R"(
        SELECT
            EPID, PlayerID, Score, RingCount, Time, Timestamp
        FROM RaceResults
        WHERE EPID = ?
        )");

    if (playerID > -1)
        sql += " AND PlayerID = ? ";

    sql += R"(
        ORDER BY Score DESC
        LIMIT 1;
        )";

    sqlite3_stmt* stmt;

    sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, epID);
    if(playerID > -1)
        sqlite3_bind_int(stmt, 2, playerID);

    RaceRanking ranking = {};
    if (sqlite3_step(stmt) != SQLITE_ROW) {
        // this race hasn't been run before, so return a blank ranking
        sqlite3_finalize(stmt);
        return ranking;
    }

    assert(epID == sqlite3_column_int(stmt, 0)); // EPIDs should always match

    ranking.EPID = epID;
    ranking.PlayerID = sqlite3_column_int(stmt, 1);
    ranking.Score = sqlite3_column_int(stmt, 2);
    ranking.RingCount = sqlite3_column_int(stmt, 3);
    ranking.Time = sqlite3_column_int64(stmt, 4);
    ranking.Timestamp = sqlite3_column_int64(stmt, 5);

    sqlite3_finalize(stmt);
    return ranking;
}

void Database::postRaceRanking(RaceRanking ranking) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        INSERT INTO RaceResults
            (EPID, PlayerID, Score, RingCount, Time, Timestamp)
        VALUES(?, ?, ?, ?, ?, ?);
        )";
    sqlite3_stmt* stmt;

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, ranking.EPID);
    sqlite3_bind_int(stmt, 2, ranking.PlayerID);
    sqlite3_bind_int(stmt, 3, ranking.Score);
    sqlite3_bind_int(stmt, 4, ranking.RingCount);
    sqlite3_bind_int64(stmt, 5, ranking.Time);
    sqlite3_bind_int64(stmt, 6, ranking.Timestamp);

    if (sqlite3_step(stmt) != SQLITE_DONE) {
        std::cout << "[WARN] Database: Failed to post race result" << std::endl;
    }

    sqlite3_finalize(stmt);
}

bool Database::isCodeRedeemed(int playerId, std::string code) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        SELECT COUNT(*)
        FROM RedeemedCodes
        WHERE PlayerID = ? AND Code = ?
        LIMIT 1;
        )";
    sqlite3_stmt* stmt;

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, playerId);
    sqlite3_bind_text(stmt, 2, code.c_str(), -1, NULL);
    sqlite3_step(stmt);
    int result = sqlite3_column_int(stmt, 0);

    sqlite3_finalize(stmt);
    return result;
}

void Database::recordCodeRedemption(int playerId, std::string code) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        INSERT INTO RedeemedCodes (PlayerID, Code)
        VALUES (?, ?);
        )";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, playerId);
    sqlite3_bind_text(stmt, 2, code.c_str(), -1, NULL);
    
    if (sqlite3_step(stmt) != SQLITE_DONE)
        std::cout << "[WARN] Database: recording of code redemption failed: " << sqlite3_errmsg(db) << std::endl;
    sqlite3_finalize(stmt);
}
