--[=[
	Cerebro - Preparation Module
	
	PURPOSE:
	The Preparation module orchestrates raid-wide readiness checks (gear quality, enchants,
	gems, consumables) and awards "Preparation Bonus" EP to eligible players. It implements
	asynchronous inspection pipelines with timeout handling and RBAC-enforced award logic.
	
	RESPONSIBILITIES:
	1. CheckRaid - Officer-only raid-wide inspection (async pipeline with timeout)
	2. CheckPersonalReadiness - Public personal readiness check (sync, UI-facing)
	3. Inspection Pipeline - Queue management, INSPECT_READY handling, graceful timeouts
	4. Award Orchestration - Roster-bound EP awards via EPGP:AwardReadinessEP
	5. Reporting - Officer chat + UI feedback for readiness status
	
	CONTRACTS (per docs/SPEC.md):
	
	# CheckRaid() -> void
	  - RBAC: MUST enforce IsOfficer() with hard gate at entry
	  - Idempotency: MUST reject if scan already in progress (self.isScanning == true)
	  - Timeout: MUST complete within 60s overall (2s per unit + queue processing)
	  - Side Effects: Broadcasts report to officer chat, awards EP to prepared players
	  - Award Eligibility: Respect config.prep toggles (requireGems/Enchants/Consumables)
	  - Award Bounds: Only award if session.roster[player] exists (roster-bound, conservative)
	  - Session Dedupe: MUST only award once per session (session.prepAwarded flag)
	  
	# CheckPersonalReadiness() -> boolean, table
	  - Public: No officer requirement
	  - Returns: isReady (boolean), report { gems, enchants, score }
	  - Note: Consumables excluded from personal readiness indicator per user request
	  
	# IsPrepared(result, config) -> boolean
	  - Helper: Determines if player passes prep check based on config toggles
	  - Logic: If requireGems=false, ignore result.hasGems (similar for enchants/consumables)
	  - Returns: true if player meets ALL enabled requirements
	
	INSPECTION PIPELINE:
	1. BuildQueue: Enumerate raid members, prioritize self (sync check), queue others
	2. ProcessNextInspect: Pop unit, NotifyInspect(unit), set 2s timeout
	3. OnInspectReady: Cancel timeout, run CheckPlayer, record result, advance queue
	4. Timeout Handler: Record "unknown" result, clear inspect, advance queue
	5. FinishScan: Aggregate results, run report, award EP if session active
	
	INVARIANTS:
	1. self.isScanning MUST be cleared on all exit paths (success, timeout, error, early return)
	2. ClearInspectPlayer() MUST be called before NotifyInspect() on next unit (WoW API requirement)
	3. session.prepAwarded MUST only be set AFTER all awards complete (atomic commit)
	4. Awards MUST validate session.roster[player] exists (roster-bound safety)
	5. Config toggles MUST be respected for award eligibility (no hardcoded assumptions)
	
	DESIGN DECISIONS:
	- 2s per-unit timeout: Balance between network latency and scan speed
	- 60s overall scan bound: Prevent infinite hangs in edge cases
	- Self-inspection is synchronous: No NotifyInspect() needed, no timeout risk
	- Report-only mode: If no session/roster, run report but skip awards (safe degradation)
	- Consumables excluded from personal check: Per user request ("Gems, Enchants Checked. No consumables")
	
	SECURITY MODEL:
	- CheckRaid: RBAC enforced at entry (hard gate), no soft checks
	- CheckPersonalReadiness: Public API, safe for all players
	- Award logic: Double-gated (IsOfficer in CheckRaid + session.roster validation)
	
	EXTERNAL DEPENDENCIES:
	- EPGP:AwardReadinessEP (Money! Audit trail via Ledger)
	- Session:GetCurrentSession (State check, roster validation)
	- Communication:SendOfficerMessage (Reporting)
	- WoW API: NotifyInspect, ClearInspectPlayer, GetInventoryItemLink, C_UnitAuras (11.0+) / UnitBuff (fallback)
	
	TEST COVERAGE:
	- TEST-PREP-001: Inspect logic (mocked gear slots)
	- TEST-PREP-002: Consumable checks (mocked UnitBuff)
	- TEST-PREP-003: Award flow (mocked EPGP:AwardReadinessEP)
	- TEST-PREP-004: Idempotent scan lock
	- TEST-PREP-005: Timeout behavior
	- TEST-PREP-006: Config toggle respect
	- TEST-PREP-007: Report-only mode (no session/roster)
	- PREP-SMOKE-001: Officer runs /cb prep in raid
	- PREP-SMOKE-002: Award idempotency (second check same session)
]=]

