local F={["\091\051\048\109"]="\114\103\098\040\048\044 \048\044 \048\041",["\091\051\049\109"]="\114\103\098\040\050\053\052\044 \057\049\044 \056\054\041",["\091\051\050\109"]="\114\103\098\040\056\057\044 \050\052\054\044 \049\052\049\041";["\091\051\051\109"]="\114\103\098\040\050\053\053\044 \050\053\053\044 \049\054\052\041";["\091\051\052\109"]="\114\103\098\040\056\055\044 \049\057\057\044 \050\053\053\041";["\091\051\053\109"]="\114\103\098\040\050\053\052\044 \049\048\053\044 \049\057\050\041",["\091\051\054\109"]="\114\103\098\040\049\053\051\044 \050\051\054\044 \050\053\052\041",["\091\051\055\109"]="\114\103\098\040\050\052\048\044 \050\052\048\044 \050\051\057\041"}local K={["\091\052\048\109"]=Color3.fromRGB(0,0,0),["\091\052\049\109"]=Color3.fromRGB(254,91,86),["\091\052\050\109"]=Color3.fromRGB(89,246,141),["\091\052\051\109"]=Color3.fromRGB(255,255,164);["\091\052\052\109"]=Color3.fromRGB(87,199,255);["\091\052\053\109"]=Color3.fromRGB(254,105,192);["\091\052\054\109"]=Color3.fromRGB(153,236,254);["\091\052\055\109"]=Color3.fromRGB(240,240,239)}local d={["\091\057\049\109"]="\114\103\098\040\048\044\049\051\057\044\050\049\057\041";["\091\057\050\109"]="\114\103\098\040\050\053\053\044\049\049\053\044\050\049\041",["\091\057\051\109"]="\114\103\098\040\050\053\053\044\048\044\048\041"}local function J()return{stack={};visibleCharacterIndex=0,backgroundRuns={};currentBackgroundColor=nil;backgroundStartIndex=nil}end local function N(F)if F.type=="\098\111\108\100"then return"\060\098\062"elseif F.type=="\105\116\097\108\105\099"then return"\060\105\062"elseif F.type=="\117\110\100\101\114\108\105\110\101"then return"\060\117\062"elseif F.type=="\115\116\114\105\107\101\116\104\114\111\117\103\104"then return"\060\115\062"elseif F.type=="\099\111\108\111\114"then return string.format("\060\102\111\110\116 \099\111\108\111\114\061\"\037\115\"\062",F.color)end return""end local function P(F)if F.type=="\098\111\108\100"then return"\060\047\098\062"elseif F.type=="\105\116\097\108\105\099"then return"\060\047\105\062"elseif F.type=="\117\110\100\101\114\108\105\110\101"then return"\060\047\117\062"elseif F.type=="\115\116\114\105\107\101\116\104\114\111\117\103\104"then return"\060\047\115\062"elseif F.type=="\099\111\108\111\114"then return"\060\047\102\111\110\116\062"end return""end local function M(F,K)for F,d in ipairs(F.stack)do if d.type==K then return true end end return false end local function j(F,K,d)table.insert(F.stack,d)K[#K+1]=N(d)end local function W(F,K,d)local J={}local N=false while#F.stack>0 do local M=F.stack[#F.stack]table.remove(F.stack)K[#K+1]=P(M)if M.type==d then N=true break else table.insert(J,1,M)end end return N,J end local function w(F,K,d)for d,J in ipairs(d)do j(F,K,J)end end local function B(F,K,d)if not M(F,d)then return end local J,N=W(F,K,d)if J then w(F,K,N)end end local function G(F,K)while#F.stack>0 do local d=F.stack[#F.stack]table.remove(F.stack)K[#K+1]=P(d)end if F.currentBackgroundColor then local K={startIndex=F.backgroundStartIndex;endIndex=F.visibleCharacterIndex-1;color=F.currentBackgroundColor}table.insert(F.backgroundRuns,K)F.currentBackgroundColor=nil F.backgroundStartIndex=nil end end local function Z(F,K,d,J)if d=="\099\111\108\111\114"then B(F,K,"\099\111\108\111\114")j(F,K,{type="\099\111\108\111\114";color=J})else if not M(F,d)then j(F,K,{type=d})end end end local function x(F,K)if F.currentBackgroundColor==K then return end if F.currentBackgroundColor then local K={startIndex=F.backgroundStartIndex;endIndex=F.visibleCharacterIndex-1;color=F.currentBackgroundColor}table.insert(F.backgroundRuns,K)F.currentBackgroundColor=nil F.backgroundStartIndex=nil end F.currentBackgroundColor=K F.backgroundStartIndex=F.visibleCharacterIndex end local function S(F)if F.currentBackgroundColor then local K={startIndex=F.backgroundStartIndex;endIndex=F.visibleCharacterIndex-1,color=F.currentBackgroundColor}table.insert(F.backgroundRuns,K)F.currentBackgroundColor=nil F.backgroundStartIndex=nil end end local function f(J,N,P)if J=="\091\048\109"then G(P,N)return end local M={}for F in J:gmatch("\037\091\040\046-\041\109")do for F in F:gmatch("\040\091\094\059\093\043\041")do table.insert(M,F)end end for J,M in ipairs(M)do local j=tonumber(M)if j==0 then G(P,N)elseif j==1 then Z(P,N,"\098\111\108\100")elseif j==22 then B(P,N,"\098\111\108\100")elseif j==3 then Z(P,N,"\105\116\097\108\105\099")elseif j==23 then B(P,N,"\105\116\097\108\105\099")elseif j==4 then Z(P,N,"\117\110\100\101\114\108\105\110\101")elseif j==24 then B(P,N,"\117\110\100\101\114\108\105\110\101")elseif j==9 then Z(P,N,"\115\116\114\105\107\101\116\104\114\111\117\103\104")elseif j==29 then B(P,N,"\115\116\114\105\107\101\116\104\114\111\117\103\104")elseif j>=30 and j<=37 then local K="\091"..(tostring(j).."\109")local d=F[K]if d then Z(P,N,"\099\111\108\111\114",d)end elseif j==39 then B(P,N,"\099\111\108\111\114")elseif j>=40 and j<=47 then local F="\091"..(tostring(j).."\109")local d=K[F]if d then x(P,d)end elseif j==49 then S(P)elseif j==91 then Z(P,N,"\099\111\108\111\114",d["\091\057\049\109"])elseif j==92 then Z(P,N,"\099\111\108\111\114",d["\091\057\050\109"])elseif j==93 then Z(P,N,"\099\111\108\111\114",d["\091\057\051\109"])end end end local function O(F,K)F=F:gsub("\226\156\149","\195\151")F=F:gsub("\060","\038\108\116\059")F=F:gsub("\062","\038\103\116\059")if not K then K=J()end local d={}local N=1 for J,P,M in F:gmatch("\040\041\040\027\037\091\091\037\100\059\093\042\109\041\040\041")do if N<J then local P=F:sub(N,J-1)d[#d+1]=P K.visibleCharacterIndex=K.visibleCharacterIndex+#P end f(P:sub(2),d,K)N=M end if N<=#F then local J=F:sub(N)d[#d+1]=J K.visibleCharacterIndex=K.visibleCharacterIndex+#J end return table.concat(d),K end local function Q(F)local K={}G(F,K)return table.concat(K)end local function n(F)local K=table.clone(F.stack)local d={}while#K>0 do local F=K[#K]table.remove(K)d[#d+1]=P(F)end if F.currentBackgroundColor then local K={startIndex=F.backgroundStartIndex;endIndex=F.visibleCharacterIndex-1;color=F.currentBackgroundColor}table.insert(F.backgroundRuns,K)F.currentBackgroundColor=nil F.backgroundStartIndex=nil end return table.concat(d)end return{defaultState=J,ansiToRichTextChunk=O;finalizeRichText=Q;getTemporaryClosingString=n}
local ANSI_COLOR_MAP = {
	["[30m"] = "rgb(0, 0, 0)",
	["[31m"] = "rgb(254, 91, 86)",
	["[32m"] = "rgb(89, 246, 141)",
	["[33m"] = "rgb(255, 255, 164)",
	["[34m"] = "rgb(87, 199, 255)",
	["[35m"] = "rgb(254, 105, 192)",
	["[36m"] = "rgb(153, 236, 254)",
	["[37m"] = "rgb(240, 240, 239)",
}

local BACKGROUND_COLOR_MAP = {
	["[40m"] = Color3.fromRGB(0, 0, 0),
	["[41m"] = Color3.fromRGB(254, 91, 86),
	["[42m"] = Color3.fromRGB(89, 246, 141),
	["[43m"] = Color3.fromRGB(255, 255, 164),
	["[44m"] = Color3.fromRGB(87, 199, 255),
	["[45m"] = Color3.fromRGB(254, 105, 192),
	["[46m"] = Color3.fromRGB(153, 236, 254),
	["[47m"] = Color3.fromRGB(240, 240, 239),
}

local MESSAGE_TYPE_COLORS = {
	["[91m"] = "rgb(0,139,219)", -- blue
	["[92m"] = "rgb(255,115,21)", -- orange
	["[93m"] = "rgb(255,0,0)", -- red
}

local function defaultState()
	return {
		stack = {}, -- stack of styles (font/color/bold/etc)
		visibleCharacterIndex = 0,

		-- Background runs
		backgroundRuns = {},
		currentBackgroundColor = nil,
		backgroundStartIndex = nil,
	}
end

local function styleOpeningTag(style)
	if style.type == "bold" then
		return "<b>"
	elseif style.type == "italic" then
		return "<i>"
	elseif style.type == "underline" then
		return "<u>"
	elseif style.type == "strikethrough" then
		return "<s>"
	elseif style.type == "color" then
		return string.format('<font color="%s">', style.color)
	end
	return ""
end

local function styleClosingTag(style)
	if style.type == "bold" then
		return "</b>"
	elseif style.type == "italic" then
		return "</i>"
	elseif style.type == "underline" then
		return "</u>"
	elseif style.type == "strikethrough" then
		return "</s>"
	elseif style.type == "color" then
		return "</font>"
	end
	return ""
end

local function isStyleActive(state, styleType)
	for _, s in ipairs(state.stack) do
		if s.type == styleType then
			return true
		end
	end
	return false
end

local function pushStyle(state, output, style)
	table.insert(state.stack, style)
	output[#output + 1] = styleOpeningTag(style)
end

local function popStylesUntil(state, output, targetType)
	local popped = {}
	local foundTarget = false
	while #state.stack > 0 do
		local top = state.stack[#state.stack]
		table.remove(state.stack)
		output[#output + 1] = styleClosingTag(top)
		if top.type == targetType then
			foundTarget = true
			break
		else
			table.insert(popped, 1, top)
		end
	end
	return foundTarget, popped
end

local function restoreStyles(state, output, styles)
	for _, st in ipairs(styles) do
		pushStyle(state, output, st)
	end
end

local function disableStyle(state, output, styleType)
	if not isStyleActive(state, styleType) then
		return
	end
	local foundTarget, popped = popStylesUntil(state, output, styleType)
	if foundTarget then
		restoreStyles(state, output, popped)
	end
end

local function resetAll(state, output)
	-- Reset all foreground styles
	while #state.stack > 0 do
		local top = state.stack[#state.stack]
		table.remove(state.stack)
		output[#output + 1] = styleClosingTag(top)
	end

	-- Also reset background if active
	if state.currentBackgroundColor then
		local run = {
			startIndex = state.backgroundStartIndex,
			endIndex = state.visibleCharacterIndex - 1,
			color = state.currentBackgroundColor,
		}
		table.insert(state.backgroundRuns, run)
		state.currentBackgroundColor = nil
		state.backgroundStartIndex = nil
	end
end

local function enableStyle(state, output, styleType, color)
	if styleType == "color" then
		disableStyle(state, output, "color")
		pushStyle(state, output, { type = "color", color = color })
	else
		if not isStyleActive(state, styleType) then
			pushStyle(state, output, { type = styleType })
		end
	end
end

local function enableBackground(state, bgColor)
	if state.currentBackgroundColor == bgColor then
		return
	end

	-- If there's already a background active, close it first
	if state.currentBackgroundColor then
		local run = {
			startIndex = state.backgroundStartIndex,
			endIndex = state.visibleCharacterIndex - 1,
			color = state.currentBackgroundColor,
		}
		table.insert(state.backgroundRuns, run)
		state.currentBackgroundColor = nil
		state.backgroundStartIndex = nil
	end

	state.currentBackgroundColor = bgColor
	state.backgroundStartIndex = state.visibleCharacterIndex
end

local function disableBackground(state)
	if state.currentBackgroundColor then
		local run = {
			startIndex = state.backgroundStartIndex,
			endIndex = state.visibleCharacterIndex - 1,
			color = state.currentBackgroundColor,
		}
		table.insert(state.backgroundRuns, run)
		state.currentBackgroundColor = nil
		state.backgroundStartIndex = nil
	end
end

local function applyCode(code, output, state)
	if code == "[0m" then
		resetAll(state, output)
		return
	end

	local codes = {}
	for c in code:gmatch("%[(.-)m") do
		for sc in c:gmatch("([^;]+)") do
			table.insert(codes, sc)
		end
	end

	for _, c in ipairs(codes) do
		local num = tonumber(c)
		if num == 0 then
			resetAll(state, output)
		elseif num == 1 then
			enableStyle(state, output, "bold")
		elseif num == 22 then
			disableStyle(state, output, "bold")
		elseif num == 3 then
			enableStyle(state, output, "italic")
		elseif num == 23 then
			disableStyle(state, output, "italic")
		elseif num == 4 then
			enableStyle(state, output, "underline")
		elseif num == 24 then
			disableStyle(state, output, "underline")
		elseif num == 9 then
			enableStyle(state, output, "strikethrough")
		elseif num == 29 then
			disableStyle(state, output, "strikethrough")
		elseif num >= 30 and num <= 37 then
			local ansiCode = "[" .. tostring(num) .. "m"
			local color = ANSI_COLOR_MAP[ansiCode]
			if color then
				enableStyle(state, output, "color", color)
			end
		elseif num == 39 then
			-- Reset foreground color
			disableStyle(state, output, "color")
		elseif num >= 40 and num <= 47 then
			local ansiCode = "[" .. tostring(num) .. "m"
			local bgColor = BACKGROUND_COLOR_MAP[ansiCode]
			if bgColor then
				enableBackground(state, bgColor)
			end
		elseif num == 49 then
			-- Reset background
			disableBackground(state)
		elseif num == 91 then
			enableStyle(state, output, "color", MESSAGE_TYPE_COLORS["[91m"])
		elseif num == 92 then
			enableStyle(state, output, "color", MESSAGE_TYPE_COLORS["[92m"])
		elseif num == 93 then
			enableStyle(state, output, "color", MESSAGE_TYPE_COLORS["[93m"])
		end
	end
end

local function ansiToRichTextChunk(text, currentState)
	text = text:gsub("✕", "×")
	text = text:gsub("<", "&lt;")
	text = text:gsub(">", "&gt;")

	if not currentState then
		currentState = defaultState()
	end

	local output = {}
	local last_end = 1
	-- Find all ANSI sequences: \27 followed by [ and then zero or more digits/semicolons, ending with 'm'.
	for start, code, finish in text:gmatch("()(\27%[[%d;]*m)()") do
		-- Add all plain text before this code
		if last_end < start then
			local chunk = text:sub(last_end, start - 1)
			output[#output + 1] = chunk
			currentState.visibleCharacterIndex = currentState.visibleCharacterIndex + #chunk
		end

		-- Apply ANSI code
		applyCode(code:sub(2), output, currentState) -- code:sub(2) to remove the initial ESC character
		last_end = finish
	end

	-- Add remaining text after last ANSI code
	if last_end <= #text then
		local chunk = text:sub(last_end)
		output[#output + 1] = chunk
		currentState.visibleCharacterIndex = currentState.visibleCharacterIndex + #chunk
	end

	return table.concat(output), currentState
end

local function finalizeRichText(currentState)
	local output = {}
	resetAll(currentState, output)
	return table.concat(output)
end

local function getTemporaryClosingString(currentState)
	local savedStack = table.clone(currentState.stack)
	local preview = {}

	while #savedStack > 0 do
		local top = savedStack[#savedStack]
		table.remove(savedStack)
		preview[#preview + 1] = styleClosingTag(top)
	end

	if currentState.currentBackgroundColor then
		local run = {
			startIndex = currentState.backgroundStartIndex,
			endIndex = currentState.visibleCharacterIndex - 1,
			color = currentState.currentBackgroundColor,
		}
		table.insert(currentState.backgroundRuns, run)
		currentState.currentBackgroundColor = nil
		currentState.backgroundStartIndex = nil
	end

	return table.concat(preview)
end

return {
	defaultState = defaultState,
	ansiToRichTextChunk = ansiToRichTextChunk,
	finalizeRichText = finalizeRichText,
	getTemporaryClosingString = getTemporaryClosingString,
}
