local _, ns = ...
local SlickUI = ns.SlickUI
local Theme = SlickUI.Theme

local GroupItemLevels = {
	Roster = {},
	Cache = {},
	Queue = {},
	Pending = nil,
	VisibleRows = 15,
	ScrollOffset = 0,
}
SlickUI.GroupItemLevels = GroupItemLevels

local EQUIPMENT_SLOTS = { 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }

local function IsReadable(value)
	if canaccessvalue and not canaccessvalue(value) then return false end
	if issecretvalue and issecretvalue(value) then return false end
	return value ~= nil
end

local function SafeNumber(value)
	return IsReadable(value) and type(value) == "number" and value or nil
end

local function SafeString(value)
	return IsReadable(value) and type(value) == "string" and value or nil
end

local function IsTrue(value)
	return IsReadable(value) and value == true
end

local function ShortName(name)
	name = SafeString(name)
	return name and Ambiguate and Ambiguate(name, "short") or name or "Unknown"
end

local function GetItemLevel(link)
	if not SafeString(link) then return nil end
	local getter = C_Item and C_Item.GetDetailedItemLevelInfo or GetDetailedItemLevelInfo
	if type(getter) ~= "function" then return nil end
	local ok, level = pcall(getter, link)
	return ok and SafeNumber(level) or nil
end

local function EquippedItemLevel(unit)
	if unit == "player" then
		local ok, _, equipped = pcall(GetAverageItemLevel)
		if ok and SafeNumber(equipped) then return equipped end
	end

	local total = 0
	local mainHandLink, offHandLink
	for _, slot in ipairs(EQUIPMENT_SLOTS) do
		local link = GetInventoryItemLink(unit, slot)
		if slot == 16 then mainHandLink = SafeString(link) end
		if slot == 17 then offHandLink = SafeString(link) end
		local level = GetItemLevel(link)
		if level then total = total + level end
	end

	-- A two-handed weapon occupies both weapon contributions in WoW's equipped
	-- average. Empty one-hand/off-hand slots deliberately contribute zero.
	if mainHandLink and not offHandLink then
		local ok, equipLocation = pcall(function() return select(9, GetItemInfo(mainHandLink)) end)
		if ok and (equipLocation == "INVTYPE_2HWEAPON" or equipLocation == "INVTYPE_RANGED" or equipLocation == "INVTYPE_RANGEDRIGHT") then
			local level = GetItemLevel(mainHandLink)
			if level then total = total + level end
		end
	end
	return total > 0 and total / #EQUIPMENT_SLOTS or nil
end

local function ClassColor(classFile)
	local color = classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile]
	return color and color.r or 0.92, color and color.g or 0.94, color and color.b or 1.00
end

local function RoleText(role)
	if role == "TANK" then return "Tank" end
	if role == "HEALER" then return "Healer" end
	if role == "DAMAGER" then return "Damage" end
	return "None"
end

function GroupItemLevels:SortRoster()
	table.sort(self.Roster, function(left, right)
		local leftLevel = type(left.itemLevel) == "number" and left.itemLevel or nil
		local rightLevel = type(right.itemLevel) == "number" and right.itemLevel or nil
		if leftLevel and rightLevel and leftLevel ~= rightLevel then
			return leftLevel > rightLevel
		end
		if leftLevel ~= nil and rightLevel == nil then return true end
		if leftLevel == nil and rightLevel ~= nil then return false end
		return string.lower(left.name or "") < string.lower(right.name or "")
	end)
end