local AddonName = "Cerebro"
local Cerebro = LibStub("AceAddon-3.0"):GetAddon(AddonName)

local Preparation = {}
Cerebro.modules.Preparation = Preparation

-- Inspection State
Preparation.inspectQueue = {}
Preparation.inspectResults = {}
Preparation.isScanning = false
Preparation.currentUnit = nil
Preparation.scanTimeout = nil
Preparation.scanStartTime = nil  -- NEW: Overall scan timeout tracking

-- Helper: Get minimum item level from config (defaults to 560)
local function GetMinIlvl()
	return Cerebro.db.global.config.prep.minIlvl or 560
end

-- Slots that should have enchants
local ENCHANTABLE_SLOTS = {
	[1] = "Head",			-- Not always enchantable in retail
	[2] = "Neck",
	[3] = "Shoulder",		-- Not always enchantable in retail
	[4] = "Shirt",
	[5] = "Chest",
	[6] = "Waist",			-- Not enchantable
	[7] = "Legs",
	[8] = "Feet",
	[9] = "Wrist",
	[10] = "Hands",
	[11] = "Finger 1",
	[12] = "Finger 2",
	[13] = "Trinket 1",
	[14] = "Trinket 2",
	[15] = "Back",
	[16] = "MainHand",
	[17] = "SecondaryHand",
}

-- Slots that MUST have an item equipped
local PRESENCE_SLOTS = {
	[1] = true, -- Head
	[2] = true, -- Neck
	[3] = true, -- Shoulder
	[5] = true, -- Chest
	-- [6] Waist skipped per instruction
	-- [7] Legs skipped per instruction (though enchant check might catch it)
	[8] = true, -- Feet
	[9] = true, -- Wrist
	[10] = true, -- Hands
	[11] = true, -- Ring 1
	[12] = true, -- Ring 2
	[13] = true, -- Trinket 1
	[14] = true, -- Trinket 2
	[15] = true, -- Back
	[16] = true, -- Main Hand
}

-- Slots where enchants are typically required (Retail focus)
local REQUIRED_ENCHANT_SLOTS = {
	[5] = true,		-- Chest
	[8] = true,		-- Feet
	[9] = true,		-- Wrist
	[11] = true,	-- Ring 1
	[12] = true,	-- Ring 2
	[15] = true,	-- Back
	[16] = true,	-- Main Hand
}

-- Helper: Determine if player is prepared based on config toggles
-- Respects requireGems/requireEnchants/requireConsumables config flags
local function IsPrepared(result, config)
	local cfg = config or Cerebro.db.global.config.prep
	
	-- Check gems (only if required)
	if cfg.requireGems and not result.hasGems then
		return false
	end
	
	-- Check enchants (only if required)
	if cfg.requireEnchants and not result.hasEnchants then
		return false
	end
	
	-- Check consumables (only if required)
	if cfg.requireConsumables and not result.hasConsumables then
		return false
	end
	
	return true
end

