local x={}x.__index=x function x.IsActive(x)local k=x._is_user_mock==true and x._profile_store._mock_loaded_profiles or x._profile_store._loaded_profiles return k[x._profile_key]==x end function x.GetMetaTag(k,x)local i=k.MetaData if i==nil then return nil end return k.MetaData.MetaTags[x]end function x.SetMetaTag(i,x,k)if type(x)~="\115\116\114\105\110\103"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \116\097\103\095\110\097\109\101 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103")elseif string.len(x)==0 then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \116\097\103\095\110\097\109\101")end i.MetaData.MetaTags[x]=k end function x.Reconcile(x)ReconcileTable(x.Data,x._profile_store._profile_template)end function x.ListenToRelease(k,x)if type(x)~="\102\117\110\099\116\105\111\110"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \079\110\108\121 \097 \102\117\110\099\116\105\111\110 \099\097\110 \098\101 \115\101\116 \097\115 \108\105\115\116\101\110\101\114 \105\110 \080\114\111\102\105\108\101\058\076\105\115\116\101\110\084\111\082\101\108\101\097\115\101\040\041")end if k._view_mode==true then return{Disconnect=function() end}end if k:IsActive()==false then local i local L local E=k.MetaData.ActiveSession if E~=nil then i=E[1]L=E[2]end x(i,L)return{Disconnect=function() end}else return k._release_listeners:Connect(x)end end function x.Save(x)if x._view_mode==true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \067\097\110\'\116 \115\097\118\101 \080\114\111\102\105\108\101 \105\110 \118\105\101\119 \109\111\100\101 - \083\104\111\117\108\100 \121\111\117 \098\101 \099\097\108\108\105\110\103 \058\079\118\101\114\119\114\105\116\101\065\115\121\110\099\040\041 \105\110\115\116\101\097\100\063")end if x:IsActive()==false then warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \065\116\116\101\109\112\116\101\100 \115\097\118\105\110\103 \097\110 \105\110\097\099\116\105\118\101 \112\114\111\102\105\108\101 "..(x:Identify()..("\059 \084\114\097\099\101\098\097\099\107\058\n"..debug.traceback())))return end if IsCustomWriteQueueEmptyFor(x._profile_store._profile_store_lookup,x._profile_key)==true then RemoveProfileFromAutoSave(x)AddProfileToAutoSave(x)task.spawn(SaveProfileAsync,x)end end function x.Release(x)if x._view_mode==true then return end if x:IsActive()==true then task.spawn(SaveProfileAsync,x,true)end end function x.ListenToHopReady(k,x)if type(x)~="\102\117\110\099\116\105\111\110"then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \079\110\108\121 \097 \102\117\110\099\116\105\111\110 \099\097\110 \098\101 \115\101\116 \097\115 \108\105\115\116\101\110\101\114 \105\110 \080\114\111\102\105\108\101\058\076\105\115\116\101\110\084\111\072\111\112\082\101\097\100\121\040\041")end if k._view_mode==true then return{Disconnect=function() end}end if k._hop_ready==true then task.spawn(x)return{Disconnect=function() end}else return k._hop_ready_listeners:Connect(x)end end function x.AddUserId(k,x)if type(x)~="\110\117\109\098\101\114"or x%1~=0 then warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \085\115\101\114\073\100 \097\114\103\117\109\101\110\116 \102\111\114 \058\065\100\100\085\115\101\114\073\100\040\041 \040"..(tostring(x)..("\041\059 \084\114\097\099\101\098\097\099\107\058\n"..debug.traceback())))return end if x<0 and(k._is_user_mock~=true and UseMockDataStore~=true)then return end if table.find(k.UserIds,x)==nil then table.insert(k.UserIds,x)end end function x.RemoveUserId(k,x)if type(x)~="\110\117\109\098\101\114"or x%1~=0 then warn("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \073\110\118\097\108\105\100 \085\115\101\114\073\100 \097\114\103\117\109\101\110\116 \102\111\114 \058\082\101\109\111\118\101\085\115\101\114\073\100\040\041 \040"..(tostring(x)..("\041\059 \084\114\097\099\101\098\097\099\107\058\n"..debug.traceback())))return end local i=table.find(k.UserIds,x)if i~=nil then table.remove(k.UserIds,i)end end function x.Identify(x)return IdentifyProfile(x._profile_store._profile_store_name,x._profile_store._profile_store_scope,x._profile_key)end function x.ClearGlobalUpdates(x)if x._view_mode~=true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \058\067\108\101\097\114\071\108\111\098\097\108\085\112\100\097\116\101\115\040\041 \099\097\110 \111\110\108\121 \098\101 \117\115\101\100 \105\110 \118\105\101\119 \109\111\100\101")end local k={_updates_latest={0;{}},_profile=x}setmetatable(k,GlobalUpdates)x.GlobalUpdates=k end function x.OverwriteAsync(x)if x._view_mode~=true then error("\091\080\114\111\102\105\108\101\083\101\114\118\105\099\101\093\058 \058\079\118\101\114\119\114\105\116\101\065\115\121\110\099\040\041 \099\097\110 \111\110\108\121 \098\101 \117\115\101\100 \105\110 \118\105\101\119 \109\111\100\101")end SaveProfileAsync(x,nil,true)end
-- Profile object:

