--[[
	Cerebro - EPGP Module
	
	PURPOSE:
	The EPGP (Effort Points / Gear Points) module is the primary "Rules Engine" for the Cerebro
	loot distribution system. It orchestrates point awards, gear cost calculations, weekly decay,
	and readiness cap enforcement. This module is authorization-aware and delegates mathematical
	operations to Models/Player.lua while maintaining transaction logs via Models/Ledger.lua.
	
	RESPONSIBILITIES:
	1. Item Cost Calculation - Compute GP cost for items based on slot/difficulty/modifiers
	2. Point Awards - AwardEP/ChargeGP operations with ledger integration
	3. Weekly Decay - Orchestrate decay across all players with transaction logging
	4. Readiness Caps - Enforce weekly EP caps for preparation activities
	5. Mass Operations - Bulk EP awards with optional filtering
	
	CONTRACTS (per docs/SPEC.md):
	
	# GetItemGP(itemLink, difficultyID, isOffspec, isMinorUpgrade) -> number
	  - Formula: SlotWeight * DifficultyMultiplier * Modifier (SPEC 2.3)
	  - Returns: Floor(GP), 0 if invalid item or non-equippable
	  - Modifiers: Offspec (0.5x), MinorUpgrade (0.5x), mutually exclusive
	  - Difficulty: Uses config.epgp.difficultyMultipliers[difficultyID], defaults to 1.0
	  - Slot Weights: config.epgp.slotWeights[itemEquipLoc]
	  
	# AwardEP(playerName, amount, reason, details) -> newEP
	  - MUST call Player:AddEP() then Ledger:AddTransaction() in that order
	  - MUST NOT award negative amounts (use separate deduction if needed)
	  - Returns: Updated EP value after award
	  - Side Effects: Broadcasts to raid, triggers Session tracking
	  
	# ChargeGP(playerName, amount, reason, details) -> newGP
	  - MUST call Player:AddGP() then Ledger:AddTransaction() in that order
	  - Returns: Updated GP value (floored at BaseGP per Player model)
	  - Side Effects: Broadcasts to raid, triggers Session tracking
	  
	# Decay() -> void
	  - Formula: Player:Decay(rate) for each player (SPEC 2.4: NewVal = OldVal * (1 - Rate))
	  - MUST log 2 transactions per player (EP decay, GP decay) with negative amounts
	  - Rate: config.epgp.decayRate (0.0 to 1.0)
	  - Updates: global.lastDecay timestamp
	  - Side Effects: Broadcasts decay event to raid
	  
	# AwardReadinessEP(playerName, amount, reason) -> actualAwarded
	  - Enforces config.epgp.readinessCap (default 1000 EP per week)
	  - Tracks: global.readiness.players[name] and global.readiness.weekID
	  - Returns: Actual EP awarded (may be less than requested if near cap)
	  - Resets: Tuesday 11AM ET (calculated via GetWeekID)
	
	INVARIANTS:
	1. All EP/GP mutations MUST create Ledger transactions (audit requirement)
	2. GetItemGP MUST return 0 (not nil) for invalid/missing items (defensive)
	3. Decay transactions MUST use negative amounts to represent removal
	4. Weekly reset MUST clear readiness.players table but preserve weekID
	
	DESIGN DECISIONS:
	- Offspec and MinorUpgrade modifiers are mutually exclusive (elseif logic, line 147-150)
	- Decay logs per-player transactions (not a single "mass decay" event) for audit granularity
	- GetWeekID uses Unix epoch math (not WoW API) for cross-region consistency
	- Communication and Session modules are optional dependencies (checked before calling)
	
	SECURITY MODEL:
	This module does NOT enforce authorization. Callers (Core.lua slash commands, RCLC_Bridge)
	MUST verify IsOfficer() before invoking state-mutating functions (AwardEP, ChargeGP, Decay).
	Read-only functions (GetItemGP, GetConfig) are safe for all roles.
	
	EXTERNAL DEPENDENCIES:
	- Models/Player.lua (AddEP, AddGP, Decay, GetPR)
	- Models/Ledger.lua (AddTransaction)
	- Modules/CommunicationV2.lua (BroadcastEPGPUpdate, BroadcastDecay, ForwardToSyncLeader)
	- Modules/Session.lua (OnPointChange, optional)
	- WoW API: C_Item.GetItemInfoInstant (item metadata)
	
	TEST COVERAGE:
	- TEST-ITEM-COST-001: GP calculation with modifiers
	- TEST-READINESS-001: Weekly cap enforcement
	- TEST-DECAY-INTEGRATION-001: Decay orchestration and ledger logging
]]--

