local j={ProcessEventsInterval=8,GameKey="",SecretKey="",_build="";_availableResourceCurrencies={},_availableResourceItemTypes={}}local c=require(script.Parent.Store)local L=require(script.Parent.Logger)local b=require(script.Parent.Version)local f=require(script.Parent.Validation)local E=require(script.Parent.Threading)local q=require(script.Parent.HttpApi)local Z=require(script.Parent.Utilities)local r=require(script.Parent.GAResourceFlowType)local o=require(script.Parent.GAProgressionStatus)local G=require(script.Parent.GAErrorSeverity)local y=game:GetService("\072\116\116\112\083\101\114\118\105\099\101")local u="\117\115\101\114"local t="\115\101\115\115\105\111\110\095\101\110\100"local U="\098\117\115\105\110\101\115\115"local H="\114\101\115\111\117\114\099\101"local M="\112\114\111\103\114\101\115\115\105\111\110"local p="\100\101\115\105\103\110"local z="\101\114\114\111\114"local C=500 local a=2000 local function A(j,L)if not L or not j then return end local b=c:GetPlayerDataFromCache(j)if b and(b.CurrentCustomDimension01 and#b.CurrentCustomDimension01>0)then L.custom_01=b.CurrentCustomDimension01 end if b and(b.CurrentCustomDimension02 and#b.CurrentCustomDimension02>0)then L.custom_02=b.CurrentCustomDimension02 end if b and(b.CurrentCustomDimension03 and#b.CurrentCustomDimension03>0)then L.custom_03=b.CurrentCustomDimension03 end end local function V(j)if not j then return os.time()end local L=c:GetPlayerDataFromCache(j)local b=os.time()local E=b+L.ClientServerTimeOffset if f:validateClientTs(E)then return E else return b end end local W=(y:GenerateGUID(false)):lower()local function K(L)local E local q if L then q=L E=c:GetPlayerDataFromCache(L)else q="\068\117\109\109\121\073\100"E={OS="\117\119\112\095\100\101\115\107\116\111\112 \048\046\048\046\048";Platform="\117\119\112\095\100\101\115\107\116\111\112";SessionID=W;Sessions=1}end local r={v=2;user_id=tostring(q),client_ts=V(L),sdk_version="\114\111\098\108\111\120 "..b.SdkVersion;os_version=E.OS;manufacturer="\117\110\107\110\111\119\110";device="\117\110\107\110\111\119\110";platform=E.Platform;session_id=E.SessionID,session_num=E.Sessions}if not Z:isStringNullOrEmpty(E.CountryCode)then r.country_code=E.CountryCode end if f:validateBuild(j._build)then r.build=j._build end return r end local function P(j,b)local f=K(j)for j in pairs(b)do f[j]=b[j]end local E=y:JSONEncode(f)L:ii("\069\118\101\110\116 \097\100\100\101\100 \116\111 \113\117\101\117\101\058 "..E)c.EventsQueue[#c.EventsQueue+1]=f end local function B()if#c.EventsQueue<=C then local j=c.EventsQueue c.EventsQueue={}return j else L:w(("\077\111\114\101 \116\104\097\110 \037\100 \101\118\101\110\116\115 \113\117\101\117\101\100\033 \083\101\110\100\105\110\103 \037\100\046"):format(C,C))if#c.EventsQueue>a then L:w(("\068\082\079\080\080\073\078\071 \069\086\069\078\084\083\058 \077\111\114\101 \116\104\097\110 \037\100 \101\118\101\110\116\115 \113\117\101\117\101\100\033"):format(a))end local j=table.create(C)for L=1,C,1 do j[L]=c.EventsQueue[L]end local b=#c.EventsQueue for j=1,math.min(a,b),1 do c.EventsQueue[j]=c.EventsQueue[j+C]end for j=a+1,b,1 do c.EventsQueue[j]=nil end return j end end local function J()local b=B()if#b==0 then L:i("\069\118\101\110\116 \113\117\101\117\101\058 \078\111 \101\118\101\110\116\115 \116\111 \115\101\110\100")return end L:i("\069\118\101\110\116 \113\117\101\117\101\058 \083\101\110\100\105\110\103 "..(tostring(#b).." \101\118\101\110\116\115\046"))local f=q:sendEventsInArray(j.GameKey,j.SecretKey,b)local E=f.statusCode local Z=f.body if E==q.EGAHTTPApiResponse.Ok and Z then L:i("\069\118\101\110\116 \113\117\101\117\101\058 "..(tostring(#b).." \101\118\101\110\116\115 \115\101\110\116\046"))else if E==q.EGAHTTPApiResponse.NoResponse then L:w("\069\118\101\110\116 \113\117\101\117\101\058 \070\097\105\108\101\100 \116\111 \115\101\110\100 \101\118\101\110\116\115 \116\111 \099\111\108\108\101\099\116\111\114 - \082\101\116\114\121\105\110\103 \110\101\120\116 \116\105\109\101")for j,L in pairs(b)do if#c.EventsQueue<a then c.EventsQueue[#c.EventsQueue+1]=L else break end end else if E==q.EGAHTTPApiResponse.BadRequest and Z then L:w("\069\118\101\110\116 \113\117\101\117\101\058 "..(tostring(#b)..(" \101\118\101\110\116\115 \115\101\110\116\046 "..(tostring(#Z).." \101\118\101\110\116\115 \102\097\105\108\101\100 \071\065 \115\101\114\118\101\114 \118\097\108\105\100\097\116\105\111\110\046"))))else L:w("\069\118\101\110\116 \113\117\101\117\101\058 \070\097\105\108\101\100 \116\111 \115\101\110\100 \101\118\101\110\116\115\046")end end end end function j.processEventQueue(c)J()E:scheduleTimer(j.ProcessEventsInterval,function()j:processEventQueue()end)end function j.setBuild(c,j)if not f:validateBuild(j)then L:w("\086\097\108\105\100\097\116\105\111\110 \102\097\105\108 - \099\111\110\102\105\103\117\114\101 \098\117\105\108\100\058 \067\097\110\110\111\116 \098\101 \110\117\108\108\044 \101\109\112\116\121 \111\114 \097\098\111\118\101 \051\050 \108\101\110\103\116\104\046 \083\116\114\105\110\103\058 "..j)return end c._build=j L:i("\083\101\116 \098\117\105\108\100 \118\101\114\115\105\111\110\058 "..j)end function j.setAvailableResourceCurrencies(c,j)if not f:validateResourceCurrencies(j)then return end c._availableResourceCurrencies=j L:i("\083\101\116 \097\118\097\105\108\097\098\108\101 \114\101\115\111\117\114\099\101 \099\117\114\114\101\110\099\105\101\115\058 \040"..(table.concat(j,"\044 ").."\041"))end function j.setAvailableResourceItemTypes(c,j)if not f:validateResourceCurrencies(j)then return end c._availableResourceItemTypes=j L:i("\083\101\116 \097\118\097\105\108\097\098\108\101 \114\101\115\111\117\114\099\101 \105\116\101\109 \116\121\112\101\115\058 \040"..(table.concat(j,"\044 ").."\041"))end function j.addSessionStartEvent(f,j,b)local E=c:GetPlayerDataFromCache(j)if b then E.Sessions=b.Sessions else local c={}c.category=u E.Sessions=E.Sessions+1 A(j,c)P(j,c)L:i("\065\100\100 \083\069\083\083\073\079\078 \083\084\065\082\084 \101\118\101\110\116")J()end end function j.addSessionEndEvent(b,j)local f=c:GetPlayerDataFromCache(j)local E=f.SessionStart local q=V(j)local Z=q-E if Z<0 then L:w("\083\101\115\115\105\111\110 \108\101\110\103\116\104 \119\097\115 \099\097\108\099\117\108\097\116\101\100 \116\111 \098\101 \108\101\115\115 \116\104\101\110 \048\046 \083\104\111\117\108\100 \110\111\116 \098\101 \112\111\115\115\105\098\108\101\046 \082\101\115\101\116\116\105\110\103 \116\111 \048\046")Z=0 end local r={}r.category=t r.length=Z A(j,r)P(j,r)f.SessionStart=0 L:i("\065\100\100 \083\069\083\083\073\079\078 \069\078\068 \101\118\101\110\116\046")J()end function j.addBusinessEvent(G,j,b,E,q,r,o)if not f:validateBusinessEvent(b,E,o,q,r)then return end local y={}local u=c:GetPlayerDataFromCache(j)u.Transactions=u.Transactions+1 y.event_id=q..("\058"..r)y.category=U y.currency=b y.amount=E y.transaction_num=u.Transactions if not Z:isStringNullOrEmpty(o)then y.cart_type=o end A(j,y)L:i("\065\100\100 \066\085\083\073\078\069\083\083 \101\118\101\110\116\058 \123\099\117\114\114\101\110\099\121\058"..(b..("\044 \097\109\111\117\110\116\058"..(tostring(E)..("\044 \105\116\101\109\084\121\112\101\058"..(q..("\044 \105\116\101\109\073\100\058"..(r..("\044 \099\097\114\116\084\121\112\101\058"..(o.."\125"))))))))))P(j,y)end function j.addResourceEvent(o,j,c,b,E,q,Z)if not f:validateResourceEvent(r,c,b,E,q,Z,o._availableResourceCurrencies,o._availableResourceItemTypes)then return end if c==r.Sink then E=-1*E end local G={}local y=r[c]G.event_id=y..("\058"..(b..("\058"..(q..("\058"..Z)))))G.category=H G.amount=E A(j,G)L:i("\065\100\100 \082\069\083\079\085\082\067\069 \101\118\101\110\116\058 \123\099\117\114\114\101\110\099\121\058"..(b..("\044 \097\109\111\117\110\116\058"..(tostring(E)..("\044 \105\116\101\109\084\121\112\101\058"..(q..("\044 \105\116\101\109\073\100\058"..(Z.."\125"))))))))P(j,G)end function j.addProgressionEvent(y,j,b,E,q,r,G)if not f:validateProgressionEvent(o,b,E,q,r)then return end local u={}local t if Z:isStringNullOrEmpty(q)then t=E elseif Z:isStringNullOrEmpty(r)then t=E..("\058"..q)else t=E..("\058"..(q..("\058"..r)))end local U=o[b]u.category=M u.event_id=U..("\058"..t)local H=0 if G~=nil and b~=o.Start then u.score=G end local p=c:GetPlayerDataFromCache(j)if b==o.Fail then local j=p.ProgressionTries[t]or 0 p.ProgressionTries[t]=j+1 end if b==o.Complete then local j=p.ProgressionTries[t]or 0 p.ProgressionTries[t]=j+1 H=p.ProgressionTries[t]u.attempt_num=H p.ProgressionTries[t]=0 end A(j,u)local z=""if not Z:isStringNullOrEmpty(q)then z=q end local C=""if not Z:isStringNullOrEmpty(r)then C=r end L:i("\065\100\100 \080\082\079\071\082\069\083\083\073\079\078 \101\118\101\110\116\058 \123\115\116\097\116\117\115\058"..(U..("\044 \112\114\111\103\114\101\115\115\105\111\110\048\049\058"..(E..("\044 \112\114\111\103\114\101\115\115\105\111\110\048\050\058"..(z..("\044 \112\114\111\103\114\101\115\115\105\111\110\048\051\058"..(C..("\044 \115\099\111\114\101\058"..(tostring(G)..("\044 \097\116\116\101\109\112\116\058"..(tostring(H).."\125"))))))))))))P(j,u)end function j.addDesignEvent(E,j,c,b)if not f:validateDesignEvent(c)then return end local q={}q.category=p q.event_id=c if b~=nil then q.value=b end A(j,q)L:i("\065\100\100 \068\069\083\073\071\078 \101\118\101\110\116\058 \123\101\118\101\110\116\073\100\058"..(c..("\044 \118\097\108\117\101\058"..(tostring(b).."\125"))))P(j,q)end function j.addErrorEvent(E,j,c,b)if not f:validateErrorEvent(G,c,b)then return end local q={}local r=G[c]q.category=z q.severity=r q.message=b A(j,q)local o=""if not Z:isStringNullOrEmpty(b)then o=b end L:i("\065\100\100 \069\082\082\079\082 \101\118\101\110\116\058 \123\115\101\118\101\114\105\116\121\058"..(r..("\044 \109\101\115\115\097\103\101\058"..(o.."\125"))))P(j,q)end return j
local events = {
	ProcessEventsInterval = 8,
	GameKey = "",
	SecretKey = "",
	_build = "",
	_availableResourceCurrencies = {},
	_availableResourceItemTypes = {},
}

local store = require(script.Parent.Store)
local logger = require(script.Parent.Logger)
local version = require(script.Parent.Version)
local validation = require(script.Parent.Validation)
local threading = require(script.Parent.Threading)
local http_api = require(script.Parent.HttpApi)
local utilities = require(script.Parent.Utilities)
local GAResourceFlowType = require(script.Parent.GAResourceFlowType)
local GAProgressionStatus = require(script.Parent.GAProgressionStatus)
local GAErrorSeverity = require(script.Parent.GAErrorSeverity)
local HTTP = game:GetService("HttpService")

local CategorySessionStart = "user"
local CategorySessionEnd = "session_end"
local CategoryBusiness = "business"
local CategoryResource = "resource"
local CategoryProgression = "progression"
local CategoryDesign = "design"
local CategoryError = "error"
local MAX_EVENTS_TO_SEND_IN_ONE_BATCH = 500
local MAX_AGGREGATED_EVENTS = 2000

local function addDimensionsToEvent(playerId, eventData)
	if not eventData or not playerId then
		return
	end

	local PlayerData = store:GetPlayerDataFromCache(playerId)

	-- add to dict (if not nil)
	if PlayerData and PlayerData.CurrentCustomDimension01 and #PlayerData.CurrentCustomDimension01 > 0 then
		eventData["custom_01"] = PlayerData.CurrentCustomDimension01
	end

	if PlayerData and PlayerData.CurrentCustomDimension02 and #PlayerData.CurrentCustomDimension02 > 0 then
		eventData["custom_02"] = PlayerData.CurrentCustomDimension02
	end

	if PlayerData and PlayerData.CurrentCustomDimension03 and #PlayerData.CurrentCustomDimension03 > 0 then
		eventData["custom_03"] = PlayerData.CurrentCustomDimension03
	end
end

local function getClientTsAdjusted(playerId)
	if not playerId then
		return os.time()
	end

	local PlayerData = store:GetPlayerDataFromCache(playerId)
	local clientTs = os.time()
	local clientTsAdjustedInteger = clientTs + PlayerData.ClientServerTimeOffset
	if validation:validateClientTs(clientTsAdjustedInteger) then
		return clientTsAdjustedInteger
	else
		return clientTs
	end
end

local DUMMY_SESSION_ID = HTTP:GenerateGUID(false):lower()

local function getEventAnnotations(playerId)
	local PlayerData
	local id

	if playerId then
		id = playerId
		PlayerData = store:GetPlayerDataFromCache(playerId)
	else
		id = "DummyId"
		PlayerData = {
			OS = "uwp_desktop 0.0.0",
			Platform = "uwp_desktop",
			SessionID = DUMMY_SESSION_ID,
			Sessions = 1,
		}
	end



	local annotations = {
		-- ---- REQUIRED ----
		-- collector event API version
		["v"] = 2,
		-- User identifier
		["user_id"] = tostring(id),
		-- Client Timestamp (the adjusted timestamp)
		["client_ts"] = getClientTsAdjusted(playerId),
		-- SDK version
		["sdk_version"] = "roblox " .. version.SdkVersion,
		-- Operation system version
		["os_version"] = PlayerData.OS,
		-- Device make (hardcoded to apple)
		["manufacturer"] = "unknown",
		-- Device version
		["device"] = "unknown",
		-- Platform (operating system)
		["platform"] = PlayerData.Platform,
		-- Session identifier
		["session_id"] = PlayerData.SessionID,
		-- Session number
		["session_num"] = PlayerData.Sessions
	}

	if not utilities:isStringNullOrEmpty(PlayerData.CountryCode) then
		annotations["country_code"] = PlayerData.CountryCode
	end

	if validation:validateBuild(events._build) then
		annotations["build"] = events._build
	end

	return annotations
end

local function addEventToStore(playerId, eventData)
	-- Get default annotations
	local ev = getEventAnnotations(playerId)

	-- Merge with eventData
	for k in pairs(eventData) do
		ev[k] = eventData[k]
	end

	-- Create json string representation
	local json = HTTP:JSONEncode(ev)

	-- output if VERBOSE LOG enabled
	logger:ii("Event added to queue: " .. json)

	-- Add to store
	store.EventsQueue[#store.EventsQueue + 1] = ev
end

local function dequeueMaxEvents()
	if #store.EventsQueue <= MAX_EVENTS_TO_SEND_IN_ONE_BATCH then
		local eventsQueue = store.EventsQueue
		store.EventsQueue = {}
		return eventsQueue
	else
		logger:w(("More than %d events queued! Sending %d."):format(MAX_EVENTS_TO_SEND_IN_ONE_BATCH, MAX_EVENTS_TO_SEND_IN_ONE_BATCH))

		if #store.EventsQueue > MAX_AGGREGATED_EVENTS then
			logger:w(("DROPPING EVENTS: More than %d events queued!"):format(MAX_AGGREGATED_EVENTS))
		end

		-- Expensive operation to get ordered events cleared out (O(n))
		local eventsQueue = table.create(MAX_EVENTS_TO_SEND_IN_ONE_BATCH)
		for i = 1, MAX_EVENTS_TO_SEND_IN_ONE_BATCH do
			eventsQueue[i] = store.EventsQueue[i]
		end

		-- Shift everything down and overwrite old events
		local eventCount = #store.EventsQueue
		for i = 1, math.min(MAX_AGGREGATED_EVENTS, eventCount) do
			store.EventsQueue[i] = store.EventsQueue[i + MAX_EVENTS_TO_SEND_IN_ONE_BATCH]
		end

		-- Clear additional events
		for i = MAX_AGGREGATED_EVENTS + 1, eventCount do
			store.EventsQueue[i] = nil
		end

		return eventsQueue
	end
end

local function processEvents()
	local queue = dequeueMaxEvents()

	if #queue == 0 then
		logger:i("Event queue: No events to send")
		return
	end

	-- Log
	logger:i("Event queue: Sending " .. tostring(#queue) .. " events.")

	local eventsResult = http_api:sendEventsInArray(events.GameKey, events.SecretKey, queue)
	local statusCode = eventsResult.statusCode
	local responseBody = eventsResult.body

	if statusCode == http_api.EGAHTTPApiResponse.Ok and responseBody then
		logger:i("Event queue: " .. tostring(#queue) .. " events sent.")
	else
		if statusCode == http_api.EGAHTTPApiResponse.NoResponse then
			logger:w("Event queue: Failed to send events to collector - Retrying next time")
			for _, e in pairs(queue) do
				if #store.EventsQueue < MAX_AGGREGATED_EVENTS then
					store.EventsQueue[#store.EventsQueue + 1] = e
				else
					break
				end
			end
		else
			if statusCode == http_api.EGAHTTPApiResponse.BadRequest and responseBody then
				logger:w("Event queue: " .. tostring(#queue) .. " events sent. " .. tostring(#responseBody) .. " events failed GA server validation.")
			else
				logger:w("Event queue: Failed to send events.")
			end
		end
	end
end

function events:processEventQueue()
	processEvents()
	threading:scheduleTimer(events.ProcessEventsInterval, function()
		events:processEventQueue()
	end)
end

function events:setBuild(build)
	if not validation:validateBuild(build) then
		logger:w("Validation fail - configure build: Cannot be null, empty or above 32 length. String: " .. build)
		return
	end

	self._build = build
	logger:i("Set build version: " .. build)
end

function events:setAvailableResourceCurrencies(availableResourceCurrencies)
	if not validation:validateResourceCurrencies(availableResourceCurrencies) then
		return
	end

	self._availableResourceCurrencies = availableResourceCurrencies
	logger:i("Set available resource currencies: (" .. table.concat(availableResourceCurrencies, ", ") .. ")")
end

function events:setAvailableResourceItemTypes(availableResourceItemTypes)
	if not validation:validateResourceCurrencies(availableResourceItemTypes) then
		return
	end

	self._availableResourceItemTypes = availableResourceItemTypes
	logger:i("Set available resource item types: (" .. table.concat(availableResourceItemTypes, ", ") .. ")")
end

function events:addSessionStartEvent(playerId, teleportData)
	local PlayerData = store:GetPlayerDataFromCache(playerId)

	if teleportData then
		PlayerData.Sessions = teleportData.Sessions
	else
		local eventDict = {}

		-- Event specific data
		eventDict["category"] = CategorySessionStart

		-- Increment session number  and persist
		PlayerData.Sessions = PlayerData.Sessions + 1

		--  Add custom dimensions
		addDimensionsToEvent(playerId, eventDict)

		-- Add to store
		addEventToStore(playerId, eventDict)

		logger:i("Add SESSION START event")

		processEvents()
	end
end

function events:addSessionEndEvent(playerId)
	local PlayerData = store:GetPlayerDataFromCache(playerId)
	local session_start_ts = PlayerData.SessionStart
	local client_ts_adjusted = getClientTsAdjusted(playerId)
	local sessionLength = client_ts_adjusted - session_start_ts

	if sessionLength < 0 then
		-- Should never happen.
		-- Could be because of edge cases regarding time altering on device.
		logger:w("Session length was calculated to be less then 0. Should not be possible. Resetting to 0.")
		sessionLength = 0
	end

	-- Event specific data
	local eventDict = {}
	eventDict["category"] = CategorySessionEnd
	eventDict["length"] = sessionLength

	-- Add custom dimensions
	addDimensionsToEvent(playerId, eventDict)

	-- Add to store
	addEventToStore(playerId, eventDict)
	PlayerData.SessionStart = 0

	logger:i("Add SESSION END event.")

	processEvents()
end

function events:addBusinessEvent(playerId, currency, amount, itemType, itemId, cartType)
	-- Validate event params
	if not validation:validateBusinessEvent(currency, amount, cartType, itemType, itemId) then
		-- TODO: add sdk error event
		return
	end

	-- Create empty eventData
	local eventDict = {}

	-- Increment transaction number and persist
	local PlayerData = store:GetPlayerDataFromCache(playerId)
	PlayerData.Transactions = PlayerData.Transactions + 1

	-- Required
	eventDict["event_id"] = itemType .. ":" .. itemId
	eventDict["category"] = CategoryBusiness
	eventDict["currency"] = currency
	eventDict["amount"] = amount
	eventDict["transaction_num"] = PlayerData.Transactions

	-- Optional
	if not utilities:isStringNullOrEmpty(cartType) then
		eventDict["cart_type"] = cartType
	end

	-- Add custom dimensions
	addDimensionsToEvent(playerId, eventDict)

	logger:i("Add BUSINESS event: {currency:" .. currency .. ", amount:" .. tostring(amount) .. ", itemType:" .. itemType .. ", itemId:" .. itemId .. ", cartType:" .. cartType .. "}")

	-- Send to store
	addEventToStore(playerId, eventDict)
end

function events:addResourceEvent(playerId, flowType, currency, amount, itemType, itemId)
	-- Validate event params
	if not validation:validateResourceEvent(GAResourceFlowType, flowType, currency, amount, itemType, itemId, self._availableResourceCurrencies, self._availableResourceItemTypes) then
		-- TODO: add sdk error event
		return
	end

	-- If flow type is sink reverse amount
	if flowType == GAResourceFlowType.Sink then
		amount = (-1 * amount)
	end

	-- Create empty eventData
	local eventDict = {}

	-- insert event specific values
	local flowTypeString = GAResourceFlowType[flowType]
	eventDict["event_id"] = flowTypeString .. ":" .. currency .. ":" .. itemType .. ":" .. itemId
	eventDict["category"] = CategoryResource
	eventDict["amount"] = amount

	-- Add custom dimensions
	addDimensionsToEvent(playerId, eventDict)

	logger:i("Add RESOURCE event: {currency:" .. currency .. ", amount:" .. tostring(amount) .. ", itemType:" .. itemType .. ", itemId:" .. itemId .. "}")

	-- Send to store
	addEventToStore(playerId, eventDict)
end

function events:addProgressionEvent(playerId, progressionStatus, progression01, progression02, progression03, score)
	-- Validate event params
	if not validation:validateProgressionEvent(GAProgressionStatus, progressionStatus, progression01, progression02, progression03) then
		-- TODO: add sdk error event
		return
	end

	-- Create empty eventData
	local eventDict = {}

	-- Progression identifier
	local progressionIdentifier
	if utilities:isStringNullOrEmpty(progression02) then
		progressionIdentifier = progression01
	elseif utilities:isStringNullOrEmpty(progression03) then
		progressionIdentifier = progression01 .. ":" .. progression02
	else
		progressionIdentifier = progression01 .. ":" .. progression02 .. ":" .. progression03
	end

	local statusString = GAProgressionStatus[progressionStatus]

	-- Append event specifics
	eventDict["category"] = CategoryProgression
	eventDict["event_id"] = statusString .. ":" .. progressionIdentifier

	-- Attempt
	local attempt_num = 0

	-- Add score if specified and status is not start
	if score ~= nil and progressionStatus ~= GAProgressionStatus.Start then
		eventDict["score"] = score
	end

	local PlayerData = store:GetPlayerDataFromCache(playerId)

	-- Count attempts on each progression fail and persist
	if progressionStatus == GAProgressionStatus.Fail then
		-- Increment attempt number
		local progressionTries = PlayerData.ProgressionTries[progressionIdentifier] or 0
		PlayerData.ProgressionTries[progressionIdentifier] = progressionTries + 1
	end

	-- increment and add attempt_num on complete and delete persisted
	if progressionStatus == GAProgressionStatus.Complete then
		-- Increment attempt number
		local progressionTries = PlayerData.ProgressionTries[progressionIdentifier] or 0
		PlayerData.ProgressionTries[progressionIdentifier] = progressionTries + 1

		-- Add to event
		attempt_num = PlayerData.ProgressionTries[progressionIdentifier]
		eventDict["attempt_num"] = attempt_num

		-- Clear
		PlayerData.ProgressionTries[progressionIdentifier] = 0
	end

	-- Add custom dimensions
	addDimensionsToEvent(playerId, eventDict)

	local progression02String = ""
	if not utilities:isStringNullOrEmpty(progression02) then
		progression02String = progression02
	end

	local progression03String = ""
	if not utilities:isStringNullOrEmpty(progression03) then
		progression03String = progression03
	end

	logger:i("Add PROGRESSION event: {status:" .. statusString .. ", progression01:" .. progression01 .. ", progression02:" .. progression02String .. ", progression03:" .. progression03String .. ", score:" .. tostring(score) .. ", attempt:" .. tostring(attempt_num) .. "}")

	-- Send to store
	addEventToStore(playerId, eventDict)
end

function events:addDesignEvent(playerId, eventId, value)
	-- Validate
	if not validation:validateDesignEvent(eventId) then
		-- TODO: add sdk error event
		return
	end

	-- Create empty eventData
	local eventData = {}

	-- Append event specifics
	eventData["category"] = CategoryDesign
	eventData["event_id"] = eventId

	if value ~= nil then
		eventData["value"] = value
	end

	-- Add custom dimensions
	addDimensionsToEvent(playerId, eventData)

	logger:i("Add DESIGN event: {eventId:" .. eventId .. ", value:" .. tostring(value) .. "}")

	-- Send to store
	addEventToStore(playerId, eventData)
end

function events:addErrorEvent(playerId, severity, message)
	-- Validate
	if not validation:validateErrorEvent(GAErrorSeverity, severity, message) then
		-- TODO: add sdk error event
		return
	end

	-- Create empty eventData
	local eventData = {}

	local severityString = GAErrorSeverity[severity]

	eventData["category"] = CategoryError
	eventData["severity"] = severityString
	eventData["message"] = message

	-- Add custom dimensions
	addDimensionsToEvent(playerId, eventData)

	local messageString = ""
	if not utilities:isStringNullOrEmpty(message) then
		messageString = message
	end

	logger:i("Add ERROR event: {severity:" .. severityString .. ", message:" .. messageString .. "}")

	-- Send to store
	addEventToStore(playerId, eventData)
end

return events
