--[[
	Cerebro - Calendar Sign-Up EP Module

	PURPOSE:
	Reads in-game calendar events (created by GoW, officers, or other sources) and awards EP
	to players whose RSVP status matches the configured qualifying statuses.

	FLOW:
	1. Officer clicks "Award Sign-Up EP" in Session Management
	2. Module scans calendar for upcoming guild/player events (±7 days)
	3. Officer selects an event from a dropdown
	4. Module opens the event via C_Calendar.OpenEvent() (async)
	5. Waits for CALENDAR_OPEN_EVENT callback, then reads invite list
	6. Filters invites by config.epgp.signupStatuses
	7. Shows confirmation popup with qualifying player count
	8. On confirm: awards EP via EPGP:AwardEP() and marks event as awarded

	ASYNC NOTE:
	The C_Calendar API is stateful and async. Opening an event fires CALENDAR_OPEN_EVENT
	when data is ready. Invite data is usually available immediately after that event.
	CALENDAR_UPDATE_INVITE_LIST may fire later after sorting but is NOT guaranteed.
	We try reading invites on CALENDAR_OPEN_EVENT first, then retry via
	CALENDAR_UPDATE_INVITE_LIST as a fallback.

	SECURITY MODEL:
	All mutation paths (EP awards) are gated by Cerebro:IsOfficer().

	EXTERNAL DEPENDENCIES:
	- Modules/EPGP.lua (AwardEP)
	- Models/Ledger.lua (AddAuditLog)
	- WoW API: C_Calendar (OpenCalendar, GetNumDayEvents, GetDayEvent, OpenEvent,
	           GetNumInvites, EventGetInvite, CloseEvent)
	- Enum.CalendarStatus (Accepted=1, Declined=2, Confirmed=3, Standby=5, Signedup=6, Tentative=8)
]]--

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

-- Friendly names for Enum.CalendarStatus values
CalendarSignup.STATUS_NAMES = {
	[0] = "Invited",
	[1] = "Accepted",
	[2] = "Declined",
	[3] = "Confirmed",
	[4] = "Out",
	[5] = "Standby",
	[6] = "Signedup",
	[7] = "NotSignedup",
	[8] = "Tentative",
}

-- State for async event opening
CalendarSignup._pendingEvent = nil    -- { event, callback, attempts }
CalendarSignup._eventPickerFrame = nil

function CalendarSignup:OnInitialize()
	self:Debug("CalendarSignup module initialized")
end

function CalendarSignup:OnEnable()
	-- C_Calendar.OpenCalendar() is called by the game on login,
	-- but we call it defensively when scanning to ensure data is ready
end

function CalendarSignup:Debug(msg)
	Cerebro:Debug("|cff88ccff[CalendarSignup]|r " .. tostring(msg))
end

--[[=============================================
	Configuration Access
===============================================]]

function CalendarSignup:GetSignUpEP()
	local config = Cerebro.db.global.config.epgp
	return config.epSignUp or 0
end

function CalendarSignup:GetQualifyingStatuses()
	local config = Cerebro.db.global.config.epgp
	return config.signupStatuses or { [1] = true, [3] = true }
end

function CalendarSignup:IsQualifyingStatus(statusID)
	local statuses = self:GetQualifyingStatuses()
	return statuses[statusID] == true
end

--[[=============================================
	Deduplication
===============================================]]

function CalendarSignup:GetEventKey(title, year, month, day)
	return string.format("%s|%04d-%02d-%02d", title, year, month, day)
end

function CalendarSignup:IsEventAwarded(eventKey)
	if not Cerebro.db.global.signupAwards then return false end
	return Cerebro.db.global.signupAwards[eventKey] ~= nil
end

function CalendarSignup:MarkEventAwarded(eventKey)
	if not Cerebro.db.global.signupAwards then
		Cerebro.db.global.signupAwards = {}
	end
	Cerebro.db.global.signupAwards[eventKey] = time()
end

--[[=============================================
	Calendar Scanning
	Scans ±7 days for guild/player calendar events
===============================================]]

