--[[
	Cerebro - Standings Frame
	
	PURPOSE:
	Displays EP/GP/PR standings with sorting, filtering, and RBAC-aware visibility.
	
	RESPONSIBILITIES:
	1. Schema-Driven Rendering - Column definitions with type-aware sorting
	2. Nil-Safety - Graceful degradation when dependencies/data unavailable
	3. Deterministic Refresh - Idempotent redraw, no duplicated rows/headers
	4. RBAC Visibility - Officer sees Pending players, members do not
	5. Performance - Debounced filter, refresh budget guard
	
	CONTRACTS:
	- Draw(container): Initial render with header + data rows
	- Refresh(container): Redraw current view with updated data
	- Redraw(container): Force full rebuild (clears and recreates)
	- GetExtraColumns(): Extension point for future officer-only columns
	
	DESIGN DECISIONS:
	- Schema-based columns for maintainability and extensibility
	- Stable sorting (primary: selected column, secondary: name)
	- Refresh budget guard (0.2s minimum between redraws)
	- Nil-safe data access with defaults (EP/GP/PR = 0, class = "Unknown")
]]--

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

-- State
StandingsFrame.sortKey = "pr"
StandingsFrame.sortAsc = false -- descending by default (High PR first)
StandingsFrame.filterText = ""
StandingsFrame.scrollContainer = nil
StandingsFrame.lastRedrawTime = 0
StandingsFrame.REDRAW_BUDGET_SECONDS = 0.2

-- Column Schema
-- Extension point: Officers can add columns via GetExtraColumns()
local BASE_COLUMNS = {
	{ id = "rank", label = "#", width = 0.06, sortKey = "rank", numeric = true },
	{ id = "name", label = "Player", width = 0.22, sortKey = "name", numeric = false },
	{ id = "guildRank", label = "Guild Rank", width = 0.12, sortKey = "guildRank", numeric = false },
	{ id = "pr", label = "PR", width = 0.10, sortKey = "pr", numeric = true },
	{ id = "ep", label = "EP", width = 0.09, sortKey = "ep", numeric = true },
	{ id = "gp", label = "GP", width = 0.09, sortKey = "gp", numeric = true },
	{ id = "status", label = "Status", width = 0.11, sortKey = "status", numeric = false },
	{ id = "attendance", label = "Attend", width = 0.20, sortKey = "attendancePct", numeric = true },
}

function StandingsFrame:OnInitialize()
	-- Initialize
end

--[[=============================================
	Helper Methods
===============================================]]

function StandingsFrame:IsOfficerSafe()
	-- Use UI helper if available, fall back to Cerebro
	if Cerebro.modules and Cerebro.modules.UI and Cerebro.modules.UI.IsOfficerSafe then
		return Cerebro.modules.UI:IsOfficerSafe()
	end
	return Cerebro and Cerebro.IsOfficer and Cerebro:IsOfficer() or false
end

function StandingsFrame:GetAllPlayersSafe()
	-- Nil-safe wrapper for GetAllPlayers
	local success, players = pcall(function()
		return Cerebro:GetAllPlayers()
	end)
	
	if not success or not players then
		Cerebro:Debug("StandingsFrame: GetAllPlayers failed, returning empty list")
		return {}
	end
	
	return players
end

--[[=============================================
	Attendance Calculation
===============================================]]

function StandingsFrame:CalculateAttendanceDisplay(playerData)
	-- Returns (displayText, colorTable) for attendance column
	-- Uses totalBossKills (synced from officers) as the denominator
	
	local bossKillsPresent = playerData.bossKillsPresent or 0
	
	-- If no coreAdmittedAt, check status to determine if player is established or new
	if not playerData.coreAdmittedAt then
		local status = playerData.status or "Core"
		if status ~= "Core" and status ~= "Officer" and status ~= "Veteran Member" and status ~= "Survivor" then
			return "— (NEW)", {r=0.7, g=0.7, b=0.7}  -- Gray for new
		end
	end
	
	-- Use eligibleBossKills (pre-computed by officer, synced to all clients)
	local eligibleBosses = playerData.eligibleBossKills or 0
	
	-- No eligible bosses = new season or no data yet
	if eligibleBosses == 0 then
		return "— (NEW)", {r=0.7, g=0.7, b=0.7}  -- Gray for new
	end
	
	-- Calculate percentage (cap at 100% for display sanity)
	local pct = math.min(100, math.floor((bossKillsPresent / eligibleBosses) * 100))
	local displayText = string.format("%d/%d (%d%%)", bossKillsPresent, eligibleBosses, pct)
	
	-- Color coding
	local color
	if pct >= 95 then
		color = {r=0.0, g=1.0, b=0.0}  -- Green
	elseif pct >= 80 then
		color = {r=1.0, g=1.0, b=0.0}  -- Yellow
	else
		color = {r=1.0, g=0.0, b=0.0}  -- Red
	end
	
	return displayText, color
