--[[
update_settings = {
	ExistingProfileHandle = function(latest_data),
	MissingProfileHandle = function(latest_data),
	EditProfile = function(lastest_data),
}
--]]


local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, update_settings, is_user_mock, is_get_call, version) --> loaded_data, key_info
	local loaded_data, key_info
	local success, error_message = pcall(function()
		local transform_function = function(latest_data)

			local missing_profile = false
			local data_corrupted = false
			local global_updates_data = {0, {}}

			if latest_data == nil then
				missing_profile = true
			elseif type(latest_data) ~= "table" then
				missing_profile = true
				data_corrupted = true
			end

			if type(latest_data) == "table" then
				-- Case #1: Profile was loaded
				if type(latest_data.Data) == "table"
					and type(latest_data.MetaData) == "table"
					and type(latest_data.GlobalUpdates) == "table" then

					latest_data.WasCorrupted = false -- Must be set to false if set previously
					global_updates_data = latest_data.GlobalUpdates
					if update_settings.ExistingProfileHandle ~= nil then
						update_settings.ExistingProfileHandle(latest_data)
					end
					-- Case #2: Profile was not loaded but GlobalUpdate data exists
				elseif latest_data.Data == nil
					and latest_data.MetaData == nil
					and type(latest_data.GlobalUpdates) == "table" then

					latest_data.WasCorrupted = false -- Must be set to false if set previously
					global_updates_data = latest_data.GlobalUpdates or global_updates_data
					missing_profile = true
				else
					missing_profile = true
					data_corrupted = true
				end
			end

			-- Case #3: Profile was not created or corrupted and no GlobalUpdate data exists
			if missing_profile == true then
				latest_data = {
					-- Data = nil,
					-- MetaData = nil,
					GlobalUpdates = global_updates_data,
				}
				if update_settings.MissingProfileHandle ~= nil then
					update_settings.MissingProfileHandle(latest_data)
				end
			end

			-- Editing profile:
			if update_settings.EditProfile ~= nil then
				update_settings.EditProfile(latest_data)
			end

			-- Data corruption handling (Silently override with empty profile) (Also run Case #1)
			if data_corrupted == true then
				latest_data.WasCorrupted = true -- Temporary tag that will be removed on first save
			end

			return latest_data, latest_data.UserIds, latest_data.RobloxMetaData
		end
		if is_user_mock == true then -- Used when the profile is accessed through ProfileStore.Mock
			loaded_data, key_info = MockUpdateAsync(UserMockDataStore, profile_store._profile_store_lookup, profile_key, transform_function, is_get_call)
			task.wait() -- Simulate API call yield
		elseif UseMockDataStore == true then -- Used when API access is disabled
			loaded_data, key_info = MockUpdateAsync(MockDataStore, profile_store._profile_store_lookup, profile_key, transform_function, is_get_call)
			task.wait() -- Simulate API call yield
		else
			loaded_data, key_info = CustomWriteQueueAsync(
				function() -- Callback
					if is_get_call == true then
						local get_data, get_key_info
						if version ~= nil then
							local success, error_message = pcall(function()
								get_data, get_key_info = profile_store._global_data_store:GetVersionAsync(profile_key, version)
							end)
							if success == false and type(error_message) == "string" and string.find(error_message, "not valid") ~= nil then
								warn("[ProfileService]: Passed version argument is not valid; Traceback:\n" .. debug.traceback())
							end
						else
							get_data, get_key_info = profile_store._global_data_store:GetAsync(profile_key)
						end
						get_data = transform_function(get_data)
						return get_data, get_key_info
					else
						return profile_store._global_data_store:UpdateAsync(profile_key, transform_function)
					end
				end,
				profile_store._profile_store_lookup, -- Store
				profile_key -- Key
			)
		end
	end)
	if success == true and type(loaded_data) == "table" then
		-- Corruption handling:
		if loaded_data.WasCorrupted == true and is_get_call ~= true then
			RegisterCorruption(
				profile_store._profile_store_name,
				profile_store._profile_store_scope,
				profile_key
			)
		end
		-- Return loaded_data:
		return loaded_data, key_info
	else
		RegisterIssue(
			(error_message ~= nil) and error_message or "Undefined error",
			profile_store._profile_store_name,
			profile_store._profile_store_scope,
			profile_key
		)
		-- Return nothing:
		return nil
	end
end

local function RemoveProfileFromAutoSave(profile)
	local auto_save_index = table.find(AutoSaveList, profile)
	if auto_save_index ~= nil then
		table.remove(AutoSaveList, auto_save_index)
		if auto_save_index < AutoSaveIndex then
			AutoSaveIndex = AutoSaveIndex - 1 -- Table contents were moved left before AutoSaveIndex so move AutoSaveIndex left as well
		end
		if AutoSaveList[AutoSaveIndex] == nil then -- AutoSaveIndex was at the end of the AutoSaveList - reset to 1
			AutoSaveIndex = 1
		end
	end
end

local function AddProfileToAutoSave(profile) -- Notice: Makes sure this profile isn't auto-saved too soon
	-- Add at AutoSaveIndex and move AutoSaveIndex right:
	table.insert(AutoSaveList, AutoSaveIndex, profile)
	if #AutoSaveList > 1 then
		AutoSaveIndex = AutoSaveIndex + 1
	elseif #AutoSaveList == 1 then
		-- First profile created - make sure it doesn't get immediately auto saved:
		LastAutoSave = os.clock()
	end
end

local function ReleaseProfileInternally(profile)
	-- 1) Remove profile object from ProfileService references: --
	-- Clear reference in ProfileStore:
	local profile_store = profile._profile_store
	local loaded_profiles = profile._is_user_mock == true and profile_store._mock_loaded_profiles or profile_store._loaded_profiles
	loaded_profiles[profile._profile_key] = nil
	if next(profile_store._loaded_profiles) == nil and next(profile_store._mock_loaded_profiles) == nil then -- ProfileStore has turned inactive
		local index = table.find(ActiveProfileStores, profile_store)
		if index ~= nil then
			table.remove(ActiveProfileStores, index)
		end
	end
	-- Clear auto update reference:
	RemoveProfileFromAutoSave(profile)
	-- 2) Trigger release listeners: --
	local place_id
	local game_job_id
	local active_session = profile.MetaData.ActiveSession
	if active_session ~= nil then
		place_id = active_session[1]
		game_job_id = active_session[2]
	end
	profile._release_listeners:Fire(place_id, game_job_id)
