#include "LeaderboardManager.h"

#include <ranges>
#include <sstream>
#include <utility>

#include "Database.h"
#include "EntityManager.h"
#include "Character.h"
#include "Game.h"
#include "GameMessages.h"
#include "Logger.h"
#include "dConfig.h"
#include "CDClientManager.h"
#include "GeneralUtils.h"
#include "Entity.h"
#include "LDFFormat.h"
#include "DluAssert.h"

#include "CDActivitiesTable.h"
#include "Metrics.hpp"

namespace LeaderboardManager {
	std::map<GameID, Leaderboard::Type> leaderboardCache;
}

Leaderboard::Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const Leaderboard::Type leaderboardType) {
	this->gameID = gameID;
	this->weekly = weekly;
	this->infoType = infoType;
	this->leaderboardType = leaderboardType;
	this->relatedPlayer = relatedPlayer;
}

Leaderboard::~Leaderboard() {
	Clear();
}

void Leaderboard::Clear() {
	for (auto& entry : entries) for (auto ldfData : entry) delete ldfData;
}

inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, LDFBaseData* data) {
	leaderboard << "\nResult[0].Row[" << index << "]." << data->GetString();
}

void Leaderboard::Serialize(RakNet::BitStream& bitStream) const {
	bitStream.Write(gameID);
	bitStream.Write(infoType);

	std::ostringstream leaderboard;

	leaderboard << "ADO.Result=7:1"; // Unused in 1.10.64, but is in captures
	leaderboard << "\nResult.Count=1:1"; // number of results, always 1
	if (!this->entries.empty()) leaderboard << "\nResult[0].Index=0:RowNumber"; // "Primary key".  Live doesn't include this if there are no entries.
	leaderboard << "\nResult[0].RowCount=1:" << entries.size();

	int32_t rowNumber = 0;
	for (auto& entry : entries) {
		for (auto* data : entry) {
			WriteLeaderboardRow(leaderboard, rowNumber, data);
		}
		rowNumber++;
	}

	// Serialize the thing to a BitStream
	uint32_t leaderboardSize = leaderboard.tellp();
	bitStream.Write<uint32_t>(leaderboardSize);
	// Doing this all in 1 call so there is no possbility of a dangling pointer.
	bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(GeneralUtils::ASCIIToUTF16(leaderboard.str()).c_str()), leaderboardSize * sizeof(char16_t));
	if (leaderboardSize > 0) bitStream.Write<uint16_t>(0);
	bitStream.Write0();
	bitStream.Write0();
}

// Takes the resulting query from a leaderboard lookup and converts it to the LDF we need
// to send it to a client.
void QueryToLdf(Leaderboard& leaderboard, const std::vector<ILeaderboard::Entry>& leaderboardEntries) {
	using enum Leaderboard::Type;
	leaderboard.Clear();
	if (leaderboardEntries.empty()) return;

	for (const auto& leaderboardEntry : leaderboardEntries) {
		constexpr int32_t MAX_NUM_DATA_PER_ROW = 9;
		auto& entry = leaderboard.PushBackEntry();
		entry.reserve(MAX_NUM_DATA_PER_ROW);
		entry.push_back(new LDFData<uint64_t>(u"CharacterID", leaderboardEntry.charId));
		entry.push_back(new LDFData<uint64_t>(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp));
		entry.push_back(new LDFData<int32_t>(u"NumPlayed", leaderboardEntry.numTimesPlayed));
		entry.push_back(new LDFData<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name)));
		entry.push_back(new LDFData<uint64_t>(u"RowNumber", leaderboardEntry.ranking));
		switch (leaderboard.GetLeaderboardType()) {
		case ShootingGallery:
			entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore));
			// Score:1
			entry.push_back(new LDFData<int32_t>(u"Streak", leaderboardEntry.secondaryScore));
			// Streak:1
			entry.push_back(new LDFData<float>(u"HitPercentage", leaderboardEntry.tertiaryScore));
			// HitPercentage:3 between 0 and 1
			break;
		case Racing:
			entry.push_back(new LDFData<float>(u"BestTime", leaderboardEntry.primaryScore));
			// BestLapTime:3
			entry.push_back(new LDFData<float>(u"BestLapTime", leaderboardEntry.secondaryScore));
			// BestTime:3
			entry.push_back(new LDFData<int32_t>(u"License", 1));
			// License:1 - 1 if player has completed mission 637 and 0 otherwise
			entry.push_back(new LDFData<int32_t>(u"NumWins", leaderboardEntry.numWins));
			// NumWins:1
			break;
		case UnusedLeaderboard4:
			entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore));
			// Points:1
			break;
		case MonumentRace:
			entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore));
			// Time:1(?)
			break;
		case FootRace:
			entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore));
			// Time:1
			break;
		case Survival:
			entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore));
			// Points:1
			entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore));
			// Time:1
			break;
		case SurvivalNS:
			entry.push_back(new LDFData<int32_t>(u"Wave", leaderboardEntry.primaryScore));
			// Wave:1
			entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore));
			// Time:1
			break;
		case Donations:
			entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore));
			// Score:1
			break;
		case None:
			[[fallthrough]];
		default:
			break;
		}
	}
}

