--[[
	Cerebro - Main UI Frame
	Main window and UI management
]]--

local AddonName, PrivateTable = ...
local Cerebro = LibStub("AceAddon-3.0"):GetAddon(AddonName)
local AceGUI = LibStub("AceGUI-3.0")

local UI = Cerebro:NewModule("UI", "AceEvent-3.0")

-- UI State
UI.mainFrame = nil
UI.currentTab = "standings"
UI.standingsSortKey = "pr"
UI.refreshTimer = nil
UI.refreshDelay = 0.3 -- seconds
UI.searchText = {} -- Search state for History/Audit tabs

function UI:OnInitialize()
	-- Init
end

function UI:OnEnable()
	-- Enable
end

--[[=============================================
	Helper Methods (Nil-Safe, RBAC, Refresh)
===============================================]]

function UI:GetModuleSafe(name)
	local mod = Cerebro:GetModule(name, true)  -- silent=true prevents error
	if not mod then
		Cerebro:Debug("UI: Module '" .. name .. "' not available")
	end
	return mod
end

function UI:IsOfficerSafe()
	return Cerebro and Cerebro.IsOfficer and Cerebro:IsOfficer() or false
end

function UI:SafeCall(fn, ...)
	local success, result = pcall(fn, ...)
	if not success then
		Cerebro:Debug("UI SafeCall error: " .. tostring(result))
	end
	return success, result
end

function UI:RequestRefresh()
	if self.refreshTimer then
		Cerebro:CancelTimer(self.refreshTimer)
	end
	self.refreshTimer = Cerebro:ScheduleTimer(function()
		self:RefreshCurrentTab()
		self.refreshTimer = nil
	end, self.refreshDelay)
end

function UI:RefreshCurrentTab()
	if self.mainFrame and self.mainFrame:IsShown() and self.currentTab and self.tabGroup then
		self:SelectTab(self.tabGroup, self.currentTab)
	end
end

function UI:ShowUnavailableMessage(container, featureName)
	local label = AceGUI:Create("Label")
	label:SetText(featureName .. " is currently unavailable.")
	label:SetFullWidth(true)
	container:AddChild(label)
end

--[[=============================================
	Custom Widgets
===============================================]]

-- SimpleHTML Widget for clickable history
--[[=============================================
	Main Window
===============================================]]

function UI:Toggle()
	if self.mainFrame and self.mainFrame:IsShown() then
		self:Hide()
	else
		self:Show()
	end
end

function UI:Update()
	if self.mainFrame and self.mainFrame:IsShown() then
		-- Refresh current tab content instead of full rebuild if possible
		if self.currentTab then
			self:SelectTab(self.tabGroup, self.currentTab)
		else
			self.tabGroup:SelectTab("myprogress")
		end
	end
end

function UI:Show()
	-- Scope verification (Guild Locking)
	local scope = Cerebro:GetAuthorizedGuild()
	local scopeStatus = Cerebro:VerifyGuildScope()
	
	-- Initialize retry counter if needed
	self.scopeRetryCount = self.scopeRetryCount or 0
	local MAX_SCOPE_RETRIES = 3
	
	-- Handle pending state (API not ready)
	if scopeStatus == nil then
		if self.scopeRetryCount < MAX_SCOPE_RETRIES then
			self.scopeRetryCount = self.scopeRetryCount + 1
			Cerebro:Debug("Scope pending, retry " .. self.scopeRetryCount .. "/" .. MAX_SCOPE_RETRIES)
			Cerebro:Print("Loading guild info, please wait...")
			Cerebro:ScheduleTimer(function() self:Show() end, 2)
		else
			Cerebro:Print("Unable to verify guild. Please /reload and try again.")
			self.scopeRetryCount = 0
		end
		return
	end
	
	self.scopeRetryCount = 0  -- Reset on success
	
	-- Handle mismatch state (out of guild)
	if scopeStatus == false then
		self:ShowRestrictedAccess()
		return
	end
	
	-- Bootstrapping: No scope set yet, officer needs to confirm
	if not scope and Cerebro:IsOfficerSafe() then
		self:ShowScopeConfirmation()
		return
	end
	
	-- Normal flow: Show main frame
	-- Check if new UI is enabled
	local useNewUI = Cerebro.db.global.config and 
	                 Cerebro.db.global.config.ui and 
	                 Cerebro.db.global.config.ui.useNewUI
	
	if useNewUI then
		-- Delegate to the new native frame UI (UISandbox style)
		local UISandbox = Cerebro:GetModule("UISandbox", true)
		if UISandbox then
			UISandbox:Show()
			return
		else
			Cerebro:Print("New UI module not available. Using classic UI.")
		end
	end
	
	-- Legacy AceGUI flow
	if not self.mainFrame then
		self:CreateMainFrame()
	else
		-- Rebuild tabs to respect current officer status
		self:RebuildTabs()
	end
	
	-- Update status text with current season info
	self:UpdateStatusText()

	-- Start sync status refresh timer (30s = V2 heartbeat cadence)
	self:StartSyncStatusTimer()

	-- Run repair on show to fix missing classes/data
	Cerebro:RepairDatabase()

	self.mainFrame:Show()
	self:RefreshStandings()
	
	-- Trigger onboarding wizard for new players (runs after guild lock is resolved)
	local Onboarding = Cerebro:GetModule("Onboarding", true)
	if Onboarding then
		Onboarding:Show()  -- Show will check onboardingCompleted flag internally
	end
end

-- Show scope confirmation dialog for bootstrapping (officer sets initial scope)
function UI:ShowScopeConfirmation()
	local guildName = GetGuildInfo("player")
	if not guildName then
		Cerebro:Print("Guild info not ready. Please try again in a moment.")
		return
	end
	
	-- Show the confirmation popup with guild name
	StaticPopup_Show("CEREBRO_CONFIRM_SCOPE_LOCK", guildName)
end

-- Show restricted access screen for out-of-guild characters
function UI:ShowRestrictedAccess()
	-- Hide main frame if it exists
	if self.mainFrame then
		self.mainFrame:Hide()
	end
	
	-- Hide previous restricted frame if exists
	if self.restrictedFrame then
		self.restrictedFrame:Release()
		self.restrictedFrame = nil
	end
	
	local scope = Cerebro:GetAuthorizedGuild()
	
	local frame = AceGUI:Create("Frame")
	frame:SetTitle("Cerebro - Restricted Access")
	frame:SetWidth(450)
	frame:SetHeight(250)
	frame:SetLayout("Flow")
	frame:SetCallback("OnClose", function(widget)
		widget:Release()
		self.restrictedFrame = nil
	end)
	
	-- Enable Escape key to close the window
	_G["CerebroRestrictedFrame"] = frame.frame
	tinsert(UISpecialFrames, "CerebroRestrictedFrame")
	
	-- Spacer
	local spacer1 = AceGUI:Create("Label")
	spacer1:SetText(" ")
	spacer1:SetFullWidth(true)
	frame:AddChild(spacer1)
	
	-- Warning icon/header
	local header = AceGUI:Create("Label")
	header:SetText("|cffff4444⚠ Access Restricted|r")
	header:SetFullWidth(true)
	header:SetFont(GameFontNormalLarge:GetFont())
	frame:AddChild(header)
	
	-- Spacer
	local spacer2 = AceGUI:Create("Label")
	spacer2:SetText(" ")
	spacer2:SetFullWidth(true)
	frame:AddChild(spacer2)
	
	-- Message
	local label = AceGUI:Create("Label")
	local guildDisplay = scope and scope.guildName or "Unknown"
	local realmDisplay = scope and scope.realm or "Unknown"
	label:SetText(string.format(
		"This character is not in the authorized guild.\n\n" ..
		"Locked to: |cff00ff00%s|r - |cff888888%s|r\n\n" ..
		"Log in with a character in that guild to access Cerebro.",
		guildDisplay, realmDisplay
	))
	label:SetFullWidth(true)
	frame:AddChild(label)
	
	-- Spacer
	local spacer3 = AceGUI:Create("Label")
	spacer3:SetText(" ")
	spacer3:SetFullWidth(true)
	frame:AddChild(spacer3)
	
	-- Override button (GM or Officer with permission)
	local canOverride = Cerebro:IsGuildMasterSafe() or 
		(Cerebro:IsOfficerSafe() and Cerebro.db.global.config.admin.allowOfficerScopeReset)
	
	if canOverride then
		local btn = AceGUI:Create("Button")
		btn:SetText("Switch Guild (Wipes All Data)")
		btn:SetWidth(250)
		btn:SetCallback("OnClick", function()
			StaticPopup_Show("CEREBRO_CONFIRM_SCOPE_OVERRIDE")
		end)
		frame:AddChild(btn)
	end
	
	frame:Show()
	self.restrictedFrame = frame
