--[[
	Cerebro - Custom EP Module (template UI)

	PURPOSE:
	- Provide an officer-only UI to create, save, preview and apply EP templates.
	- Templates are simple (reason + amount + includePending flag) and always apply to the currently
	  selected players in the Standings (scope is implicitly "Selected").
	- Prevent duplicate templates by normalizing the reason on save (overwrite on name match).
	- Templates are shown in a list that grows inside the main scroll so downstream sections (e.g. CustomGP)
	  are pushed down automatically.

	NOTES:
	- All "apply" actions act only on players currently selected in the Standings. If no players are selected,
	  the UI prompts the user.
	- This file intentionally documents behavior and return values for public helpers so future edits keep intent clear.
]]--

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

-- Module state (kept minimal; UI widgets are local to Draw)
CustomEP.frame = nil

-- Exposed hook used by SaveTemplate/DeleteTemplate to update the UI in-place when Draw is active.
-- Draw will set this to a function that re-renders the templates area; when nil we fallback to a gentler UI refresh.
CustomEP._onTemplatesChanged = nil

-- Maximum length for reason strings (derived from "Custom EP Daily Activities" per request)
local MAX_REASON_TEXT = #("Custom EP Daily Activities") -- 26

-- Called at addon initialization time (DB defaults)
function CustomEP:OnInitialize()
	-- Ensure config table exists and preserve previous stored user defaults/templates if present.
	-- Structure:
	-- Cerebro.db.global.customEP = {
	--   default = { amount = number, reason = string, includePending = bool },
	--   templates = { { amount=number, reason=string, includePending=bool, createdAt=timestamp }, ... }
	-- }
	Cerebro.db.global.customEP = Cerebro.db.global.customEP or {}
	Cerebro.db.global.customEP.default = Cerebro.db.global.customEP.default or {
		amount = 50,
		reason = "Custom EP",
		includePending = false,
	}
	Cerebro.db.global.customEP.templates = Cerebro.db.global.customEP.templates or {}
end

-- Called when module is enabled (UI may need refreshing)
function CustomEP:OnEnable()
	Cerebro:Debug("CustomEP enabled")
	-- If the main UI is currently visible, force the Officer tab to refresh so this module's controls render.
	local ui = Cerebro:GetModule("UI", true)
	if ui and ui.mainFrame and ui.mainFrame:IsShown() and ui.tabGroup then
		if ui.currentTab == "officer" then
			-- Re-selecting the tab forces a rebuild of the officer tab contents.
			ui:SelectTab(ui.tabGroup, "officer")
		else
			ui:SelectTab(ui.tabGroup, ui.currentTab or "myprogress")
		end
	end
end

-- Convenience wrapper that checks officer permissions in a safe manner.
-- Returns boolean.
function CustomEP:IsOfficer()
	return Cerebro and Cerebro.IsOfficer and Cerebro:IsOfficer()
end

-- Helper: normalize reason strings for duplicate detection
-- - trims whitespace and lowercases the string
-- - returns empty string when input is nil/empty
local function NormalizeReason(s)
	if not s then return "" end
	-- trim leading/trailing whitespace and lower-case
	local t = s:match("^%s*(.-)%s*$") or s
	return string.lower(t)
end

-- Obtain the currently selected players from StandingsFrame.
-- Returns:
--   selSet = map fullName -> true for quick membership tests
--   selList = ordered list of selected fullNames (may be empty)
function CustomEP:GetSelectedPlayersSetAndList()
	local sf = Cerebro:GetModule("StandingsFrame", true)
	local selList = {}
	local selSet = {}
	if sf and sf.selected then
		for name, val in pairs(sf.selected) do
			if val then
				selSet[name] = true
				table.insert(selList, name)
			end
		end
		table.sort(selList)  -- Alphabetical for consistent preview
	end
	return selSet, selList
end

