local T={}T.__index=T function T.IsActive(T)local E=T._is_user_mock==true and T._profile_store._mock_loaded_profiles or T._profile_store._loaded_profiles return E[T._profile_key]==T end function T.GetMetaTag(E,T)local i=E.MetaData if i==nil then return nil end return E.MetaData.MetaTags[T]end function T.SetMetaTag(i,T,E)if type(T)~="\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(T)==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[T]=E end function T.Reconcile(T)ReconcileTable(T.Data,T._profile_store._profile_template)end function T.ListenToRelease(E,T)if type(T)~="\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 E._view_mode==true then return{Disconnect=function() end}end if E:IsActive()==false then local i local C local W=E.MetaData.ActiveSession if W~=nil then i=W[1]C=W[2]end T(i,C)return{Disconnect=function() end}else return E._release_listeners:Connect(T)end end function T.Save(T)if T._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 T: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 "..(T:Identify()..("\059 \084\114\097\099\101\098\097\099\107\058\n"..debug.traceback())))return end if IsCustomWriteQueueEmptyFor(T._profile_store._profile_store_lookup,T._profile_key)==true then RemoveProfileFromAutoSave(T)AddProfileToAutoSave(T)task.spawn(SaveProfileAsync,T)end end function T.Release(T)if T._view_mode==true then return end if T:IsActive()==true then task.spawn(SaveProfileAsync,T,true)end end function T.ListenToHopReady(E,T)if type(T)~="\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 E._view_mode==true then return{Disconnect=function() end}end if E._hop_ready==true then task.spawn(T)return{Disconnect=function() end}else return E._hop_ready_listeners:Connect(T)end end function T.AddUserId(E,T)if type(T)~="\110\117\109\098\101\114"or T%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(T)..("\041\059 \084\114\097\099\101\098\097\099\107\058\n"..debug.traceback())))return end if T<0 and(E._is_user_mock~=true and UseMockDataStore~=true)then return end if table.find(E.UserIds,T)==nil then table.insert(E.UserIds,T)end end function T.RemoveUserId(E,T)if type(T)~="\110\117\109\098\101\114"or T%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(T)..("\041\059 \084\114\097\099\101\098\097\099\107\058\n"..debug.traceback())))return end local i=table.find(E.UserIds,T)if i~=nil then table.remove(E.UserIds,i)end end function T.Identify(T)return IdentifyProfile(T._profile_store._profile_store_name,T._profile_store._profile_store_scope,T._profile_key)end function T.ClearGlobalUpdates(T)if T._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 E={_updates_latest={0;{}},_profile=T}setmetatable(E,GlobalUpdates)T.GlobalUpdates=E end function T.OverwriteAsync(T)if T._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(T,nil,true)end


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