end


-- Refresh the footer status text with current version and season info
function UI:UpdateStatusText()
	if not self.mainFrame then return end
	
	-- Sync status indicator prefix
	local syncPrefix = self:GetSyncStatusText()
	
	local statusParts = {}
	if syncPrefix then
		table.insert(statusParts, syncPrefix)
	end
	table.insert(statusParts, "v" .. Cerebro.version)
	
	-- Add roster hash for visual sync comparison across clients
	local commV2 = Cerebro:GetModule("CommunicationV2", true)
	if commV2 and commV2.ComputeRosterHash then
		local rosterHash = commV2:ComputeRosterHash()
		if rosterHash then
			table.insert(statusParts, rosterHash)
		end
	end
	
	-- Add game version with Retail/PTR/Beta label
	local ver = Cerebro:GetGameVersion()
	if ver and ver.version then
		local buildType = ver.isRelease and "Retail" or "PTR/Beta"
		table.insert(statusParts, buildType .. " " .. ver.version)
	end
	
	-- Add season info (spelled out)
	local info = Cerebro:GetSeasonInfo()
	if info then
		if info.isPrepatch then
			table.insert(statusParts, "Pre-Patch")
		elseif info.seasonID and info.seasonID > 0 then
			table.insert(statusParts, "Season " .. info.seasonID)
			if info.week then
				table.insert(statusParts, "Week " .. info.week)
			elseif info.beforeStart then
				table.insert(statusParts, "Starting Soon")
			end
		end
	end
	
	self.mainFrame:SetStatusText(table.concat(statusParts, " | "))
end

-- Get colored sync status text for footer
function UI:GetSyncStatusText()
	local commV2 = Cerebro:GetModule("CommunicationV2", true)
	if not commV2 then return nil end
	
	local state, details = commV2:GetSyncStatus()
	
	if state == "synced" then
		return "|cff00ff00Synced|r"
	elseif state == "syncing" then
		return "|cffffff00Syncing...|r"
	elseif state == "diverged" then
		local count = details and details.divergedCount or 0
		return string.format("|cffff4444Diverged (%d)|r", count)
	elseif state == "no_peers" then
		return "|cff888888No Peers|r"
	end
	
	return nil
end

-- Start repeating timer to refresh sync status in footer (30s = V2 heartbeat)
function UI:StartSyncStatusTimer()
	self:StopSyncStatusTimer()
	self.syncStatusTimer = Cerebro:ScheduleRepeatingTimer(function()
		self:UpdateStatusText()
	end, 30)
end

-- Cancel sync status refresh timer
function UI:StopSyncStatusTimer()
	if self.syncStatusTimer then
		Cerebro:CancelTimer(self.syncStatusTimer)
		self.syncStatusTimer = nil
	end
end

function UI:Hide()
	-- Cancel sync status timer
	self:StopSyncStatusTimer()
	
	-- Hide legacy UI
	if self.mainFrame then
		self.mainFrame:Hide()
	end
	if self.restrictedFrame then
		self.restrictedFrame:Release()
		self.restrictedFrame = nil
	end
	
	-- Also hide new UI if active
	local UISandbox = Cerebro:GetModule("UISandbox", true)
	if UISandbox then
		UISandbox:Hide()
	end
end

function UI:CreateMainFrame()
	-- Create main frame using AceGUI
	local frame = AceGUI:Create("Frame")
	frame:SetTitle("Cerebro - EPGP System")
	
	-- Enable Escape key to close the window
	-- Give the underlying frame a global name and register with UISpecialFrames
	_G["CerebroMainFrame"] = frame.frame
	tinsert(UISpecialFrames, "CerebroMainFrame")
	
	-- Build status text with version, game version, and season info
	local statusParts = {"v" .. Cerebro.version}
	
	-- Add roster hash for visual sync comparison across clients
	local commV2 = Cerebro:GetModule("CommunicationV2", true)
	if commV2 and commV2.ComputeRosterHash then
		local rosterHash = commV2:ComputeRosterHash()
		if rosterHash then
			table.insert(statusParts, rosterHash)
		end
	end
	
	-- Add game version with Retail/PTR/Beta label
	local ver = Cerebro:GetGameVersion()
	if ver and ver.version then
		local buildType = ver.isRelease and "Retail" or "PTR/Beta"
		table.insert(statusParts, buildType .. " " .. ver.version)
	end
	
	-- Add season info (spelled out)
	local info = Cerebro:GetSeasonInfo()
	if info then
		if info.isPrepatch then
			table.insert(statusParts, "Pre-Patch")
		elseif info.seasonID and info.seasonID > 0 then
			table.insert(statusParts, "Season " .. info.seasonID)
			if info.week then
				table.insert(statusParts, "Week " .. info.week)
			elseif info.beforeStart then
				table.insert(statusParts, "Starting Soon")
			end
		end
	end
	
	frame:SetStatusText(table.concat(statusParts, " | "))
	frame:SetLayout("Fill")
	frame:SetWidth(890)
	frame:SetHeight(550)

	-- Set callbacks
	frame:SetCallback("OnClose", function(widget)
		self:Hide()
	end)
	
	-- Refresh current tab when frame regains focus (fixes desync with Blizzard Options)
	frame.frame:SetScript("OnShow", function()
		if self.currentTab == "weights" or self.currentTab == "settings" then
			Cerebro:ScheduleTimer(function()
				if self.tabGroup and self.currentTab then
					self:SelectTab(self.tabGroup, self.currentTab)
				end
			end, 0.1)
		end
	end)

	-- Create tab group
	local tabGroup = AceGUI:Create("TabGroup")
	tabGroup:SetLayout("Flow")
	
	-- Build tab list with officer-only filtering
	local tabs = self:BuildTabList()
	
	tabGroup:SetTabs(tabs)

	tabGroup:SetCallback("OnGroupSelected", function(container, event, group)
		self:SelectTab(container, group)
	end)

	-- Default to My Progress
	tabGroup:SelectTab("myprogress")

	frame:AddChild(tabGroup)

	self.mainFrame = frame
	self.tabGroup = tabGroup
end

function UI:BuildTabList()
	local tabs = {
		{text="My Progress", value="myprogress"},
		{text="Standings", value="standings"},
		{text="Wishlist", value="wishlist"},
		{text="History", value="history"},
	}
	
	-- Add officer-only tabs
	if self:IsOfficerSafe() then
		table.insert(tabs, {text="Officer Tools", value="officer"})
		table.insert(tabs, {text="Guild M+ History", value="raider_audit"})
		table.insert(tabs, {text="System Logs", value="logs"})
		table.insert(tabs, {text="Settings", value="settings"})
		table.insert(tabs, {text="Weights", value="weights"})
	end
	
	return tabs
end

function UI:RebuildTabs()
	if not self.tabGroup then
		return
	end
	
	local tabs = self:BuildTabList()
	self.tabGroup:SetTabs(tabs)
end