-- GetFilteredPlayers(filters)
-- Build and return a flat list of player objects from the DB / helper.
-- Parameters:
--   filters: table | nil. Supported key:
--     includePending = boolean (when true, include players with status "Pending")
-- Returns:
--   array of player tables { name = fullName, class = ..., status = ..., guildRank = ... }
-- Note: This does NOT apply the "selected-only" rule; selection intersection happens in ComputeAwardsForSelected.
function CustomEP:GetFilteredPlayers(filters)
	local players = {}
	if Cerebro.GetAllPlayers then
		local ok, p = pcall(function() return Cerebro:GetAllPlayers() end)
		if ok and type(p) == "table" then players = p else players = {} end
	else
		-- Fallback: iterate DB entries if helper is not available
		for name, data in pairs(Cerebro.db.global.players or {}) do
			table.insert(players, {
				name = name,
				class = data.class,
				status = data.status,
				guildRank = data.guildRank
			})
		end
	end

	local out = {}
	local includePending = filters and filters.includePending
	for _, p in ipairs(players) do
		-- Skip pending players unless explicitly requested
		if includePending or p.status ~= "Pending" then
			table.insert(out, p)
		end
	end

	return out
end

-- ComputeAwardsForSelected(filters, baseAmount)
-- Computes awards for players that both match the provided filters and are currently selected in the Standings.
-- Parameters:
--   filters: table or nil; supported key: includePending (boolean)
--   baseAmount: number (amount of EP to apply per player)
-- Returns:
--   awards: map name -> amount
--   total: sum of all award amounts
--   filteredList: list of player objects that match filters AND are selected (for preview display)
--   selList: ordered list of selected names (returned for caller convenience)
function CustomEP:ComputeAwardsForSelected(filters, baseAmount)
	local filtered = self:GetFilteredPlayers(filters)
	local selSet, selList = self:GetSelectedPlayersSetAndList()

	-- Intersect filtered list with selected set and compute amounts
	local resultFiltered = {}
	local awards = {}
	local total = 0
	for _, p in ipairs(filtered) do
		if selSet[p.name] then
			local amount = math.floor(baseAmount + 0.5)
			if amount ~= 0 then
				awards[p.name] = amount
				total = total + amount
			end
			table.insert(resultFiltered, p)
		end
	end
	return awards, total, resultFiltered, selList
end

-- ApplyAwards(awards, reason)
-- Performs the actual EPGP module calls to award EP to the provided map of recipients.
-- Parameters:
--   awards: map fullName -> amount
--   reason: string (logged in ledger/audit)
-- Returns:
--   count: number of recipients processed
-- Behavior:
--   - Ensures recipient names are full names (adds realm if missing using Cerebro:GetFullPlayerName)
--   - Writes an audit entry via Cerebro.Ledger if present
--   - Calls Cerebro:RefreshAllUI() after completion
function CustomEP:ApplyAwards(awards, reason)
	local epgp = Cerebro.modules.EPGP
	if not epgp then
		Cerebro:Print("EPGP module not loaded; cannot award.")
		return
	end

	local count = 0
	for name, amount in pairs(awards) do
		local full = name
		if not full:find("-") then full = Cerebro:GetFullPlayerName(full) end
		epgp:AwardEP(full, amount, "CUSTOM_EP", reason)
		count = count + 1
	end

	if Cerebro.Ledger then
		Cerebro.Ledger:AddAuditLog("CUSTOM_EP_APPLY", string.format("Applied custom EP: %d recipients, reason=%s", count, reason or "Custom EP"))
	end

	Cerebro:RefreshAllUI()
	return count
end

-- MoveTemplate(index, delta)
-- Moves a template up or down (delta = -1 up, +1 down). Updates UI in-place if hook available,
-- otherwise requests a tab refresh.
function CustomEP:MoveTemplate(index, delta)
	local t = Cerebro.db.global.customEP and Cerebro.db.global.customEP.templates
	if not t or not t[index] then return end
	local newIndex = index + delta
	if newIndex < 1 or newIndex > #t then return end

	-- swap
	t[index], t[newIndex] = t[newIndex], t[index]

	-- Inform user
	local movedTpl = t[newIndex] -- the template we moved (now at newIndex)
	Cerebro:Print(string.format("Moved template '%s' %s.", movedTpl.reason or "(no reason)", (delta == -1) and "up" or "down"))

	-- run in-place UI update if available, otherwise trigger a gentle tab refresh
	if CustomEP._onTemplatesChanged and type(CustomEP._onTemplatesChanged) == "function" then
		CustomEP._onTemplatesChanged()
	else
		local ui = Cerebro:GetModule("UI", true)
		if ui and type(ui.RequestRefresh) == "function" then ui:RequestRefresh() else Cerebro:RefreshAllUI() end
	end
end