std::vector<ILeaderboard::Entry> FilterTo10(const std::vector<ILeaderboard::Entry>& leaderboard, const uint32_t relatedPlayer, const Leaderboard::InfoType infoType) {
	std::vector<ILeaderboard::Entry> toReturn;

	int32_t index = 0;
	// for friends and top, we dont need to find this players index.
	if (infoType == Leaderboard::InfoType::MyStanding || infoType == Leaderboard::InfoType::Friends) {
		for (; index < leaderboard.size(); index++) {
			if (leaderboard[index].charId == relatedPlayer) break;
		}
	}

	if (leaderboard.size() < 10) {
		toReturn.assign(leaderboard.begin(), leaderboard.end());
		index = 0;
	} else if (index < 10) {
		toReturn.assign(leaderboard.begin(), leaderboard.begin() + 10); // get the top 10 since we are in the top 10
		index = 0;
	} else if (index > leaderboard.size() - 10) {
		toReturn.assign(leaderboard.end() - 10, leaderboard.end()); // get the bottom 10 since we are in the bottom 10
		index = leaderboard.size() - 10;
	} else {
		toReturn.assign(leaderboard.begin() + index - 5, leaderboard.begin() + index + 5); // get the 5 above and below
		index -= 5;
	}

	int32_t i = index;
	for (auto& entry : toReturn) {
		entry.ranking = ++i;
	}

	return toReturn;
}

std::vector<ILeaderboard::Entry> FilterWeeklies(const std::vector<ILeaderboard::Entry>& leaderboard) {
	// Filter the leaderboard to only include entries from the last week
	const auto currentTime = std::chrono::system_clock::now();
	auto epochTime = currentTime.time_since_epoch().count();
	constexpr auto SECONDS_IN_A_WEEK = 60 * 60 * 24 * 7; // if you think im taking leap seconds into account thats cute.

	std::vector<ILeaderboard::Entry> weeklyLeaderboard;
	for (const auto& entry : leaderboard) {
		if (epochTime - entry.lastPlayedTimestamp < SECONDS_IN_A_WEEK) {
			weeklyLeaderboard.push_back(entry);
		}
	}

	return weeklyLeaderboard;
}

std::vector<ILeaderboard::Entry> FilterFriends(const std::vector<ILeaderboard::Entry>& leaderboard, const uint32_t relatedPlayer) {
	// Filter the leaderboard to only include friends of the player
	auto friendOfPlayer = Database::Get()->GetFriendsList(relatedPlayer);
	std::vector<ILeaderboard::Entry> friendsLeaderboard;
	for (const auto& entry : leaderboard) {
		const auto res = std::ranges::find_if(friendOfPlayer, [&entry, relatedPlayer](const FriendData& data) {
			return entry.charId == data.friendID;
			});
		if (res != friendOfPlayer.cend() || entry.charId == relatedPlayer) {
			friendsLeaderboard.push_back(entry);
		}
	}

	return friendsLeaderboard;
}

std::vector<ILeaderboard::Entry> ProcessLeaderboard(
	const std::vector<ILeaderboard::Entry>& leaderboard,
	const bool weekly,
	const Leaderboard::InfoType infoType,
	const uint32_t relatedPlayer) {
	std::vector<ILeaderboard::Entry> toReturn;

	if (infoType == Leaderboard::InfoType::Friends) {
		const auto friendsLeaderboard = FilterFriends(leaderboard, relatedPlayer);
		toReturn = FilterTo10(weekly ? FilterWeeklies(friendsLeaderboard) : friendsLeaderboard, relatedPlayer, infoType);
	} else {
		toReturn = FilterTo10(weekly ? FilterWeeklies(leaderboard) : leaderboard, relatedPlayer, infoType);
	}

	return toReturn;
}