function UI:SelectTab(container, tab)
	-- Prevent StandingsFrame from hijacking recycled widgets
	Cerebro:GetModule("StandingsFrame"):Clear()

	container:ReleaseChildren()
	self.currentTab = tab

	if tab == "myprogress" then
		-- Check if module exists (safeguard)
		if Cerebro.modules.MyProgress then
			Cerebro.modules.MyProgress:Draw(container)
		else
			local lbl = AceGUI:Create("Label")
			lbl:SetText("My Progress module not loaded.")
			container:AddChild(lbl)
		end
	elseif tab == "standings" then
		Cerebro:GetModule("StandingsFrame"):Draw(container)
	elseif tab == "wishlist" then
		self:DrawWishlistTab(container)
	elseif tab == "history" then
		self:DrawHistoryTab(container)
	elseif tab == "officer" then
		if Cerebro:IsOfficer() then
			self:DrawOfficerTab(container)
		else
			local label = AceGUI:Create("Label")
			label:SetText("Officer tools are only available to raid officers and leaders.")
			label:SetFullWidth(true)
			container:AddChild(label)
		end

    elseif tab == "raider_audit" then
        if Cerebro:IsOfficer() then
            self:DrawRaiderAudit(container)
        else
            local label = AceGUI:Create("Label")
            label:SetText("Officer tools are only available to raid officers and leaders.")
            label:SetFullWidth(true)
            container:AddChild(label)
        end
	elseif tab == "logs" then
		if Cerebro:IsOfficer() then
			self:DrawAuditTab(container)
		else
			local label = AceGUI:Create("Label")
			label:SetText("Officer tools are only available to raid officers and leaders.")
			label:SetFullWidth(true)
			container:AddChild(label)
		end
	elseif tab == "settings" then
		if self:IsOfficerSafe() then
			self:DrawSettingsTab(container)
		else
			local label = AceGUI:Create("Label")
			label:SetText("Settings are only available to raid officers and leaders.")
			label:SetFullWidth(true)
			container:AddChild(label)
		end
	elseif tab == "weights" then
		if self:IsOfficerSafe() then
			self:DrawWeightsTab(container)
		else
			local label = AceGUI:Create("Label")
			label:SetText("Weights configuration is only available to raid officers and leaders.")
			label:SetFullWidth(true)
			container:AddChild(label)
		end
	end
end

--[[=============================================
	Officer Tab
===============================================]]

