/* Copyright (C) 2025 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. 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.
 *
 * 0 A.D. 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 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "precompiled.h"

#include "NetServerTurnManager.h"

#include "lib/debug.h"
#include "lib/utf8.h"
#include "network/NetHost.h"
#include "network/NetMessage.h"
#include "network/NetServer.h"
#include "network/NetSession.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "simulation2/system/TurnManager.h"

#include <limits>
#include <utility>

#if 0
#include "ps/Util.h"
#define NETSERVERTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETSERVERTURN_LOG(...)
#endif

CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
	: m_NetServer(server), m_ReadyTurn(COMMAND_DELAY_MP - 1), m_TurnLength(DEFAULT_TURN_LENGTH)
{
	// Turn 0 is not actually executed, store a dummy value.
	m_SavedTurnLengths.push_back(0);
	// Turns [1..COMMAND_DELAY - 1] are special: all clients run them without waiting on a server command batch.
	// Because of this, they are always run with the default MP turn length.
	for (u32 i = 1; i < COMMAND_DELAY_MP; ++i)
		m_SavedTurnLengths.push_back(m_TurnLength);
}

void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn)
{
	int client = session.GetHostID();

	NETSERVERTURN_LOG("NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn);

	// Must be a client we've already heard of
	ENSURE(m_ClientsData.find(client) != m_ClientsData.end());

	// Clients must advance one turn at a time
	if (turn != m_ClientsData[client].readyTurn + 1)
	{
		LOGERROR("NotifyFinishedClientCommands: Client %d (%s) is ready for turn %d, but expected %d",
			client,
			utf8_from_wstring(session.GetUserName()).c_str(),
			turn,
			m_ClientsData[client].readyTurn + 1);

		session.Disconnect(NDR_INCORRECT_READY_TURN_COMMANDS);
		return;
	}

	m_ClientsData[client].readyTurn = turn;

	// Check whether this was the final client to become ready
	CheckClientsReady();
}

void CNetServerTurnManager::CheckClientsReady()
{
	int max_observer_lag{g_ConfigDB.Get("network.observermaxlag", -1)};
	// Clamp to 0-10000 turns, below/above that is no limit.
	max_observer_lag = max_observer_lag < 0 ? -1 : max_observer_lag > 10000 ? -1 : max_observer_lag;

	// See if all clients (including self) are ready for a new turn
	for (const std::pair<const int, Client>& clientData : m_ClientsData)
	{
		// Observers are allowed to lag more than regular clients.
		if (clientData.second.isObserver && (max_observer_lag == -1 || clientData.second.readyTurn > m_ReadyTurn - max_observer_lag))
			continue;
		NETSERVERTURN_LOG("  %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn);
		if (clientData.second.readyTurn <= m_ReadyTurn)
			return; // wasn't ready for m_ReadyTurn+1
	}

	++m_ReadyTurn;

	NETSERVERTURN_LOG("CheckClientsReady: ready for turn %d\n", m_ReadyTurn);

	// Tell all clients that the next turn is ready
	CEndCommandBatchMessage msg;
	msg.m_TurnLength = m_TurnLength;
	msg.m_Turn = m_ReadyTurn;
	m_NetServer.Multicast(&msg, { NSS_INGAME });

	ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
	m_SavedTurnLengths.push_back(m_TurnLength);
}

void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash)
{

	int client = session.GetHostID();
	const CStrW& playername = session.GetUserName();

	// Clients must advance one turn at a time
	if (turn != m_ClientsData[client].simulatedTurn + 1)
	{
		LOGERROR("NotifyFinishedClientUpdate: Client %d (%s) is ready for turn %d, but expected %d",
			client,
			utf8_from_wstring(playername).c_str(),
			turn,
			m_ClientsData[client].simulatedTurn + 1);

		session.Disconnect(NDR_INCORRECT_READY_TURN_SIMULATED);
		return;
	}

	m_ClientsData[client].simulatedTurn = turn;

	// Check for OOS only if in sync
	if (m_HasSyncError)
		return;

	m_ClientsData[client].playerName = playername;
	m_ClientStateHashes[turn][client] = hash;

	// Find the newest turn which we know all clients have simulated
	u32 newest = std::numeric_limits<u32>::max();
	for (const std::pair<const int, Client>& clientData : m_ClientsData)
		if (clientData.second.simulatedTurn < newest)
			newest = clientData.second.simulatedTurn;

	// For every set of state hashes that all clients have simulated, check for OOS
	for (const std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
	{
		if (clientStateHash.first > newest)
			break;

		// Assume the host is correct (maybe we should choose the most common instead to help debugging)
		std::string expected = clientStateHash.second.begin()->second;

		// Find all players that are OOS on that turn
		std::vector<CStrW> OOSPlayerNames;
		for (const std::pair<const int, std::string>& hashPair : clientStateHash.second)
		{
			NETSERVERTURN_LOG("sync check %d: %d = %hs\n", clientStateHash.first, hashPair.first, Hexify(hashPair.second).c_str());
			if (hashPair.second != expected)
			{
				// Oh no, out of sync
				m_HasSyncError = true;
				m_ClientsData[hashPair.first].isOOS = true;
				OOSPlayerNames.push_back(m_ClientsData[hashPair.first].playerName);
			}
		}

		// Tell everyone about it
		if (m_HasSyncError)
		{
			CSyncErrorMessage msg;
			msg.m_Turn = clientStateHash.first;
			msg.m_HashExpected = expected;
			for (const CStrW& oosPlayername : OOSPlayerNames)
			{
				CSyncErrorMessage::S_m_PlayerNames h;
				h.m_Name = oosPlayername;
				msg.m_PlayerNames.push_back(h);
			}
			m_NetServer.Multicast(&msg, { NSS_INGAME });
			break;
		}
	}

	// Delete the saved hashes for all turns that we've already verified
	m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}

void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer)
{
	NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);

	ENSURE(m_ClientsData.find(client) == m_ClientsData.end());
	Client& data = m_ClientsData[client];
	data.readyTurn = turn + COMMAND_DELAY_MP - 1;
	data.simulatedTurn = turn;
	data.isObserver = observer;
}

void CNetServerTurnManager::UninitialiseClient(int client)
{
	NETSERVERTURN_LOG("UninitialiseClient(client=%d)\n", client);

	ENSURE(m_ClientsData.find(client) != m_ClientsData.end());
	bool checkOOS = m_ClientsData[client].isOOS;
	m_ClientsData.erase(client);

	for (std::pair<const u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
		clientStateHash.second.erase(client);

	// Check whether we're ready for the next turn now that we're not
	// waiting for this client any more
	CheckClientsReady();

	// Check whether we're still OOS.
	if (checkOOS)
	{
		for (const std::pair<const int, Client>& clientData : m_ClientsData)
			if (clientData.second.isOOS)
				return;
		m_HasSyncError = false;
	}
}

void CNetServerTurnManager::SetTurnLength(u32 msecs)
{
	m_TurnLength = msecs;
}

u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
{
	ENSURE(turn <= m_ReadyTurn);
	return m_SavedTurnLengths.at(turn);
}
