-- This is so that we don't attempt another spawn right away (or at least until this one is done)
-- .This should be cleared when the server announcement is seen
-- .In the future, matchmaking may cause more than a single server to be spawned per lobby.

local _pendingServerCreation = {}

local _joinEvent = nil
local _serverListEvent = nil
local _statusEvent = nil

local _messageHandlers = {}

function Lobby._teleportWithRetry(placeId, token, player, spawnName, teleportData, customLoadingScreen)
	local connection

	connection = TeleportService.TeleportInitFailed:Connect(function(failedPlayer, status, errorMessage)
		if failedPlayer == player then
			Log.Error.Lobby("Teleport failed for %s, TeleportResult: %s", player.Name, status.Name)

			if status == Enum.TeleportResult.Failure or status == Enum.TeleportResult.Flooded then
				connection:Disconnect()

				delay(2, function()
					Log.Warn.Lobby("Reattempting teleport")
					TeleportService:TeleportToPrivateServer(placeId, token, {player}, spawnName, teleportData, customLoadingScreen)
				end)
			end
		end
	end)

	TeleportService:TeleportToPrivateServer(placeId, token, {player}, spawnName, teleportData, customLoadingScreen)
end

function Lobby._retryJoin(pending)
	local player = Players:GetPlayerByUserId(pending.player_id)

	if player then
		Log.Warn.Lobby("Retrying")
		Lobby._teleportPlayer(player, pending.place_id, pending.game_mode)

	else
		-- This player didn't make it.  Long live this player.
		_pendingTickets[pending.player_id] = nil
	end
end

function Lobby._requestServerList()
	Log.Debug.Lobby("requestServerList")

	local req = {
		type = "ANNOUNCE:REQUEST",
		lobby = game.JobId,
	}

	MessagingService:PublishAsync("SERVER", req)
end

function Lobby._getClientServerData()
	local data = {}

	for serverId, serverData in pairs(_serverList) do
		local entry = {
			server = serverId,
			slots_available = serverData.slots_available,
			nonresponsive = _nonresponsiveServers[serverId],
		}

		table.insert(data, entry)
	end

	return data
end

function Lobby._sendServerListToAllClients()
	-- Sending everything all at once right now
	local data = Lobby._getClientServerData()
	_serverListEvent:FireAllClients(data)
end

function Lobby._sendServerListToClient(player)
	local data = Lobby._getClientServerData()
	_serverListEvent:FireClient(player, data)
end

function Lobby._handleServerAnnounce(pub)
	local data = pub.Data
	local server = data.server
	local creationId = data.creation_id

	Log.Debug.Lobby("Heard from server: %s", server)
	Log.Debug.LobbyData(Util.tableToString(pub))

	_serverList[server] = data
	_nonresponsiveServers[server] = nil

	if creationId then
		_pendingServerCreation[creationId] = nil
	end

	Lobby._sendServerListToAllClients()
end

function Lobby._handleServerRemoval(pub)
	local removedServer = pub.Data.server

	Log.Debug.Lobby("Server %s removed", removedServer)

	local removals = {}

	-- Remove the actual server entry
	_serverList[removedServer] = nil

	-- Removing any pending joins to the removed server
	for playerId, pendingData in pairs(_pendingTickets) do
		if pendingData.server == removedServer then
			table.insert(removals, pendingData)
		end
	end

	for _, pendingData in ipairs(removals) do
		local playerId = pendingData.player_id

		_pendingTickets[playerId] = nil

		-- This player was attempting to join to the removed server, so send them to another one.
		local player = Players:GetPlayerByUserId(playerId)
		Lobby._teleportPlayer(player, pendingData.place_id, pendingData.game_mode)
	end

	Lobby._sendServerListToAllClients()
end