function GroupItemLevels:BuildRoster()
	wipe(self.Roster)
	local units = { "player" }
	if IsInRaid() then
		wipe(units)
		for index = 1, GetNumGroupMembers() do units[#units + 1] = "raid" .. index end
	elseif IsInGroup() then
		for index = 1, GetNumSubgroupMembers() do units[#units + 1] = "party" .. index end
	end

	for _, unit in ipairs(units) do
		local guid = UnitGUID(unit)
		local name = UnitName(unit)
		if SafeString(guid) and SafeString(name) then
			local _, classFile = UnitClass(unit)
			local entry = {
				unit = unit,
				guid = guid,
				name = ShortName(name),
				classFile = SafeString(classFile),
				role = SafeString(UnitGroupRolesAssigned(unit)) or "NONE",
				level = SafeNumber(UnitLevel(unit)),
				connected = IsTrue(UnitIsConnected(unit)),
			}
			local cached = self.Cache[guid]
			if unit == "player" then
				entry.itemLevel = EquippedItemLevel("player")
				entry.status = entry.itemLevel and "Ready" or "Unavailable"
				self.Cache[guid] = { itemLevel = entry.itemLevel, updated = GetTime() or 0 }
			elseif cached and cached.itemLevel then
				entry.itemLevel = cached.itemLevel
				entry.status = "Cached"
			elseif not entry.connected then
				entry.status = "Offline"
			elseif not IsTrue(UnitIsVisible(unit)) then
				entry.status = "Out of range"
			else
				entry.status = "Waiting"
			end
			self.Roster[#self.Roster + 1] = entry
		end
	end

	self:SortRoster()
end

function GroupItemLevels:QueueInspections(force)
	if InCombatLockdown() then return end
	wipe(self.Queue)
	for _, entry in ipairs(self.Roster) do
		if entry.unit ~= "player" and entry.connected and IsTrue(UnitIsVisible(entry.unit)) then
			local cached = self.Cache[entry.guid]
			local stale = not cached or not cached.itemLevel or force or ((GetTime() or 0) - (cached.updated or 0) > 300)
			if stale then self.Queue[#self.Queue + 1] = entry end
		end
	end
	self:InspectNext()
end

function GroupItemLevels:InspectNext()
	if self.Pending or InCombatLockdown() then return end
	if InspectFrame and InspectFrame:IsShown() then
		C_Timer.After(2, function() GroupItemLevels:InspectNext() end)
		return
	end

	while #self.Queue > 0 do
		local entry = table.remove(self.Queue, 1)
		if IsTrue(UnitExists(entry.unit)) and UnitGUID(entry.unit) == entry.guid and IsTrue(UnitIsConnected(entry.unit)) and IsTrue(UnitIsVisible(entry.unit)) and IsTrue(CanInspect(entry.unit)) then
			entry.status = "Inspecting"
			self.Pending = { guid = entry.guid, unit = entry.unit, requested = GetTime() or 0 }
			local ok = pcall(NotifyInspect, entry.unit)
			if ok then
				C_Timer.After(4, function()
					if GroupItemLevels.Pending and GroupItemLevels.Pending.guid == entry.guid then
						GroupItemLevels.Pending = nil
						entry.status = "Timed out"
						GroupItemLevels:RefreshDisplays()
						GroupItemLevels:InspectNext()
					end
				end)
				return
			end
			self.Pending = nil
			entry.status = "Unavailable"
		end
	end
	self:RefreshDisplays()
end

function GroupItemLevels:OnInspectReady(guid)
	if not self.Pending or not SafeString(guid) or guid ~= self.Pending.guid then return end
	local pending = self.Pending
	self.Pending = nil
	local itemLevel = EquippedItemLevel(pending.unit)
	if itemLevel then
		self.Cache[pending.guid] = { itemLevel = itemLevel, updated = GetTime() or 0 }
	end
	for _, entry in ipairs(self.Roster) do
		if entry.guid == pending.guid then
			entry.itemLevel = itemLevel
			entry.status = itemLevel and "Ready" or "Unavailable"
			break
		end
	end
	self:SortRoster()
	if type(ClearInspectPlayer) == "function" and (not InspectFrame or not InspectFrame:IsShown()) then pcall(ClearInspectPlayer) end
	self:RefreshDisplays()
	C_Timer.After(1.5, function() GroupItemLevels:InspectNext() end)
end

function GroupItemLevels:GetSummary()
	local total, known = 0, 0
	for _, entry in ipairs(self.Roster) do
		if entry.itemLevel then total, known = total + entry.itemLevel, known + 1 end
	end
	return known > 0 and total / known or nil, known, #self.Roster
end

function GroupItemLevels:GetBarText()
	local average, known, count = self:GetSummary()
	if count <= 1 then return average and string.format("iLvl: %.1f", average) or "iLvl: ?" end
	return average and string.format("Group iLvl: %.1f (%d/%d)", average, known, count) or string.format("Group iLvl: ? (0/%d)", count)
end

function GroupItemLevels:BuildTooltip(tooltip)
	if not self.Pending and #self.Queue == 0 and not InCombatLockdown() then self:QueueInspections(false) end
	local average, known, count = self:GetSummary()
	tooltip:AddLine("Group Item Levels", unpack(Theme.Colors.goldText))
	tooltip:AddDoubleLine("Known", string.format("%d/%d", known, count), 0.80, 0.84, 0.92, 1, 1, 1)
	if average then tooltip:AddDoubleLine("Group average", string.format("%.1f", average), 0.80, 0.84, 0.92, 0.40, 0.82, 1.00) end
	tooltip:AddLine(" ")
	for _, entry in ipairs(self.Roster) do
		local r, g, b = ClassColor(entry.classFile)
		local value = entry.itemLevel and string.format("%.1f", entry.itemLevel) or entry.status
		local vr, vg, vb = entry.itemLevel and 0.40 or 0.68, entry.itemLevel and 0.90 or 0.71, entry.itemLevel and 1.00 or 0.78
		tooltip:AddDoubleLine(entry.name, value, r, g, b, vr, vg, vb)
	end
	if InCombatLockdown() then
		tooltip:AddLine("Scanning pauses during combat.", 1.00, 0.52, 0.30, true)
	elseif known < count then
		tooltip:AddLine("Stay near group members while SlickUI inspects one at a time.", 0.65, 0.72, 0.82, true)
	end
end

function GroupItemLevels:CreateWindow()
	if self.Window then return self.Window end
	local window = CreateFrame("Frame", "SlickUIGroupItemLevelsFrame", UIParent, "BackdropTemplate")
	window:SetSize(590, 570)
	window:SetPoint("CENTER")
	window:SetFrameStrata("DIALOG")
	window:SetClampedToScreen(true)
	window:SetMovable(true)
	window:EnableMouse(true)
	window:RegisterForDrag("LeftButton")
	window:SetScript("OnDragStart", window.StartMoving)
	window:SetScript("OnDragStop", window.StopMovingOrSizing)
	Theme:CreatePanel(window)
	window:Hide()
	table.insert(UISpecialFrames, window:GetName())

	local title = Theme:CreateText(window, 18)
	title:SetPoint("TOPLEFT", 18, -16)
	title:SetText("SlickUI Group Item Levels")
	title:SetTextColor(unpack(Theme.Colors.goldText))
	local close = CreateFrame("Button", nil, window, "UIPanelCloseButton")
	close:SetPoint("TOPRIGHT", -4, -4)
	local summary = Theme:CreateText(window, 11)
	summary:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
	window.Summary = summary
	local refresh = CreateFrame("Button", nil, window, "UIPanelButtonTemplate")
	refresh:SetSize(130, 24)
	refresh:SetPoint("TOPRIGHT", -42, -48)
	refresh:SetText("Refresh Group")
	refresh:SetScript("OnClick", function() GroupItemLevels:RefreshRoster(true) end)

	local header = CreateFrame("Frame", nil, window, "BackdropTemplate")
	header:SetPoint("TOPLEFT", 18, -86)
	header:SetPoint("TOPRIGHT", -18, -86)
	header:SetHeight(24)
	header:SetBackdrop({ bgFile = Theme.Texture })
	header:SetBackdropColor(unpack(Theme.Colors.panelSoft))
	for _, info in ipairs({ {"Player", 8}, {"Role", 225}, {"Level", 340}, {"Item Level", 410}, {"Status", 490} }) do
		local text = Theme:CreateText(header, 10)
		text:SetPoint("LEFT", info[2], 0)
		text:SetText(info[1])
		text:SetTextColor(unpack(Theme.Colors.goldText))
	end

	local list = CreateFrame("Frame", nil, window)
	list:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -2)
	list:SetPoint("BOTTOMRIGHT", window, "BOTTOMRIGHT", -18, 42)
	list:EnableMouseWheel(true)
	list:SetScript("OnMouseWheel", function(_, delta)
		local maximum = math.max(0, #GroupItemLevels.Roster - GroupItemLevels.VisibleRows)
		GroupItemLevels.ScrollOffset = math.max(0, math.min(maximum, GroupItemLevels.ScrollOffset - delta * 3))
		GroupItemLevels:RefreshWindow()
	end)
	window.Rows = {}
	for index = 1, self.VisibleRows do
		local row = CreateFrame("Frame", nil, list, "BackdropTemplate")
		row:SetPoint("TOPLEFT", 0, -(index - 1) * 27)
		row:SetPoint("TOPRIGHT", 0, -(index - 1) * 27)
		row:SetHeight(25)
		row:SetBackdrop({ bgFile = Theme.Texture })
		row:SetBackdropColor(index % 2 == 0 and 0.07 or 0.04, 0.05, 0.08, 0.72)
		row.Fields = {}
		for field, info in ipairs({ {8,210}, {225,105}, {340,60}, {410,70}, {490,78} }) do
			local text = Theme:CreateText(row, 9)
			text:SetPoint("LEFT", info[1], 0)
			text:SetWidth(info[2])
			text:SetJustifyH("LEFT")
			row.Fields[field] = text
		end
		window.Rows[index] = row
	end
	local hint = Theme:CreateText(window, 9)
	hint:SetPoint("BOTTOMLEFT", 18, 16)
	hint:SetText("Item levels are cached. Scanning pauses in combat and waits for members who are offline or out of range.")
	hint:SetTextColor(unpack(Theme.Colors.mutedText))
	self.Window = window
	return window
end

function GroupItemLevels:RefreshWindow()
	if not self.Window then return end
	local average, known, count = self:GetSummary()
	self.Window.Summary:SetText(string.format("Members: %d    Known: %d    Average: %s", count, known, average and string.format("%.1f", average) or "--"))
	local maximum = math.max(0, count - self.VisibleRows)
	self.ScrollOffset = math.min(self.ScrollOffset, maximum)
	for index, row in ipairs(self.Window.Rows) do
		local entry = self.Roster[self.ScrollOffset + index]
		row:SetShown(entry ~= nil)
		if entry then
			local r, g, b = ClassColor(entry.classFile)
			row.Fields[1]:SetText(entry.name)
			row.Fields[1]:SetTextColor(r, g, b)
			row.Fields[2]:SetText(RoleText(entry.role))
			row.Fields[3]:SetText(entry.level or "--")
			row.Fields[4]:SetText(entry.itemLevel and string.format("%.1f", entry.itemLevel) or "--")
			row.Fields[5]:SetText(entry.status or "Waiting")
		end
	end
end

function GroupItemLevels:RefreshDisplays()
	if self.Window and self.Window:IsShown() then self:RefreshWindow() end
	if SlickUI.InformationBars then SlickUI.InformationBars:UpdateBlocks() end
end

function GroupItemLevels:RefreshRoster(force)
	self:BuildRoster()
	self:RefreshDisplays()
	local block = SlickUI.DB and SlickUI.DB.informationBars and SlickUI.DB.informationBars.blocks and SlickUI.DB.informationBars.blocks.groupItemLevel
	local active = (block and block.enabled) or (self.Window and self.Window:IsShown())
	if active then self:QueueInspections(force == true) end
end

function GroupItemLevels:ShowWindow()
	local window = self:CreateWindow()
	self:RefreshRoster(false)
	self:RefreshWindow()
	window:Show()
	window:Raise()
end

function GroupItemLevels:ToggleWindow()
	if self.Window and self.Window:IsShown() then self.Window:Hide() else self:ShowWindow() end
end

function GroupItemLevels:Initialize()
	local frame = CreateFrame("Frame")
	for _, event in ipairs({ "PLAYER_ENTERING_WORLD", "GROUP_ROSTER_UPDATE", "PLAYER_EQUIPMENT_CHANGED", "PLAYER_REGEN_ENABLED", "INSPECT_READY" }) do frame:RegisterEvent(event) end
	frame:SetScript("OnEvent", function(_, event, ...)
		if event == "INSPECT_READY" then
			GroupItemLevels:OnInspectReady(...)
		elseif event == "PLAYER_REGEN_ENABLED" then
			GroupItemLevels:QueueInspections(false)
		else
			C_Timer.After(0.5, function() GroupItemLevels:RefreshRoster(event == "PLAYER_EQUIPMENT_CHANGED") end)
		end
	end)
	self.Driver = frame
	self:RefreshRoster(false)
end

SlickUI:RegisterModule("GroupItemLevels", GroupItemLevels)