end

function StandingsFrame:GetExtraColumns()
	-- Extension point for officer-only columns (not implemented yet)
	-- Future: Add "Actions" column with +EP/-GP buttons for officers
	return {}
end

function StandingsFrame:GetColumns()
	local columns = {}
	
	-- Add base columns
	for _, col in ipairs(BASE_COLUMNS) do
		table.insert(columns, col)
	end
	
	-- Add officer-only columns if applicable
	if self:IsOfficerSafe() then
		local extraCols = self:GetExtraColumns()
		for _, col in ipairs(extraCols) do
			table.insert(columns, col)
		end
	end
	
	return columns
end

function StandingsFrame:CanRedraw()
	-- Refresh budget guard: prevent refresh storms
	local now = time()
	local elapsed = now - self.lastRedrawTime
	
	if elapsed < self.REDRAW_BUDGET_SECONDS then
		Cerebro:Debug("StandingsFrame: Refresh throttled (budget guard)")
		return false
	end
	
	return true
end

--[[=============================================
	Rendering Engine
===============================================]]

function StandingsFrame:Draw(container)
	container:ReleaseChildren()
	
	-- Filter/Search Bar
	local filterGroup = AceGUI:Create("SimpleGroup")
	filterGroup:SetFullWidth(true)
	filterGroup:SetLayout("Flow")
	container:AddChild(filterGroup)
	
	local searchBox = AceGUI:Create("EditBox")
	searchBox:SetLabel("Search Player")
	searchBox:SetWidth(200)
	searchBox:SetText(self.filterText)
	searchBox:DisableButton(true)
	searchBox:SetCallback("OnTextChanged", function(widget, event, text)
		self.filterText = text:lower()
		-- Debounced refresh (basic timer approach)
		if self.filterTimer then
			Cerebro:CancelTimer(self.filterTimer)
		end
		self.filterTimer = Cerebro:ScheduleTimer(function()
			self:Refresh(container)
			self.filterTimer = nil
		end, 0.3)
	end)
	filterGroup:AddChild(searchBox)
	
	-- Sync Button (RBAC-aware)
	local syncBtn = AceGUI:Create("Button")
	syncBtn:SetText("Force Sync")
	syncBtn:SetWidth(120)
	syncBtn:SetCallback("OnClick", function()
		if self:IsOfficerSafe() then
			-- Full sync reset: clear delta state and force immediate full sync (same as /cb syncreset)
			local s = Cerebro.db.global.syncState
			if s then
				s.pendingSyncs = {}
				s.dirtyPlayers = {}
				s.lastFullSyncAt = 0
				s.dirtyRetainUntil = nil
			end
			
			Cerebro:RepairDatabase()
			
			if Cerebro.modules.CommunicationV2 then
				Cerebro:RecalculateAllPR()
				local sent = Cerebro.modules.CommunicationV2:SyncToRaid()
				if sent then
					Cerebro:Print("|cff00ff00Sync state reset.|r Full sync sent to all clients.")
				else
					Cerebro:Print("|cffff8800Sync deferred until combat ends.|r")
				end
			end
			
			-- Audit log
			if Cerebro.Ledger then
				Cerebro.Ledger:AddAuditLog("SYNC_RESET", "Force Sync button pressed (UI)")
			end
		else
			if Cerebro.modules.CommunicationV2 then
				Cerebro.modules.CommunicationV2:RequestSync()
			end
			Cerebro:Print("Sync requested from officers...")
		end
		-- Update status bar immediately
		if Cerebro.modules.UI then
			Cerebro.modules.UI:UpdateStatusText()
		end
		self:Refresh(container)
		
		-- Schedule a synccheck after 10s to verify convergence
		local v2 = Cerebro:GetModule("CommunicationV2", true)
		if v2 and v2.SendHeartbeat then
			Cerebro:ScheduleTimer(function()
				-- Clear stale peerHashes for fresh check
				local sv2 = Cerebro.db.global.syncStateV2 or {}
				sv2.peerHashes = {}
				Cerebro.db.global.syncStateV2 = sv2
				
				v2:SendHeartbeat()
				Cerebro:Print("|cff00ff00[Sync Check]|r Verifying peer convergence (8s)...")
				
				Cerebro:ScheduleTimer(function()
					local sv2State = Cerebro.db.global.syncStateV2 or {}
					local myRosterHash = v2:ComputeRosterHash()
					local myConfigHash = v2:ComputeConfigHash()
					local matched, mismatched = 0, 0
					local mismatchNames = {}
					
					for name, peer in pairs(sv2State.peerHashes or {}) do
						if peer.lastSeen and (time() - peer.lastSeen) < 10 then
							local rOk = peer.rosterHash == myRosterHash
							local cOk = peer.configHash == myConfigHash
							if rOk and cOk then
								matched = matched + 1
							else
								mismatched = mismatched + 1
								local shortName = Cerebro:GetShortPlayerName(name) or name
								local detail = ""
								if not rOk then detail = detail .. "roster" end
								if not cOk then
									if detail ~= "" then detail = detail .. "+" end
									detail = detail .. "config"
								end
								table.insert(mismatchNames, string.format("%s(%s)", shortName, detail))
							end
						end
					end
					
					Cerebro:Print(string.format("|cff00ff00[Sync Check]|r Peers: |cff00ff00%d matched|r, |cffff4444%d mismatched|r", matched, mismatched))
					if mismatched > 0 then
						Cerebro:Print("  Mismatched: " .. table.concat(mismatchNames, ", "))
					elseif matched > 0 then
						Cerebro:Print("  |cff00ff00All peers converged!|r")
					end
					
					-- Update status bar with fresh state
					if Cerebro.modules.UI then
						Cerebro.modules.UI:UpdateStatusText()
					end
				end, 8)
			end, 10)
		end
	end)
	filterGroup:AddChild(syncBtn)

	-- Spacer
	local spacer = AceGUI:Create("Label")
	spacer:SetText(" ")
	spacer:SetFullWidth(true)
	container:AddChild(spacer)

	-- Scroll Frame
	local scroll = AceGUI:Create("ScrollFrame")
	scroll:SetFullWidth(true)
	scroll:SetFullHeight(true)
	scroll:SetLayout("Flow")
	container:AddChild(scroll)
	
	self.scrollContainer = scroll
	self:DrawTable(scroll)