function Lobby._sendJoinRequest(serverData, player, mode, destinationKey)
	local server = serverData.server

	local req = {
		type = "JOIN:REQUEST",
		lobby = game.JobId,
		player = player.UserId,
		count = 1,
	}

	_statusEvent:FireClient(player, { status = string.format("Connecting to %s", server) })

	Log.Debug.Lobby("Join request sent to %s for player %s", server, player.UserId)

	MessagingService:PublishAsync(tostring(server), req)

	_pendingTickets[player.UserId] = {
		player_id = player.UserId,
		place_id = serverData.place_id,
		server = server,
		game_mode = mode,
		time = tick(),
		match_key = destinationKey,
	}
end

function Lobby._handleJoinResponse(pub)
	local status = pub.Data.status
	local token = pub.Data.token
	local userId = pub.Data.player
	local player = Players:GetPlayerByUserId(userId)

	local pendingData = _pendingTickets[userId]
	_pendingTickets[userId] = nil

	if status == Util._joinCodes.SUCCESS then
		-- TODO:  Warn on no pending ticket

		if pendingData then
			Lobby._teleportWithRetry(pendingData.place_id, token, player)
		end

	else
		Log.Warn.JoinFailure("Player %s failed to join %s (%s)",
							 userId,
							 pub.Data.server,
							 Util.getJoinCodeText(status))

		-- Join a different server
		-- .If the player was forbidden, may need to store that information somehow so we don't try the same one again
		if pendingData then
			Lobby._teleportPlayer(player, _pendingTickets.place_id, _pendingTickets.game_mode)
		end
	end
end

function Lobby._handleJoinExpired(pub)
	local playerId = pub.Data.player

	Log.Error.LobbyJoin("Expired %s", playerId)

	_pendingTickets[playerId] = nil
end

function Lobby._registerHandler(messageType, handler)
	_messageHandlers[messageType] = handler
end

function Lobby._dispatchMessage(pub)
	local handler = _messageHandlers[pub.Data.type]

	if handler then
		handler(pub)
	else
		-- TODO:  Error
	end
end

function Lobby._generateMatchKey(placeId, mode)
	return string.format("%s:%s", placeId, mode)
end

function Lobby._teleportPlayer(player, destinationId, mode)
	local destinationKey = Lobby._generateMatchKey(destinationId, mode)

	-- If there are pending server creations, we wait until they have completed or timed out
	-- .We could continuously iterate through the server list to see if another server comes online during this time, but for this case that complicates things needlessly.

	-- TODO:  Evaluate whether this needs to be wrapped in a coroutine or similar and whether that should be done implicitly here
	-- .Currently _processJoinQueue is wrapped, but honoring the queue is done in a single thread.
	-- .Also, in that same high-level loop, _detectDeadServers would inherently pause against the completion of _processJoinQueue, which could account for some old stuff.

	_statusEvent:FireClient(player, { status = "Waiting..." })

	while true do
		local waiting = false
		local creationTimeouts = {}

		for creationId, pendingCreation in pairs(_pendingServerCreation) do
			if pendingCreation.match_key == destinationKey then
				if tick() - pendingCreation.time >= _serverCreationTimeout then
					table.insert(creationTimeouts, creationId)
					Log.Error.Lobby("Server creation '%s' exceeded timeout", creationId)
				else
					waiting = true
				end
			end
		end

		for _, creationId in ipairs(creationTimeouts) do
			_pendingServerCreation[creationId] = nil
		end

		if not waiting then
			break
		end

		wait(1)
	end

	for _, serverData in pairs(_serverList) do
		if serverData.match_key == destinationKey and serverData.slots_available > 0 and not Lobby._isServerBlacklisted(player, serverData.server) then
			Lobby._sendJoinRequest(serverData, player, mode, destinationKey)
			return
		end
	end

	_statusEvent:FireClient(player, { status = "Creating fresh server" })

	-- Create a gameplay server
	local token = TeleportService:ReserveServer(destinationId)
	Log.Debug.Lobby("Reserved %s for place %s", token, destinationId)

	_statusEvent:FireClient(player, { status = "Teleporting" })

	-- HACK:  The token is sent along with the teleport data because there's no way to get access to it from the server itself
	local creationData = {
		lobby = game.JobId,
		creation_id = HttpService:GenerateGUID(),
		token = token,
		time = tick(),
		match_key = destinationKey,
		place_id = destinationId,
		game_mode = mode,
	}

	_pendingServerCreation[creationData.creation_id] = creationData

	Lobby._teleportWithRetry(destinationId, token, player, nil, creationData)