local AddonName, PrivateTable = ...
local Cerebro = LibStub("AceAddon-3.0"):GetAddon(AddonName)
local EPGP = Cerebro:NewModule("EPGP", "AceEvent-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale(AddonName)

-- User-requested motivational quotes for weekly reset
EPGP.quotes = {
	"Success occurs when opportunity meets preparation.",
	"By failing to prepare, you are preparing to fail.",
	"Victory awaiting those who are ready.",
	"The best preparation for tomorrow is doing your best today.",
	"Fortune favors the prepared mind.",
	"Excellence is not an act, but a habit. Stay prepared!",
	"Give me six hours to chop down a tree and I will spend the first four sharpening the axe.",
	"Raid preparedness is half the victory.",
	"Champions are made in the preparation, not just the battle.",
	"A prepared raider is a surviving raider.",
	"Focus on the process, and the results will follow.",
	"Your readiness determines your success. Cap it out!",
}

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

function EPGP:OnEnable()
	-- Event registrations
	self:CheckWeeklyReset()
end

function EPGP:Debug(msg)
	Cerebro:Debug("|cff00ccff[EPGP]|r " .. tostring(msg))
end

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

function EPGP:GetConfig()
	return Cerebro.db.global.config.epgp
end

--[[=============================================
	Readiness (Weekly Cap)
=============================================]]

-- Calculate current Week ID (Tuesday 11AM ET / 15:00 UTC approximately)
-- Simplified: Just use standard WoW reset API if available, or calendar week.
-- User requested Tuesday 11AM ET.
function EPGP:GetWeekID()
	local regionStart = 1136041200 -- A Tuesday in the past (Dec 31, 2005 11:00 AM ET est)
	local now = time()
	local diff = now - regionStart
	local weeks = math.floor(diff / 604800)
	return "W" .. weeks
end

function EPGP:CheckWeeklyReset()
	if not Cerebro.db.global.readiness then
		Cerebro.db.global.readiness = { weekID = self:GetWeekID(), players = {} }
	end
	
	local currentWeek = self:GetWeekID()
	local savedWeek = Cerebro.db.global.readiness.weekID
	
	if currentWeek ~= savedWeek then
		-- RESET
		Cerebro.db.global.readiness.weekID = currentWeek
		Cerebro.db.global.readiness.players = {} -- Clear all weekly progress
		
		-- Motivational Quote
		local quote = self.quotes[math.random(1, #self.quotes)]
		Cerebro:Print("|cff00ff00Weekly Readiness Reset!|r Time to prepare for the raid.")
		Cerebro:Print(string.format("|cffffaa00\"%s\"|r", quote))
	end
end

function EPGP:GetReadiness(player)
	if not Cerebro.db.global.readiness then return 0 end
	if not Cerebro.db.global.readiness.players then return 0 end
	return Cerebro.db.global.readiness.players[player] or 0
end

-- Awards EP but caps it based on weekly readiness limit
-- Contract: Returns actual EP awarded (may be < amount if near cap)
-- Weekly Cap: Resets every Tuesday 11AM ET (tracked via GetWeekID)
-- Use Case: Preparation module awards EP for gems/enchants/flasks, capped to prevent farming
-- Phase 1 Extension: Accepts optional `details` for Mythic+ deduplication metadata
function EPGP:AwardReadinessEP(playerName, amount, reason, details)
	local config = self:GetConfig()
	local cap = config.readinessCap or 1000 -- Default 1000 EP per week
	
	local current = self:GetReadiness(playerName) -- Tracks weekly progress
	
	-- Early exit if already capped (dedupe message to once per 5 seconds)
	if current >= cap then
		local now = time()
		if not self.lastCapMessage or (now - self.lastCapMessage) >= 5 then
			Cerebro:Print(string.format("You have reached the maximum weekly cap pre raid of %d. More points available during raid time.", cap))
			self.lastCapMessage = now
		end
		return 0
	end
	
	-- Calculate how much we can actually award (may be partial)
	local available = cap - current
	local actualAmount = math.min(amount, available)
	
	if actualAmount > 0 then
		-- Award the capped EP (delegates to AwardEP for ledger/broadcast)
		self:AwardEP(playerName, actualAmount, reason .. " (Readiness)", details)
		
		-- Update weekly tracking (separate from main EP total)
		if not Cerebro.db.global.readiness.players then Cerebro.db.global.readiness.players = {} end
		Cerebro.db.global.readiness.players[playerName] = current + actualAmount
		
		-- Notify if we just hit the cap with this award
		if (current + actualAmount) >= cap then
			Cerebro:Print(string.format("You have reached the maximum weekly cap pre raid of %d. More points available during raid time.", cap))
		end
	end
	
	return actualAmount -- Return actual amount awarded (for caller feedback)
end

--[[=============================================
	Calculations (Projected)
===============================================]]

-- Get Base GP for a specific slot
function EPGP:GetSlotBaseGP(slot)
	local weights = self:GetConfig().slotWeights
	return weights[slot] or 0
end

-- Calculate GP cost for an item
-- Contract: SlotWeight * DifficultyMultiplier * Modifier (SPEC 2.3)
-- Returns 0 for invalid items (defensive against broken itemLinks from RCLC)
-- Get GP value for an item link
-- overrideItemID: Optional, ID to force lookup (e.g. from TooltipData.id)
function EPGP:GetItemGP(itemLink, difficultyID, isOffspec, isMinorUpgrade, overrideItemID)
	if not itemLink then return 0 end
    
    -- Defensive: Ensure itemLink is actually a link or string containing item info
    if type(itemLink) ~= "string" or not itemLink:find("item:") then 
        return 0 
    end
	
	-- Determine ID: Use override if provided (most accurate), otherwise reliable extraction
	local itemID = overrideItemID or Cerebro:GetReliableItemID(itemLink)
	
	-- Get properties using the resolved ID
	local _, _, _, itemEquipLoc, _, classID, subclassID = C_Item.GetItemInfoInstant(itemID)
	
	-- Token Handling (Patch)
	-- If item is a token, resolve its target slot (e.g., "head" -> "INVTYPE_HEAD")
	-- This ensures tokens cost the same as the gear they create.
	local isToken, tokenSlot = Cerebro:IsItemToken(itemLink)
	if isToken then
		if tokenSlot and Cerebro.TokenSlotToEquipLoc and Cerebro.TokenSlotToEquipLoc[tokenSlot] then
			itemEquipLoc = Cerebro.TokenSlotToEquipLoc[tokenSlot]
		else
			-- Fallback removed: If token slot can't be resolved, treat as 0 GP (no cost)
			return 0
		end
	end
	
	-- Handle Curios (Context Tokens)
	if Cerebro:IsCurio(itemLink) then
		itemEquipLoc = "Curio"
	end
	
	-- Handle Recipes (Class 9)
	if classID == 9 then
		itemEquipLoc = "INVTYPE_RECIPE"
	end

	-- Handle Toys (collectible fun items)
	-- C_ToyBox.GetToyInfo returns (itemID, toyName, icon, ...) — toyName is nil if not a toy
	if C_ToyBox and C_ToyBox.GetToyInfo and itemID then
		local _, toyName = C_ToyBox.GetToyInfo(itemID)
		if toyName then
			itemEquipLoc = "INVTYPE_TOY"
		end
	end

	-- Handle Companion Pets (classID 15 subclassID 2 = Misc/Pet, classID 17 = Battle Pet)
	if classID == 17 or (classID == 15 and subclassID == 2) then
		itemEquipLoc = "INVTYPE_PET"
	end

	-- Handle Mounts (classID 15 = Miscellaneous, subclassID 5 = Mount)
	if classID == 15 and subclassID == 5 then
		itemEquipLoc = "INVTYPE_MOUNT"
	end

	-- Handle Cosmetic items (appearance-only gear, since Retail 9.0.2)
	-- Must come AFTER token/curio checks to avoid overriding real gear classifications
	if C_Item.IsCosmeticItem and C_Item.IsCosmeticItem(itemLink) then
		itemEquipLoc = "INVTYPE_COSMETIC"
	end

	-- Handle Decor items (classID 20 — Delves/decoration items)
	if classID == 20 then
		itemEquipLoc = "INVTYPE_DECOR"
	end

	local baseGP = self:GetSlotBaseGP(itemEquipLoc)
	if baseGP == 0 then return 0 end -- Non-equippable or unconfigured slot
	
	-- 2. Difficulty Multiplier (Default to Normal/1.0 if not found)
	-- Example: Mythic might be 1.5x, Heroic 1.2x
	-- Note: Config uses string keys "14", "15", "16" for UI compatibility
	local diffMult = self:GetConfig().difficultyMultipliers[tostring(difficultyID)] or 1.0
	local gp = baseGP * diffMult
	
	-- 3. Apply Modifiers (Offspec and MinorUpgrade are mutually exclusive)
	-- Offspec: Typically 0.5x (half cost for non-main-spec items)
	-- MinorUpgrade: Typically 0.5x (half cost for small ilvl increases)
	if isOffspec then
		gp = gp * self:GetConfig().offspecMultiplier
	elseif isMinorUpgrade then
		gp = gp * self:GetConfig().minorUpgradeMultiplier
	end
	
	-- Return floored GP (standard rounding to avoid fractional costs)
	return math.floor(gp + 0.5)
end

--[[=============================================
	Actions (State Modifiers)
===============================================]]

-- Award EP to a player
-- Contract: MUST mutate Player state THEN log transaction (order matters for audit replay)
-- Authorization: Caller (Core.lua, Session.lua) MUST verify IsOfficer() before calling
function EPGP:AwardEP(playerName, amount, reason, details)
	-- Block EP awards to Pending players — they must be admitted to Core first
	local pData = Cerebro:GetPlayerData(playerName)
	if pData and pData.status == "Pending" then
		Cerebro:Debug("AwardEP: skipping Pending player " .. playerName)
		return pData.ep or 0
	end

	local player = Cerebro.Player:New(playerName)
	local newEP = player:AddEP(amount) -- Mutate state first
	
	-- INVARIANT: All EP changes must be logged for audit trail
	Cerebro.Ledger:AddTransaction(playerName, amount, "EP", reason, details)
	
	-- Optional: Broadcast to raid members (CommunicationV2 module may not be loaded)
	if Cerebro.modules.CommunicationV2 then
		Cerebro.modules.CommunicationV2:BroadcastEPGPUpdate(playerName, newEP, amount, reason, "EP")
	end
	
	-- Optional: Session tracking for attendance reporting
	if Cerebro.modules.Session and Cerebro.modules.Session.OnPointChange then
		Cerebro.modules.Session:OnPointChange(playerName, amount, "EP")
	end

	-- Fix 2c: Forward to sync leader if we're not the leader
	self:MaybeForwardToSyncLeader(playerName, amount, "EP", reason, details)

	-- Refresh officer's own UI immediately (HandleRPC self-exclusion drops our own echo,
	-- so RefreshAllUI() in HandleEPGPUpdate never fires for the awarding officer)
	Cerebro:RefreshAllUI()

	return newEP
end

-- Fix 2c: Forward award to Sync Leader (non-leader officers during raids)
function EPGP:MaybeForwardToSyncLeader(playerName, amount, currency, reason, details)
	-- Only forward during raids when we are NOT the sync leader
	if not IsInRaid() then return end
	if not Cerebro.modules.SyncScheduler then return end
	if Cerebro.modules.SyncScheduler:IsSyncLeader() then return end

	local leaderName = Cerebro.modules.SyncScheduler:GetSyncLeaderName()
	if not leaderName then return end

	local leaderShort = Cerebro:GetShortPlayerName(leaderName)
	if not leaderShort then return end

	if not Cerebro.modules.CommunicationV2 then return end

	local fwdData = {
		player = playerName,
		amount = amount,
		currency = currency,
		reason = reason,
		details = details,
		source = Cerebro:GetPlayerFullName(),
	}

	Cerebro.modules.CommunicationV2:ForwardToSyncLeader(fwdData, leaderShort)
	Cerebro:Debug(string.format("EPGP_FORWARD (V2): %s %+d %s to leader %s", playerName, amount, currency, leaderShort))
end

-- Charge GP to a player
-- details can be string (item link) or table with rich data
function EPGP:ChargeGP(playerName, amount, reason, details)
	-- Block GP charges to Pending players — they must be admitted to Core first
	local pData = Cerebro:GetPlayerData(playerName)
	if pData and pData.status == "Pending" then
		Cerebro:Debug("ChargeGP: skipping Pending player " .. playerName)
		return pData.gp or (self:GetConfig().baseGP or 200)
	end

	local player = Cerebro.Player:New(playerName)
	local preGP = player:GetData().gp or self:GetConfig().baseGP or 200
	local newGP = player:AddGP(amount)
	
	-- Build rich details for history
	local richDetails = details
	if type(details) == "string" then
		-- Convert item link to rich details
		local itemLink = details
		local _, _, _, equipLoc = C_Item.GetItemInfoInstant(itemLink)
		richDetails = {
			item = itemLink,
			slot = equipLoc and Cerebro:GetFriendlySlotName(equipLoc) or "Unknown",
			preGP = preGP,
			postGP = newGP
		}
	elseif type(details) == "table" then
		-- Add pre/post GP to existing details
		details.preGP = preGP
		details.postGP = newGP
	else
		richDetails = {
			preGP = preGP,
			postGP = newGP
		}
	end
	
	Cerebro.Ledger:AddTransaction(playerName, amount, "GP", reason, richDetails)
	
	if Cerebro.modules.CommunicationV2 then
		Cerebro.modules.CommunicationV2:BroadcastEPGPUpdate(playerName, newGP, amount, reason, "GP")
	end

	-- Session Tracking
	if Cerebro.modules.Session and Cerebro.modules.Session.OnPointChange then
		Cerebro.modules.Session:OnPointChange(playerName, amount, "GP")
	end

	-- Fix 2c: Forward to sync leader if we're not the leader
	self:MaybeForwardToSyncLeader(playerName, amount, "GP", reason, richDetails)

	-- Refresh officer's own UI immediately (HandleRPC self-exclusion drops our own echo,
	-- so RefreshAllUI() in HandleEPGPUpdate never fires for the awarding officer)
	Cerebro:RefreshAllUI()

	return newGP
end

-- Mass Award EP
-- filterFunc: function(name, data) return boolean end
function EPGP:MassAwardEP(amount, reason, filterFunc)
	local count = 0
	for name, data in pairs(Cerebro.db.global.players) do
		if not filterFunc or filterFunc(name, data) then
			-- Using lightweight check to avoid object creation overhead if list is huge?
			-- Optimization: Just use Player model, it's cheap (just a table wrapper)
			self:AwardEP(name, amount, reason, {mass = true})
			count = count + 1
		end
	end
	return count
end

-- Perform Weekly Decay
-- Contract: Iterate all players, apply Player:Decay(), log 2 transactions per player
-- Formula: NewVal = OldVal * (1 - Rate) (SPEC 2.4), delegated to Player model
-- Authorization: Caller MUST verify IsOfficer() before calling (typically Core.lua /cb decay)
function EPGP:Decay()
	local rate = self:GetConfig().decayRate -- 0.0 to 1.0 (e.g., 0.1 = 10% decay)
	local count = 0
	
	-- Orchestrate decay for all players
	-- Design Decision: Log per-player transactions (not a single "mass decay" event)
	-- Rationale: Enables granular audit trail and replay-from-ledger capability
	
	for name, _ in pairs(Cerebro.db.global.players) do
		local player = Cerebro.Player:New(name)
		local decayedEP, decayedGP = player:Decay(rate) -- Returns amounts removed
		
		-- INVARIANT: Decay transactions use NEGATIVE amounts to represent removal
		if decayedEP > 0 or decayedGP > 0 then
			Cerebro.Ledger:AddTransaction(name, -decayedEP, "EP", "DECAY", {rate = rate})
			Cerebro.Ledger:AddTransaction(name, -decayedGP, "GP", "DECAY", {rate = rate})
			count = count + 1
		end
	end
	
	Cerebro:Print(string.format("Decayed EP/GP by %d%% for %d players.", rate * 100, count))
	Cerebro.db.global.lastDecay = time() -- Record timestamp for UI display

	if Cerebro.modules.CommunicationV2 then
		Cerebro.modules.CommunicationV2:BroadcastDecay(rate * 100)
	end

	-- Refresh UI so standings reflect updated EP/GP immediately
	Cerebro:RefreshAllUI()
end

-- Helper to check if player can afford (though EPGP usually doesn't strictly forbid negatives, 
-- but MinGP is a thing. Usually you can go high GP, PR just drops.)
function EPGP:CanAfford(playerName, _)
	local player = Cerebro.Player:New(playerName)
	local minEP = self:GetConfig().minEP or 0
	
	if player:GetEP() < minEP then
		return false, "Below Min EP"
	end
	
	return true
end
