--[[
	Cerebro - EP Import Module
	
	PURPOSE:
	Rebuild EP from the transaction ledger to achieve deterministic convergence
	across officers. Mirrors the RCLC Import pattern: void stale EP transactions,
	clone them as fresh antedated transactions, replay history.
	
	Unlike RCLC_Import which reads from an external data source (RCLootCouncilDB),
	this module reads from Cerebro's own transaction ledger. It discovers sessions
	by scanning EP transactions with session-related reasons, groups them by
	sessionID, voids them, then recreates them as fresh authoritative transactions.
	This forces ReplayHistory to rebuild all EP from a clean, canonical state,
	eliminating divergence caused by duplicate or conflicting transactions across
	officers.
	
	RESPONSIBILITIES:
	1. Discover sessions from the transaction ledger (not db.global.history)
	2. Void existing session-based EP transactions in a date range
	3. Clone voided transactions as fresh antedated replacements
	4. Trigger ReplayHistory for deterministic state rebuild
	5. Bump lastUpdate on affected players for LWW convergence
	6. Provide dry-run simulation for safety
	
	SECURITY:
	- All import functions gated by IsOfficer()
	- In-raid, further gated by IsSyncLeader()
	- Read-only functions (GetSessionDates, SimulateReimport) available to officers
	
	EXTERNAL DEPENDENCIES:
	- Models/Ledger.lua (AddTransaction, VoidTransactions, ReplayHistory)
	- Modules/SyncScheduler.lua (IsSyncLeader)
]]--

local AddonName, _PrivateTable = ...
local Cerebro = LibStub("AceAddon-3.0"):GetAddon(AddonName)
local EPImport = Cerebro:NewModule("EP_Import", "AceEvent-3.0")

-- luacheck: globals IsInRaid GetServerTime

--[[=============================================
	Constants
===============================================]]

-- EP transaction reasons that are session-sourced and eligible for reimport
local SESSION_EP_REASONS = {
	["FARM_BOSS"] = true,       -- Farm boss kill (prefix match)
	["PROG_BOSS"] = true,       -- Prog boss kill (prefix match)
	["RAID_START"] = true,      -- On-time bonus
	["RAID_END"] = true,        -- Full attendance
	["PROGRESSION"] = true,     -- 30-min progression EP (prefix match)
	["SESSION_START"] = true,   -- Session marker (0 EP, informational)
	["SESSION_END"] = true,     -- Session marker (0 EP, informational)
	["CUSTOM_EP"] = true,       -- Custom EP awards during sessions
}

--- Check if a transaction reason matches any session EP reason (exact or prefix).
-- @param reason string: the transaction reason
-- @return boolean
local function IsSessionEPReason(reason)
	if not reason then return false end
	for pattern in pairs(SESSION_EP_REASONS) do
		if reason == pattern or reason:find("^" .. pattern) then
			return true
		end
	end
	return false
end

--[[=============================================
	Lifecycle
===============================================]]

function EPImport:OnEnable()
	Cerebro:Debug("EP_Import: Module enabled")
end

--[[=============================================
	Date Parsing
===============================================]]

--- Parse a user-supplied date argument (MM-DD-YYYY) into a Unix timestamp.
-- @param dateArg string: date in MM-DD-YYYY format (e.g. "03-19-2026")
-- @return number|nil: Unix timestamp for start of that day, or nil if invalid
function EPImport:ParseDateArg(dateArg)
	if not dateArg or type(dateArg) ~= "string" then return nil end
	
	local m, d, y = dateArg:match("^(%d%d)-(%d%d)-(%d%d%d%d)$")
	if not m then return nil end
	
	-- Validate ranges
	local month, day, year = tonumber(m), tonumber(d), tonumber(y)
	if month < 1 or month > 12 then return nil end
	if day < 1 or day > 31 then return nil end
	if year < 2020 or year > 2099 then return nil end
	
	return time({ year = year, month = month, day = day, hour = 0, min = 0, sec = 0 })
end

--[[=============================================
	Transaction-Based Session Discovery
===============================================]]