local Profile = {
	--[[
		Data = {}, -- [table] -- Loaded once after ProfileStore:LoadProfileAsync() finishes
		MetaData = {}, -- [table] -- Updated with every auto-save
		GlobalUpdates = GlobalUpdates, -- [GlobalUpdates]
		
		_profile_store = ProfileStore, -- [ProfileStore]
		_profile_key = "", -- [string]
		
		_release_listeners = [ScriptSignal] / nil, -- [table / nil]
		_hop_ready_listeners = [ScriptSignal] / nil, -- [table / nil]
		_hop_ready = false,
		
		_view_mode = true / nil, -- [bool] or nil
		
		_load_timestamp = os.clock(),
		
		_is_user_mock = false, -- ProfileStore.Mock
		_mock_key_info = {},
	--]]
}
Profile.__index = Profile

function Profile:IsActive() --> [bool]
	local loaded_profiles = self._is_user_mock == true and self._profile_store._mock_loaded_profiles
		or self._profile_store._loaded_profiles
	return loaded_profiles[self._profile_key] == self
end

function Profile:GetMetaTag(tag_name) --> value
	local meta_data = self.MetaData
	if meta_data == nil then
		return nil
		-- error("[ProfileService]: This Profile hasn't been loaded before - MetaData not available")
	end
	return self.MetaData.MetaTags[tag_name]
end

function Profile:SetMetaTag(tag_name, value)
	if type(tag_name) ~= "string" then
		error("[ProfileService]: tag_name must be a string")
	elseif string.len(tag_name) == 0 then
		error("[ProfileService]: Invalid tag_name")
	end
	self.MetaData.MetaTags[tag_name] = value
end

function Profile:Reconcile()
	ReconcileTable(self.Data, self._profile_store._profile_template)
end

function Profile:ListenToRelease(listener) --> [ScriptConnection] (place_id / nil, game_job_id / nil)
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in Profile:ListenToRelease()")
	end
	if self._view_mode == true then
		return { Disconnect = function() end }
	end
	if self:IsActive() == false then
		-- Call release listener immediately if profile is expired
		local place_id
		local game_job_id
		local active_session = self.MetaData.ActiveSession
		if active_session ~= nil then
			place_id = active_session[1]
			game_job_id = active_session[2]
		end
		listener(place_id, game_job_id)
		return { Disconnect = function() end }
	else
		return self._release_listeners:Connect(listener)
	end
end

function Profile:Save()
	if self._view_mode == true then
		error("[ProfileService]: Can't save Profile in view mode - Should you be calling :OverwriteAsync() instead?")
	end
	if self:IsActive() == false then
		warn(
			"[ProfileService]: Attempted saving an inactive profile "
				.. self:Identify()
				.. "; Traceback:\n"
				.. debug.traceback()
		)
		return
	end
	-- Reject save request if a save is already pending in the queue - this will prevent the user from
	--	unecessary API request spam which we could not meaningfully execute anyways!
	if IsCustomWriteQueueEmptyFor(self._profile_store._profile_store_lookup, self._profile_key) == true then
		-- We don't want auto save to trigger too soon after manual saving - this will reset the auto save timer:
		RemoveProfileFromAutoSave(self)
		AddProfileToAutoSave(self)
		-- Call save function in a new thread:
		task.spawn(SaveProfileAsync, self)
	end
end

function Profile:Release()
	if self._view_mode == true then
		return
	end
	if self:IsActive() == true then
		task.spawn(SaveProfileAsync, self, true) -- Call save function in a new thread with release_from_session = true
	end
end

function Profile:ListenToHopReady(listener) --> [ScriptConnection] ()
	if type(listener) ~= "function" then
		error("[ProfileService]: Only a function can be set as listener in Profile:ListenToHopReady()")
	end
	if self._view_mode == true then
		return { Disconnect = function() end }
	end
	if self._hop_ready == true then
		task.spawn(listener)
		return { Disconnect = function() end }
	else
		return self._hop_ready_listeners:Connect(listener)
	end
end

function Profile:AddUserId(user_id) -- Associates user_id with profile (GDPR compliance)
	if type(user_id) ~= "number" or user_id % 1 ~= 0 then
		warn(
			"[ProfileService]: Invalid UserId argument for :AddUserId() ("
				.. tostring(user_id)
				.. "); Traceback:\n"
				.. debug.traceback()
		)
		return
	end

	if user_id < 0 and self._is_user_mock ~= true and UseMockDataStore ~= true then
		return -- Avoid giving real Roblox APIs negative UserId's
	end

	if table.find(self.UserIds, user_id) == nil then
		table.insert(self.UserIds, user_id)
	end
end

function Profile:RemoveUserId(user_id) -- Unassociates user_id with profile (safe function)
	if type(user_id) ~= "number" or user_id % 1 ~= 0 then
		warn(
			"[ProfileService]: Invalid UserId argument for :RemoveUserId() ("
				.. tostring(user_id)
				.. "); Traceback:\n"
				.. debug.traceback()
		)
		return
	end

	local index = table.find(self.UserIds, user_id)

	if index ~= nil then
		table.remove(self.UserIds, index)
	end
end

function Profile:Identify() --> [string]
	return IdentifyProfile(
		self._profile_store._profile_store_name,
		self._profile_store._profile_store_scope,
		self._profile_key
	)
end

function Profile:ClearGlobalUpdates() -- Clears all global updates data from a profile payload
	if self._view_mode ~= true then
		error("[ProfileService]: :ClearGlobalUpdates() can only be used in view mode")
	end

	local global_updates_object = {
		_updates_latest = { 0, {} },
		_profile = self,
	}
	setmetatable(global_updates_object, GlobalUpdates)

	self.GlobalUpdates = global_updates_object
end

function Profile:OverwriteAsync() -- Saves the profile to the DataStore and removes the session lock
	if self._view_mode ~= true then
		error("[ProfileService]: :OverwriteAsync() can only be used in view mode")
	end

	SaveProfileAsync(self, nil, true)
end