-- SaveTemplate(spec)
-- Persist a template; template spec: { amount = number, reason = string, includePending = bool }
-- Behavior:
--  - Normalize the reason and if a template with the same normalized reason exists, overwrite it
--  - Insert new/updated template at the front so the newest templates appear first
-- NOTE: this function will attempt to run the in-place UI update hook (CustomEP._onTemplatesChanged) if set by Draw.
-- If the hook isn't present it falls back to a gentler UI refresh request.
function CustomEP:SaveTemplate(spec)
	Cerebro.db.global.customEP = Cerebro.db.global.customEP or {}
	Cerebro.db.global.customEP.templates = Cerebro.db.global.customEP.templates or {}
	local tpl = {
		amount = tonumber(spec.amount) or 0,
		reason = tostring(spec.reason or ""),
		includePending = not not spec.includePending,
		createdAt = time()
	}

	-- Truncate reason to configured maximum to be safe
	if tpl.reason and #tpl.reason > MAX_REASON_TEXT then
		tpl.reason = tpl.reason:sub(1, MAX_REASON_TEXT)
	end

	local norm = NormalizeReason(tpl.reason)
	if norm ~= "" then
		for i, existing in ipairs(Cerebro.db.global.customEP.templates) do
			if NormalizeReason(existing.reason) == norm then
				-- overwrite existing template with same normalized reason
				table.remove(Cerebro.db.global.customEP.templates, i)
				table.insert(Cerebro.db.global.customEP.templates, 1, tpl)
				Cerebro:Print(string.format("Overwrote template for reason '%s' with %d EP.", tpl.reason, tpl.amount))
				-- run in-place UI update if available, otherwise trigger a gentle tab refresh
				if CustomEP._onTemplatesChanged and type(CustomEP._onTemplatesChanged) == "function" then
					CustomEP._onTemplatesChanged()
				else
					local ui = Cerebro:GetModule("UI", true)
					if ui and type(ui.RequestRefresh) == "function" then ui:RequestRefresh() else Cerebro:RefreshAllUI() end
				end
			return
			end
		end
	end

	table.insert(Cerebro.db.global.customEP.templates, 1, tpl)
	Cerebro:Print(string.format("Saved template: %d EP � %s", tpl.amount, tpl.reason))
	-- run in-place UI update if available, otherwise trigger a gentler tab refresh
	if CustomEP._onTemplatesChanged and type(CustomEP._onTemplatesChanged) == "function" then
		CustomEP._onTemplatesChanged()
	else
		local ui = Cerebro:GetModule("UI", true)
		if ui and type(ui.RequestRefresh) == "function" then ui:RequestRefresh() else Cerebro:RefreshAllUI() end
	end
end

-- DeleteTemplate(index)
-- Removes template at given 1-based index from the persisted list.
-- This will attempt an in-place UI update via CustomEP._onTemplatesChanged if present; otherwise requests a tab refresh.
function CustomEP:DeleteTemplate(index)
	local t = Cerebro.db.global.customEP and Cerebro.db.global.customEP.templates
	if not t or not t[index] then return end
	table.remove(t, index)
	Cerebro:Print("Template deleted.")
	-- run in-place UI update if available, otherwise trigger a gentle tab refresh
	if CustomEP._onTemplatesChanged and type(CustomEP._onTemplatesChanged) == "function" then
		CustomEP._onTemplatesChanged()
	else
		local ui = Cerebro:GetModule("UI", true)
		if ui and type(ui.RequestRefresh) == "function" then ui:RequestRefresh() else Cerebro:RefreshAllUI() end
	end
end

-- Helper: show a simple popup informing the user that no players are selected
local function ShowNoSelectedPlayersPopup()
	StaticPopupDialogs["CEREBRO_NO_SELECTED_PLAYERS"] = StaticPopupDialogs["CEREBRO_NO_SELECTED_PLAYERS"] or {
		text = "No players are selected. Select at least one player in the Standings to apply EP.",
		button1 = "OK",
		timeout = 0,
		whileDead = true,
		hideOnEscape = true,
		preferredIndex = 5,
	}
	StaticPopup_Show("CEREBRO_NO_SELECTED_PLAYERS")
end