--- Discover sessions by scanning the transaction ledger.
-- Groups session-related EP transactions by sessionID (or by date if no sessionID).
-- This works even when db.global.history has been wiped by a reset.
-- @return table: sorted array of { sessionID, raidName, startTime, endTime, dateStr, txnCount, playerCount }
function EPImport:GetSessionDates()
	local transactions = Cerebro.db.global.transactions or {}
	local sessionMap = {}  -- sessionID -> session info
	
	for _, t in ipairs(transactions) do
		if t.currency == "EP" and not t.voided and IsSessionEPReason(t.reason) then
			-- Group by sessionID if available, otherwise by date
			local key = t.sessionID or date("%Y-%m-%d", t.timestamp)
			
			if not sessionMap[key] then
				-- Extract raid name from sessionID (format: "RaidName_YYYY-MM-DD-NNN")
				-- or from session name field
				local raidName = t.sessionName or "Unknown"
				if raidName == "Unknown" and t.sessionID then
					raidName = t.sessionID:match("^(.-)_") or t.sessionID
				end
				
				sessionMap[key] = {
					sessionID = key,
					raidName = raidName,
					startTime = t.timestamp,
					endTime = t.timestamp,
					txnCount = 0,
					players = {},
				}
			end
			
			local s = sessionMap[key]
			s.txnCount = s.txnCount + 1
			if t.timestamp < s.startTime then s.startTime = t.timestamp end
			if t.timestamp > s.endTime then s.endTime = t.timestamp end
			if t.player and t.player ~= "SESSION" then
				s.players[t.player] = true
			end
		end
	end
	
	-- Convert to sorted array
	local sessions = {}
	for _, s in pairs(sessionMap) do
		-- Count unique players
		local playerCount = 0
		for _ in pairs(s.players) do
			playerCount = playerCount + 1
		end
		
		table.insert(sessions, {
			sessionID = s.sessionID,
			raidName = s.raidName,
			startTime = s.startTime,
			endTime = s.endTime,
			dateStr = date("%m-%d-%Y", s.startTime),
			txnCount = s.txnCount,
			playerCount = playerCount,
		})
	end
	
	-- Sort by startTime (most recent first)
	table.sort(sessions, function(a, b) return a.startTime > b.startTime end)
	
	return sessions
end

--- Collect all session EP transactions in a date range from the ledger.
-- @param sinceTimestamp number: Unix timestamp; only include txns >= this
-- @return table: array of transaction references (non-voided, session EP, in range)
function EPImport:CollectSessionEPTransactions(sinceTimestamp)
	local transactions = Cerebro.db.global.transactions or {}
	local result = {}
	
	for _, t in ipairs(transactions) do
		if t.currency == "EP"
			and not t.voided
			and t.timestamp >= sinceTimestamp
			and IsSessionEPReason(t.reason)
		then
			table.insert(result, t)
		end
	end
	
	return result
end

--[[=============================================
	Import Pipeline
===============================================]]

