--[[
	CarpentersTelemetry SDK v1.0
	Drop-in telemetry module for Roblox games.
	Place this ModuleScript in ServerScriptService.

	Usage:
		local Telemetry = require(script.CarpentersTelemetry)
		Telemetry.init({
			apiKey = "your-api-key-here",
			gameId = "your-game-id",
		})

		-- Custom events
		Telemetry.trackEvent("level_complete", { level = 5, time = 32.4 })
		Telemetry.trackError("NPC pathfind failed", debug.traceback())
--]]

local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")

--------------------------------------------------------------------------------
-- Module
--------------------------------------------------------------------------------

local CarpentersTelemetry = {}

--------------------------------------------------------------------------------
-- Internal state
--------------------------------------------------------------------------------

local config = {
	apiKey = nil :: string?,
	gameId = nil :: string?,
	endpoint = "https://carpenterskit.com/api/telemetry",
}

local initialized: boolean = false

--- Map of `Player.UserId` -> session info
local activeSessions: { [number]: { sessionId: string, startTime: number, platform: string } } = {}

--- Reference to the heartbeat loop so we can cancel it if needed
local heartbeatThread: thread? = nil

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------

--- Generate a short random session ID (32 hex characters).
local function generateSessionId(): string
	local chars = {}
	for _ = 1, 32 do
		table.insert(chars, string.format("%x", math.random(0, 15)))
	end
	return table.concat(chars)
end

--- Detect the platform a player is using.
--- Uses GuiService and UserInputService heuristics from the server side.
local function detectPlatform(player: Player): string
	-- On the server we can't directly query UserInputService for each client,
	-- so we check player:GetPlatform() (returns Enum.Platform).
	local ok, platform = pcall(function()
		return tostring(player:GetPlatform())
	end)

	if ok and platform then
		-- Enum.Platform values: Windows, OSX, IOS, Android, XBoxOne, UWP, etc.
		local p = platform:lower()
		if p:find("windows") or p:find("uwp") then
			return "Windows"
		elseif p:find("osx") then
			return "Mac"
		elseif p:find("ios") or p:find("android") then
			return "Mobile"
		elseif p:find("xbox") or p:find("playstation") then
			return "Console"
		end
	end

	return "Unknown"
end

--- Send a JSON payload to the telemetry endpoint.
--- All HTTP errors are caught silently so they never break the game.
local function sendPayload(payload: { [string]: any })
	if not initialized then
		warn("[CarpentersTelemetry] SDK not initialized. Call .init() first.")
		return
	end

	payload.apiKey = config.apiKey
	payload.gameId = config.gameId
	payload.timestamp = os.time()

	local jsonBody: string
	local encodeOk, encodeErr = pcall(function()
		jsonBody = HttpService:JSONEncode(payload)
	end)
	if not encodeOk then
		warn("[CarpentersTelemetry] JSON encode error:", encodeErr)
		return
	end

	local postOk, postErr = pcall(function()
		HttpService:PostAsync(config.endpoint :: string, jsonBody, Enum.HttpContentType.ApplicationJson)
	end)
	if not postOk then
		warn("[CarpentersTelemetry] HTTP error:", postErr)
	end
end

--------------------------------------------------------------------------------
-- Heartbeat
--------------------------------------------------------------------------------

--- Collect current player count and per-platform breakdown, then send.
local function sendHeartbeat()
	local players = Players:GetPlayers()
	local platforms: { [string]: number } = {}

	for _, player in players do
		local plat = "Unknown"
		local session = activeSessions[player.UserId]
		if session then
			plat = session.platform
		else
			plat = detectPlatform(player)
		end
		platforms[plat] = (platforms[plat] or 0) + 1
	end

	sendPayload({
		type = "heartbeat",
		data = {
			playerCount = #players,
			platforms = platforms,
		},
	})
end

--- Start the repeating heartbeat loop (every 60 seconds).
local function startHeartbeatLoop()
	heartbeatThread = task.spawn(function()
		while initialized do
			sendHeartbeat()
			task.wait(60)
		end
	end)
end

--------------------------------------------------------------------------------
-- Session tracking
--------------------------------------------------------------------------------

local function onPlayerAdded(player: Player)
	local platform = detectPlatform(player)
	local sessionId = generateSessionId()

	activeSessions[player.UserId] = {
		sessionId = sessionId,
		startTime = os.time(),
		platform = platform,
	}

	sendPayload({
		type = "session_start",
		playerId = tostring(player.UserId),
		platform = platform,
		sessionId = sessionId,
	})
end

local function onPlayerRemoving(player: Player)
	local session = activeSessions[player.UserId]
	if not session then
		return
	end

	local duration = os.time() - session.startTime

	sendPayload({
		type = "session_end",
		playerId = tostring(player.UserId),
		sessionId = session.sessionId,
		durationSeconds = duration,
	})

	activeSessions[player.UserId] = nil
end

--------------------------------------------------------------------------------
-- Public API
--------------------------------------------------------------------------------

export type InitConfig = {
	apiKey: string,
	gameId: string,
	endpoint: string?,
}

--- Initialize the telemetry SDK. Must be called once before any other method.
--- Starts the automatic heartbeat and hooks into player join/leave events.
function CarpentersTelemetry.init(cfg: InitConfig)
	assert(cfg.apiKey and #cfg.apiKey > 0, "[CarpentersTelemetry] apiKey is required")
	assert(cfg.gameId and #cfg.gameId > 0, "[CarpentersTelemetry] gameId is required")

	config.apiKey = cfg.apiKey
	config.gameId = cfg.gameId

	if cfg.endpoint and #cfg.endpoint > 0 then
		config.endpoint = cfg.endpoint
	end

	initialized = true

	-- Register session tracking
	Players.PlayerAdded:Connect(onPlayerAdded)
	Players.PlayerRemoving:Connect(onPlayerRemoving)

	-- Track players who joined before this script ran (studio / late init)
	for _, player in Players:GetPlayers() do
		task.spawn(onPlayerAdded, player)
	end

	-- Start heartbeat
	startHeartbeatLoop()

	print("[CarpentersTelemetry] Initialized for game", config.gameId)
end

--- Send a custom event with an optional data payload.
function CarpentersTelemetry.trackEvent(eventName: string, data: { [string]: any }?)
	assert(type(eventName) == "string" and #eventName > 0, "[CarpentersTelemetry] eventName is required")

	sendPayload({
		type = "custom",
		data = {
			eventName = eventName,
			payload = data or {},
		},
	})
end

--- Send an error event with an optional stack trace.
function CarpentersTelemetry.trackError(message: string, stack: string?)
	assert(type(message) == "string" and #message > 0, "[CarpentersTelemetry] error message is required")

	sendPayload({
		type = "error",
		data = {
			message = message,
			stack = stack,
		},
	})
end

--- Returns true if the SDK has been initialized.
function CarpentersTelemetry.isInitialized(): boolean
	return initialized
end

return CarpentersTelemetry