function UI:DrawOfficerTab(container)
	-- Wrapper group to provide proper sizing for ScrollFrame
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetFullWidth(true)
	wrapper:SetFullHeight(true)
	wrapper:SetLayout("Fill")
	container:AddChild(wrapper)
	
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	scroll:SetLayout("Flow")
	wrapper:AddChild(scroll)

	-- Session
	local sessionGroup = AceGUI:Create("InlineGroup")
	sessionGroup:SetTitle("Session Management")
	sessionGroup:SetFullWidth(true)
	sessionGroup:SetLayout("Flow")
	scroll:AddChild(sessionGroup)

	local session = Cerebro.modules.Session and Cerebro.modules.Session:GetCurrentSession()

	if session then
		local label = AceGUI:Create("Label")
		label:SetText(string.format("|cff00ff00Active Session:|r %s", session.raidName or "Raid"))
		label:SetFullWidth(true)
		sessionGroup:AddChild(label)

		local endBtn = AceGUI:Create("Button")
		endBtn:SetText("End Session")
		endBtn:SetWidth(150)
		endBtn:SetCallback("OnClick", function()
			Cerebro.modules.Session:EndSession()
			self:SelectTab(self.tabGroup, "officer")
		end)
		sessionGroup:AddChild(endBtn)

		-- Test Mode Toggle
		local testCheck = AceGUI:Create("CheckBox")
		testCheck:SetLabel("Enable RCLC Test Awards")
		testCheck:SetDescription("If checked, RCLC 'Test Mode' awards will charge GP and be logged.")
		testCheck:SetValue(Cerebro.db.global.config.epgp.allowTestAwards)
		testCheck:SetCallback("OnValueChanged", function(widget, event, value)
			Cerebro.db.global.config.epgp.allowTestAwards = value
			Cerebro:Print("Test Awards are now " .. (value and "ENABLED" or "DISABLED"))
		end)
		testCheck:SetWidth(200)
		sessionGroup:AddChild(testCheck)

	else
		local startBtn = AceGUI:Create("Button")
		startBtn:SetText("Start Session")
		startBtn:SetWidth(150)
	startBtn:SetCallback("OnClick", function()
		Cerebro.modules.Session:StartSession()
		self:SelectTab(self.tabGroup, "officer")
	end)
		sessionGroup:AddChild(startBtn)
	end

	-- Prep Check
	local prepBtn = AceGUI:Create("Button")
	prepBtn:SetText("Run Prep Check")
	prepBtn:SetWidth(150)
	prepBtn:SetCallback("OnClick", function()
		Cerebro.modules.Preparation:CheckRaid()
	end)
	sessionGroup:AddChild(prepBtn)
	
	-- Decay Button (In line with Prep)
	local decayBtn = AceGUI:Create("Button")
	decayBtn:SetText("Run Weekly Decay")
	decayBtn:SetWidth(150)
	decayBtn:SetCallback("OnClick", function()
		StaticPopup_Show("CEREBRO_CONFIRM_DECAY")
	end)
	sessionGroup:AddChild(decayBtn)
    
    -- Candidate Management
    local candidateGroup = AceGUI:Create("InlineGroup")
    candidateGroup:SetTitle("Admit Candidates")
    candidateGroup:SetFullWidth(true)
    candidateGroup:SetLayout("Flow")
    scroll:AddChild(candidateGroup)
    
    local scanBtn = AceGUI:Create("Button")
    scanBtn:SetText("Scan Guild for Candidates")
    scanBtn:SetWidth(200)
    scanBtn:SetCallback("OnClick", function()
        if Cerebro.modules.Communication then
            Cerebro.modules.CommunicationV2:BroadcastDiscovery()
            Cerebro:Print("Scanning guild for pending candidates...")
        end
    end)
    candidateGroup:AddChild(scanBtn)
    
    local pendingList = {}
    local visibleRanks = Cerebro.db.global.config and Cerebro.db.global.config.sync
        and Cerebro.db.global.config.sync.visibleRanks
    for name, data in pairs(Cerebro.db.global.players) do
        if data.status == "Pending" then
            -- Apply rank visibility filter (fail-closed: unknown rank = excluded)
            local rankVisible = true
            if visibleRanks then
                local rankIndex = Cerebro:GetPlayerGuildRankIndex(name)
                if rankIndex == nil or not visibleRanks[rankIndex] then
                    rankVisible = false
                end
            end
            if rankVisible then
                pendingList[name] = string.format("%s (Seen: %s)", name, date("%m/%d", data.lastSeen or 0))
            end
        end
    end
    
    if next(pendingList) then
        local candidateSelect = AceGUI:Create("Dropdown")
        candidateSelect:SetLabel("Select Candidate")
        candidateSelect:SetWidth(250)
        candidateSelect:SetList(pendingList)
        candidateGroup:AddChild(candidateSelect)
        
        local admitBtn = AceGUI:Create("Button")
        admitBtn:SetText("Admit to Core")
        admitBtn:SetWidth(150)
        admitBtn:SetCallback("OnClick", function()
            local target = candidateSelect:GetValue()
            if target then
                local data = Cerebro:GetPlayerData(target)
                data.status = "Core"
                data.coreAdmittedAt = time()  -- Track admission time for fair attendance %
                data.lastUpdate = time()      -- Ensure sync propagation via LWW
                
                -- Try to populate class from guild roster if missing
                if not data.class then
                    for i = 1, GetNumGuildMembers() do
                        local name, _, _, _, _, _, _, _, _, _, classFileName = GetGuildRosterInfo(i)
                        if name and Cerebro:GetFullPlayerName(name) == target then
                            data.class = classFileName
                            break
                        end
                    end
                end
                
                Cerebro:Print(string.format("Admitted %s to Core.", Cerebro:ColorPlayerName(target)))
                Cerebro.Ledger:AddAuditLog("ADMIT_PLAYER", "Promoted " .. target .. " to Core status.")
                
                -- Broadcast update to GUILD (Real-time sync) - include class
                if Cerebro.modules.CommunicationV2 and Cerebro.modules.CommunicationV2.BroadcastPlayerUpdate then
                    Cerebro.modules.CommunicationV2:BroadcastPlayerUpdate(target, {status = "Core", class = data.class})
                end
                
                self:RefreshStandings()
                self:DrawOfficerTab(container) -- Redraw to remove from list
            end
        end)
        candidateGroup:AddChild(admitBtn)
    else
        local lbl = AceGUI:Create("Label")
        lbl:SetText("No pending candidates.")
        lbl:SetColor(0.5, 0.5, 0.5)
        candidateGroup:AddChild(lbl)
    end

	-- Fix 5: Standings Visibility (Guild Ranks)
	local rankGroup = AceGUI:Create("InlineGroup")
	rankGroup:SetTitle("Standings Visibility (Guild Ranks)")
	rankGroup:SetFullWidth(true)
	rankGroup:SetLayout("Flow")
	scroll:AddChild(rankGroup)

	local rankDesc = AceGUI:Create("Label")
	rankDesc:SetText("Select which guild ranks appear in Standings and are included in sync. Unchecked ranks will be hidden and excluded from data sync.")
	rankDesc:SetFullWidth(true)
	rankDesc:SetColor(0.7, 0.7, 0.7)
	rankGroup:AddChild(rankDesc)

	-- Spacer
	local rankSpacer = AceGUI:Create("Label")
	rankSpacer:SetText(" ")
	rankSpacer:SetFullWidth(true)
	rankGroup:AddChild(rankSpacer)

	-- Get current visibleRanks config
	if not Cerebro.db.global.config.sync then
		Cerebro.db.global.config.sync = {}
	end
	if not Cerebro.db.global.config.sync.visibleRanks then
		Cerebro.db.global.config.sync.visibleRanks = {}
	end
	local visibleRanks = Cerebro.db.global.config.sync.visibleRanks

	-- Auto-detect: if empty, populate from existing player data ranks
	local hasAny = false
	for _ in pairs(visibleRanks) do hasAny = true; break end
	if not hasAny then
		local players = Cerebro.db.global.players or {}
		for _, p in pairs(players) do
			if p.rankIndex ~= nil then
				visibleRanks[p.rankIndex] = true
			end
		end
	end

	-- Dynamically list all guild ranks with checkboxes
	local numRanks = GuildControlGetNumRanks and GuildControlGetNumRanks() or 0
	if numRanks > 0 then
		for rankIndex = 0, numRanks - 1 do
			local rankName = GuildControlGetRankName and GuildControlGetRankName(rankIndex + 1) or ("Rank " .. rankIndex)
			local cb = AceGUI:Create("CheckBox")
			cb:SetLabel(string.format("%s (Rank %d)", rankName, rankIndex))
			cb:SetValue(visibleRanks[rankIndex] or false)
			cb:SetWidth(200)
			cb:SetCallback("OnValueChanged", function(_, _, value)
				visibleRanks[rankIndex] = value or nil  -- Remove key if unchecked
				Cerebro:Print(string.format("Rank '%s' (%d) is now %s in Standings.",
					rankName, rankIndex, value and "VISIBLE" or "HIDDEN"))
				Cerebro.Ledger:AddAuditLog("RANK_VISIBILITY",
					string.format("%s rank %d (%s)", value and "Enabled" or "Disabled", rankIndex, rankName))
			end)
			rankGroup:AddChild(cb)
		end
	else
		local noRanksLabel = AceGUI:Create("Label")
		noRanksLabel:SetText("|cffff4444Guild rank data not available. Try reopening this tab.|r")
		noRanksLabel:SetFullWidth(true)
		rankGroup:AddChild(noRanksLabel)
	end

	-- Manual Adjustments
	local adjGroup = AceGUI:Create("InlineGroup")
	adjGroup:SetTitle("Manual EP/GP Adjustment")
	adjGroup:SetFullWidth(true)
	adjGroup:SetLayout("Flow")
	scroll:AddChild(adjGroup)

	-- Player Select
	local playerSelect = AceGUI:Create("Dropdown")
	playerSelect:SetLabel("Player")
	playerSelect:SetWidth(200)
	local playerList = {}
	local adjVisibleRanks = Cerebro.db.global.config and Cerebro.db.global.config.sync
		and Cerebro.db.global.config.sync.visibleRanks
	for name, data in pairs(Cerebro.db.global.players) do
		-- Apply rank visibility filter (same as Admit Candidates)
		local rankVisible = true
		if adjVisibleRanks then
			local rankIndex = data.rankIndex or Cerebro:GetPlayerGuildRankIndex(name)
			if rankIndex == nil or not adjVisibleRanks[rankIndex] then
				rankVisible = false
			end
		end
		if rankVisible then
			playerList[name] = name
		end
	end
	playerSelect:SetList(playerList)
	adjGroup:AddChild(playerSelect)

	-- Operation
	local opSelect = AceGUI:Create("Dropdown")
	opSelect:SetLabel("Operation")
	opSelect:SetList({
		["add_ep"] = "Add EP (+)",
		["sub_ep"] = "Subtract EP (-)",
		["add_gp"] = "Add GP (+)",
		["sub_gp"] = "Subtract GP (-)",
	})
	opSelect:SetValue("add_ep")
	opSelect:SetWidth(150)
	adjGroup:AddChild(opSelect)

	-- Amount
	local amountBox = AceGUI:Create("EditBox")
	amountBox:SetLabel("Amount")
	amountBox:SetWidth(100)
	adjGroup:AddChild(amountBox)

	-- Reason
	local reasonBox = AceGUI:Create("EditBox")
	reasonBox:SetLabel("Reason")
	reasonBox:SetWidth(200)
	reasonBox:SetText("Manual Adjust")
	adjGroup:AddChild(reasonBox)

	-- Apply
	local applyBtn = AceGUI:Create("Button")
	applyBtn:SetText("Apply")
	applyBtn:SetWidth(100)
	applyBtn:SetCallback("OnClick", function()
		local player = playerSelect:GetValue()
		local op = opSelect:GetValue()
		local amt = tonumber(amountBox:GetText())
		local reason = reasonBox:GetText()

		if player and amt and amt > 0 then
			local epgp = Cerebro:GetModule("EPGP")
			if op == "add_ep" then epgp:AwardEP(player, amt, "OFFICER", reason) end
			if op == "sub_ep" then epgp:AwardEP(player, -amt, "OFFICER", reason) end
			if op == "add_gp" then epgp:ChargeGP(player, amt, "OFFICER", reason) end
			if op == "sub_gp" then epgp:ChargeGP(player, -amt, "OFFICER", reason) end
			
			Cerebro.Ledger:AddAuditLog("MANUAL_ADJUST", string.format("%s %s %d to %s (%s)", op, "amount", amt, player, reason))
			Cerebro:Print("Adjustment applied.")
			self:RefreshStandings()
		else
			Cerebro:Print("Invalid input.")
		end
	end)
	adjGroup:AddChild(applyBtn)
	


	-- GP Configuration
	local gpGroup = AceGUI:Create("InlineGroup")
	gpGroup:SetTitle("GP Base Values Configuration")
	gpGroup:SetFullWidth(true)
	gpGroup:SetLayout("Flow")
	scroll:AddChild(gpGroup)
	
	local instruction = AceGUI:Create("Label")
	instruction:SetText("Set the default GP cost for each item slot. Press Enter to save.")
	instruction:SetFullWidth(true)
	gpGroup:AddChild(instruction)
	
	local slots = Cerebro.SlotList
	
	local config = Cerebro.db.global.config.epgp.slotWeights or {}
	
	for _, slot in ipairs(slots) do
		local edit = AceGUI:Create("EditBox")
		edit:SetLabel(slot.label)
		edit:SetWidth(80) -- Smaller width
		edit:SetText(tostring(config[slot.key] or 0))
		edit:SetCallback("OnEnterPressed", function(widget, event, text)
			local val = tonumber(text)
			if val and val >= 0 then
				config[slot.key] = val
				Cerebro.Ledger:AddAuditLog("GP_CONFIG", string.format("Updated %s to %d", slot.label, val))
				Cerebro:Print(string.format("Updated %s GP to %d", slot.label, val))
			else
				Cerebro:Print("Invalid value for " .. slot.label)
				edit:SetText(tostring(config[slot.key] or 0)) -- Reset
			end
		end)
		gpGroup:AddChild(edit)
	end
	
	-- Force layout recalculation after all children are added
	-- This fixes the scrollbar not appearing until window resize
	Cerebro:ScheduleTimer(function()
		if scroll and scroll.frame then
			scroll:DoLayout()
		end
	end, 0.01)
end

--[[=============================================
	History Tab
===============================================]]