end

local function CheckForNewGlobalUpdates(profile, old_global_updates_data, new_global_updates_data)
	local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
	local pending_update_lock = global_updates_object._pending_update_lock -- {update_id, ...}
	local pending_update_clear = global_updates_object._pending_update_clear -- {update_id, ...}
	-- "old_" or "new_" global_updates_data = {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
	for _, new_global_update in ipairs(new_global_updates_data[2]) do
		-- Find old global update with the same update_id:
		local old_global_update
		for _, global_update in ipairs(old_global_updates_data[2]) do
			if global_update[1] == new_global_update[1] then
				old_global_update = global_update
				break
			end
		end
		-- A global update is new when it didn't exist before or its version_id or update_locked state changed:
		local is_new = false
		if old_global_update == nil or new_global_update[2] > old_global_update[2] or new_global_update[3] ~= old_global_update[3] then
			is_new = true
		end
		if is_new == true then
			-- Active global updates:
			if new_global_update[3] == false then
				-- Check if update is not pending to be locked: (Preventing firing new active update listeners more than necessary)
				local is_pending_lock = false
				for _, update_id in ipairs(pending_update_lock) do
					if new_global_update[1] == update_id then
						is_pending_lock = true
						break
					end
				end
				if is_pending_lock == false then
					-- Trigger new active update listeners:
					global_updates_object._new_active_update_listeners:Fire(new_global_update[1], new_global_update[4])
				end
			end
			-- Locked global updates:
			if new_global_update[3] == true then
				-- Check if update is not pending to be cleared: (Preventing firing new locked update listeners after marking a locked update for clearing)
				local is_pending_clear = false
				for _, update_id in ipairs(pending_update_clear) do
					if new_global_update[1] == update_id then
						is_pending_clear = true
						break
					end
				end
				if is_pending_clear == false then
					-- Trigger new locked update listeners:

					global_updates_object._new_locked_update_listeners:FireUntil(
						function()
							-- Check if listener marked the update to be cleared:
							-- Normally there should be only one listener per profile for new locked global updates, but
							-- in case several listeners are connected we will not trigger more listeners after one listener
							-- marks the locked global update to be cleared.
							return table.find(pending_update_clear, new_global_update[1]) == nil
						end,
						new_global_update[1], new_global_update[4]
					)

				end
			end
		end
	end
end

local function SaveProfileAsync(profile, release_from_session, is_overwriting)
	if type(profile.Data) ~= "table" then
		RegisterCorruption(
			profile._profile_store._profile_store_name,
			profile._profile_store._profile_store_scope,
			profile._profile_key
		)
		error("[ProfileService]: PROFILE DATA CORRUPTED DURING RUNTIME! Profile: " .. profile:Identify())
	end
	if release_from_session == true and is_overwriting ~= true then
		ReleaseProfileInternally(profile)
	end
	ActiveProfileSaveJobs = ActiveProfileSaveJobs + 1
	local last_session_load_count = profile.MetaData.SessionLoadCount
	-- Compare "SessionLoadCount" when writing to profile to prevent a rare case of repeat last save when the profile is loaded on the same server again
	local repeat_save_flag = true -- Released Profile save calls have to repeat until they succeed
	while repeat_save_flag == true do
		if release_from_session ~= true then
			repeat_save_flag = false
		end
		local loaded_data, key_info = StandardProfileUpdateAsyncDataStore(
			profile._profile_store,
			profile._profile_key,
			{
				ExistingProfileHandle = nil,
				MissingProfileHandle = nil,
				EditProfile = function(latest_data)

					local session_owns_profile = false
					local force_load_pending = false

					if is_overwriting ~= true then
						-- 1) Check if this session still owns the profile: --
						local active_session = latest_data.MetaData.ActiveSession
						local force_load_session = latest_data.MetaData.ForceLoadSession
						local session_load_count = latest_data.MetaData.SessionLoadCount

						if type(active_session) == "table" then
							session_owns_profile = IsThisSession(active_session) and session_load_count == last_session_load_count
						end
						if type(force_load_session) == "table" then
							force_load_pending = not IsThisSession(force_load_session)
						end
					else
						session_owns_profile = true
					end

					if session_owns_profile == true then -- We may only edit the profile if this session has ownership of the profile

						if is_overwriting ~= true then
							-- 2) Manage global updates: --
							local latest_global_updates_data = latest_data.GlobalUpdates -- {update_index, {{update_id, version_id, update_locked, update_data}, ...}}
							local latest_global_updates_list = latest_global_updates_data[2]

							local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
							local pending_update_lock = global_updates_object._pending_update_lock -- {update_id, ...}
							local pending_update_clear = global_updates_object._pending_update_clear -- {update_id, ...}
							-- Active update locking:
							for i = 1, #latest_global_updates_list do
								for _, lock_id in ipairs(pending_update_lock) do
									if latest_global_updates_list[i][1] == lock_id then
										latest_global_updates_list[i][3] = true
										break
									end
								end
							end
							-- Locked update clearing:
							for _, clear_id in ipairs(pending_update_clear) do
								for i = 1, #latest_global_updates_list do
									if latest_global_updates_list[i][1] == clear_id and latest_global_updates_list[i][3] == true then
										table.remove(latest_global_updates_list, i)
										break
									end
								end
							end
						end

						-- 3) Save profile data: --
						latest_data.Data = profile.Data
						latest_data.RobloxMetaData = profile.RobloxMetaData
						latest_data.UserIds = profile.UserIds

						if is_overwriting ~= true then
							latest_data.MetaData.MetaTags = profile.MetaData.MetaTags -- MetaData.MetaTags is the only actively savable component of MetaData
							latest_data.MetaData.LastUpdate = os.time()
							if release_from_session == true or force_load_pending == true then
								latest_data.MetaData.ActiveSession = nil
							end
						else
							latest_data.MetaData = profile.MetaData
							latest_data.MetaData.ActiveSession = nil
							latest_data.MetaData.ForceLoadSession = nil
							latest_data.GlobalUpdates = profile.GlobalUpdates._updates_latest
						end

					end
				end,
			},
			profile._is_user_mock
		)
		if loaded_data ~= nil and key_info ~= nil then
			if is_overwriting == true then
				break
			end
			repeat_save_flag = false
			-- 4) Set latest data in profile: --
			-- Updating DataStoreKeyInfo:
			profile.KeyInfo = key_info
			-- Setting global updates:
			local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates]
			local old_global_updates_data = global_updates_object._updates_latest
			local new_global_updates_data = loaded_data.GlobalUpdates
			global_updates_object._updates_latest = new_global_updates_data
			-- Setting MetaData:
			local session_meta_data = profile.MetaData
			local latest_meta_data = loaded_data.MetaData
			for key in pairs(SETTINGS.MetaTagsUpdatedValues) do
				session_meta_data[key] = latest_meta_data[key]
			end
			session_meta_data.MetaTagsLatest = latest_meta_data.MetaTags
			-- 5) Check if session still owns the profile: --
			local active_session = loaded_data.MetaData.ActiveSession
			local session_load_count = loaded_data.MetaData.SessionLoadCount
			local session_owns_profile = false
			if type(active_session) == "table" then
				session_owns_profile = IsThisSession(active_session) and session_load_count == last_session_load_count
			end
			local is_active = profile:IsActive()
			if session_owns_profile == true then
				-- 6) Check for new global updates: --
				if is_active == true then -- Profile could've been released before the saving thread finished
					CheckForNewGlobalUpdates(profile, old_global_updates_data, new_global_updates_data)
				end
			else
				-- Session no longer owns the profile:
				-- 7) Release profile if it hasn't been released yet: --
				if is_active == true then
					ReleaseProfileInternally(profile)
				end
				-- Cleanup reference in custom write queue:
				CustomWriteQueueMarkForCleanup(profile._profile_store._profile_store_lookup, profile._profile_key)
				-- Hop ready listeners:
				if profile._hop_ready == false then
					profile._hop_ready = true
					profile._hop_ready_listeners:Fire()
				end
			end
			-- Signaling MetaTagsUpdated listeners after a possible external profile release was handled:
			profile.MetaTagsUpdated:Fire(profile.MetaData.MetaTagsLatest)
			-- Signaling KeyInfoUpdated listeners:
			profile.KeyInfoUpdated:Fire(key_info)
		elseif repeat_save_flag == true then
			task.wait() -- Prevent infinite loop in case DataStore API does not yield
		end
	end
	ActiveProfileSaveJobs = ActiveProfileSaveJobs - 1
end