local _, namespace = ...
local GuildPing = namespace.GuildPing
local UI = GuildPing.UIFactory

if not UI then
	return
end

local CreateWindow = UI.CreateWindow
local MakeDraggable = UI.MakeDraggable
local SetTitle = UI.SetTitle
local CreateButton = UI.CreateButton
local CreateLabel = UI.CreateLabel
local CreateInput = UI.CreateInput
local ROW_BACKDROP = UI.RowBackdrop
local DISCORD_BLUE = UI.DiscordBlue
local GUILD_GREEN = UI.GuildGreen
local MUTED_COLOR = UI.MutedColor
local WARNING_GOLD = UI.WarningGold
local UPDATE_RED = UI.UpdateRed
local Unpack = unpack or table.unpack

local RESIGN_POPUP_KEY = "GUILD_PING_DEATH_ROLL_RESIGN"
local END_PRACTICE_POPUP_KEY = "GUILD_PING_DEATH_ROLL_END_PRACTICE"
local ABANDON_POPUP_KEY = "GUILD_PING_DEATH_ROLL_ABANDON"
local ROLL_GOLD = { 0.94, 0.70, 0.25, 1 }
local CHAMBER_SURFACE = { 0.105, 0.075, 0.050, 0.97 }
local HISTORY_SURFACE = { 0.047, 0.055, 0.078, 0.92 }
local PRESETS = { 100, 1000, 10000 }

local function FormatNumber(value)
	local formatted = tostring(math.floor(tonumber(value) or 0))
	while true do
		local changed
		formatted, changed = formatted:gsub("^(-?%d+)(%d%d%d)", "%1,%2")
		if changed == 0 then
			return formatted
		end
	end
end

local function FormatInviteAge(receivedAt)
	local currentTime = GetTime and GetTime() or 0
	local seconds = math.max(0, math.floor(currentTime - (receivedAt or currentTime)))
	if seconds < 60 then
		return "just now"
	end
	return string.format("%dm ago", math.floor(seconds / 60))
end

local function SeatLabel(game, seat, localSeat)
	if game.practice then
		return seat == GuildPing.DEATH_ROLL_CHALLENGER and "Player 1" or "Player 2"
	end
	if seat == localSeat then
		return "You"
	end
	return GuildPing:GetDeathRollSeatPlayer(game, seat) or "Opponent"
end

local function CreateInviteRow(parent, width)
	local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
	row:SetSize(width, 54)
	row:SetBackdrop(ROW_BACKDROP)
	row:SetBackdropColor(Unpack(HISTORY_SURFACE))
	row:SetBackdropBorderColor(0.180, 0.204, 0.275, 0.95)

	row.senderText = CreateLabel(row, "", "GameFontNormal")
	row.senderText:SetPoint("LEFT", 10, 7)
	row.senderText:SetPoint("RIGHT", -190, 7)
	row.senderText:SetWordWrap(false)
	row.detailText = CreateLabel(row, "", "GameFontHighlightSmall")
	row.detailText:SetPoint("LEFT", 10, -10)
	row.detailText:SetPoint("RIGHT", -190, -10)
	row.detailText:SetWordWrap(false)
	row.detailText:SetTextColor(MUTED_COLOR:GetRGB())

	row.declineButton = CreateButton(row, "Decline", 78, 24)
	row.declineButton:SetPoint("RIGHT", -8, 0)
	row.declineButton:SetScript("OnClick", function()
		if row.inviteID then
			GuildPing:DeclineDeathRollChallenge(row.inviteID)
		end
	end)
	row.acceptButton = CreateButton(row, "Accept", 82, 24, "primary")
	row.acceptButton:SetPoint("RIGHT", row.declineButton, "LEFT", -6, 0)
	row.acceptButton:SetScript("OnClick", function()
		if not row.inviteID then
			return
		end
		local game, errorMessage = GuildPing:AcceptDeathRollChallenge(row.inviteID)
		if game then
			GuildPing:ShowDeathRollGame()
		else
			GuildPing:Print(errorMessage)
		end
	end)
	row:Hide()
	return row
end

local function PopulateInviteRow(row, invite, canAccept)
	if not invite then
		row.inviteID = nil
		row:Hide()
		return
	end
	row.inviteID = invite.id
	row.senderText:SetText(invite.sender .. " challenged you")
	row.detailText:SetText(string.format(
		"Starts at 1–%s  •  %s",
		FormatNumber(invite.startingMax),
		FormatInviteAge(invite.receivedAt)
	))
	row.acceptButton:SetEnabled(canAccept)
	row:Show()
end