function UI:DrawHistoryTab(container)
	-- Search Bar
	local searchGroup = AceGUI:Create("SimpleGroup")
	searchGroup:SetFullWidth(true)
	searchGroup:SetLayout("Flow")
	container:AddChild(searchGroup)
	
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("Search (press Enter)")
	searchBox:SetWidth(300)
	searchBox:SetText(self.searchText["history"] or "")
	searchBox:SetCallback("OnEnterPressed", function(widget, event, text)
		self.searchText["history"] = text
		self:RequestRefresh()
	end)
	searchGroup:AddChild(searchBox)
	
	-- Content Area
	local contentGroup = AceGUI:Create("SimpleGroup")
	contentGroup:SetFullWidth(true)
	contentGroup:SetFullHeight(true)
	contentGroup:SetLayout("Fill")
	container:AddChild(contentGroup)

	local historyText = ""
	local list = Cerebro.db.global.transactions or {}
	local searchFilter = (self.searchText["history"] or ""):lower()
	
	if #list == 0 then
		local lbl = AceGUI:Create("Label")
		lbl:SetText("No history found.")
		contentGroup:AddChild(lbl)
		return
	end
	
	-- Show last 100 (filtered)
	local count = 0
	for i = #list, 1, -1 do
		local tx = list[i]
		
		-- Specifics (Item Link, Boss Name, etc)
		local specifics = ""
		if type(tx.details) == "string" then
			specifics = string.format("[%s]", tx.details)
		elseif type(tx.details) == "table" then
			if tx.details.item then
				-- Loot award with item link
				local slotPart = tx.details.slot and string.format("[%s] ", tx.details.slot) or ""
				local gpDelta = ""
				if tx.details.preGP and tx.details.postGP then
					gpDelta = string.format(" (GP: %d → %d)", tx.details.preGP, tx.details.postGP)
				end
				specifics = string.format("%s%s%s", slotPart, tx.details.item, gpDelta)
			elseif tx.details.preGP and tx.details.postGP then
				-- GP charge without item (manual)
				specifics = string.format("(GP: %d → %d)", tx.details.preGP, tx.details.postGP)
			elseif tx.details.reason then
				specifics = string.format("[%s]", tx.details.reason)
			elseif tx.details.mapID then
				specifics = string.format("[M+ Level %s]", tostring(tx.details.level))
			end
		end
		
		local admin = tx.admin or "System"
		
		-- Session Tag
		local sessionTag = ""
		if tx.sessionName and tx.sessionID then
			sessionTag = string.format(" [%s_%s]", tx.sessionName, tx.sessionID)
		elseif tx.sessionName then
			sessionTag = string.format(" [%s]", tx.sessionName)
		end
		
		-- Format: [DATE, TIME] [Session] Player: +/-Amount Type (Category) [Specifics] - By: Admin
		local amountSign = (tx.amount >= 0) and "+" or ""
		
		-- Legacy transaction suffix (missing logicVersion = ingested from old version)
		local legacySuffix = ""
		if not tx.logicVersion then
			legacySuffix = "|cffff8800*|r"  -- Orange asterisk for legacy
		end
		
		-- RAW MODE (Default and only mode)
		local line
		
		-- Special formatting for session events
		if tx.reason == "SESSION_START" or tx.reason == "SESSION_END" then
			local sessionInfo = tx.details and tx.details.sessionInfo or "Session Event"
			line = string.format("|cff999999[%s]|r |cffffaa00%s|r - %s - |cff00ccffBy: %s|r%s",
				date("%m/%d, %H:%M", tx.timestamp),
				tx.reason,
				sessionInfo,
				Cerebro:GetShortPlayerName(admin),
				legacySuffix
			)
		else
			-- Normal transaction format
			line = string.format("|cff999999[%s]|r%s |cffffffff%s|r: %s%d %s |cffaaaaaa(%s)|r %s - |cff00ccffBy: %s|r%s",
				date("%m/%d, %H:%M", tx.timestamp),
				sessionTag,
				Cerebro:GetShortPlayerName(tx.player),
				amountSign,
				tx.amount,
				tx.currency or tx.type or "?",
				tx.reason or "Unknown",
				specifics,
				Cerebro:GetShortPlayerName(admin),
				legacySuffix
			)
		end
		
		-- Apply search filter (case-insensitive)
		if searchFilter == "" or line:lower():find(searchFilter, 1, true) then
			historyText = historyText .. line .. "\n"
			count = count + 1
			if count >= 100 then break end
		end
	end
	
	if count == 0 then
		historyText = "No matching entries found."
	end
	
	local editBox = AceGUI:Create("MultiLineEditBox")
	editBox:SetLabel("")
	editBox:SetFullWidth(true)
	editBox:SetFullHeight(true)
	editBox:DisableButton(true) -- Hide the "Accept" button
	editBox:SetText(historyText)
	
	-- Make Read-Only (Revert on change)
	editBox:SetCallback("OnTextChanged", function(widget)
		if widget:GetText() ~= historyText then
			widget:SetText(historyText)
		end
	end)
	
	contentGroup:AddChild(editBox)
end

--[[=============================================
	Audit Tab
===============================================]]

function UI:DrawAuditTab(container)
	-- Search Bar
	local searchGroup = AceGUI:Create("SimpleGroup")
	searchGroup:SetFullWidth(true)
	searchGroup:SetLayout("Flow")
	container:AddChild(searchGroup)
	
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("Search (press Enter)")
	searchBox:SetWidth(300)
	searchBox:SetText(self.searchText["audit"] or "")
	searchBox:SetCallback("OnEnterPressed", function(widget, event, text)
		self.searchText["audit"] = text
		self:RequestRefresh()
	end)
	searchGroup:AddChild(searchBox)
	
	-- Content Area
	local contentGroup = AceGUI:Create("SimpleGroup")
	contentGroup:SetFullWidth(true)
	contentGroup:SetFullHeight(true)
	contentGroup:SetLayout("Fill")
	container:AddChild(contentGroup)
	
	local editBox = AceGUI:Create("MultiLineEditBox")
	editBox:SetLabel("")
	editBox:SetFullWidth(true)
	editBox:SetFullHeight(true)
	editBox:DisableButton(true)
	contentGroup:AddChild(editBox)

	local auditText = ""
	local list = Cerebro.db.global.audit or {}
	local searchFilter = (self.searchText["audit"] or ""):lower()
	
	if #list == 0 then
		editBox:SetText("No audit logs found.")
		return
	end
	
	-- Show last 100 (filtered)
	local count = 0
	local currentEpoch = Cerebro.db.global.resetEpoch or 0
	for i = #list, 1, -1 do
		local entry = list[i]
		
		-- Epoch marker: show [Epoch N] for entries from previous epochs
		local epochTag = ""
		if entry.epoch and entry.epoch ~= currentEpoch then
			epochTag = string.format("|cff888888[Epoch %d]|r ", entry.epoch)
		end
		
		-- Format: [DATE TIME] [Epoch N] Admin: ACTION - Details
		local line = string.format("[%s] %s|cff00ccff%s|r: |cffffaa00%s|r - %s",
			date("%m/%d %H:%M", entry.timestamp),
			epochTag,
			Cerebro:GetShortPlayerName(entry.admin),
			entry.action or "ACTION",
			entry.details or ""
		)
		
		-- Apply search filter (case-insensitive)
		if searchFilter == "" or line:lower():find(searchFilter, 1, true) then
			auditText = auditText .. line .. "\n"
			count = count + 1
			if count >= 100 then break end
		end
	end
	
	if count == 0 then
		auditText = "No matching entries found."
	end
	
	editBox:SetText(auditText)
	
	-- Make Read-Only (Revert on change)
	editBox:SetCallback("OnTextChanged", function(widget)
		if widget:GetText() ~= auditText then
			widget:SetText(auditText)
		end
	end)
end

--[[=============================================
	Guild M+ History Tab (formerly Raider Audit)
	Shows all guild M+ runs pushed from raiders to officers
	Uses same visual style as My Progress tab
==============================================]]

-- Track native frames for cleanup
UI.guildMplusNativeFrames = UI.guildMplusNativeFrames or {}

local function CleanupGuildMplusFrames()
	for _, frame in ipairs(UI.guildMplusNativeFrames or {}) do
		if frame and frame.Hide then
			frame:Hide()
			frame:SetParent(nil)
			frame:ClearAllPoints()
		end
	end
	UI.guildMplusNativeFrames = {}
end

local function TrackGuildMplusFrame(frame)
	if frame then
		table.insert(UI.guildMplusNativeFrames, frame)
	end
	return frame
end

local GUILD_MPLUS_ROW_HEIGHT = 36