--- Main reimport function. Voids old session EP transactions, clones them as
-- fresh transactions, replays history, and bumps LWW timestamps.
-- @param sinceTimestamp number: Unix timestamp; only process txns >= this
-- @return table: import results { imported=N, voided=N, sessions=N, errors={} }
function EPImport:ReimportFromSessions(sinceTimestamp)
	-- Gate: Officer check
	if not Cerebro:IsOfficer() then
		Cerebro:Print("EP Reimport requires officer permissions.")
		return { imported = 0, voided = 0, sessions = 0, errors = { "Not an officer" } }
	end
	
	-- Gate: In-raid, only SyncLeader can reimport
	if IsInRaid() and Cerebro.modules.SyncScheduler
		and not Cerebro.modules.SyncScheduler:IsSyncLeader() then
		Cerebro:Print("In raid, only the Sync Leader can run EP reimport.")
		return { imported = 0, voided = 0, sessions = 0, errors = { "Not Sync Leader" } }
	end
	
	-- Collect all session EP transactions in scope
	local scopedTxns = self:CollectSessionEPTransactions(sinceTimestamp)
	
	if #scopedTxns == 0 then
		Cerebro:Print("No session EP transactions found since the specified date.")
		return { imported = 0, voided = 0, sessions = 0, errors = { "No EP transactions in scope" } }
	end
	
	-- Discover unique sessions from the scoped transactions
	local sessionSet = {}
	for _, t in ipairs(scopedTxns) do
		local key = t.sessionID or date("%Y-%m-%d", t.timestamp)
		sessionSet[key] = true
	end
	local sessionCount = 0
	for _ in pairs(sessionSet) do sessionCount = sessionCount + 1 end
	
	Cerebro:Debug(string.format("EP_Import: %d transactions across %d sessions since %s",
		#scopedTxns, sessionCount, date("%Y-%m-%d", sinceTimestamp)))
	
	-- Snapshot: Save transaction data BEFORE voiding (we need the original values)
	local cloneData = {}
	for _, t in ipairs(scopedTxns) do
		table.insert(cloneData, {
			player = t.player,
			amount = t.amount,
			reason = t.reason,
			timestamp = t.timestamp,
			sessionID = t.sessionID,
			sessionName = t.sessionName,
		})
	end
	
	-- Phase 1: Void all existing session EP transactions in the date range
	local voidedCount = Cerebro.Ledger:VoidTransactions(function(t)
		if t.currency ~= "EP" then return false end
		if t.voided then return false end
		if t.timestamp < sinceTimestamp then return false end
		return IsSessionEPReason(t.reason)
	end)
	
	-- Phase 2: Clone voided transactions as fresh, authoritative replacements
	local importedCount = 0
	
	for _, data in ipairs(cloneData) do
		-- Skip SESSION_START/SESSION_END markers (0 EP, player="SESSION" creates ghost records)
		-- Only clone transactions with non-zero amounts
		if data.reason ~= "SESSION_START" and data.reason ~= "SESSION_END" and data.amount ~= 0 then
			Cerebro.Ledger:AddTransaction(
				data.player,      -- Full player name (Name-Realm)
				data.amount,      -- EP amount (preserved from original)
				"EP",             -- Currency
				data.reason,      -- Original reason (FARM_BOSS: X, RAID_START, etc.)
				{                 -- Details
					source = "EP-Reimport",
					originalSessionID = data.sessionID,
					reimportedAt = time(),
				},
				true,             -- isImport = true (suppress broadcast)
				{                 -- options
					overrideTimestamp = data.timestamp,  -- Antedate to original time
					silent = true,  -- Don't broadcast individual transactions
				}
			)
			
			importedCount = importedCount + 1
		end
	end
	
	-- Phase 3: Replay — rebuild all EP/GP from the unified ledger
	Cerebro.Ledger:ReplayHistory()
	
	-- Phase 4: LWW Anchor — bump lastUpdate for ALL players
	local now = GetServerTime and GetServerTime() or time()
	for name, pData in pairs(Cerebro.db.global.players) do
		if pData then
			pData.lastUpdate = now
		end
		-- Suppress unused warning
		local _ = name
	end
	
	-- Phase 5: Recalculate PR
	Cerebro:RecalculateAllPR()
	
	-- Phase 6: Refresh UI
	Cerebro:RefreshAllUI()
	
	-- Store import metadata
	Cerebro.db.global.epLastReimport = {
		timestamp = time(),
		sinceDate = sinceTimestamp,
		imported = importedCount,
		voided = voidedCount,
		sessions = sessionCount,
		reimportedBy = Cerebro:GetFullPlayerName(),
	}
	
	-- Generate report
	local results = {
		imported = importedCount,
		voided = voidedCount,
		sessions = sessionCount,
		errors = {},
	}
	self:GenerateReport(results)
	
	-- Audit log
	Cerebro.Ledger:AddAuditLog("EP_REIMPORT",
		string.format("Reimported %d EP transactions from %d sessions (voided %d) since %s by %s",
			importedCount, sessionCount, voidedCount,
			date("%m-%d-%Y", sinceTimestamp),
			Cerebro:GetFullPlayerName()))
	
	return results
end

--[[=============================================
	Dry-Run Simulation
===============================================]]

--- Dry-run: shows exactly what a reimport WOULD do without touching data.
-- Completely read-only: no transactions, no voiding, no ReplayHistory.
-- @param sinceTimestamp number: Unix timestamp from ParseDateArg
function EPImport:SimulateReimport(sinceTimestamp)
	Cerebro:Print("=== EP Reimport Dry-Run ===")
	Cerebro:Print(string.format("Scope: EP transactions from %s forward", date("%m-%d-%Y", sinceTimestamp)))
	
	-- Collect session EP transactions
	local scopedTxns = self:CollectSessionEPTransactions(sinceTimestamp)
	
	if #scopedTxns == 0 then
		Cerebro:Print("|cffff0000No session EP transactions found in scope.|r")
		return
	end
	
	-- Group by session for breakdown
	local sessionMap = {}
	local playerEP = {}
	
	for _, t in ipairs(scopedTxns) do
		local key = t.sessionID or date("%Y-%m-%d", t.timestamp)
		if not sessionMap[key] then
			local raidName = t.sessionName or "Unknown"
			if raidName == "Unknown" and t.sessionID then
				raidName = t.sessionID:match("^(.-)_") or t.sessionID
			end
			sessionMap[key] = {
				raidName = raidName,
				startTime = t.timestamp,
				txnCount = 0,
				totalEP = 0,
			}
		end
		
		local s = sessionMap[key]
		s.txnCount = s.txnCount + 1
		s.totalEP = s.totalEP + t.amount
		if t.timestamp < s.startTime then s.startTime = t.timestamp end
		
		if t.player and t.player ~= "SESSION" and t.amount ~= 0 then
			playerEP[t.player] = (playerEP[t.player] or 0) + t.amount
		end
	end
	
	-- Print per-session breakdown
	local sessionCount = 0
	local sessionList = {}
	for key, s in pairs(sessionMap) do
		sessionCount = sessionCount + 1
		table.insert(sessionList, { key = key, data = s })
	end
	table.sort(sessionList, function(a, b) return a.data.startTime < b.data.startTime end)
	
	Cerebro:Print(string.format("  Sessions found: %d (%d total EP transactions)",
		sessionCount, #scopedTxns))
	
	for _, entry in ipairs(sessionList) do
		local s = entry.data
		Cerebro:Print(string.format("  |cff00ccff%s|r %s — %d txns, %d total EP",
			date("%m-%d-%Y", s.startTime),
			s.raidName,
			s.txnCount,
			s.totalEP))
	end
	
	-- Per-player breakdown
	local sortedPlayers = {}
	for name in pairs(playerEP) do
		table.insert(sortedPlayers, name)
	end
	table.sort(sortedPlayers)
	
	local totalEP = 0
	Cerebro:Print("")
	Cerebro:Print("|cffFFD700--- Results ------|r")
	Cerebro:Print(string.format("  Would void & re-create: %d EP transactions", #scopedTxns))
	Cerebro:Print(string.format("  |cff00ff00EP totals for %d players:|r", #sortedPlayers))
	for _, name in ipairs(sortedPlayers) do
		local shortName = name:match("^([^-]+)") or name
		Cerebro:Print(string.format("    %s: %d EP", shortName, playerEP[name]))
		totalEP = totalEP + playerEP[name]
	end
	
	Cerebro:Print("")
	Cerebro:Print(string.format("|cffFFD700Total: %d EP across %d players from %d sessions|r",
		totalEP, #sortedPlayers, sessionCount))
	Cerebro:Print("|cff00ff00This is a dry run. No data was modified.|r")
	Cerebro:Print("To run the real reimport: /cb ep reimport " .. date("%m-%d-%Y", sinceTimestamp))
end

--[[=============================================
	Reporting
===============================================]]

--- Print reimport summary to chat.
-- @param results table: { imported=N, voided=N, sessions=N, errors={} }
function EPImport:GenerateReport(results)
	Cerebro:Print("=== EP Reimport Summary ===")
	Cerebro:Print(string.format("  Sessions processed: %d", results.sessions))
	Cerebro:Print(string.format("  Voided:   %d old transactions", results.voided))
	Cerebro:Print(string.format("  Created:  %d new transactions", results.imported))
	
	if results.errors and #results.errors > 0 then
		Cerebro:Print("  Errors:")
		for _, err in ipairs(results.errors) do
			Cerebro:Print("    - " .. err)
		end
	end
	
	Cerebro:Print("EP values updated. V2 sync will propagate to all online players.")
end

--- Print status of last reimport.
function EPImport:PrintStatus()
	local last = Cerebro.db.global.epLastReimport
	if not last then
		Cerebro:Print("No EP reimport has been performed yet.")
		return
	end
	
	Cerebro:Print("=== EP Reimport Status ===")
	Cerebro:Print(string.format("  Last reimport: %s", date("!%Y-%m-%d %H:%M:%S", last.timestamp)))
	Cerebro:Print(string.format("  Since date:    %s", date("%m-%d-%Y", last.sinceDate)))
	Cerebro:Print(string.format("  Sessions:      %d", last.sessions))
	Cerebro:Print(string.format("  Created:       %d transactions", last.imported))
	Cerebro:Print(string.format("  Voided:        %d old transactions", last.voided))
	Cerebro:Print(string.format("  Reimported by: %s", last.reimportedBy or "?"))
end

return EPImport