function Preparation:CheckRaid()
	-- RBAC: Hard gate (CRITICAL - officer-only execution)
	if not Cerebro:IsOfficer() then
		Cerebro:Debug("Prep: Non-officer attempted CheckRaid, ignoring")
		return
	end
	
	if not IsInRaid() then
		Cerebro:Print("You must be in a raid to run preparation checks.")
		return
	end

	-- Idempotency: Scan lock
	if self.isScanning then
		Cerebro:Debug("Prep: Scan already in progress, ignoring duplicate request")
		Cerebro:Print("Preparation check already in progress...")
		return
	end

	Cerebro:Print("Starting preparation check (scanning players)...")
	
	-- Register event explicitly
	Cerebro:RegisterEvent("INSPECT_READY", function(...) self:OnInspectReady(...) end)
	
	self.inspectQueue = {}
	self.inspectResults = {}
	self.isScanning = true
	self.scanStartTime = time()  -- Track overall scan start

	-- Build queue
	for i = 1, GetNumGroupMembers() do
		local unit = "raid" .. i
		if UnitExists(unit) and UnitIsConnected(unit) then
			-- Prioritize ourselves (no inspect needed)
			if UnitIsUnit(unit, "player") then
				-- Process synchronously, do NOT trigger queue advancement
				local result = self:CheckPlayer(unit)
				table.insert(self.inspectResults, result)
				Cerebro:Debug("Checked self (synchronous)")
			else
				table.insert(self.inspectQueue, unit)
			end
		end
	end

	-- Start processing queue
	Cerebro:Debug("Queue size: " .. #self.inspectQueue)
	self:ProcessNextInspect()
end

function Preparation:ProcessNextInspect()
	-- Overall timeout guard (60s max)
	if self.scanStartTime and (time() - self.scanStartTime) > 60 then
		Cerebro:Debug("Prep: Overall scan timeout (60s), forcing completion")
		self:FinishScan()
		return
	end
	
	if #self.inspectQueue == 0 then
		self:FinishScan()
		return
	end

	local unit = table.remove(self.inspectQueue, 1)
	self.currentUnit = unit
	
	Cerebro:Debug("Inspecting: " .. (UnitName(unit) or "Unknown"))
	
	-- Clear any previous inspect state
	ClearInspectPlayer()
	
	-- Notify server we want to inspect
	if CanInspect(unit) then
		NotifyInspect(unit)
		
		-- Set timeout (2 seconds)
		self.scanTimeout = Cerebro:ScheduleTimer(function()
			Cerebro:Debug("Timeout for " .. (UnitName(unit) or "Unknown"))
			self:RecordResultAndNext(unit) 
		end, 2)
	else
		Cerebro:Debug("Cannot inspect " .. (UnitName(unit) or "Unknown") .. ", skipping detail check")
		self:RecordResultAndNext(unit)
	end
end

function Preparation:OnInspectReady(event, guid)
	if not self.isScanning or not self.currentUnit then return end
	
	-- Check if this event is for the unit we are currently waiting for
	if UnitGUID(self.currentUnit) == guid then
		Cerebro:Debug("Inspect ready for " .. (UnitName(self.currentUnit) or "Unknown"))
		
		-- Cancel timeout
		if self.scanTimeout then
			Cerebro:CancelTimer(self.scanTimeout)
			self.scanTimeout = nil
		end
		
		-- Process
		self:RecordResultAndNext(self.currentUnit)
	end
end

function Preparation:RecordResultAndNext(unit)
	local result = self:CheckPlayer(unit)
	table.insert(self.inspectResults, result)
	
	-- Clear inspect state before next
	ClearInspectPlayer()
	
	-- Schedule next
	if self.isScanning then
		Cerebro:ScheduleTimer(function() self:ProcessNextInspect() end, 0.2)
	end
end

function Preparation:FinishScan()
	Cerebro:Debug("Scan finished.")
	
	self.isScanning = false
	self.currentUnit = nil
	Cerebro:UnregisterEvent("INSPECT_READY")
	
	local results = self.inspectResults
	local cfg = Cerebro.db.global.config.prep
	local unprepared = {}

	-- Use IsPrepared helper to respect config toggles
	for _, result in ipairs(results) do
		if not IsPrepared(result, cfg) then
			table.insert(unprepared, result)
		end
	end

	-- Report results
	self:ReportPreparationResults(results, unprepared)

	-- Award prepared bonus to those who pass
	local session = Cerebro.modules.Session:GetCurrentSession()

	-- Conservative: Only award if session AND roster exist
	if not session then
		Cerebro:Debug("Prep: No active session, running report-only mode (no awards)")
		return
	end
	
	if not session.roster then
		Cerebro:Debug("Prep: Session exists but no roster, running report-only mode (no awards)")
		return
	end

	-- Session dedupe: only award once per session
	if session.prepAwarded then
		Cerebro:Print("Preparation Bonus already awarded for this session. Skipping awards.")
		return
	end
	
	-- Roster-bound awards: only award players in session.roster
	local epgp = Cerebro:GetModule("EPGP")
	local awardAmount = cfg.prepAwardAmount or 50
	local awardedCount = 0
	
	for _, result in ipairs(results) do
		-- Double-check: player must be prepared AND in roster
		if IsPrepared(result, cfg) and session.roster[result.player] then
			-- Mark as prepared in session
			session.roster[result.player].prepared = true
			
			-- Award bonus using Readiness (capped) EP
			if epgp.AwardReadinessEP then
				epgp:AwardReadinessEP(result.player, awardAmount, "PREP_BONUS")
			else
				-- Fallback if readiness module missing (shouldn't happen)
				epgp:AwardEP(result.player, awardAmount, "PREP_BONUS")
			end
			
			awardedCount = awardedCount + 1
		end
	end
	
	-- Atomic: Set flag AFTER all awards complete
	session.prepAwarded = true
	
	Cerebro:Print(string.format("Preparation Bonus awarded to %d/%d players.", awardedCount, #results))
end

--[[=============================================
	Personal Check (Public API)
=============================================]]

-- Returns: boolean isReady, table report
function Preparation:CheckPersonalReadiness()
	-- Run check on player
	-- Note: personal check excludes Consumables as requested by user ("Gems, Enchants Checked. No consumables...")
	
	local result = self:CheckPlayer("player")
	
	-- Override checks to ignore consumables for the visual indicator
	local isReady = result.hasGems and result.hasEnchants
	
	return isReady, {
		gems = result.hasGems,
		enchants = result.hasEnchants,
		score = result.score
	}
end

--[[=============================================
	Main Preparation Check
===============================================]]

function Preparation:CheckPlayer(unit)
	unit = unit or "player"

	local player = Cerebro:GetFullPlayerName(UnitName(unit))

	local result = {
		player = player,
		hasGems = true,
		hasEnchants = true,
		hasConsumables = true,
		gemsMissing = {},
		enchantsMissing = {},
		consumablesMissing = {},
		score = 100,
	}

	-- Relevant slots to check (Union of Gems, Enchants, and Presence)
	-- Presence: 1-3, 5, 8-16
	-- Enchants: 5, 8, 9, 11, 12, 15, 16
	-- Gems: 2, 11, 12 (Already covered)
	local slotsToCheck = {1, 2, 3, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16}
	table.sort(slotsToCheck)

	Cerebro:Debug("Checking player: " .. player)

	for _, slot in ipairs(slotsToCheck) do
		local itemLink = GetInventoryItemLink(unit, slot)
		local slotName = ENCHANTABLE_SLOTS[slot] or "Slot " .. slot
		
		if itemLink then
			Cerebro:Debug(string.format("Checking Slot %d: %s", slot, itemLink))
			
			-- Check Item Level (Obsolete Check)
			local _, _, _, itemLevel = GetItemInfo(itemLink)
			-- GetItemInfo might return nil for level if not cached, default to safe?
			-- Actually GetInventoryItemLink guarantees link string, but GetItemInfo might delay.
			-- If itemLevel is nil, we skip this check to avoid false positives.
			
			local isObsolete = false
			local minIlvl = GetMinIlvl()  -- Use config-driven threshold
			if itemLevel and itemLevel < minIlvl then
				isObsolete = true
				Cerebro:Debug(string.format("  Slot %d: Obsolete Item (iLvl %d < %d)", slot, itemLevel, minIlvl))
				
				-- Mark as effectively missing item quality
				-- If it's a gem slot, fail gems. If enchant slot, fail enchant.
				
				if (slot == 2 or slot == 11 or slot == 12) and Cerebro.db.global.config.prep.requireGems then
					result.hasGems = false
					table.insert(result.gemsMissing, {
						slot = slot,
						slotName = slotName .. " (Obsolete Item)",
						itemLink = itemLink,
					})
				end
				
				if REQUIRED_ENCHANT_SLOTS[slot] and Cerebro.db.global.config.prep.requireEnchants then
					result.hasEnchants = false
					table.insert(result.enchantsMissing, {
						slot = slot,
						slotName = slotName .. " (Obsolete Item)",
						itemLink = itemLink,
					})
				end
			end
			
			if not isObsolete then
				-- Check Gems (Neck, Rings)
				if (slot == 2 or slot == 11 or slot == 12) and Cerebro.db.global.config.prep.requireGems then
					self:CheckSlotGems(unit, slot, itemLink, result)
				end

				-- Check Enchants
				if REQUIRED_ENCHANT_SLOTS[slot] and Cerebro.db.global.config.prep.requireEnchants then
					self:CheckSlotEnchant(unit, slot, itemLink, result)
				end
			end
		else
			-- Item missing
			Cerebro:Debug(string.format("  Slot %d: Empty or Info not loaded", slot))
			
			-- Fail Presence
			if PRESENCE_SLOTS[slot] then
				-- We categorize "Missing Item" under Consumables? No, create a general failure?
				-- The reporting function splits errors into Gems/Enchants/Consumables.
				-- We should probably add it to Enchants or create a new category.
				-- For now, if an item is missing, it definitely misses Enchants (if required).
				-- If not an enchant slot (like Head/Trinket), we need a way to report it.
				
				-- Failing "Consumables" is confusing.
				-- Failing "Enchants" is acceptable for slots 5,8,9,etc.
				-- For Head/Trinket, we might need to piggyback on one of the lists or add to results reporting.
				-- Let's add it to 'gemsMissing' or 'enchantsMissing' arbitrarily or make reporting handle a new type?
				-- Since we can't change reporting structure easily without rewriting ReportPreparationResults,
				-- let's piggyback on 'enchantsMissing' as "Missing Item" usually implies gear failure.
				
				result.hasEnchants = false -- Treat as general gear failure
				table.insert(result.enchantsMissing, {
					slot = slot,
					slotName = slotName .. " (Missing Item)",
					itemLink = "Missing",
				})
			elseif REQUIRED_ENCHANT_SLOTS[slot] and Cerebro.db.global.config.prep.requireEnchants then
				-- Required for enchant but empty (e.g. Legs which are excluded from presence check but required for enchant)
				result.hasEnchants = false
				table.insert(result.enchantsMissing, {
					slot = slot,
					slotName = slotName .. " (Missing Item)",
					itemLink = "Missing",
				})
			end
		end
	end

	-- Check consumables (Global check)
	if Cerebro.db.global.config.prep.requireConsumables then
		result.hasConsumables, result.consumablesMissing = self:CheckConsumables(unit)
	end

	-- Calculate score
	local penalties = 0
	penalties = penalties + #result.gemsMissing * 5
	penalties = penalties + #result.enchantsMissing * 10
	penalties = penalties + #result.consumablesMissing * 5

	result.score = math.max(0, 100 - penalties)

	return result
end

--[[=============================================
	Helpers
===============================================]]

function Preparation:GetScanTooltip()
	if not self.scanTooltip then
		self.scanTooltip = CreateFrame("GameTooltip", "CerebroPrepTooltip", nil, "GameTooltipTemplate")
		self.scanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
	end
	return self.scanTooltip
end

function Preparation:GetItemRequirement(itemLink)
	local tooltip = self:GetScanTooltip()
	tooltip:SetHyperlink(itemLink)
	
	for i = 1, tooltip:NumLines() do
		local line = _G["CerebroPrepTooltipTextLeft" .. i]
		if line then
			local text = line:GetText()
			if text then
				-- Standard Track Text (e.g. "Hero 1/6")
				if text:match("Champion") then return 1 end
				
				-- Match "Hero" track but EXCLUDE "Heroic" difficulty
				if text:match("Hero") and not text:match("Heroic") then return 2 end
				
				-- Match "Myth" track but EXCLUDE "Mythic" difficulty
				if text:match("Myth") and not text:match("Mythic") then return 3 end

				-- Crafted Item Upgrade Levels (e.g. "Upgrade Level: Champion 1/8")
				if text:match("Upgrade Level:") then
					if text:match("Champion") then return 1 end
					if text:match("Hero") then return 2 end
					if text:match("Myth") then return 3 end
				end
			end
		end
	end
	-- Default: Require Tier 1 for everything else (Veteran, Explorer, etc.)
	return 1
end

function Preparation:GetQualityFromText(text)
	if not text then return 0 end
	-- Check for Tier icon texture pattern (Broadened match)
	local tier = text:match("Quality%-Tier(%d)")
	return tonumber(tier) or 0
end

--[[=============================================
	Slot Checkers
===============================================]]

function Preparation:CheckSlotGems(unit, slot, itemLink, result)
	local stats = C_Item.GetItemStats(itemLink)
	local gemSlots = 0

	-- Count gem slots
	if stats then
		for stat, value in pairs(stats) do
			if stat:match("EMPTY_SOCKET") then
				gemSlots = gemSlots + value
			end
		end
	end

	if gemSlots > 0 then
		local reqQuality = self:GetItemRequirement(itemLink)
		local socketedGems = 0
		local lowQualityGems = 0
		
		Cerebro:Debug(string.format("  [Gems] Slots: %d | Req Quality: %d", gemSlots, reqQuality))

		-- Parse item link for gems
		local linkParts = {strsplit(":", itemLink)}
		
		for i = 1, 4 do
			local gemID = linkParts[3 + i] -- gem1 is at index 4
			
			if gemID and gemID ~= "" and gemID ~= "0" then
				-- Verify this is actually a GEM and not a socket modifier
				-- GetItemInfo returns classID at index 12
				local _, _, _, _, _, _, _, _, _, _, _, classID = GetItemInfo(gemID)
				
				if classID then
					if classID == 3 then -- ClassID 3 is Gem
						socketedGems = socketedGems + 1
						
						-- Check quality
						if reqQuality > 0 then
							local _, gemLink = GetItemInfo(gemID)
							if gemLink then
								local tooltip = self:GetScanTooltip()
								tooltip:SetHyperlink(gemLink)
								local qualityFound = 0
								for j = 1, tooltip:NumLines() do
									local line = _G["CerebroPrepTooltipTextLeft" .. j]
									if line then
										local q = self:GetQualityFromText(line:GetText())
										if q > 0 then qualityFound = q; break end
									end
								end
								
								Cerebro:Debug(string.format("    Gem %d (ID %s): Quality %d", i, gemID, qualityFound))
								
								if qualityFound > 0 and qualityFound < reqQuality then
									lowQualityGems = lowQualityGems + 1
								end
							end
						end
					else
						Cerebro:Debug(string.format("    Gem %d (ID %s): Not a gem (ClassID: %d), ignoring", i, gemID, classID))
					end
				else
					-- Info not loaded, assume valid
					socketedGems = socketedGems + 1
					Cerebro:Debug(string.format("    Gem %d (ID %s): Info not loaded", i, gemID))
				end
			end
		end

		local slotName = ENCHANTABLE_SLOTS[slot] or "Slot " .. slot

		if socketedGems < gemSlots then
			result.hasGems = false
			table.insert(result.gemsMissing, {
				slot = slot,
				slotName = slotName .. " (Missing Gem)",
				itemLink = itemLink,
			})
			Cerebro:Debug("    FAIL: Missing Gems")
		elseif lowQualityGems > 0 then
			result.hasGems = false
			table.insert(result.gemsMissing, {
				slot = slot,
				slotName = slotName .. " (Low Quality Gem)",
				itemLink = itemLink,
			})
			Cerebro:Debug("    FAIL: Low Quality Gems")
		end
	end
end

function Preparation:CheckSlotEnchant(unit, slot, itemLink, result)
	local hasEnchant = self:HasEnchant(itemLink, slot)
	local slotName = ENCHANTABLE_SLOTS[slot] or "Slot " .. slot
	local reqQuality = self:GetItemRequirement(itemLink)
	
	if not hasEnchant then
		result.hasEnchants = false
		table.insert(result.enchantsMissing, {
			slot = slot,
			slotName = slotName .. " (Missing Enchant)",
			itemLink = itemLink,
		})
		Cerebro:Debug("  [Enchant] MISSING")
	else
		-- Check enchant quality
		local qualityFound = 0
		local enchantText = ""
		
		if reqQuality > 0 then
			local tooltip = self:GetScanTooltip()
			tooltip:SetHyperlink(itemLink)
			
			for i = 1, tooltip:NumLines() do
				local line = _G["CerebroPrepTooltipTextLeft" .. i]
				if line then
					local text = line:GetText()
					if text and (text:match("Enchanted:") or text:match("Illuminated")) then
						enchantText = text
						local q = self:GetQualityFromText(text)
						if q > 0 then qualityFound = q end
						if qualityFound > 0 then break end
					end
				end
			end
		end

		Cerebro:Debug(string.format("  [Enchant] Found: '%s' | Quality: %d | Req: %d", 
			enchantText:gsub("|", "||"), qualityFound, reqQuality))

		if reqQuality > 0 and qualityFound > 0 and qualityFound < reqQuality then
			result.hasEnchants = false
			table.insert(result.enchantsMissing, {
				slot = slot,
				slotName = slotName .. " (Low Quality Enchant)",
				itemLink = itemLink,
			})
			Cerebro:Debug("    FAIL: Low Quality Enchant")
		end
	end
end

function Preparation:HasEnchant(itemLink, slot)
	if not itemLink then return false end

	-- Parse item link: item:itemID:enchantID:gemID1:gemID2:gemID3:gemID4...
	-- EnchantID is the 3rd element (index 2 in 0-based split logic, but 3rd return)
	local _, _, enchantID = strsplit(":", itemLink)
	
	-- Check if enchantID exists and is valid
	return enchantID and enchantID ~= "" and enchantID ~= "0"
end

--[[=============================================
	Consumable Checking
===============================================]]

function Preparation:CheckConsumables(unit)
	local missing = {}

	-- Check for active food buff
	local hasFood = self:HasBuff(unit, "Food")

	if not hasFood then
		table.insert(missing, {
			type = "food",
			name = "Food Buff",
		})
	end

	-- Check for active flask/elixir
	local hasFlask = self:HasBuff(unit, "Flask") or self:HasBuff(unit, "Elixir")

	if not hasFlask then
		table.insert(missing, {
			type = "flask",
			name = "Flask/Elixir",
		})
	end

	-- Check for weapon buffs (if applicable)
	-- This is more relevant for Classic; retail doesn't have weapon oils/stones typically

	return #missing == 0, missing
end

function Preparation:HasBuff(unit, buffType)
	for i = 1, 40 do
		local name
		
		-- WoW 11.0+ uses C_UnitAuras
		if C_UnitAuras and C_UnitAuras.GetBuffDataByIndex then
			local buffData = C_UnitAuras.GetBuffDataByIndex(unit, i)
			if buffData then
				name = buffData.name
			end
		elseif UnitBuff then
			-- Fallback for older clients
			name = UnitBuff(unit, i)
		end

		if not name then break end

		-- Check buff type
		if buffType == "Food" then
			if name and (name:match("Food") or name:match("Feast")) then
				return true
			end
		elseif buffType == "Flask" then
			if name and (name:match("Flask") or name:match("Phial")) then -- Phial for retail
				return true
			end
		elseif buffType == "Elixir" then
			if name and name:match("Elixir") then
				return true
			end
		end
	end

	return false
end

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

function Preparation:ReportPreparationResults(results, unprepared)
	if #unprepared == 0 then
		Cerebro:Print("|cff00ff00All raid members are prepared!|r")
		Cerebro:SendOfficerMessage("[Cerebro] All players are prepared! (gems, enchants, consumables)")
		return
	end

	Cerebro:Print(string.format("|cffff8800%d/%d players are unprepared:|r",
		#unprepared, #results))

	-- Group by issue type
	local gemReports = {}
	local enchantReports = {}
	local consumableReports = {}

	for _, result in ipairs(unprepared) do
		local shortName = Cerebro:GetShortPlayerName(result.player)

		if not result.hasGems and #result.gemsMissing > 0 then
			local slots = {}
			for _, m in ipairs(result.gemsMissing) do
				-- Get base slot name
				local name = m.slotName
				if name:match("Obsolete Item") then
					name = name:gsub(" %(.+%)", " (Obsolete Item)")
				else
					name = name:gsub(" %(.+%)", "")
				end
				table.insert(slots, name)
			end
			table.insert(gemReports, string.format("%s (%d)_%s", shortName, #result.gemsMissing, table.concat(slots, ", ")))
		end

		if not result.hasEnchants and #result.enchantsMissing > 0 then
			local slots = {}
			for _, m in ipairs(result.enchantsMissing) do
				local name = m.slotName
				if name:match("Missing Item") then
					name = name:gsub(" %(.+%)", " (Missing Item)")
				elseif name:match("Obsolete Item") then
					name = name:gsub(" %(.+%)", " (Obsolete Item)")
				else
					name = name:gsub(" %(.+%)", "")
				end
				table.insert(slots, name)
			end
			table.insert(enchantReports, string.format("%s (%d)_%s", shortName, #result.enchantsMissing, table.concat(slots, ", ")))
		end

		if not result.hasConsumables and #result.consumablesMissing > 0 then
			table.insert(consumableReports, shortName)
		end
	end

	-- Report to officer chat
	if #gemReports > 0 then
		local msg = "[Cerebro] Missing GEMS: " .. table.concat(gemReports, "; ")
		Cerebro:SendOfficerMessage(msg)
	end

	if #enchantReports > 0 then
		local msg = "[Cerebro] Missing ENCHANTS: " .. table.concat(enchantReports, "; ")
		Cerebro:SendOfficerMessage(msg)
	end

	if #consumableReports > 0 then
		local msg = "[Cerebro] Missing CONSUMABLES: " .. table.concat(consumableReports, ", ")
		Cerebro:SendOfficerMessage(msg)
	end

	-- Print detailed report
	Cerebro:Print("=== Preparation Report ===")

	for _, result in ipairs(unprepared) do
		local issues = {}

		if not result.hasGems then
			local gemDetails = {}
			for _, item in ipairs(result.gemsMissing) do
				table.insert(gemDetails, item.slotName)
			end
			table.insert(issues, "Gems: " .. table.concat(gemDetails, ", "))
		end

		if not result.hasEnchants then
			local enchantDetails = {}
			for _, item in ipairs(result.enchantsMissing) do
				table.insert(enchantDetails, item.slotName)
			end
			table.insert(issues, "Enchants: " .. table.concat(enchantDetails, ", "))
		end

		if not result.hasConsumables then
			table.insert(issues, string.format("%d consumable(s)", #result.consumablesMissing))
		end

		Cerebro:Print(string.format("  %s: %s (Score: %d/100)",
			Cerebro:ColorPlayerName(result.player),
			table.concat(issues, " | "),
			result.score
		))
	end
end

function Preparation:GetShortNames(players)
	local names = {}

	for _, player in ipairs(players) do
		table.insert(names, Cerebro:GetShortPlayerName(player))
	end

	return names
end

--[[=============================================
	Whisper Report to Player
===============================================]]

function Preparation:WhisperPlayerReport(player)
	local result = self:CheckPlayer(player)

	if result.hasGems and result.hasEnchants and result.hasConsumables then
		SendChatMessage("[Cerebro] You are fully prepared! Great job!", "WHISPER", nil, Cerebro:GetShortPlayerName(player))
		return
	end

	SendChatMessage("[Cerebro] Preparation check:", "WHISPER", nil, Cerebro:GetShortPlayerName(player))

	if not result.hasGems and #result.gemsMissing > 0 then
		SendChatMessage("Missing GEMS on: " .. table.concat(self:GetSlotNames(result.gemsMissing), ", "),
			"WHISPER", nil, Cerebro:GetShortPlayerName(player))
	end

	if not result.hasEnchants and #result.enchantsMissing > 0 then
		SendChatMessage("Missing ENCHANTS on: " .. table.concat(self:GetSlotNames(result.enchantsMissing), ", "),
			"WHISPER", nil, Cerebro:GetShortPlayerName(player))
	end

	if not result.hasConsumables and #result.consumablesMissing > 0 then
		local consumables = {}
		for _, c in ipairs(result.consumablesMissing) do
			table.insert(consumables, c.name)
		end
		SendChatMessage("Missing CONSUMABLES: " .. table.concat(consumables, ", "),
			"WHISPER", nil, Cerebro:GetShortPlayerName(player))
	end

	SendChatMessage(string.format("Preparation score: %d/100", result.score),
		"WHISPER", nil, Cerebro:GetShortPlayerName(player))
end

function Preparation:GetSlotNames(items)
	local names = {}

	for _, item in ipairs(items) do
		table.insert(names, item.slotName)
	end

	return names
end

return Preparation