function CalendarSignup:ScanCalendarEvents()
	-- Ensure calendar data is initialized
	C_Calendar.OpenCalendar()

	local events = {}
	local today = C_DateAndTime.GetCurrentCalendarTime()
	if not today then
		self:Debug("Calendar time not available")
		return events
	end

	-- Get actual days in the current month for boundary checks
	local currentMonthInfo = C_Calendar.GetMonthInfo(0)
	local daysInMonth = currentMonthInfo and currentMonthInfo.numDays or 31

	-- Scan from -1 day to +7 days
	for dayOffset = -1, 7 do
		local monthOffset = 0
		local checkDay = today.monthDay + dayOffset
		local checkMonth = today.month
		local checkYear = today.year

		-- Handle month boundaries using actual month lengths
		if checkDay < 1 then
			monthOffset = -1
			local prevMonthInfo = C_Calendar.GetMonthInfo(monthOffset)
			if prevMonthInfo then
				checkDay = prevMonthInfo.numDays + checkDay
				checkMonth = prevMonthInfo.month
				checkYear = prevMonthInfo.year
			end
		elseif checkDay > daysInMonth then
			monthOffset = 1
			local nextMonthInfo = C_Calendar.GetMonthInfo(monthOffset)
			if nextMonthInfo then
				checkDay = checkDay - daysInMonth
				checkMonth = nextMonthInfo.month
				checkYear = nextMonthInfo.year
			end
		end

		local numEvents = C_Calendar.GetNumDayEvents(monthOffset, checkDay)

		for i = 1, numEvents do
			local dayEvent = C_Calendar.GetDayEvent(monthOffset, checkDay, i)

			if dayEvent and (dayEvent.calendarType == "PLAYER" or dayEvent.calendarType == "GUILD_EVENT") then
				local eventKey = self:GetEventKey(dayEvent.title or "Unknown", checkYear, checkMonth, checkDay)
				local isAwarded = self:IsEventAwarded(eventKey)

				table.insert(events, {
					title = dayEvent.title or "Unknown Event",
					calendarType = dayEvent.calendarType,
					monthOffset = monthOffset,
					day = checkDay,
					month = checkMonth,
					year = checkYear,
					eventIndex = i,
					eventKey = eventKey,
					isAwarded = isAwarded,
					-- Format for dropdown display
					displayText = string.format("%s (%02d/%02d)%s",
						dayEvent.title or "Unknown",
						checkMonth, checkDay,
						isAwarded and " |cff888888[Awarded]|r" or ""),
				})
			end
		end
	end

	self:Debug(string.format("Scanned calendar: found %d events", #events))
	return events
end

--[[=============================================
	Async Event Opening + Invite Reading

	C_Calendar API async flow:
	1. C_Calendar.OpenEvent(offset, day, index) → fires CALENDAR_OPEN_EVENT
	2. Invite data is usually available immediately after CALENDAR_OPEN_EVENT
	3. CALENDAR_UPDATE_INVITE_LIST may fire after invite sorting, but is NOT guaranteed

	Strategy: Try reading invites on CALENDAR_OPEN_EVENT (with a small delay to let
	data settle). If 0 invites found, fall back to CALENDAR_UPDATE_INVITE_LIST.
===============================================]]

function CalendarSignup:OpenEventAndReadInvites(event, callback)
	if not event then return end

	-- Store pending state for async callback
	self._pendingEvent = {
		event = event,
		callback = callback,
		attempts = 0,
	}

	-- Register for calendar events
	self:RegisterEvent("CALENDAR_OPEN_EVENT", "OnEventOpened")
	self:RegisterEvent("CALENDAR_UPDATE_INVITE_LIST", "OnInviteListUpdated")

	-- Safety timeout (8 seconds)
	self._pendingTimer = self:ScheduleTimer(function()
		self:Debug("Timeout waiting for calendar event data")
		-- Last-ditch attempt: try reading invites anyway before giving up
		local totalInvites = C_Calendar.GetNumInvites()
		if totalInvites and totalInvites > 0 then
			self:Debug(string.format("Timeout recovery: found %d invites", totalInvites))
			self:ProcessInvites("timeout_recovery")
		else
			self:CleanupPendingEvent()
			Cerebro:Print("|cffff4444Calendar event failed to load. Try again.|r")
		end
	end, 8)

	-- Open the event (async — will fire CALENDAR_OPEN_EVENT)
	self:Debug(string.format("Opening event: %s (offset=%d, day=%d, index=%d)",
		event.title, event.monthOffset, event.day, event.eventIndex))
	C_Calendar.OpenEvent(event.monthOffset, event.day, event.eventIndex)
end

function CalendarSignup:OnEventOpened()
	self:Debug("CALENDAR_OPEN_EVENT received")
	if not self._pendingEvent then return end

	-- Try reading invites after a small delay to let data settle
	self:ScheduleTimer(function()
		if self._pendingEvent then
			self:ProcessInvites("CALENDAR_OPEN_EVENT")
		end
	end, 0.3)
end

function CalendarSignup:OnInviteListUpdated()
	self:Debug("CALENDAR_UPDATE_INVITE_LIST received")
	if not self._pendingEvent then return end

	-- This is a fallback — try reading invites now
	self:ProcessInvites("CALENDAR_UPDATE_INVITE_LIST")
end

function CalendarSignup:ProcessInvites(source)
	if not self._pendingEvent then return end

	local pending = self._pendingEvent
	pending.attempts = (pending.attempts or 0) + 1
	self:Debug(string.format("ProcessInvites (source=%s, attempt=%d)", source, pending.attempts))

	local totalInvites = C_Calendar.GetNumInvites()
	self:Debug(string.format("GetNumInvites() = %d", totalInvites))

	if totalInvites == 0 and pending.attempts < 4 then
		-- No invites yet — wait for more events or retry
		self:Debug("No invites yet, waiting for update...")
		if source == "CALENDAR_OPEN_EVENT" then
			-- Schedule a fallback retry in case CALENDAR_UPDATE_INVITE_LIST never fires
			self:ScheduleTimer(function()
				if self._pendingEvent then
					self:ProcessInvites("fallback_retry")
				end
			end, 1.5)
		end
		return
	end

	-- We have invites (or exhausted retries) — process them
	local callback = pending.callback
	local qualifyingPlayers = {}

	-- Log current qualifying config for debugging
	local configStatuses = self:GetQualifyingStatuses()
	local enabledList = {}
	for statusID, enabled in pairs(configStatuses) do
		local label = self.STATUS_NAMES[statusID] or tostring(statusID)
		if enabled then
			table.insert(enabledList, label .. "=" .. tostring(statusID))
		end
	end
	self:Debug("Qualifying statuses config: " .. table.concat(enabledList, ", "))

	for i = 1, totalInvites do
		local inviteInfo = C_Calendar.EventGetInvite(i)
		if inviteInfo then
			local name = inviteInfo.name
			local status = inviteInfo.inviteStatus
			local statusName = self.STATUS_NAMES[status] or ("Unknown(" .. tostring(status) .. ")")
			local qualifies = self:IsQualifyingStatus(status)

			self:Debug(string.format("  Invite %d: %s — status: %s (%s) — qualifies: %s",
				i, name or "?", statusName, tostring(status), tostring(qualifies)))

			if name and qualifies then
				-- Resolve to full name (Name-Realm) for Cerebro player lookup
				local fullName = Cerebro:GetFullPlayerName(name)
				if fullName and Cerebro.db.global.players[fullName] then
					table.insert(qualifyingPlayers, {
						name = fullName,
						shortName = name,
						status = status,
						statusName = statusName,
					})
				else
					self:Debug(string.format("  → Skipped %s — not in Cerebro player database", name or "?"))
				end
			elseif name and not qualifies then
				self:Debug(string.format("  → Skipped %s — status %s (%s) not enabled in config",
					name, statusName, tostring(status)))
			end
		end
	end

	-- Close the event
	C_Calendar.CloseEvent()

	-- Cleanup
	self:CleanupPendingEvent()

	-- Fire callback with results
	if callback then
		callback(qualifyingPlayers, pending.event)
	end
end

function CalendarSignup:CleanupPendingEvent()
	self._pendingEvent = nil
	self:UnregisterEvent("CALENDAR_UPDATE_INVITE_LIST")
	self:UnregisterEvent("CALENDAR_OPEN_EVENT")
	if self._pendingTimer then
		self:CancelTimer(self._pendingTimer)
		self._pendingTimer = nil
	end
end

--[[=============================================
	Award EP to Qualifying Players
===============================================]]

function CalendarSignup:AwardSignUpEP(players, event)
	if not Cerebro:IsOfficer() then
		Cerebro:Print("Sign-Up EP requires officer permissions.")
		return 0
	end

	if not players or #players == 0 then
		Cerebro:Print("No qualifying players to award Sign-Up EP.")
		return 0
	end

	local amount = self:GetSignUpEP()
	if amount <= 0 then
		Cerebro:Print("Sign-Up EP amount is 0. Configure in Settings → EP Awards.")
		return 0
	end

	local epgp = Cerebro:GetModule("EPGP")
	local count = 0

	-- FIX: Use mass=true + batch broadcast to avoid sync storm.
	-- Old code called AwardEP() individually per player, each triggering
	-- BroadcastEPGPUpdate → NotifyStateChange → heartbeat + full snapshot.
	-- With 20 players, that's 20× sync bursts right after session start,
	-- amplifying the session-kill race condition.
	local batchAwards = {}

	for _, player in ipairs(players) do
		local newEP, txId = epgp:AwardEP(player.name, amount, "SIGNUP", {
			event = event.title,
			date = string.format("%04d-%02d-%02d", event.year, event.month, event.day),
			status = player.statusName,
			mass = true, silent = true,
		})
		count = count + 1
		table.insert(batchAwards, {
			player = player.name, newBalance = newEP, amount = amount,
			reason = "SIGNUP: " .. (event.title or "Calendar"), currency = "EP", txId = txId,
		})
	end

	-- Single batch broadcast instead of N individual ones
	if #batchAwards > 0 and Cerebro.modules.CommunicationV2 then
		Cerebro.modules.CommunicationV2:BroadcastBatchEPGPUpdate(batchAwards)
	end

	-- Mark event as awarded (deduplication)
	self:MarkEventAwarded(event.eventKey)

	-- Audit log
	Cerebro.Ledger:AddAuditLog("SIGNUP_EP",
		string.format("Awarded %d EP to %d players for '%s' (%04d-%02d-%02d)",
			amount, count, event.title, event.year, event.month, event.day))

	Cerebro:Print(string.format("|cff00ff00Awarded %d Sign-Up EP to %d players for '%s'.|r",
		amount, count, event.title))

	Cerebro:RefreshAllUI()
	return count
end

--[[=============================================
	UI: Event Picker + Confirmation
===============================================]]

function CalendarSignup:ShowEventPicker(uiModule, container)
	if not Cerebro:IsOfficer() then return end

	-- Scan calendar for events
	local events = self:ScanCalendarEvents()

	if #events == 0 then
		Cerebro:Print("|cffff8800No calendar events found. Create events in-game or via GoW addon.|r")
		return
	end

	-- Build dropdown list
	local eventList = {}
	local eventMap = {}
	for i, event in ipairs(events) do
		local key = tostring(i)
		eventList[key] = event.displayText
		eventMap[key] = event
	end

	-- Show a popup frame with event selection
	if self._eventPickerFrame then
		self._eventPickerFrame:Release()
		self._eventPickerFrame = nil
	end

	local frame = AceGUI:Create("Frame")
	frame:SetTitle("Award Sign-Up EP")
	frame:SetWidth(400)
	frame:SetHeight(220)
	frame:SetLayout("Flow")
	frame:SetCallback("OnClose", function(widget)
		self._eventPickerFrame = nil
		widget:Release()
	end)
	self._eventPickerFrame = frame

	-- Event Dropdown
	local dropdown = AceGUI:Create("Dropdown")
	dropdown:SetLabel("Select Calendar Event")
	dropdown:SetFullWidth(true)
	dropdown:SetList(eventList)
	frame:AddChild(dropdown)

	-- Status info
	local statusLabel = AceGUI:Create("Label")
	local statusNames = {}
	for statusID, enabled in pairs(self:GetQualifyingStatuses()) do
		if enabled then
			table.insert(statusNames, self.STATUS_NAMES[statusID] or tostring(statusID))
		end
	end
	statusLabel:SetText(string.format("|cff888888Qualifying statuses: %s | EP: %d|r",
		table.concat(statusNames, ", "), self:GetSignUpEP()))
	statusLabel:SetFullWidth(true)
	frame:AddChild(statusLabel)

	-- Award Button
	local awardBtn = AceGUI:Create("Button")
	awardBtn:SetText("Scan & Award EP")
	awardBtn:SetWidth(200)
	awardBtn:SetCallback("OnClick", function()
		local selectedKey = dropdown:GetValue()
		if not selectedKey then
			Cerebro:Print("Select a calendar event first.")
			return
		end

		local selectedEvent = eventMap[selectedKey]
		if not selectedEvent then return end

		if selectedEvent.isAwarded then
			-- Confirm re-award
			StaticPopup_Show("CEREBRO_SIGNUP_EP_RECONFIRM", selectedEvent.title)
			-- Store event for re-confirm handler
			CalendarSignup._reconfirmEvent = selectedEvent
			return
		end

		-- Close picker and proceed with async event reading
		if self._eventPickerFrame then
			self._eventPickerFrame:Release()
			self._eventPickerFrame = nil
		end

		Cerebro:Print(string.format("Reading invites for '%s'...", selectedEvent.title))
		self:OpenEventAndReadInvites(selectedEvent, function(players, event)
			self:ShowConfirmation(players, event)
		end)
	end)
	frame:AddChild(awardBtn)
end

function CalendarSignup:ShowConfirmation(players, event)
	if not players or #players == 0 then
		Cerebro:Print(string.format("|cffff8800No qualifying players found for '%s'.|r", event.title))
		return
	end

	local amount = self:GetSignUpEP()

	-- Print player list to chat for officer review
	Cerebro:Print(string.format("|cffFFD700Sign-Up EP Preview:|r %d EP for %d players from '%s':",
		amount, #players, event.title))
	for _, p in ipairs(players) do
		Cerebro:Print(string.format("  • %s (%s)", Cerebro:ColorPlayerName(p.name), p.statusName))
	end

	-- Store data for popup
	CalendarSignup._confirmData = {
		players = players,
		event = event,
	}

	-- Show confirmation popup
	-- Note: StaticPopup_Show only supports 2 format args (text_arg1, text_arg2)
	local text_arg1 = string.format("%d EP (Sign-Up) to %d qualifying players", amount, #players)
	local dialog = StaticPopup_Show("CEREBRO_SIGNUP_EP_CONFIRM", text_arg1, event.title)
	if dialog then
		dialog.data = CalendarSignup._confirmData
	end
end

--[[=============================================
	Static Popup Dialogs
===============================================]]

StaticPopupDialogs["CEREBRO_SIGNUP_EP_CONFIRM"] = {
	text = "Award |cffFFD700%s|r for\n\"%s\"?",
	button1 = "Award",
	button2 = "Cancel",
	OnAccept = function(_, data)
		if data and data.players and data.event then
			local CalSU = Cerebro:GetModule("CalendarSignup", true)
			if CalSU then
				CalSU:AwardSignUpEP(data.players, data.event)
			end
		end
	end,
	OnCancel = function()
		Cerebro:Print("|cffff8800Sign-Up EP cancelled.|r")
	end,
	timeout = 0,
	whileDead = true,
	hideOnEscape = true,
	preferredIndex = 4,
	hasItemFrame = false,
}

StaticPopupDialogs["CEREBRO_SIGNUP_EP_RECONFIRM"] = {
	text = "|cffff4444'%s' has already been awarded Sign-Up EP.|r\n\nAward again? This will result in duplicate EP.",
	button1 = "Award Again",
	button2 = "Cancel",
	OnAccept = function()
		local CalSU = Cerebro:GetModule("CalendarSignup", true)
		if CalSU and CalSU._reconfirmEvent then
			local event = CalSU._reconfirmEvent
			CalSU._reconfirmEvent = nil

			-- Close picker
			if CalSU._eventPickerFrame then
				CalSU._eventPickerFrame:Release()
				CalSU._eventPickerFrame = nil
			end

			Cerebro:Print(string.format("Reading invites for '%s'...", event.title))
			CalSU:OpenEventAndReadInvites(event, function(players, ev)
				CalSU:ShowConfirmation(players, ev)
			end)
		end
	end,
	OnCancel = function()
		local CalSU = Cerebro:GetModule("CalendarSignup", true)
		if CalSU then CalSU._reconfirmEvent = nil end
	end,
	timeout = 0,
	whileDead = true,
	hideOnEscape = true,
	preferredIndex = 4,
	hasItemFrame = false,
}

--[[=============================================
	Health Checks (Governance Requirement)
===============================================]]

function CalendarSignup:DescribeHealthChecks()
	return {
		{
			name = "CalendarSignup Config",
			check = function()
				local config = Cerebro.db.global.config.epgp
				if not config then return false, "EPGP config missing" end
				if not config.epSignUp then return false, "epSignUp not set" end
				if not config.signupStatuses then return false, "signupStatuses not set" end

				-- Verify at least one status is enabled
				local anyEnabled = false
				for _, enabled in pairs(config.signupStatuses) do
					if enabled then anyEnabled = true break end
				end
				if not anyEnabled then
					return false, "No qualifying statuses enabled"
				end

				return true, string.format("EP=%d, statuses configured", config.epSignUp)
			end,
		},
	}
end