end

function StandingsFrame:Refresh(container)
	if not self:CanRedraw() then
		return
	end
	
	if self.scrollContainer and self.scrollContainer.frame and self.scrollContainer.frame:IsShown() then
		-- Only refresh if the widget is actually visible/attached
		self:Redraw(self.scrollContainer)
	elseif container then
		self:Draw(container)
	end
end

function StandingsFrame:Redraw(container)
	-- Deterministic, idempotent redraw: clear all children and rebuild
	container:ReleaseChildren()
	self:DrawTable(container)
	self.lastRedrawTime = time()
end

function StandingsFrame:Clear()
	self.scrollContainer = nil
end

function StandingsFrame:DrawTable(container)
	local columns = self:GetColumns()
	
	-- Header
	local headerGroup = AceGUI:Create("SimpleGroup")
	headerGroup:SetFullWidth(true)
	headerGroup:SetLayout("Flow")
	container:AddChild(headerGroup)
	
	-- Check officer status for Pending player visibility
	local isOfficer = self:IsOfficerSafe()
	local myStatus = nil
	local myName = UnitName("player")
	
	local success, playerData = pcall(function()
		return Cerebro:GetPlayerData(myName)
	end)
	
	if success and playerData then
		myStatus = playerData.status
	end
	
	-- Show "Awaiting Approval" screen for pending non-officers
	if myStatus == "Pending" and not isOfficer then
		self:DrawAwaitingApproval(container)
		return
	end

	-- Render column headers
	for _, col in ipairs(columns) do
		local btn = AceGUI:Create("Button")
		
		local sortIndicator = ""
		if self.sortKey == col.sortKey then
			sortIndicator = self.sortAsc 
				and " |TInterface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up:16:16|t" 
				or " |TInterface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up:16:16|t"
		end
		
		btn:SetText(col.label .. sortIndicator)
		btn:SetRelativeWidth(col.width)
		btn:SetCallback("OnClick", function()
			if self.sortKey == col.sortKey then
				self.sortAsc = not self.sortAsc
			else
				self.sortKey = col.sortKey
				self.sortAsc = (col.numeric == false) -- String cols default asc, numeric default desc
			end
			self:Refresh(nil)
		end)
		headerGroup:AddChild(btn)
	end

	-- Data rows
	local players = self:GetAllPlayersSafe()
	local filtered = {}
	
	-- Get visible ranks config for display filtering (Fix 5 Layer 4)
	local visibleRanks = nil
	if Cerebro.db and Cerebro.db.global and Cerebro.db.global.config
		and Cerebro.db.global.config.sync then
		visibleRanks = Cerebro.db.global.config.sync.visibleRanks
	end

	-- Filter players
	for _, p in ipairs(players) do
		local name = p.name or ""
		local status = p.status or "Core"
		
		-- RBAC: Exclude Pending players for non-officers
		if not (status == "Pending" and not isOfficer) then
			-- Fix 5 Layer 4: Exclude players with non-visible guild ranks (fail-closed)
			local rankVisible = true
			if visibleRanks then
				local rankIndex = Cerebro:GetPlayerGuildRankIndex(name)
				-- Fail-closed: if rank unknown (nil) or not in visibleRanks, hide
				if rankIndex == nil or not visibleRanks[rankIndex] then
					rankVisible = false
				end
			end

			-- Apply filter text
			if rankVisible and (self.filterText == "" or name:lower():find(self.filterText, 1, true)) then
				table.insert(filtered, p)
			end
		end
	end
	
	-- Stable sort (primary: selected column, secondary: name)
	table.sort(filtered, function(a, b)
		local valA = a[self.sortKey]
		local valB = b[self.sortKey]
		
		-- Nil-safe defaults
		if valA == nil then valA = 0 end
		if valB == nil then valB = 0 end
		
		-- Find column type
		local isNumeric = false
		for _, col in ipairs(columns) do
			if col.sortKey == self.sortKey then
				isNumeric = col.numeric
				break
			end
		end
		
		if not isNumeric then
			-- String sort
			valA = tostring(valA):lower()
			valB = tostring(valB):lower()
		end
		
		-- Primary sort
		if valA ~= valB then
			if self.sortAsc then
				return valA < valB
			else
				return valA > valB
			end
		end
		
		-- Secondary sort: name ascending (for stability)
		local nameA = (a.name or ""):lower()
		local nameB = (b.name or ""):lower()
		return nameA < nameB
	end)

	-- Render rows
	for i, data in ipairs(filtered) do
		local rowGroup = AceGUI:Create("SimpleGroup")
		rowGroup:SetFullWidth(true)
		rowGroup:SetLayout("Flow")
		container:AddChild(rowGroup)

		-- Nil-safe data extraction with defaults
		local name = data.name or "Unknown"
		local class = data.class or "Unknown"
		local classColor = Cerebro:GetClassColor(class)
		local pr = data.pr or 0
		local ep = data.ep or 0
		local gp = data.gp or 0
		local status = data.status or "Core"
		local guildRank = "Unknown"
		
		-- Nil-safe guild rank fetch
		local fetchSuccess, rank = pcall(function()
			return Cerebro:GetPlayerGuildRank(name)
		end)
		if fetchSuccess and rank then
			guildRank = rank
		end
		
		-- Render each column
		for _, col in ipairs(columns) do
			local label = AceGUI:Create("Label")
			local text = ""
			local color = nil
			
			if col.id == "rank" then
				text = tostring(i)
			elseif col.id == "name" then
				text = name  -- Use full Name-Realm for auditing
				color = classColor
			elseif col.id == "guildRank" then
				text = guildRank
			elseif col.id == "pr" then
				text = string.format("%.4f", pr)
				color = {r=1, g=0.8, b=0} -- Gold/Orange for PR
			elseif col.id == "ep" then
				text = string.format("%d", ep)
			elseif col.id == "gp" then
				text = string.format("%d", gp == 0 and 1 or gp) -- Display floor of 1
			elseif col.id == "status" then
				if status == "Pending" then
					text = "|cffff0000Pending|r"
				else
					text = status
				end
			elseif col.id == "attendance" then
				-- Calculate fair attendance using coreAdmittedAt
				local attendText, attendColor = self:CalculateAttendanceDisplay(data)
				text = attendText
				color = attendColor
			end
			
			
			-- Add padding to left-aligned columns only (rank is centered, doesn't need it)
			if col.id == "rank" then
				label:SetText(text or "")
			else
				label:SetText("  " .. (text or ""))  -- Add padding to match button header offset
			end
			label:SetRelativeWidth(col.width)
			-- Center-align rank column, left-align all others
			if col.id == "rank" then
				label:SetJustifyH("CENTER")
			else
				label:SetJustifyH("LEFT")
			end
			if color then
				label:SetColor(color.r, color.g, color.b)
			end
			rowGroup:AddChild(label)
		end
	end
	
	-- No results message
	if #filtered == 0 then
		local label = AceGUI:Create("Label")
		label:SetText("No players found matching filter.")
		label:SetFullWidth(true)
		container:AddChild(label)
	else
		-- Add spacer at the bottom to prevent last row cutoff
		local spacer = AceGUI:Create("SimpleGroup")
		spacer:SetLayout("Fill")
		spacer:SetFullWidth(true)
		spacer:SetHeight(5)
		container:AddChild(spacer)
	end
end

function StandingsFrame:DrawAwaitingApproval(container)
	local f = AceGUI:Create("Label")
	f:SetText("\n\n\n\n|cffff0000Awaiting Officer Approval...|r\n\nYou are currently in 'Pending' status.\nYour EP/GP data is visible in 'My Progress',\nbut you will not appear in the Standings until an officer\nadmits you or starts a raid session.")
	f:SetFontObject(GameFontNormalHuge)
	f:SetJustifyH("CENTER")
	f:SetFullWidth(true)
	container:AddChild(f)
end