local function RefreshPresetButtons(lobby)
	for value, button in pairs(lobby.presetButtons) do
		button:SetVariant(value == lobby.startingMax and "primary" or "secondary")
	end
end

function GuildPing:CreateDeathRollLobby()
	if self.deathRollLobby then
		return self.deathRollLobby
	end

	local lobby = CreateWindow("GuildPingDeathRollLobbyFrame")
	lobby:SetSize(640, 650)
	lobby:SetPoint("CENTER")
	lobby:SetFrameStrata("DIALOG")
	MakeDraggable(lobby, lobby.header)
	SetTitle(lobby, "Guild Ping — Death Roll")

	lobby.heading = CreateLabel(lobby, "Death Roll", "GameFontNormalLarge")
	lobby.heading:SetPoint("TOPLEFT", 24, -49)
	lobby.description = CreateLabel(
		lobby,
		"Take turns shrinking the range. The first player to roll 1 loses. Challenge a guild member anywhere in the world, or enter your own name for practice.",
		"GameFontHighlight"
	)
	lobby.description:SetPoint("TOPLEFT", 24, -79)
	lobby.description:SetPoint("TOPRIGHT", -24, -79)
	lobby.description:SetWordWrap(true)

	lobby.challengeLabel = CreateLabel(lobby, "Guild member", "GameFontNormal")
	lobby.challengeLabel:SetPoint("TOPLEFT", 24, -137)
	lobby.challengeInput = CreateInput(lobby, 390, "Character or Character-Realm", 48)
	lobby.challengeInput:SetPoint("TOPLEFT", 26, -159)
	lobby.challengeButton = CreateButton(lobby, "Send Challenge", 164, 27, "primary")
	lobby.challengeButton:SetPoint("LEFT", lobby.challengeInput, "RIGHT", 12, 0)

	lobby.startingLabel = CreateLabel(lobby, "Starting range", "GameFontNormal")
	lobby.startingLabel:SetPoint("TOPLEFT", 24, -205)
	lobby.startingMax = 1000
	lobby.presetButtons = {}
	for index, value in ipairs(PRESETS) do
		local button = CreateButton(lobby, "1–" .. FormatNumber(value), 116, 25)
		button:SetPoint("TOPLEFT", 26 + ((index - 1) * 124), -227)
		button:SetScript("OnClick", function()
			lobby.startingMax = value
			RefreshPresetButtons(lobby)
		end)
		lobby.presetButtons[value] = button
	end
	RefreshPresetButtons(lobby)

	lobby.challengeButton:SetScript("OnClick", function()
		local challenge, errorMessage = GuildPing:ChallengeDeathRollPlayer(
			lobby.challengeInput:GetText(),
			lobby.startingMax
		)
		if challenge then
			lobby.challengeInput:ClearFocus()
			if challenge.practice then
				lobby.statusText:SetText("Practice game started at 1–" .. FormatNumber(challenge.startingMax) .. ".")
				GuildPing:ShowDeathRollGame()
			else
				lobby.statusText:SetText("Challenge sent to " .. challenge.target .. ".")
			end
			lobby.statusText:SetTextColor(GUILD_GREEN:GetRGB())
		else
			lobby.statusText:SetText(errorMessage)
			lobby.statusText:SetTextColor(UPDATE_RED:GetRGB())
		end
		GuildPing:RefreshDeathRollLobby()
	end)
	lobby.challengeInput:SetScript("OnEnterPressed", function(self)
		self:ClearFocus()
		lobby.challengeButton:Click()
	end)

	lobby.statusText = CreateLabel(lobby, "", "GameFontHighlightSmall")
	lobby.statusText:SetPoint("TOPLEFT", 26, -266)
	lobby.statusText:SetPoint("RIGHT", -26, 0)
	lobby.statusText:SetWordWrap(false)
	lobby.statusText:SetTextColor(MUTED_COLOR:GetRGB())

	local activeDivider = lobby:CreateTexture(nil, "ARTWORK")
	activeDivider:SetColorTexture(0.235, 0.267, 0.345, 0.9)
	activeDivider:SetPoint("TOPLEFT", 24, -292)
	activeDivider:SetPoint("TOPRIGHT", -24, -292)
	activeDivider:SetHeight(1)
	lobby.activeHeader = CreateLabel(lobby, "Current Death Roll", "GameFontNormalLarge")
	lobby.activeHeader:SetPoint("TOPLEFT", 24, -310)
	lobby.activeText = CreateLabel(lobby, "", "GameFontHighlight")
	lobby.activeText:SetPoint("TOPLEFT", 26, -341)
	lobby.activeText:SetPoint("RIGHT", -180, 0)
	lobby.activeText:SetWordWrap(false)
	lobby.resumeButton = CreateButton(lobby, "Open Game", 128, 25, "primary")
	lobby.resumeButton:SetPoint("TOPRIGHT", -24, -331)
	lobby.resumeButton:SetScript("OnClick", function()
		GuildPing:ShowDeathRollGame()
	end)
	lobby.resumeButton:Hide()

	lobby.inviteHeader = CreateLabel(lobby, "Incoming challenges", "GameFontNormalLarge")
	lobby.inviteHeader:SetPoint("TOPLEFT", 24, -386)
	lobby.inviteCount = CreateLabel(lobby, "", "GameFontHighlightSmall")
	lobby.inviteCount:SetPoint("LEFT", lobby.inviteHeader, "RIGHT", 10, -1)
	lobby.inviteCount:SetTextColor(MUTED_COLOR:GetRGB())
	lobby.inviteRows = {}
	for index = 1, 3 do
		local row = CreateInviteRow(lobby, 592)
		row:SetPoint("TOPLEFT", 24, -416 - ((index - 1) * 59))
		lobby.inviteRows[index] = row
	end
	lobby.emptyInvites = CreateLabel(lobby, "No incoming Death Roll challenges.", "GameFontDisable")
	lobby.emptyInvites:SetPoint("TOPLEFT", 28, -426)

	lobby.allGamesButton = CreateButton(lobby, "All Games", 104, 25)
	lobby.allGamesButton:SetPoint("BOTTOMLEFT", 24, 14)
	lobby.allGamesButton:SetScript("OnClick", function()
		if GuildPing.ShowGames then
			GuildPing:ShowGames()
		end
	end)
	lobby.closeButton = CreateButton(lobby, "Close", 90, 25)
	lobby.closeButton:SetPoint("BOTTOMRIGHT", -24, 14)
	lobby.closeButton:SetScript("OnClick", function() lobby:Hide() end)
	lobby:SetScript("OnShow", function() GuildPing:RefreshDeathRollLobby() end)
	lobby:Hide()
	self.deathRollLobby = lobby
	return lobby
