--!strict
--[=[
	This service lets you shut down servers without losing a bunch of players.
	When game.OnClose is called, the script teleports everyone in the server
	into a reserved server.

	When the reserved servers start up, they wait a few seconds, and then
	send everyone back into the main place.

	Originally written by Merely
	https://github.com/MerelyRBLX/ROBLOX-Lua/blob/master/SoftShutdown.lua

	Modified by Quenty

	@class SoftShutdownService
]=]

local require = require(script.Parent.loader).load(script)

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local BindToCloseService = require("BindToCloseService")
local DataStorePromises = require("DataStorePromises")
local Maid = require("Maid")
local Promise = require("Promise")
local ServiceBag = require("ServiceBag")
local SoftShutdownConstants = require("SoftShutdownConstants")
local TeleportServiceUtils = require("TeleportServiceUtils")

local SoftShutdownService = {}
SoftShutdownService.ServiceName = "SoftShutdownService"

export type SoftShutdownService = typeof(setmetatable(
	{} :: {
		_maid: Maid.Maid,
		_serviceBag: ServiceBag.ServiceBag,
		_bindToCloseService: BindToCloseService.BindToCloseService,
		_dataStore: DataStore,
	},
	{} :: typeof({ __index = SoftShutdownService })
))

--[=[
	Initialize the service. Should be done via [ServiceBag].

	:::warning
	Initializing this service effectively initializes side effects, which is, to initialize
	the soft shutdown behavior.
	:::

	@param serviceBag ServiceBag
]=]
function SoftShutdownService.Init(self: SoftShutdownService, serviceBag: ServiceBag.ServiceBag): ()
	self._maid = Maid.new()
	self._serviceBag = assert(serviceBag, "No serviceBag")

	self._bindToCloseService = self._serviceBag:GetService(BindToCloseService) :: any
	self._serviceBag:GetService(require("SoftShutdownTranslator"))

	self._dataStore = DataStoreService:GetDataStore("IsSoftShutdownServer")

	self:_promiseIsLobby():Then(function(isLobby)
		if isLobby then
			self:_promiseRedirectAllPlayers()
		else
			self._maid:GiveTask(self._bindToCloseService:RegisterPromiseOnCloseCallback(function()
				return self:_promiseTeleportPlayersToLobby()
			end))
		end
	end)
end

function SoftShutdownService._isReservedServer(_self: SoftShutdownService): boolean
	return game.PrivateServerId ~= "" and game.PrivateServerOwnerId == 0
end

function SoftShutdownService._promiseIsLobby(self: SoftShutdownService): Promise.Promise<boolean>
	if not self:_isReservedServer() then
		return Promise.resolved(false)
	end

	return self._maid:GivePromise(DataStorePromises.getAsync(self._dataStore, game.PrivateServerId))
end

function SoftShutdownService._promiseTeleportPlayersToLobby(self: SoftShutdownService): Promise.Promise<()>
	local players = Players:GetPlayers()
	if RunService:IsStudio() or #players == 0 or game.JobId == "" then
		return Promise.resolved()
	end

	Workspace:SetAttribute(SoftShutdownConstants.IS_SOFT_SHUTDOWN_UPDATING_ATTRIBUTE, true)

	local initialTeleportOptions = Instance.new("TeleportOptions")
	initialTeleportOptions.ShouldReserveServer = true
	initialTeleportOptions:SetTeleportData({
		isSoftShutdownReserveServer = true,
	})

	-- Collect any players remaining
	local remainingPlayers = {}
	(self._maid :: any)._playerAddedCollector = Players.PlayerAdded:Connect(function(player: Player)
		table.insert(remainingPlayers, player)
	end)

	return Promise.spawn(function(resolve, _reject)
		-- Wait to let the teleport GUI be set
		task.delay(1, resolve)
	end)
		:Then(function()
			return TeleportServiceUtils.promiseTeleport(game.PlaceId, players, initialTeleportOptions)
		end)
		:Then(function(teleportResult)
			(self._maid :: any)._playerAddedCollector = nil

			-- Construct new teleport options
			local newTeleportOptions = Instance.new("TeleportOptions")
			newTeleportOptions.ServerInstanceId = teleportResult.PrivateServerId
			newTeleportOptions.ReservedServerAccessCode = teleportResult.ReservedServerAccessCode
			newTeleportOptions:SetTeleportData({
				isSoftShutdownReserveServer = true,
			})

			-- Teleport any players that joined during initial teleport
			local promises = {}

			if #remainingPlayers > 0 then
				table.insert(
					promises,
					TeleportServiceUtils.promiseTeleport(game.PlaceId, remainingPlayers, newTeleportOptions)
				)
			end

			self._maid:GiveTask(Players.PlayerAdded:Connect(function(player: Player)
				table.insert(
					promises,
					TeleportServiceUtils.promiseTeleport(game.PlaceId, { player }, newTeleportOptions)
				)
			end))

			-- We hope this works!
			table.insert(
				promises,
				DataStorePromises.setAsync(self._dataStore, teleportResult.PrivateServerId, true :: any)
			)

			return Promise.spawn(function(resolve)
				while #Players:GetPlayers() > 0 and self:_containsPending(promises) do
					task.wait(1)
				end

				resolve()
			end)
		end)
end

function SoftShutdownService._containsPending(_self: SoftShutdownService, promises: { any }): boolean
	for _, item in promises do
		if item:IsPending() then
			return true
		end
	end

	return false
end

function SoftShutdownService._promiseRedirectAllPlayers(self: SoftShutdownService): Promise.Promise<()>
	Workspace:SetAttribute(SoftShutdownConstants.IS_SOFT_SHUTDOWN_LOBBY_ATTRIBUTE, true)

	-- Wait for some players
	return Promise.spawn(function(resolve, reject)
		task.wait(1) -- Let the teleport GUI be set
		local players = Players:GetPlayers()
		while #players <= 0 do
			task.wait(1)
			players = Players:GetPlayers()
		end

		self._maid:GiveTask(reject)

		resolve(players)
	end):Then(function(players)
		local teleportOptions = Instance.new("TeleportOptions")
		teleportOptions:SetTeleportData({
			isSoftShutdownArrivingIntoUpdatedServer = true,
		})

		-- Teleport all remaining players
		self._maid:GiveTask(Players.PlayerAdded:Connect(function(player: Player)
			task.wait(1) -- Let the teleport GUI be set
			TeleportServiceUtils.promiseTeleport(game.PlaceId, { player }, teleportOptions)
		end))

		-- Try to keep players in the same group
		return TeleportServiceUtils.promiseTeleport(game.PlaceId, players, teleportOptions)
	end)
end

function SoftShutdownService.Destroy(self: SoftShutdownService): ()
	self._maid:DoCleaning()
end

return SoftShutdownService