void Leaderboard::SetupLeaderboard(bool weekly) {
	const auto leaderboardType = LeaderboardManager::GetLeaderboardType(gameID);
	std::vector<ILeaderboard::Entry> leaderboardRes;

	switch (leaderboardType) {
	case Type::SurvivalNS:
		leaderboardRes = Database::Get()->GetNsLeaderboard(gameID);
		break;
	case Type::Survival:
		leaderboardRes = Database::Get()->GetAgsLeaderboard(gameID);
		break;
	case Type::Racing:
		[[fallthrough]];
	case Type::MonumentRace:
		leaderboardRes = Database::Get()->GetAscendingLeaderboard(gameID);
		break;
	case Type::ShootingGallery:
		[[fallthrough]];
	case Type::FootRace:
		[[fallthrough]];
	case Type::Donations:
		[[fallthrough]];
	case Type::None:
		[[fallthrough]];
	default:
		leaderboardRes = Database::Get()->GetDescendingLeaderboard(gameID);
		break;
	}

	const auto processedLeaderboard = ProcessLeaderboard(leaderboardRes, weekly, infoType, relatedPlayer);

	QueryToLdf(*this, processedLeaderboard);
}

void Leaderboard::Send(const LWOOBJID targetID) const {
	auto* player = Game::entityManager->GetEntity(relatedPlayer);
	if (player != nullptr) {
		GameMessages::SendActivitySummaryLeaderboardData(targetID, this, player->GetSystemAddress());
	}
}

void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore, const float tertiaryScore) {
	const Leaderboard::Type leaderboardType = GetLeaderboardType(activityId);

	const auto oldScore = Database::Get()->GetPlayerScore(playerID, activityId);

	ILeaderboard::Score newScore{ .primaryScore = primaryScore, .secondaryScore = secondaryScore, .tertiaryScore = tertiaryScore };
	if (oldScore.has_value()) {
		bool lowerScoreBetter = leaderboardType == Leaderboard::Type::Racing || leaderboardType == Leaderboard::Type::MonumentRace;
		bool newHighScore = lowerScoreBetter ? newScore < oldScore : newScore > oldScore;
		// Nimbus station has a weird leaderboard where we need a custom scoring system
		if (leaderboardType == Leaderboard::Type::SurvivalNS) {
			newHighScore = newScore.primaryScore > oldScore->primaryScore ||
				(newScore.primaryScore == oldScore->primaryScore && newScore.secondaryScore < oldScore->secondaryScore);
		} else if (leaderboardType == Leaderboard::Type::Survival && Game::config->GetValue("classic_survival_scoring") == "1") {
			ILeaderboard::Score oldScoreFlipped{oldScore->secondaryScore, oldScore->primaryScore, oldScore->tertiaryScore};
			ILeaderboard::Score newScoreFlipped{newScore.secondaryScore, newScore.primaryScore, newScore.tertiaryScore};
			newHighScore = newScoreFlipped > oldScoreFlipped;
		}

		if (newHighScore) {
			Database::Get()->UpdateScore(playerID, activityId, newScore);
		} else {
			Database::Get()->IncrementTimesPlayed(playerID, activityId);
		}
	} else {
		Database::Get()->SaveScore(playerID, activityId, newScore);
	}

	// track wins separately
	if (leaderboardType == Leaderboard::Type::Racing && tertiaryScore != 0.0f) {
		Database::Get()->IncrementNumWins(playerID, activityId);
	}
}

void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID) {
	Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID));
	leaderboard.SetupLeaderboard(weekly);
	leaderboard.Send(targetID);
}

Leaderboard::Type LeaderboardManager::GetLeaderboardType(const GameID gameID) {
	auto lookup = leaderboardCache.find(gameID);
	if (lookup != leaderboardCache.end()) return lookup->second;

	auto* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>();
	std::vector<CDActivities> activities = activitiesTable->Query([gameID](const CDActivities& entry) {
		return entry.ActivityID == gameID;
		});
	auto type = !activities.empty() ? static_cast<Leaderboard::Type>(activities.at(0).leaderboardType) : Leaderboard::Type::None;
	leaderboardCache.insert_or_assign(gameID, type);
	return type;
}