-- Draw(container)
-- Build the AceGUI UI controls inside the provided container.
-- Layout:
--   - InlineGroup with inputs (Amount, Reason, Include Pending)
--   - Preview, Apply, Save Template buttons
--   - Preview multi-line output
--   - Saved Templates InlineGroup added directly to the outer container so it grows the main scroll
--     and pushes subsequent UI sections further down (this prevents overlap with CustomGP).
function CustomEP:Draw(container)
	if not container or not container.AddChild then
		Cerebro:Debug("CustomEP:Draw called with invalid container")
		return
	end

	-- Small helper to clear any leftover button textures from pooled native frames.
	-- Always call this for buttons that should show text only; set textured arrows only for up/down buttons.
	local function clearButtonTextures(btn)
		pcall(function()
			if btn and btn.frame and btn.frame.SetNormalTexture then
				btn.frame:SetNormalTexture(nil)
				btn.frame:SetPushedTexture(nil)
				btn.frame:SetHighlightTexture(nil)
				if btn.frame.SetDisabledTexture then btn.frame:SetDisabledTexture(nil) end
			end
		end)
	end

	-- Main inputs group
	local group = AceGUI:Create("InlineGroup")
	group:SetTitle("Custom EP Options")
	group:SetFullWidth(true)
	group:SetLayout("Flow")
	container:AddChild(group)


	local widgets = {} -- luacheck: ignore 241  -- Track widget references for template load

	local defaults = Cerebro.db.global.customEP and Cerebro.db.global.customEP.default or {}

	-- Amount input
	local amtBox = AceGUI:Create("EditBox")
	amtBox:SetLabel("Amount per player")
	amtBox:SetWidth(120)
	amtBox:SetText(tostring(defaults.amount or 50))
	group:AddChild(amtBox)
	widgets.amtBox = amtBox

	-- Reason input (used for template name/label and audit reason)
	-- Shrink the edit box width slightly to match the requested shorter reason length.
	local reasonBox = AceGUI:Create("EditBox")
	reasonBox:SetLabel("Reason")
	reasonBox:SetWidth(220) -- reduced from 300 to better match the 26-char limit
	reasonBox:SetText(defaults.reason or "Custom EP")
	group:AddChild(reasonBox)
	widgets.reasonBox = reasonBox

	-- Try to set the underlying EditBox max letters (defensive pcall)
	pcall(function()
		if reasonBox.editbox and reasonBox.editbox.SetMaxLetters then
			reasonBox.editbox:SetMaxLetters(MAX_REASON_TEXT)
		end
	end)


	-- Action buttons
	local previewBtn = AceGUI:Create("Button")
	previewBtn:SetText("Preview")
	previewBtn:SetWidth(120)
	-- ensure no leftover texture from pooled frame
	clearButtonTextures(previewBtn)
	group:AddChild(previewBtn)

	local applyBtn = AceGUI:Create("Button")
	applyBtn:SetText("Apply")
	applyBtn:SetWidth(120)
	-- ensure no leftover texture from pooled frame
	clearButtonTextures(applyBtn)
	group:AddChild(applyBtn)

	local saveTplBtn = AceGUI:Create("Button")
	saveTplBtn:SetText("Save Template")
	saveTplBtn:SetWidth(140)
	-- ensure no leftover texture from pooled frame
	clearButtonTextures(saveTplBtn)
	group:AddChild(saveTplBtn)
	widgets.saveTplBtn = saveTplBtn

	-- Preview output (read-only)
	local previewBox = AceGUI:Create("MultiLineEditBox")
	previewBox:SetLabel("Preview (read-only)")
	previewBox:SetFullWidth(true)
	previewBox:SetHeight(180)
	previewBox:DisableButton(true)
	previewBox:SetText("Preview will appear here after clicking Preview.")
	group:AddChild(previewBox)
	widgets.previewBox = previewBox

	-- Templates list container: add to outer container so it expands the main scroll.
	local templatesContainer = AceGUI:Create("InlineGroup")
	templatesContainer:SetFullWidth(true)
	templatesContainer:SetLayout("Flow")
	templatesContainer:SetTitle("Saved Templates")
	container:AddChild(templatesContainer)
	widgets.templatesContainer = templatesContainer

	-- Internal renderer for templates
	local function renderTemplates()
		-- Clear previous children and rebuild from DB state
		templatesContainer:ReleaseChildren()
		local tlist = Cerebro.db.global.customEP and Cerebro.db.global.customEP.templates or {}
		if not tlist or #tlist == 0 then
			local lbl = AceGUI:Create("Label")
			lbl:SetText("(No templates saved yet)")
			lbl:SetFullWidth(true)
			templatesContainer:AddChild(lbl)
			return
		end

		-- Each row layout: Load | Apply | Reason | EP | Delete | Up | Down
		for idx, tpl in ipairs(tlist) do
			local row = AceGUI:Create("SimpleGroup")
			row:SetFullWidth(true)
			row:SetLayout("Flow")
			templatesContainer:AddChild(row)

			-- Capture locals to avoid closure loop-variable bug (ensures callbacks operate on the intended template)
			local localIdx = idx
			local localTpl = tpl

			-- Load: populate the main inputs with this template's values and trigger preview
			local loadBtn = AceGUI:Create("Button")
			loadBtn:SetText("Load")
			loadBtn:SetWidth(80)
			-- clear any leftover textures
			clearButtonTextures(loadBtn)
			loadBtn:SetCallback("OnClick", function()
				amtBox:SetText(tostring(localTpl.amount or 0))
				-- ensure shown reason is not longer than the max
				local r = localTpl.reason or ""
				if #r > MAX_REASON_TEXT then r = r:sub(1, MAX_REASON_TEXT) end
				reasonBox:SetText(r)
				-- Fire preview programmatically
				previewBtn:Fire("OnClick")
			end)
			row:AddChild(loadBtn)

			-- Apply: apply this template to the currently selected players (prompts if none selected)
			local applyTplBtn = AceGUI:Create("Button")
			applyTplBtn:SetText("Apply")
			applyTplBtn:SetWidth(80)
			-- ensure no leftover texture on this text button
			clearButtonTextures(applyTplBtn)
			applyTplBtn:SetCallback("OnClick", function()
				if not CustomEP:IsOfficer() then Cerebro:Print("Officer permissions required.") return end

				-- Check selection
				local _, selList = CustomEP:GetSelectedPlayersSetAndList()
				if not selList or #selList == 0 then
					ShowNoSelectedPlayersPopup()
					return
				end

				local amount = tonumber(localTpl.amount) or 0
				if amount == 0 then
					Cerebro:Print("Amount must be a non-zero number.")
					return
				end

				-- Compute awards for the selected players using the template's includePending flag
				local filters = { includePending = localTpl.includePending or false }
				local awards, total, filtered = CustomEP:ComputeAwardsForSelected(filters, amount)

				if not awards or next(awards) == nil then
					Cerebro:Print("No selected players match filters - nothing to apply.")
					return
				end

				-- Confirmation dialog (re-used ID ensures text updates before show)
				local popupText = string.format("Apply %d EP to %d selected players (total %d EP)?\nReason: %s", amount, #filtered, total, localTpl.reason or "Custom EP")
				StaticPopupDialogs["CEREBRO_CUSTOM_EP_CONFIRM"] = StaticPopupDialogs["CEREBRO_CUSTOM_EP_CONFIRM"] or {
					text = popupText,
					button1 = "Apply",
					button2 = "Cancel",
					OnAccept = function()
						local count = CustomEP:ApplyAwards(awards, localTpl.reason)
						Cerebro:Print(string.format("Applied custom EP to %d players (total %d EP).", count, total))
					end,
					OnShow = function(self) self.text:SetText(popupText) end,
					timeout = 0,
					whileDead = true,
					hideOnEscape = true,
					preferredIndex = 5,
				}
				StaticPopup_Show("CEREBRO_CUSTOM_EP_CONFIRM")
			end)
			row:AddChild(applyTplBtn)

			-- Reason label (serves as the template name) -- shrink width to match shorter reason length
			local reasonLabel = AceGUI:Create("Label")
			local displayReason = localTpl.reason or "(no reason)"
			if #displayReason > MAX_REASON_TEXT then displayReason = displayReason:sub(1, MAX_REASON_TEXT) end
			reasonLabel:SetText(displayReason)
			reasonLabel:SetWidth(200) -- reduced from 240
			row:AddChild(reasonLabel)

			-- EP amount label
			local epLabel = AceGUI:Create("Label")
			epLabel:SetText(tostring(localTpl.amount or 0) .. " EP")
			epLabel:SetWidth(80)
			row:AddChild(epLabel)

			-- Delete button (with confirmation)
			local delBtn = AceGUI:Create("Button")
			delBtn:SetText("Delete")
			delBtn:SetWidth(80)
			-- clear any leftover textures
			clearButtonTextures(delBtn)
			delBtn:SetCallback("OnClick", function()
				StaticPopupDialogs["CEREBRO_CUSTOM_EP_DELETE"] = StaticPopupDialogs["CEREBRO_CUSTOM_EP_DELETE"] or {
					text = "Delete this template?",
					button1 = "Delete",
					button2 = "Cancel",
					OnAccept = function()
						-- call module-level delete; DeleteTemplate will run the in-place update hook when available
						CustomEP:DeleteTemplate(localIdx)
					end,
					timeout = 0,
					whileDead = true,
					hideOnEscape = true,
					preferredIndex = 5,
				}
				StaticPopup_Show("CEREBRO_CUSTOM_EP_DELETE")
			end)
			row:AddChild(delBtn)

			-- Up / Down buttons: always show, disable when movement is not possible
			local total = #tlist

			-- Up: use letter "U" instead of texture to avoid pooled-texture bleed
			local upBtn = AceGUI:Create("Button")
			upBtn:SetText("Up")
			-- increased width so the letter is visible
			upBtn:SetWidth(65)
			local upEnabled = not (total <= 1 or localIdx == 1)
			upBtn:SetDisabled(not upEnabled)
			-- ensure no textured leftovers (pooled frames) � we only use text now
			clearButtonTextures(upBtn)
			-- ensure the native fontstring is readable and centered
			pcall(function()
				if upBtn.frame and upBtn.frame.GetFontString then
					local fs = upBtn.frame:GetFontString()
					if fs and fs.SetFont then
						fs:SetFont(GameFontNormalSmall:GetFont(), 12, "") -- readable size
						fs:SetJustifyH("CENTER")
						fs:SetJustifyV("MIDDLE")
					end
				end
			end)
			upBtn:SetCallback("OnClick", function() CustomEP:MoveTemplate(localIdx, -1) end)
			row:AddChild(upBtn)

			-- Down: use letter "D" instead of texture to avoid pooled-texture bleed
			local downBtn = AceGUI:Create("Button")
			downBtn:SetText("Down")
			-- increased width so the letter is visible
			downBtn:SetWidth(70)
			local downEnabled = not (total <= 1 or localIdx == total)
			downBtn:SetDisabled(not downEnabled)
			-- ensure no textured leftovers (pooled frames) � we only use text now
			clearButtonTextures(downBtn)
			-- ensure the native fontstring is readable and centered
			pcall(function()
				if downBtn.frame and downBtn.frame.GetFontString then
					local dfs = downBtn.frame:GetFontString()
					if dfs and dfs.SetFont then
						dfs:SetFont(GameFontNormalSmall:GetFont(), 12, "")
						dfs:SetJustifyH("CENTER")
						dfs:SetJustifyV("MIDDLE")
					end
				end
			end)
			downBtn:SetCallback("OnClick", function() CustomEP:MoveTemplate(localIdx, 1) end)
			row:AddChild(downBtn)
		end
	end

	-- Render initial templates list
	renderTemplates()

	-- Make the module-level hook point to our renderer so SaveTemplate/DeleteTemplate can update in-place
	CustomEP._onTemplatesChanged = function()
		-- re-render only the templates area to avoid a full UI refresh and the associated screen jump
		if templatesContainer and templatesContainer.ReleaseChildren then
			-- Rebuild the templates list
			renderTemplates()

			-- Run layout passes so AceGUI and parent scroll update sizes.
			pcall(function() templatesContainer:DoLayout() end)
			pcall(function() if container and container.DoLayout then container:DoLayout() end end)

			-- Schedule tiny delayed layout passes to handle AceGUI timing
			Cerebro:ScheduleTimer(function()
				pcall(function() templatesContainer:DoLayout() end)
				pcall(function() if container and container.DoLayout then container:DoLayout() end end)
			end, 0.01)
			Cerebro:ScheduleTimer(function()
				pcall(function() templatesContainer:DoLayout() end)
				pcall(function() if container and container.DoLayout then container:DoLayout() end end)
			end, 0.15)
		else
			-- fallback: if the UI is not currently drawn, request a tab refresh to ensure consistency
			local ui = Cerebro:GetModule("UI", true)
			if ui and type(ui.RequestRefresh) == "function" then
				ui:RequestRefresh()
			else
				Cerebro:RefreshAllUI()
			end
		end
	end

	-- Clear the module-level hook when the templates container is released so we don't keep dangling references
	templatesContainer:SetCallback("OnRelease", function()
		CustomEP._onTemplatesChanged = nil
	end)

	-- Preview button behavior (always previews selected players)
	-- - If no players selected, informs the user in the preview box.
	previewBtn:SetCallback("OnClick", function()
		local amount = tonumber(amtBox:GetText()) or 0
		local reason = reasonBox:GetText() or "Custom EP"
		local _ = reason  -- Consumed by template save; preview uses amount only
		local filters = { includePending = false }
		local awards, total, filtered, selList = CustomEP:ComputeAwardsForSelected(filters, amount)

		local lines = {}
		if not selList or #selList == 0 then
			table.insert(lines, "No players selected. Select players in Standings to preview.")
		else
			table.insert(lines, string.format("Preview: %d selected players will receive EP. Total EP: %d", #filtered, total))
			table.insert(lines, "-------------------------------------------")
			for _, p in ipairs(filtered) do
				local amt = awards[p.name] or 0
				table.insert(lines, string.format("%s : %d EP (%s)", Cerebro:GetShortPlayerName(p.name), amt, p.status or "Core"))
			end
			if #filtered == 0 then table.insert(lines, "(No selected players match the filters)") end
		end
		previewBox:SetText(table.concat(lines, "\n"))
	end)

	-- Main Apply button: applies the values currently set in the main inputs to selected players.
	-- Will prompt if no players are selected and confirm before applying.
	applyBtn:SetCallback("OnClick", function()
		if not CustomEP:IsOfficer() then Cerebro:Print("Officer permissions required.") return end

		-- Validate selection exists
		local _, selList = CustomEP:GetSelectedPlayersSetAndList()
		if not selList or #selList == 0 then
			ShowNoSelectedPlayersPopup()
			return
		end

		local amount = tonumber(amtBox:GetText()) or 0
		if amount == 0 then
			Cerebro:Print("Amount must be a non-zero number.")
			return
		end

		local reason = reasonBox:GetText() or "Custom EP"
		if #reason > MAX_REASON_TEXT then reason = reason:sub(1, MAX_REASON_TEXT) end
		local filters = { includePending = false }
		local awards, total, filtered = CustomEP:ComputeAwardsForSelected(filters, amount)

		if not awards or next(awards) == nil then
			Cerebro:Print("No selected players match the filters - nothing to apply.")
			return
		end

		-- Confirm action
		local popupText = string.format("Apply %d EP to %d selected players (total %d EP)?\nReason: %s", amount, #filtered, total, reason)
		StaticPopupDialogs["CEREBRO_CUSTOM_EP_CONFIRM"] = StaticPopupDialogs["CEREBRO_CUSTOM_EP_CONFIRM"] or {
			text = popupText,
			button1 = "Apply",		
			button2 = "Cancel",
			OnAccept = function()
				local count = CustomEP:ApplyAwards(awards, reason)
				Cerebro:Print(string.format("Applied custom EP to %d players (total %d EP).", count, total))
			end,
			OnShow = function(self) self.text:SetText(popupText) end,
			timeout = 0,
			whileDead = true,
			hideOnEscape = true,
			preferredIndex = 5,
		}
		StaticPopup_Show("CEREBRO_CUSTOM_EP_CONFIRM")
	end)

	-- Save template: store the current amount/reason/includePending.
	-- Duplicate reasons (normalized) will overwrite existing templates.
	saveTplBtn:SetCallback("OnClick", function()
		local amount = tonumber(amtBox:GetText()) or 0
		local reason = reasonBox:GetText() or ""
		-- truncate reason to allowed max for storage
		if #reason > MAX_REASON_TEXT then reason = reason:sub(1, MAX_REASON_TEXT) end
		local includePending = false

		if amount == 0 then
			Cerebro:Print("Amount must be a non-zero number to save a template.")
			return
		end

		CustomEP:SaveTemplate({ amount = amount, reason = reason, includePending = includePending })
		-- If in-place hook is present it already refreshed templates; otherwise re-render to keep UI responsive
		if not (CustomEP._onTemplatesChanged and type(CustomEP._onTemplatesChanged) == "function") then
			renderTemplates()
			-- best-effort layout passes
			pcall(function() templatesContainer:DoLayout() end)
			pcall(function() if container and container.DoLayout then container:DoLayout() end end)
		end
	end)
end

-- Publicly exposed draw function used by MainFrame
function CustomEP:DrawInto(container)
	self:Draw(container)
end

return CustomEP