-- Dungeon lookup table (same as My Progress)
local DUNGEON_NAMES = {
	[2660] = "Ara-Kara",
	[2662] = "The Dawnbreaker",
	[2651] = "The Stonevault",
	[2649] = "Priory",
	[2669] = "Floodgate",
	[2773] = "Eco-Dome",
	[2287] = "Halls of Atonement",
	[2441] = "Tazavesh: Streets",
	[375] = "Mists",
	[376] = "Necrotic Wake",
	[377] = "De Other Side",
	[378] = "Halls of Atonement",
	[379] = "Plaguefall",
}

local function GetDungeonDisplayName(run)
	if run.mapName and run.mapName ~= "" then
		return run.mapName
	end
	if run.mapID and DUNGEON_NAMES[run.mapID] then
		return DUNGEON_NAMES[run.mapID]
	end
	if run.mapID then
		local name = C_ChallengeMode.GetMapUIInfo(run.mapID)
		if name then return name end
	end
	return "Unknown"
end

-- Filter state
UI.guildMplusFilters = UI.guildMplusFilters or { player = "", dungeon = "", level = "", result = "" }

function UI:DrawRaiderAudit(container)
	CleanupGuildMplusFrames()
	
	local runs = Cerebro.db.global.guildMplusRuns or {}
	
	-- Build unique values for dropdowns
	local players = { [""] = "All Players" }
	local dungeons = { [""] = "All Dungeons" }
	local levels = { [""] = "All Levels" }
	local results = { [""] = "All Results", ["success"] = "Timed", ["fail"] = "Untimed" }
	
	for _, run in ipairs(runs) do
		local p = Cerebro:GetFullPlayerName(run.uploadedBy or "Unknown")
		players[p] = p
		local d = GetDungeonDisplayName(run)
		dungeons[d] = d
		local l = "+" .. (run.level or 0)
		levels[l] = l
	end
	
	-- Filter bar (compact)
	local filterGroup = AceGUI:Create("SimpleGroup")
	filterGroup:SetFullWidth(true)
	filterGroup:SetLayout("Flow")
	container:AddChild(filterGroup)
	
	-- Player dropdown
	local playerDD = AceGUI:Create("Dropdown")
	playerDD:SetLabel("")
	playerDD:SetWidth(150)
	playerDD:SetList(players)
	playerDD:SetValue(self.guildMplusFilters.player or "")
	playerDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.player = val
		self:SelectTab(self.tabGroup, "raider_audit")
	end)
	filterGroup:AddChild(playerDD)
	
	-- Dungeon dropdown
	local dungeonDD = AceGUI:Create("Dropdown")
	dungeonDD:SetLabel("")
	dungeonDD:SetWidth(120)
	dungeonDD:SetList(dungeons)
	dungeonDD:SetValue(self.guildMplusFilters.dungeon or "")
	dungeonDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.dungeon = val
		self:SelectTab(self.tabGroup, "raider_audit")
	end)
	filterGroup:AddChild(dungeonDD)
	
	-- Level dropdown
	local levelDD = AceGUI:Create("Dropdown")
	levelDD:SetLabel("")
	levelDD:SetWidth(70)
	levelDD:SetList(levels)
	levelDD:SetValue(self.guildMplusFilters.level or "")
	levelDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.level = val
		self:SelectTab(self.tabGroup, "raider_audit")
	end)
	filterGroup:AddChild(levelDD)
	
	-- Result dropdown
	local resultDD = AceGUI:Create("Dropdown")
	resultDD:SetLabel("")
	resultDD:SetWidth(90)
	resultDD:SetList(results)
	resultDD:SetValue(self.guildMplusFilters.result or "")
	resultDD:SetCallback("OnValueChanged", function(_, _, val)
		self.guildMplusFilters.result = val
		self:SelectTab(self.tabGroup, "raider_audit")
	end)
	filterGroup:AddChild(resultDD)
	
	-- Apply filters
	local filtered = {}
	for _, run in ipairs(runs) do
		local pass = true
		
		-- Player filter
		if self.guildMplusFilters.player ~= "" then
			local p = Cerebro:GetFullPlayerName(run.uploadedBy or "Unknown")
			if p ~= self.guildMplusFilters.player then pass = false end
		end
		
		-- Dungeon filter
		if pass and self.guildMplusFilters.dungeon ~= "" then
			local d = GetDungeonDisplayName(run)
			if d ~= self.guildMplusFilters.dungeon then pass = false end
		end
		
		-- Level filter
		if pass and self.guildMplusFilters.level ~= "" then
			local l = "+" .. (run.level or 0)
			if l ~= self.guildMplusFilters.level then pass = false end
		end
		
		-- Result filter
		if pass and self.guildMplusFilters.result ~= "" then
			if self.guildMplusFilters.result == "success" and not run.success then pass = false end
			if self.guildMplusFilters.result == "fail" and run.success then pass = false end
		end
		
		if pass then table.insert(filtered, run) end
	end
	
	-- Record count (native frame, right-aligned)
	-- Create a small frame to hold the font string (so it can be tracked for cleanup)
	local countFrame = TrackGuildMplusFrame(CreateFrame("Frame", nil, filterGroup.frame))
	countFrame:SetSize(120, 20)
	countFrame:SetPoint("RIGHT", filterGroup.frame, "RIGHT", -20, 0)
	local countFS = countFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	countFS:SetAllPoints()
	countFS:SetText("|cff888888" .. #filtered .. " of " .. #runs .. " runs|r")
	countFS:SetJustifyH("RIGHT")
	
	-- Main content area
	local mainGroup = AceGUI:Create("InlineGroup")
	mainGroup:SetTitle("")
	mainGroup:SetFullWidth(true)
	mainGroup:SetFullHeight(true)
	mainGroup:SetLayout("Fill")
	container:AddChild(mainGroup)
	
	mainGroup:SetCallback("OnRelease", function()
		CleanupGuildMplusFrames()
	end)
	
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetLayout("Flow")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	mainGroup:AddChild(scroll)
	
	-- Empty state
	if #filtered == 0 then
		local lbl = AceGUI:Create("Label")
		if #runs == 0 then
			lbl:SetText("\n\nNo guild M+ runs recorded yet.\n\nRuns will appear here as guild members complete M+ dungeons with the addon installed.")
		else
			lbl:SetText("\n\nNo runs match the selected filters.")
		end
		lbl:SetColor(0.5, 0.5, 0.5)
		lbl:SetFullWidth(true)
		scroll:AddChild(lbl)
		return
	end
	
	-- Sort by timestamp descending
	table.sort(filtered, function(a, b)
		return (a.timestamp or 0) > (b.timestamp or 0)
	end)
	
	-- Header row
	local headerWrapper = AceGUI:Create("SimpleGroup")
	headerWrapper:SetLayout("Fill")
	headerWrapper:SetFullWidth(true)
	headerWrapper:SetHeight(24)
	scroll:AddChild(headerWrapper)
	
	local hf = TrackGuildMplusFrame(CreateFrame("Frame", nil, headerWrapper.frame))
	hf:SetAllPoints()
	
	local fsPlayer = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsPlayer:SetPoint("LEFT", 10, 0)
	fsPlayer:SetText("|cffFFD700Player|r")
	
	local fsLevel = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsLevel:SetPoint("LEFT", 180, 0)
	fsLevel:SetText("|cffFFD700Level / Dungeon|r")
	
	local fsParty = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsParty:SetPoint("LEFT", 380, 0)
	fsParty:SetText("|cffFFD700Party|r")
	
	local fsResult = hf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	fsResult:SetPoint("RIGHT", -10, 0)
	fsResult:SetText("|cffFFD700Result|r")
	
	-- Data rows
	local displayCount = math.min(#filtered, 100)
	
	for i = 1, displayCount do
		local run = filtered[i]
		local isStripe = (i % 2 == 0)
		
		local wrapper = AceGUI:Create("SimpleGroup")
		wrapper:SetLayout("Fill")
		wrapper:SetFullWidth(true)
		wrapper:SetHeight(GUILD_MPLUS_ROW_HEIGHT)
		scroll:AddChild(wrapper)
		
		local f = TrackGuildMplusFrame(CreateFrame("Frame", nil, wrapper.frame, "BackdropTemplate"))
		f:SetAllPoints()
		
		if isStripe then
			local bg = f:CreateTexture(nil, "BACKGROUND")
			bg:SetAllPoints()
			bg:SetColorTexture(1, 1, 1, 0.05)
		end
		
		-- A. Player Name (full Name-Realm with class color like Standings)
		local rawName = run.uploadedBy or "Unknown"
		-- Normalize to full Name-Realm format
		local fullName = Cerebro:GetFullPlayerName(rawName)
		
		-- Try to find class from: 1) group data (uploader in party), 2) players database
		local playerClass = nil
		
		-- Check group data first - uploader should be in the party
		if run.group then
			for _, member in ipairs(run.group) do
				local memberName = member.name or ""
				-- Match by short name (without realm)
				if memberName:find("^" .. rawName) or rawName:find("^" .. Cerebro:GetShortPlayerName(memberName)) then
					playerClass = member.class and string.upper(member.class)
					break
				end
			end
		end
		
		-- Fallback to players database
		if not playerClass then
			local playerData = Cerebro.db.global.players and Cerebro.db.global.players[fullName]
			playerClass = playerData and playerData.class
		end
		
		local classColor = "|cffFFFFFF"  -- Default white
		if playerClass then
			local cc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[playerClass]
			if cc then
				classColor = "|c" .. cc.colorStr
			end
		end
		local playerFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		playerFS:SetPoint("LEFT", 10, 0)
		playerFS:SetWidth(160)
		playerFS:SetText(classColor .. fullName .. "|r")
		playerFS:SetJustifyH("LEFT")
		
		-- B. Level Badge
		local level = run.level or 0
		local levelColor = "|cffFFFFFF"
		if level >= 20 then levelColor = "|cffFF8000"
		elseif level >= 15 then levelColor = "|cffA335EE"
		elseif level >= 10 then levelColor = "|cff0070DD"
		end
		
		local lvlFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
		lvlFS:SetPoint("LEFT", 180, 0)
		lvlFS:SetText(levelColor .. "+" .. level .. "|r")
		
		-- C. Dungeon Name (using lookup)
		local mapName = GetDungeonDisplayName(run)
		
		local nameFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		nameFS:SetPoint("LEFT", 225, 0)
		nameFS:SetWidth(140)
		nameFS:SetText(mapName)
		nameFS:SetJustifyH("LEFT")
		nameFS:SetWordWrap(false)
		
		-- D. Party Icons
		local startX = 380
		local iconSize = 24
		
		local roleOrder = { TANK = 1, HEALER = 2, HEALS = 2, DAMAGER = 3, DPS = 3, DMG = 3 }
		local sortedGroup = {}
		for _, m in ipairs(run.group or {}) do table.insert(sortedGroup, m) end
		table.sort(sortedGroup, function(a, b)
			return (roleOrder[a.role] or 4) < (roleOrder[b.role] or 4)
		end)
		
		for j, member in ipairs(sortedGroup) do
			local iframe = TrackGuildMplusFrame(CreateFrame("Frame", nil, f))
			iframe:SetSize(iconSize, iconSize)
			iframe:SetPoint("LEFT", startX + ((j - 1) * (iconSize + 4)), 0)
			
			local bgCircle = iframe:CreateTexture(nil, "BACKGROUND")
			bgCircle:SetAllPoints()
			bgCircle:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
			
			local classKey = string.upper(member.class or "PRIEST")
			local cc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[classKey]
			if cc then
				bgCircle:SetVertexColor(cc.r, cc.g, cc.b, 1)
			else
				bgCircle:SetVertexColor(0.5, 0.5, 0.5, 1)
			end
			
			local roleIcon = iframe:CreateTexture(nil, "ARTWORK")
			roleIcon:SetSize(15, 15)
			roleIcon:SetPoint("CENTER")
			
			local role = member.role
			if role == "TANK" then
				roleIcon:SetAtlas("roleicon-tiny-tank")
			elseif role == "HEALER" or role == "HEALS" then
				roleIcon:SetAtlas("roleicon-tiny-healer")
			else
				roleIcon:SetAtlas("roleicon-tiny-dps")
			end
			
			iframe:SetScript("OnEnter", function(self)
				GameTooltip:SetOwner(self, "ANCHOR_TOP")
				local colorStr = member.class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[string.upper(member.class)]
					and "|c" .. RAID_CLASS_COLORS[string.upper(member.class)].colorStr or "|cffFFFFFF"
				GameTooltip:AddLine(colorStr .. (member.name or "Unknown") .. "|r")
				GameTooltip:Show()
			end)
			iframe:SetScript("OnLeave", function() GameTooltip:Hide() end)
		end
		
		-- E. Result
		local statusIcon = run.success
			and "|TInterface\\RaidFrame\\ReadyCheck-Ready:20|t"
			or "|TInterface\\RaidFrame\\ReadyCheck-NotReady:20|t"
		
		local statusFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
		statusFS:SetPoint("RIGHT", -10, 0)
		statusFS:SetText(statusIcon)
		
		-- F. EP Award
		local epFS = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
		epFS:SetPoint("RIGHT", statusFS, "LEFT", -10, 0)
		epFS:SetText("|cff00ff00+" .. (run.totalEP or 0) .. " EP|r")
		
		-- G. Time
		local mins = math.floor((run.duration or 0) / 60)
		local secs = (run.duration or 0) % 60
		local timeStr = string.format("%d:%02d", mins, secs)
		if run.upgrade and run.upgrade > 0 then
			timeStr = timeStr .. " |cff00ff00(+" .. run.upgrade .. ")|r"
		end
		
		local timeFS = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
		timeFS:SetPoint("RIGHT", epFS, "LEFT", -20, 0)
		timeFS:SetText(timeStr)
	end
	
	if #filtered > displayCount then
		local moreLabel = AceGUI:Create("Label")
		moreLabel:SetText(string.format("\n|cff888888Showing %d of %d runs.|r", displayCount, #filtered))
		moreLabel:SetFullWidth(true)
		scroll:AddChild(moreLabel)
	end
end

--[[=============================================
	Wishlist Tab
===============================================]]

function UI:DrawWishlistTab(container)
	local label = AceGUI:Create("Label")
	label:SetText("Wishlist management coming soon.")
	label:SetFullWidth(true)
	container:AddChild(label)
	
	-- TODO: Use existing Wishlist module if ported
end


--[[=============================================
	Helpers
===============================================]]

function UI:ShowStandings()
	self:Show()
	if self.tabGroup then
		self.tabGroup:SelectTab("standings")
	end
end

function UI:ShowHistory()
	self:Show()
	if self.tabGroup then
		self.tabGroup:SelectTab("history")
	end
end

function UI:ShowWishlist()
	self:Show()
	if self.tabGroup then
		self.tabGroup:SelectTab("wishlist")
	end
end

function UI:ShowConfig()
	LibStub("AceConfigDialog-3.0"):Open("Cerebro")
end

function UI:RefreshStandings()
	Cerebro:GetModule("StandingsFrame"):Refresh()
end

--[[=============================================
	Settings Tab (Command Registry)
===============================================]]

function UI:DrawSettingsTab(container)
	if not _G.UI_CommandRegistry then
		self:ShowUnavailableMessage(container, "Command Registry")
		return
	end
	
	-- Wrapper group to provide proper sizing for ScrollFrame (OfficerTab Pattern)
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetFullWidth(true)
	wrapper:SetFullHeight(true)
	wrapper:SetLayout("Fill")
	container:AddChild(wrapper)

	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	scroll:SetLayout("Flow")
	wrapper:AddChild(scroll)
	
	-- Header (Inside Scroll)
	local header = AceGUI:Create("Heading")
	header:SetText("Command Center")
	header:SetFullWidth(true)
	scroll:AddChild(header)
	
	-- Search box (Inside Scroll)
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("Search Commands")
	searchBox:SetWidth(300)
	searchBox:DisableButton(true)
	searchBox:SetCallback("OnTextChanged", function(widget, event, text)
		self:RefreshSettingsTab(scroll, text)
	end)
	scroll:AddChild(searchBox)
	
	-- Spacer
	local spacer = AceGUI:Create("Label")
	spacer:SetText("\n")
	spacer:SetFullWidth(true)
	scroll:AddChild(spacer)
	
	-- Command list container (InlineGroup mirrors sessionGroup in OfficerTab)
	local cmdListContainer = AceGUI:Create("InlineGroup")
	cmdListContainer:SetTitle("Commands")
	cmdListContainer:SetFullWidth(true)
	cmdListContainer:SetLayout("Flow")
	scroll:AddChild(cmdListContainer)
	
	-- Store reference for refresh
	scroll.commandListContainer = cmdListContainer
	
	-- Render commands
	self:RenderCommandList(cmdListContainer, "")
	
	-- Force layout recalculation after all children are added
	-- This fixes the scrollbar not appearing until window resize
	Cerebro:ScheduleTimer(function()
		if scroll and scroll.frame then
			scroll:DoLayout()
		end
	end, 0.01)
end

function UI:RefreshSettingsTab(scrollFrame, searchText)
	-- Get the command list container
	local cmdList = scrollFrame.commandListContainer
	if not cmdList then return end
	
	-- Clear only the command list (preserves Header, SearchBox, Spacer)
	cmdList:ReleaseChildren()
	
	-- Re-render commands
	self:RenderCommandList(cmdList, searchText or "")
end

function UI:RenderCommandList(container, searchText)
	if not _G.UI_CommandRegistry then
		return
	end
	
	local commands = _G.UI_CommandRegistry:GetFiltered(searchText)
	
	for _, cmd in ipairs(commands) do
		local cmdGroup = AceGUI:Create("InlineGroup")
		cmdGroup:SetTitle(cmd.displayName or cmd.id)
		cmdGroup:SetFullWidth(true)
		cmdGroup:SetLayout("Flow")
		container:AddChild(cmdGroup)
		
		-- Description
		local desc = AceGUI:Create("Label")
		desc:SetText((cmd.description or "") .. (cmd.slash and ("\n|cffaaaaaa" .. cmd.slash .. "|r") or ""))
		desc:SetFullWidth(true)
		cmdGroup:AddChild(desc)
		
		-- Run button
		local runBtn = AceGUI:Create("Button")
		runBtn:SetText("Run")
		runBtn:SetWidth(100)
		runBtn:SetCallback("OnClick", function()
			-- Click-time RBAC check
			if cmd.officerOnly and not self:IsOfficerSafe() then
				Cerebro:Print("This command requires officer permissions.")
				return
			end
			
			local success, err = _G.UI_CommandRegistry:Execute(cmd.id, {})
			if not success then
				Cerebro:Print("Command failed: " .. tostring(err))
			end
		end)
		cmdGroup:AddChild(runBtn)
	end
	
	if #commands == 0 then
		local noResults = AceGUI:Create("Label")
		noResults:SetText("No commands found matching search.")
		noResults:SetColor(0.7, 0.7, 0.7)
		noResults:SetFullWidth(true)
		container:AddChild(noResults)
	end
	-- Bottom Spacer to prevent cutoff
	local bottomSpacer = AceGUI:Create("Label")
	bottomSpacer:SetText(" ")
	bottomSpacer:SetFullWidth(true)
	container:AddChild(bottomSpacer)
end

--[[=============================================
	Weights Tab (Config Schema)
===============================================]]

function UI:DrawWeightsTab(container)
	if not _G.UI_ConfigSchema then
		self:ShowUnavailableMessage(container, "Config Schema")
		return
	end
	
	-- Wrapper group to provide proper sizing for ScrollFrame
	local wrapper = AceGUI:Create("SimpleGroup")
	wrapper:SetFullWidth(true)
	wrapper:SetFullHeight(true)
	wrapper:SetLayout("Fill")
	container:AddChild(wrapper)
	
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	scroll:SetLayout("Flow")
	wrapper:AddChild(scroll)
	
	-- Header
	local header = AceGUI:Create("Heading")
	header:SetText("Addon Configuration")
	header:SetFullWidth(true)
	scroll:AddChild(header)
	
	local subtitle = AceGUI:Create("Label")
	subtitle:SetText("Adjust EPGP weights, preparation requirements, and other parameters.")
	subtitle:SetFullWidth(true)
	subtitle:SetColor(0.7, 0.7, 0.7)
	scroll:AddChild(subtitle)
	
	-- Spacer
	local spacer = AceGUI:Create("Label")
	spacer:SetText("\n")
	spacer:SetFullWidth(true)
	scroll:AddChild(spacer)
	
	-- Render categories
	local categories = _G.UI_ConfigSchema:GetCategories()
	
	for _, category in ipairs(categories) do
		local catGroup = AceGUI:Create("InlineGroup")
		catGroup:SetTitle(category.category)
		catGroup:SetFullWidth(true)
		catGroup:SetLayout("Flow")
		scroll:AddChild(catGroup)
		
		for _, item in ipairs(category.items) do
			if item.type == "number" then
				self:RenderNumberConfig(catGroup, item)
			elseif item.type == "bool" then
				self:RenderBoolConfig(catGroup, item)
			elseif item.type == "select" then
				self:RenderSelectConfig(catGroup, item)
			end
		end
	end
	
	-- Bottom Spacer to prevent cutoff
	local bottomSpacer = AceGUI:Create("Label")
	bottomSpacer:SetText(" ")
	bottomSpacer:SetFullWidth(true)
	scroll:AddChild(bottomSpacer)
	
	-- Force layout recalculation after all children are added
	-- This fixes the scrollbar not appearing until window resize
	Cerebro:ScheduleTimer(function()
		if scroll and scroll.frame then
			scroll:DoLayout()
		end
	end, 0.01)
end

function UI:RenderNumberConfig(container, item)
	local currentValue = _G.UI_ConfigSchema:GetValue(item.key) or 0
	
	local editBox = AceGUI:Create("EditBox")
	editBox:SetLabel(item.label)
	editBox:SetText(tostring(currentValue))
	editBox:SetWidth(150)
	editBox:SetCallback("OnEnterPressed", function(widget, event, value)
		-- Click-time RBAC check
		if item.requiresOfficer and not self:IsOfficerSafe() then
			Cerebro:Print("Officer permissions required to modify this setting.")
			widget:SetText(tostring(currentValue))
			return
		end
		
		-- Validate
		local valid, result = _G.UI_ConfigSchema:Validate(item, value)
		if not valid then
			Cerebro:Print("Invalid value: " .. tostring(result))
			widget:SetText(tostring(currentValue))
			return
		end
		
		-- Save
		_G.UI_ConfigSchema:SetValue(item.key, result)
		Cerebro:Print("Updated " .. item.label .. " to " .. tostring(result))
		
		-- Trigger refresh
		self:RequestRefresh()
	end)
	container:AddChild(editBox)
end

function UI:RenderBoolConfig(container, item)
	local checkbox = AceGUI:Create("CheckBox")
	checkbox:SetLabel(item.label)
	
	-- Read current value from DB (not captured in closure)
	checkbox:SetValue(_G.UI_ConfigSchema:GetValue(item.key) or false)
	
	checkbox:SetCallback("OnValueChanged", function(widget, event, value)
		-- Re-read current value to check for external changes
		local currentValue = _G.UI_ConfigSchema:GetValue(item.key) or false
		
		-- Click-time RBAC check
		if item.requiresOfficer and not self:IsOfficerSafe() then
			Cerebro:Print("Officer permissions required to modify this setting.")
			widget:SetValue(currentValue)
			return
		end
		
		-- Save
		_G.UI_ConfigSchema:SetValue(item.key, value)
		Cerebro:Print("Updated " .. item.label .. " to " .. tostring(value))
		
		-- Trigger refresh
		self:RequestRefresh()
	end)
	container:AddChild(checkbox)
end

function UI:RenderSelectConfig(container, item)
	local dropdown = AceGUI:Create("Dropdown")
	dropdown:SetLabel(item.label)
	dropdown:SetWidth(180)
	
	-- Build list from choices
	local list = item.choices or {}
	dropdown:SetList(list)
	
	-- Set current value
	local currentValue = _G.UI_ConfigSchema:GetValue(item.key) or "off"
	dropdown:SetValue(currentValue)
	
	dropdown:SetCallback("OnValueChanged", function(widget, event, value)
		-- Click-time RBAC check
		if item.requiresOfficer and not self:IsOfficerSafe() then
			Cerebro:Print("Officer permissions required to modify this setting.")
			widget:SetValue(currentValue)
			return
		end
		
		-- Save
		_G.UI_ConfigSchema:SetValue(item.key, value)
		local displayName = list[value] or value
		Cerebro:Print("Updated " .. item.label .. " to " .. displayName)
		
		-- Trigger refresh
		self:RequestRefresh()
	end)
	container:AddChild(dropdown)
end