end

function Lobby._detectDeadServers()
	local expirations = {}
	local updateClients = false

	for playerId, request in pairs(_pendingTickets) do
		if tick() - request.time >= _serverResponseTimeout then
			expirations[playerId] = request
		end
	end

	for playerId, request in pairs(expirations) do
		_pendingTickets[playerId] = nil

		-- Flag this server as non-responsive until it's heard from again
		local player = Players:GetPlayerByUserId(playerId)

		Lobby._recordServerFailure(player, request.server)
		updateClients = true

		Log.Warn.Lobby("Dead server detected: %s", request.server)

		Lobby._retryJoin(request)
	end

	if updateClients then
		Lobby._sendServerListToAllClients()
	end
end

function Lobby._recordServerFailure(player, server)
	local userId = player.UserId
	local failureList = _playerServerFailures[userId]

	if not failureList then
		failureList = {}
		_playerServerFailures[userId] = failureList
	end

	failureList[server] = (failureList[server] or 0) + 1

	_nonresponsiveServers[server] = (_nonresponsiveServers[server] or 0) + 1
end

function Lobby._isServerBlacklisted(player, server)
	local failureList = _playerServerFailures[player.UserId]
	return failureList and
		((failureList[server] or 0) >= Conf.lobby_teleportation_retries or
			_nonresponsiveServers[server])
end

function Lobby._onJoin(player, data)
	local pending = _pendingTickets[player.UserId]

	if data.type == "JOIN" then
		local mode = data.queue_selection or Conf.default_game_mode
		local destinationId = Conf.modes[mode].temp_queue_place

		if Conf.lobby_use_boring_flow then
			-- Alternate queue place
			TeleportService:Teleport(destinationId, player)
			_statusEvent:FireClient(player, { status = "Queueing..." })
		else
			if pending then
				Log.Warn.Lobby("User %s already has pending join request.", player.Name)
				return
			end

			Log.Debug.Lobby("Player join requested")

			_joinQueue[player.UserId] = {
				player = player,
				place_id = destinationId,
				game_mode = mode,
			}

			TeleportService:Teleport(destinationId, player)
			_statusEvent:FireClient(player, { status = "Queueing..." })
		end

	elseif data.type == "CANCEL" then
		_pendingTickets[player.UserId] = nil
		_statusEvent:FireClient(player, { canceled = true })

	elseif data.type == "FAILURE" then
		-- The client failed during teleportation and we need to retry or blacklist the current server
		Log.Error.Lobby("Client teleport failed: %s (%s)", player.Name, data.message)

		if pending then
			Lobby._recordServerFailure(player, pending.server)
			Lobby._retryJoin(pending)
		end
	end
end

function Lobby._onPlayerRemoving(player)
	_pendingTickets[player.UserId] = nil
	-- TODO:  Should this directly tell any server waiting for this player that they won't be making it?
	-- .Currently that will be handled by the ticket timeout
end

function Lobby._onPlayerAdded(player)
	Lobby._sendServerListToClient(player)
end

function Lobby._onTeleportFailure(player, status, message)
	Log.Error.Lobby("Teleport Failed: %s (%s)", player.UserId, message)
end

function Lobby._processJoinQueue()
	local queue = _joinQueue
	_joinQueue = {}

	for _, joinData in pairs(queue) do
		Lobby._teleportPlayer(joinData.player, joinData.place_id, joinData.game_mode)
	end
end