end

function GuildPing:RefreshDeathRollLobby()
	local lobby = self.deathRollLobby
	if not lobby then
		return
	end
	self:CleanupDeathRollInvites()
	local game = self.activeDeathRollGame
	local active = game and game.status == "ACTIVE"
	lobby.challengeButton:SetEnabled(self.deathRollInitialized and not active)
	local currentStatus = lobby.statusText:GetText() or ""
	if self.outgoingDeathRollChallenge then
		lobby.statusText:SetText("Waiting for " .. self.outgoingDeathRollChallenge.target .. " to respond.")
		lobby.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	elseif currentStatus == "" or currentStatus:find("^Waiting for ") then
		lobby.statusText:SetText("Challenges expire after five minutes.")
		lobby.statusText:SetTextColor(MUTED_COLOR:GetRGB())
	end

	if game then
		local stateText = game.status == "FINISHED" and "Finished" or "In progress"
		if game.practice then
			lobby.activeText:SetText(string.format(
				"Practice  •  Current range 1–%s  •  %s",
				FormatNumber(game.currentMax),
				stateText
			))
		else
			lobby.activeText:SetText(string.format(
				"vs %s  •  1–%s  •  %s",
				self:GetDeathRollOpponent(game) or "Unknown",
				FormatNumber(game.currentMax),
				stateText
			))
		end
		lobby.resumeButton:Show()
		lobby.resumeButton:SetEnabled(true)
	else
		lobby.activeText:SetText("No Death Roll game in this session.")
		lobby.resumeButton:Hide()
		lobby.resumeButton:SetEnabled(false)
	end

	local invites = self:GetDeathRollInvites()
	lobby.inviteCount:SetText(string.format("(%d)", #invites))
	lobby.emptyInvites:SetShown(#invites == 0)
	for rowIndex, row in ipairs(lobby.inviteRows) do
		PopulateInviteRow(row, invites[#invites - rowIndex + 1], not active)
	end
end

local function EnsureResignPopups()
	if not StaticPopupDialogs[RESIGN_POPUP_KEY] then
		StaticPopupDialogs[RESIGN_POPUP_KEY] = {
			text = "Resign this Death Roll game? Your opponent will win.",
			button1 = "Resign",
			button2 = CANCEL,
			timeout = 0,
			whileDead = true,
			hideOnEscape = true,
			preferredIndex = 3,
			OnAccept = function()
				local resigned, errorMessage = GuildPing:ResignDeathRollGame()
				if not resigned then GuildPing:Print(errorMessage) end
			end,
		}
	end
	if not StaticPopupDialogs[END_PRACTICE_POPUP_KEY] then
		StaticPopupDialogs[END_PRACTICE_POPUP_KEY] = {
			text = "End this local Death Roll practice game?",
			button1 = "End Practice",
			button2 = CANCEL,
			timeout = 0,
			whileDead = true,
			hideOnEscape = true,
			preferredIndex = 3,
			OnAccept = function()
				local ended, errorMessage = GuildPing:ResignDeathRollGame()
				if not ended then GuildPing:Print(errorMessage) end
			end,
		}
	end
	if not StaticPopupDialogs[ABANDON_POPUP_KEY] then
		StaticPopupDialogs[ABANDON_POPUP_KEY] = {
			text = "Abandon this saved Death Roll game? It will be removed from this character.",
			button1 = "Abandon Game",
			button2 = CANCEL,
			timeout = 0,
			whileDead = true,
			hideOnEscape = true,
			preferredIndex = 3,
			OnAccept = function()
				local abandoned, errorMessage = GuildPing:AbandonDeathRollGame()
				if abandoned then GuildPing:ShowDeathRollLobby() else GuildPing:Print(errorMessage) end
			end,
		}
	end
end

local function CreateDie(parent, horizontalOffset)
	local die = CreateFrame("Frame", nil, parent, "BackdropTemplate")
	die:SetSize(48, 48)
	die:SetPoint("CENTER", horizontalOffset, -8)
	die:SetBackdrop(ROW_BACKDROP)
	die:SetBackdropColor(0.90, 0.86, 0.74, 1)
	die:SetBackdropBorderColor(Unpack(ROLL_GOLD))
	die.value = CreateLabel(die, "?", "GameFontNormalHuge")
	die.value:SetPoint("CENTER", 0, 1)
	die.value:SetTextColor(0.18, 0.12, 0.08, 1)
	return die
end

local function SetDiceResult(frame, result)
	if not result then
		frame.leftDie.value:SetText("?")
		frame.rightDie.value:SetText("?")
		return
	end
	frame.leftDie.value:SetText(tostring(((result - 1) % 6) + 1))
	frame.rightDie.value:SetText(tostring((math.floor((result - 1) / 6) % 6) + 1))
end

local function RefreshHistory(frame, game, excludeSequence)
	local localSeat = GuildPing:GetLocalDeathRollSeat(game)
	local historyIndex = #game.history
	for _, line in ipairs(frame.historyLines) do
		while historyIndex > 0 and game.history[historyIndex].sequence == excludeSequence do
			historyIndex = historyIndex - 1
		end
		local roll = game.history[historyIndex]
		if roll then
			local roller = SeatLabel(game, roll.seat, localSeat)
			line:SetText(string.format(
				"%s rolled %s  •  from 1–%s",
				roller,
				FormatNumber(roll.result),
				FormatNumber(roll.upperBound)
			))
			if roll.result == 1 then
				line:SetTextColor(UPDATE_RED:GetRGB())
			elseif historyIndex == #game.history then
				line:SetTextColor(WARNING_GOLD:GetRGB())
			else
				line:SetTextColor(MUTED_COLOR:GetRGB())
			end
			line:Show()
			historyIndex = historyIndex - 1
		else
			line:Hide()
		end
	end
	frame.emptyHistory:SetShown(#game.history == 0 or (#game.history == 1 and excludeSequence ~= nil))
end

function GuildPing:CreateDeathRollGameFrame()
	if self.deathRollGameFrame then
		return self.deathRollGameFrame
	end

	local frame = CreateWindow("GuildPingDeathRollGameFrame")
	frame:SetSize(620, 680)
	frame:SetPoint("CENTER")
	frame:SetFrameStrata("DIALOG")
	MakeDraggable(frame, frame.header)
	SetTitle(frame, "Guild Ping — Death Roll")

	frame.opponentText = CreateLabel(frame, "", "GameFontHighlight")
	frame.opponentText:SetPoint("TOPLEFT", 24, -50)
	frame.opponentText:SetPoint("TOPRIGHT", -24, -50)
	frame.opponentText:SetJustifyH("CENTER")
	frame.statusText = CreateLabel(frame, "", "GameFontNormal")
	frame.statusText:SetPoint("TOPLEFT", 24, -76)
	frame.statusText:SetPoint("TOPRIGHT", -24, -76)
	frame.statusText:SetJustifyH("CENTER")

	frame.chamber = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	frame.chamber:SetSize(430, 230)
	frame.chamber:SetPoint("TOP", 0, -108)
	frame.chamber:SetBackdrop(ROW_BACKDROP)
	frame.chamber:SetBackdropColor(Unpack(CHAMBER_SURFACE))
	frame.chamber:SetBackdropBorderColor(Unpack(ROLL_GOLD))
	frame.chamberGlow = frame.chamber:CreateTexture(nil, "BACKGROUND")
	frame.chamberGlow:SetColorTexture(ROLL_GOLD[1], ROLL_GOLD[2], ROLL_GOLD[3], 1)
	frame.chamberGlow:SetPoint("TOPLEFT", 2, -2)
	frame.chamberGlow:SetPoint("BOTTOMRIGHT", -2, 2)
	frame.chamberGlow:SetAlpha(0.06)

	frame.rangeLabel = CreateLabel(frame.chamber, "CURRENT RANGE", "GameFontNormalSmall")
	frame.rangeLabel:SetPoint("TOP", 0, -18)
	frame.rangeLabel:SetTextColor(Unpack(ROLL_GOLD))
	frame.rangeText = CreateLabel(frame.chamber, "", "GameFontNormalLarge")
	frame.rangeText:SetPoint("TOP", frame.rangeLabel, "BOTTOM", 0, -5)
	frame.rangeText:SetTextColor(1, 0.94, 0.78, 1)

	frame.resultText = CreateLabel(frame.chamber, "—", "GameFontNormalHuge")
	local fontPath, _, fontFlags = GameFontNormalHuge:GetFont()
	if fontPath then
		frame.resultText:SetFont(fontPath, 58, fontFlags or "OUTLINE")
	end
	frame.resultText:SetPoint("CENTER", 0, -7)
	frame.resultText:SetTextColor(1, 0.94, 0.78, 1)
	frame.resultSubtext = CreateLabel(frame.chamber, "Waiting for the first roll", "GameFontHighlightSmall")
	frame.resultSubtext:SetPoint("BOTTOM", 0, 18)
	frame.resultSubtext:SetTextColor(MUTED_COLOR:GetRGB())
	frame.leftDie = CreateDie(frame.chamber, -145)
	frame.rightDie = CreateDie(frame.chamber, 145)

	frame.rollButton = CreateButton(frame, "Roll", 230, 32, "primary")
	frame.rollButton:SetPoint("TOP", frame.chamber, "BOTTOM", 0, -18)
	frame.rollButton:SetScript("OnClick", function()
		local roll, errorMessage = GuildPing:RollDeathRoll()
		if not roll then GuildPing:Print(errorMessage) end
	end)

	frame.historyHeader = CreateLabel(frame, "Roll history", "GameFontNormalLarge")
	frame.historyHeader:SetPoint("TOPLEFT", 42, -405)
	frame.historyLines = {}
	for index = 1, 6 do
		local line = CreateLabel(frame, "", "GameFontHighlight")
		line:SetPoint("TOPLEFT", 48, -438 - ((index - 1) * 27))
		line:SetPoint("RIGHT", -48, 0)
		line:SetWordWrap(false)
		frame.historyLines[index] = line
	end
	frame.emptyHistory = CreateLabel(frame, "No rolls yet. Make the first one count.", "GameFontDisable")
	frame.emptyHistory:SetPoint("TOPLEFT", 48, -441)

	frame.lobbyButton = CreateButton(frame, "Death Roll Lobby", 132, 25)
	frame.lobbyButton:SetPoint("BOTTOMLEFT", 24, 16)
	frame.lobbyButton:SetScript("OnClick", function()
		frame:Hide()
		GuildPing:ShowDeathRollLobby()
	end)
	frame.resignButton = CreateButton(frame, "Resign", 112, 25, "danger")
	frame.resignButton:SetPoint("BOTTOM", 0, 16)
	frame.resignButton:SetScript("OnClick", function()
		EnsureResignPopups()
		local popupKey = GuildPing.activeDeathRollGame and GuildPing.activeDeathRollGame.practice
			and END_PRACTICE_POPUP_KEY or RESIGN_POPUP_KEY
		StaticPopup_Hide(popupKey)
		StaticPopup_Show(popupKey)
	end)
	frame.playAgainButton = CreateButton(frame, "Play Again", 112, 25, "primary")
	frame.playAgainButton:SetPoint("BOTTOM", 0, 16)
	frame.playAgainButton:SetScript("OnClick", function()
		local challenge, errorMessage = GuildPing:PlayDeathRollAgain()
		if not challenge then
			GuildPing:Print(errorMessage)
		elseif not challenge.practice then
			GuildPing:Print("Rematch challenge sent to " .. challenge.target .. ".")
		end
	end)
	frame.playAgainButton:Hide()
	frame.lifecycleResumeButton = CreateButton(frame, "Resume", 112, 25, "primary")
	frame.lifecycleResumeButton:SetPoint("BOTTOM", -66, 16)
	frame.lifecycleResumeButton:SetScript("OnClick", function()
		local resumed, errorMessage = GuildPing:ResumeDeathRollGame()
		if not resumed then GuildPing:Print(errorMessage) end
	end)
	frame.lifecycleResumeButton:Hide()
	frame.abandonButton = CreateButton(frame, "Abandon Game", 126, 25, "danger")
	frame.abandonButton:SetPoint("BOTTOM", 62, 16)
	frame.abandonButton:SetScript("OnClick", function()
		EnsureResignPopups()
		StaticPopup_Hide(ABANDON_POPUP_KEY)
		StaticPopup_Show(ABANDON_POPUP_KEY)
	end)
	frame.abandonButton:Hide()
	frame.closeButton = CreateButton(frame, "Close", 90, 25)
	frame.closeButton:SetPoint("BOTTOMRIGHT", -24, 16)
	frame.closeButton:SetScript("OnClick", function() frame:Hide() end)

	frame:SetScript("OnShow", function() GuildPing:RefreshDeathRollGame() end)
	frame:SetScript("OnHide", function()
		frame:SetScript("OnUpdate", nil)
		frame.animation = nil
		frame.resultText:SetScale(1)
		frame.resultText:ClearAllPoints()
		frame.resultText:SetPoint("CENTER", frame.chamber, "CENTER", 0, -7)
	end)
	frame:Hide()
	self.deathRollGameFrame = frame
	return frame
end

function GuildPing:BeginDeathRollAnimation(roll)
	local frame = self.deathRollGameFrame
	if not frame or not frame:IsShown() or not roll then
		return
	end
	local game = self.activeDeathRollGame
	local rollerLabel = game and SeatLabel(game, roll.seat, self:GetLocalDeathRollSeat(game)) or roll.player or "Player"
	frame.animation = {
		roll = roll,
		rollerLabel = rollerLabel,
		elapsed = 0,
		lastTick = -1,
		previewSeed = (roll.result * 977) + (roll.sequence * 131) + roll.upperBound,
	}
	frame.rangeText:SetText("1 — " .. FormatNumber(roll.upperBound))
	frame.resultText:SetText("?")
	frame.resultSubtext:SetText(rollerLabel .. " is rolling...")
	frame.resultText:SetTextColor(1, 0.94, 0.78, 1)
	frame.chamberGlow:SetAlpha(0.16)
	frame.rollButton:SetEnabled(false)
	RefreshHistory(frame, self.activeDeathRollGame, roll.sequence)
	frame:SetScript("OnUpdate", function(_, elapsed)
		GuildPing:UpdateDeathRollAnimation(elapsed)
	end)
end

function GuildPing:UpdateDeathRollAnimation(elapsed)
	local frame = self.deathRollGameFrame
	local animation = frame and frame.animation
	if not animation then
		return
	end
	animation.elapsed = animation.elapsed + (tonumber(elapsed) or 0)
	local roll = animation.roll
	local revealAt = 1.15
	local finishAt = 1.70

	if animation.elapsed < revealAt then
		local tick = math.floor(animation.elapsed / 0.055)
		if tick ~= animation.lastTick then
			animation.lastTick = tick
			animation.previewSeed = ((animation.previewSeed * 1103515245) + 12345) % 2147483647
			local preview = (animation.previewSeed % roll.upperBound) + 1
			frame.resultText:SetText(FormatNumber(preview))
			frame.leftDie.value:SetText(tostring((animation.previewSeed % 6) + 1))
			frame.rightDie.value:SetText(tostring((math.floor(animation.previewSeed / 7) % 6) + 1))
		end
		local remaining = 1 - (animation.elapsed / revealAt)
		local offsetX = math.sin(animation.elapsed * 39) * 8 * remaining
		local offsetY = math.cos(animation.elapsed * 31) * 5 * remaining
		frame.resultText:ClearAllPoints()
		frame.resultText:SetPoint("CENTER", frame.chamber, "CENTER", offsetX, -7 + offsetY)
		frame.leftDie:SetScale(0.92 + (math.abs(math.sin(animation.elapsed * 16)) * 0.16))
		frame.rightDie:SetScale(0.92 + (math.abs(math.cos(animation.elapsed * 17)) * 0.16))
		frame.chamberGlow:SetAlpha(0.08 + (math.abs(math.sin(animation.elapsed * 10)) * 0.16))
		return
	end

	local impactProgress = math.min(1, (animation.elapsed - revealAt) / (finishAt - revealAt))
	frame.resultText:SetText(FormatNumber(roll.result))
	frame.resultText:ClearAllPoints()
	frame.resultText:SetPoint("CENTER", frame.chamber, "CENTER", 0, -7)
	frame.resultText:SetScale(1 + ((1 - impactProgress) * 0.30))
	frame.leftDie:SetScale(1)
	frame.rightDie:SetScale(1)
	SetDiceResult(frame, roll.result)
	if roll.result == 1 then
		frame.resultText:SetTextColor(UPDATE_RED:GetRGB())
		frame.resultSubtext:SetText("DEATH — " .. animation.rollerLabel .. " rolled 1")
		frame.chamberGlow:SetColorTexture(UPDATE_RED:GetRGBA())
	else
		frame.resultText:SetTextColor(GUILD_GREEN:GetRGB())
		frame.resultSubtext:SetText("The range collapses to 1–" .. FormatNumber(roll.result))
		frame.chamberGlow:SetColorTexture(GUILD_GREEN:GetRGBA())
	end
	frame.chamberGlow:SetAlpha((1 - impactProgress) * 0.34 + 0.05)

	if animation.elapsed >= finishAt then
		frame:SetScript("OnUpdate", nil)
		frame.animation = nil
		frame.resultText:SetScale(1)
		frame.chamberGlow:SetColorTexture(ROLL_GOLD[1], ROLL_GOLD[2], ROLL_GOLD[3], 1)
		frame.chamberGlow:SetAlpha(0.06)
		self:RefreshDeathRollGame()
	end
end

function GuildPing:RefreshDeathRollGame()
	local frame = self.deathRollGameFrame
	if not frame then
		return
	end
	local game = self.activeDeathRollGame
	if not game then
		frame:Hide()
		return
	end
	local localSeat = self:GetLocalDeathRollSeat(game)
	local currentGuildGame = self:IsDeathRollGameForCurrentGuild(game)
	local waitingForResume = self:IsGameWaitingForResume(game)
	if game.practice then
		frame.opponentText:SetText("Practice mode  •  You control Player 1 and Player 2")
	else
		frame.opponentText:SetText("Opponent: " .. (self:GetDeathRollOpponent(game) or "Unknown"))
	end

	if frame.animation then
		frame.statusText:SetText(SeatLabel(game, frame.animation.roll.seat, localSeat) .. " is rolling...")
		frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
		frame.rollButton:SetEnabled(false)
		frame.resignButton:SetEnabled(false)
		frame.playAgainButton:Hide()
		frame.lifecycleResumeButton:Hide()
		frame.abandonButton:Hide()
		return
	end

	frame.rangeText:SetText("1 — " .. FormatNumber(game.currentMax))
	if game.lastRoll then
		frame.resultText:SetText(FormatNumber(game.lastRoll.result))
		frame.resultSubtext:SetText(string.format(
			"%s rolled from 1–%s",
			SeatLabel(game, game.lastRoll.seat, localSeat),
			FormatNumber(game.lastRoll.upperBound)
		))
		if game.lastRoll.result == 1 then
			frame.resultText:SetTextColor(UPDATE_RED:GetRGB())
		else
			frame.resultText:SetTextColor(GUILD_GREEN:GetRGB())
		end
		SetDiceResult(frame, game.lastRoll.result)
	else
		frame.resultText:SetText("—")
		frame.resultSubtext:SetText(game.sequence > 0 and "History reset after synchronization" or "Waiting for the first roll")
		frame.resultText:SetTextColor(1, 0.94, 0.78, 1)
		SetDiceResult(frame)
	end

	if not currentGuildGame then
		frame.statusText:SetText("This game belongs to a different guild and cannot continue here.")
		frame.statusText:SetTextColor(UPDATE_RED:GetRGB())
	elseif game.persistenceState == "RECONNECTING" then
		frame.statusText:SetText("Reconnecting to " .. (self:GetDeathRollOpponent(game) or "your opponent") .. "…")
		frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	elseif game.persistenceState == "STALE" then
		frame.statusText:SetText("No response from your opponent. Resume to try again or abandon this game.")
		frame.statusText:SetTextColor(UPDATE_RED:GetRGB())
	elseif game.persistenceState == "RESTORED" then
		frame.statusText:SetText("Practice game restored. Resume when you're ready.")
		frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	elseif game.status == "FINISHED" then
		if game.finishReason == "practice ended" then
			frame.statusText:SetText("Practice game ended.")
			frame.statusText:SetTextColor(MUTED_COLOR:GetRGB())
		elseif game.practice then
			frame.statusText:SetText(string.format(
				"%s wins — %s rolled 1.",
				SeatLabel(game, game.winnerSeat, localSeat),
				SeatLabel(game, game.loserSeat, localSeat)
			))
			frame.statusText:SetTextColor(GUILD_GREEN:GetRGB())
		elseif game.winnerSeat == localSeat then
			local loser = SeatLabel(game, game.loserSeat, localSeat)
			if game.finishReason == "opponent resigned" then
				frame.statusText:SetText("You won — " .. loser .. " resigned.")
			else
				frame.statusText:SetText("You won — " .. loser .. " rolled 1.")
			end
			frame.statusText:SetTextColor(GUILD_GREEN:GetRGB())
		else
			frame.statusText:SetText(game.finishReason == "resignation" and "You resigned." or "You lost — you rolled 1.")
			frame.statusText:SetTextColor(UPDATE_RED:GetRGB())
		end
	elseif game.turn == localSeat then
		frame.statusText:SetText("Your turn — roll from 1 to " .. FormatNumber(game.currentMax) .. ".")
		frame.statusText:SetTextColor(GUILD_GREEN:GetRGB())
	else
		frame.statusText:SetText("Waiting for " .. SeatLabel(game, game.turn, localSeat) .. " to roll.")
		frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	end

	frame.rollButton:SetText("Roll 1–" .. FormatNumber(game.currentMax))
	frame.rollButton:SetEnabled(currentGuildGame and game.status == "ACTIVE" and not waitingForResume and game.turn == localSeat)
	frame.resignButton:SetText(game.practice and "End Practice" or "Resign")
	frame.resignButton:SetShown(game.status == "ACTIVE" and not waitingForResume)
	frame.resignButton:SetEnabled(currentGuildGame and game.status == "ACTIVE" and not waitingForResume)
	local waitingForRematch = self.outgoingDeathRollChallenge ~= nil
	frame.playAgainButton:SetShown(game.status == "FINISHED" and not waitingForResume)
	frame.playAgainButton:SetText(waitingForRematch and "Challenge Sent" or "Play Again")
	frame.playAgainButton:SetEnabled(currentGuildGame and game.status == "FINISHED" and not waitingForRematch)
	frame.lifecycleResumeButton:SetShown(waitingForResume)
	frame.lifecycleResumeButton:SetEnabled(currentGuildGame and game.persistenceState ~= "RECONNECTING")
	frame.abandonButton:SetShown(waitingForResume)
	frame.abandonButton:SetEnabled(currentGuildGame)
	RefreshHistory(frame, game)
end

function GuildPing:ShowDeathRollLobby()
	if self.gamesHub then self.gamesHub:Hide() end
	if self.checkersLobby then self.checkersLobby:Hide() end
	if self.checkersBoard then self.checkersBoard:Hide() end
	if self.pokerLobby then self.pokerLobby:Hide() end
	if self.pokerTableFrame then self.pokerTableFrame:Hide() end
	if self.connectFourLobby then self.connectFourLobby:Hide() end
	if self.connectFourBoard then self.connectFourBoard:Hide() end
	if self.deathRollGameFrame then self.deathRollGameFrame:Hide() end
	local lobby = self:CreateDeathRollLobby()
	lobby:Show()
	self:RefreshDeathRollLobby()
end

function GuildPing:ShowDeathRollGame()
	if not self.activeDeathRollGame then
		self:ShowDeathRollLobby()
		return
	end
	self:AcknowledgeGameTurn("deathRoll")
	if self.gamesHub then self.gamesHub:Hide() end
	if self.checkersLobby then self.checkersLobby:Hide() end
	if self.checkersBoard then self.checkersBoard:Hide() end
	if self.deathRollLobby then self.deathRollLobby:Hide() end
	if self.pokerLobby then self.pokerLobby:Hide() end
	if self.pokerTableFrame then self.pokerTableFrame:Hide() end
	if self.connectFourLobby then self.connectFourLobby:Hide() end
	if self.connectFourBoard then self.connectFourBoard:Hide() end
	local frame = self:CreateDeathRollGameFrame()
	frame:Show()
	self:RefreshDeathRollGame()
end

function GuildPing:ShowDeathRoll()
	if self.fiveDiceFrame then self.fiveDiceFrame:Hide() end
	if self.runeMastermindFrame then self.runeMastermindFrame:Hide() end
	if self.activeDeathRollGame then
		self:ShowDeathRollGame()
	else
		self:ShowDeathRollLobby()
	end
end
