--[[
	Cerebro - Database Management
	Handles SavedVariables initialization, defaults, and migrations
]]--

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

-- Default database structure
local defaults = {
	global = {
		-- Version for migration tracking
		dbVersion = 1,
		
		-- RCLC Deduplication (No migration needed, AceDB backfills)
		-- Key: session|winner|itemID|response|status -> timestamp
		rclcAwardDedupe = {},

		-- Sync status tracking (for /cb status)
		-- lastOfficerSync: Unix timestamp of last SYNC_DATA receipt from officer
		syncStatus = {
			lastOfficerSync = 0,
		},

		-- Factory Reset Quarantine System
		-- Epoch: Increments on every factory reset; used to reject stale sync payloads
		-- Quarantine: Blocks sync for 15 min after reset to prevent data restoration
		resetEpoch = 0,
		resetQuarantine = nil,  -- { at = timestamp }

		-- Guild Scope (Guild Locking)
		-- Binds database to a specific guild to prevent alt data leakage
		-- See SPEC.md §3.5 and SECURITY_MODEL.md §7
		scope = nil,  -- { guildGUID, guildName, realm, setBy, setAt, migrated }

		-- Purge Ledger (Negative Ledger pattern)
		-- Tracks permanently deleted players to prevent sync resurrection
		-- See docs/proposals/PLAYER_PURGE.md
		purgedPlayers = {},  -- { ["Name-Realm"] = timestamp }

		-- Season tracking (for M+ week calculation)
		-- Stores last seen season ID to detect season changes
		lastKnownSeasonID = 0,

		-- Attendance Tracking (per-season)
		-- Reset when season changes or factory reset
		seasonBossKills = 0,      -- Total boss kills this season
		seasonID = 0,             -- Current M+ season ID for reset detection

		-- Minimap icon position (for LibDBIcon)
		minimap = {
			hide = false,
		},

		-- Configuration
		config = {
			debug = false,

			epgp = {
				baseGP = 200,
				minEP = 500,
				decayRate = 0.10, -- 10%
				epOnTime = 50,
				epBossKill = 50,
				epFullAttendance = 100,
				epProgressionHourly = 50,
				benchMultiplier = 0.5, -- 50% for standby
				offspecMultiplier = 0.5,
				minorUpgradeMultiplier = 0.5,
				minGP = 100, -- Minimum GP
				decay = 10, -- 10% Weekly Decay
				allowTestAwards = false, -- Allow RCLC Test Mode to charge GP
				onTimeGracePeriod = 5,   -- Minutes after session start for on-time arrivals

				-- Slot Weights (Base GP)
				slotWeights = {
					["INVTYPE_WEAPON"] = 800,
					["INVTYPE_2HWEAPON"] = 800,
					["INVTYPE_WEAPONMAINHAND"] = 800,
					["INVTYPE_WEAPONOFFHAND"] = 800,
					["INVTYPE_SHIELD"] = 800,
					["INVTYPE_RANGED"] = 800,
					["INVTYPE_TRINKET"] = 800,
					
					["INVTYPE_HEAD"] = 700,
					["INVTYPE_CHEST"] = 700,
					["INVTYPE_LEGS"] = 700,
					["INVTYPE_ROBE"] = 700,
					
					["INVTYPE_SHOULDER"] = 600,
					["INVTYPE_HAND"] = 600,
					["INVTYPE_FEET"] = 600,
					
					["INVTYPE_WAIST"] = 500,
					["INVTYPE_WRIST"] = 500,
					["INVTYPE_CLOAK"] = 500,
					
					["INVTYPE_FINGER"] = 450,
					["INVTYPE_NECK"] = 450,
					["INVTYPE_HOLDABLE"] = 450,
				},
				
				-- Difficulty Multipliers
				difficultyMultipliers = {
					[14] = 1.00, -- Normal
					[15] = 1.15, -- Heroic
					[16] = 1.30, -- Mythic
				},
				
				-- Mythic+ Configuration
				mplusBase = 20,              -- Base EP for completing any M+ dungeon
				mplusPerLevel = 5,           -- EP per keystone level
				mplusMaxLevel = 10,          -- Cap level for EP calculation (e.g., +15 counted as +10)
				mplusGuildBonus = 10,        -- Bonus EP per guild member (0-4 scaling)
				readinessCap = 1000,         -- Weekly pre-raid EP cap

				-- Season Start Override (officer-set, Unix timestamp)
				-- Used when hardcoded season start dates are missing/incorrect
				seasonStartOverride = nil,
			},

			prep = {
				requireGems = true,
				requireEnchants = true,
				requireConsumables = false,
				checkOnRaidStart = true,
				minIlvl = 560,              -- Minimum item level for prep checks
				prepAwardAmount = 50,       -- EP awarded for passing prep check
			},

			ui = {
				showMinimapIcon = true,
				lockFrames = false,
			},

			-- Admin/Governance Settings
			admin = {
				allowOfficerFactoryReset = false, -- GM-only by default
				allowOfficerScopeReset = false,   -- Officers can switch guilds (GM-only by default)
			},

			-- Sync Configuration (Fix 5 + Fix 2 merged)
			-- Synced to all clients via config payload in BuildSyncPayload
			sync = {
				-- Guild ranks visible in Standings and included in sync payload
				-- Key: rankIndex (0=GM, 1=Officer, etc.), Value: true/false
				-- Players with non-visible ranks are excluded from sync and ANN_SELF
				visibleRanks = { [0] = true, [1] = true, [3] = true },
				-- Guild ranks eligible for Sync Leader election (Fix 2)
				-- Only candidates with these ranks participate in leader election
				syncLeaderRanks = { [0] = true, [1] = true },
			},
		},

		-- Factory Reset Backup (Rolling single backup)
		-- Created before any factory reset; can be restored by GM only
		-- Includes resetEpoch so restore doesn't reject future syncs as stale
		backup = nil, -- { timestamp, version, players, transactions, audit, epgpConfig, resetEpoch }

		-- Player data (EP, GP, PR)
		-- Delta Sync v2 adds: rev (global revision), fieldRev (per-field revisions)
		players = {
			-- ["PlayerName-RealmName"] = {
			--     ep = 0,
			--     gp = 200,
			--     lifetimeEarned = 0,
			--     lifetimeSpent = 0,
			--     class = "WARRIOR",
			--     lastSeen = 0,
			--     attendance = { total = 0, onTime = 0, prepared = 0 },
			--     rev = 0,           -- Delta Sync v2: player revision
			--     fieldRev = {},     -- Delta Sync v2: { ep = N, gp = N, status = N, ... }
			-- }
		},

		-- Wishlists
		wishlists = {
			-- ["PlayerName-RealmName"] = {
			--     [itemID] = {
			--         priority = 1,
			--         autoBid = 50,
			--         notes = "",
			--         willPass = false,
			--         encounterID = 12345,    -- Which boss drops this
			--         bossRank = 1,           -- Rank among items from same boss (1=highest)
			--         addedTime = 0,
			--     }
			-- }
		},

		-- Current raid session
		currentSession = nil,

		-- Historical sessions
		history = {
			-- ["sessionID"] = { session data }
		},

		-- EP/GP transaction log
		transactions = {
			-- {
			--     id = "tx_001",
			--     timestamp = 0,
			--     player = "PlayerName-RealmName",
			--     amount = 0,
			--     balance = 0,
			--     reason = "BOSS_KILL",
			--     details = {},
			--     sessionID = nil,
			-- }
		},

		-- Item awards
		awards = {
			-- {
			--     timestamp = 0,
			--     itemID = 0,
			--     itemLink = "",
			--     winner = "PlayerName-RealmName",
			--     amount = 0,
			--     source = "Boss Name",
			--     sessionID = nil,
			--     allBids = {},
			--     passedTo = nil,
			-- }
		},
		-- Total boss kills tracked
		totalBossKills = 0,
		
		-- Officer Audit Log (Config changes, Admin actions)
		audit = {
			-- {
			--     timestamp = 0,
			--     admin = "Name",
			--     action = "GP_UPDATE",
			--     details = "Updated Head to 1000",
			-- }
		},
		
		-- Pending Discord Webhook payloads
		discordQueue = {
			-- [timestamp] = { payload = "json_string", sent = false }
		},

		-- Last decay timestamp
		lastDecay = 0,
		
		-- Local M+ run history (per-character)
		mplusRuns = {},
		
		-- Guild-wide M+ run history (officer-only, synced between officers)
		-- 500 cap, FIFO - stores runs pushed from all raiders
		guildMplusRuns = {},
		
		-- Delta Sync Protocol v2 State (Officer)
		-- See docs/DELTA_SYNC_IMPLEMENTATION.md for protocol details
		syncState = {
			nextRev = 0,          -- Monotonic revision generator for player fields
			nextSeq = 0,          -- Monotonic sequence for outgoing SYNC_DATA
			lastFullSyncAt = 0,   -- Timestamp of last full snapshot
			dirtyPlayers = {},    -- { [name] = { [field] = true } } dirty tracking
			dirtyPurgesAt = 0,    -- Last purge timestamp included in sync
			dirtyMplusAt = 0,     -- Last M+ receivedAt included in sync
			pendingSyncs = {},    -- { [seq] = { sentAt, target, retryCount } } for WHISPER only
			maxPendingSyncs = 5,  -- Cap to prevent unbounded memory growth
			-- Raid Sync v2: Peer Status Tracking (Officer-only)
			-- Tracks all raid members' sync freshness via SYNC_ACK responses
			lastBroadcastSeq = 0, -- seq of last RAID broadcast SYNC_DATA
			lastBroadcastAt = 0,  -- timestamp of last RAID broadcast
			peerStatus = {},      -- { [fullName] = { lastSeenAt, lastAckSeq, lastAckAt, ... } }
			-- Raid Sync v2: Configurable intervals (officer-tuneable)
			raidSyncConfig = {
				fullSyncInterval = 600,     -- 10 min safety net for forced full sync
				heartbeatInterval = 30,     -- seconds between OnHeartbeat ticks
				targetedSyncThreshold = 5,  -- stale peer count: <= this uses whisper, > uses broadcast
				gossipDigestInterval = 180, -- 3 min: officer-to-officer anti-entropy (v0.9.3+)
			},
		},
		
		-- Delta Sync Protocol v2 State (Raider)
		lastSyncSeq = 0,          -- Last SYNC_DATA.seq applied
		receiptAckedBy = {},      -- { [officerName] = { ts, seq } } cursor per officer
		staleCursors = {},        -- { [officerName] = true } marks failed receipt syncs
		
		-- Receipt Cursor Tracking (see docs/proposals/Receipt_cursor_tracking.md)
		receiptSeq = 0,           -- Monotonic sequence for locally created receipts
		receiptSyncState = {
			pendingBatches = {},  -- { [batchId] = { sentAt, target, cursor, retryCount } }
			maxBatchSize = 40,    -- Receipts per batch
			lastResetAt = 0,      -- Timestamp of last cursor state reset
		},
	},

	-- Per-character settings
	profile = {
		ui = {
			mainFrame = {
				point = "CENTER",
				x = 0,
				y = 0,
				width = 700,
				height = 500,
			},
		},

		notifications = {
			showBidConfirm = true,
			showEPGPChanges = true,
			showItemWon = true,
			playSound = true,
		},
	},
}

--[[=============================================
	Guild Scope - Guest Stub
	
	Frozen read-only player stub returned when scope
	verification fails. Prevents Lua errors at call sites
	while blocking data writes.
===============================================]]

-- Proxy pattern: real data is hidden, proxy blocks ALL writes
local GUEST_STUB_DATA = {
	ep = 0,
	gp = 0,
	pr = 0,
	status = "Guest",
	class = nil,
	lastSeen = 0,
	attendance = { total = 0, onTime = 0, prepared = 0 },
	bossKillsPresent = 0,
	role = nil,
}
local GUEST_STUB = setmetatable({}, {
	__index = GUEST_STUB_DATA,        -- Reads proxy to real data
	__newindex = function() end,      -- ALL writes silently ignored
	__metatable = false,              -- Prevent tampering
})

-- Expose for testing
Cerebro.GUEST_STUB = GUEST_STUB

--[[=============================================
	Database Initialization
===============================================]]


function Cerebro:InitDatabase()
	-- Initialize AceDB
	self.db = LibStub("AceDB-3.0"):New("CerebroDB", defaults, true)

	-- Run migrations if needed
	self:MigrateDatabase()
	
	-- Cleanup stale purge ledger entries (30-day TTL)
	self:CleanupPurgeLedger()

	-- Register callbacks
	self.db.RegisterCallback(self, "OnProfileChanged", "RefreshConfig")
	self.db.RegisterCallback(self, "OnProfileCopied", "RefreshConfig")
	self.db.RegisterCallback(self, "OnProfileReset", "RefreshConfig")

	self:Debug("Database initialized (version " .. self.db.global.dbVersion .. ")")
end

function Cerebro:RefreshConfig()
	-- Reload UI or settings when profile changes
	self:Print("Settings reloaded.")
end

--[[=============================================
	Database Migrations
===============================================]]

function Cerebro:MigrateDatabase()
	local currentVersion = self.db.global.dbVersion or 0

	if currentVersion < 1 then
		self:Debug("Migrating database to version 1...")
		-- Initial version, nothing to migrate
		self.db.global.dbVersion = 1
	end
	
	if currentVersion < 2 then
		self:Debug("Migrating database to version 2 (Guild Scope)...")
		
		-- Infer scope from existing player data if possible
		local inferredScope = self:InferGuildFromPlayers()
		if inferredScope and inferredScope.realm then
			self.db.global.scope = {
				guildGUID = nil,  -- Cannot infer GUID from player names
				guildName = inferredScope.guildName,
				realm = inferredScope.realm,
				setBy = "Migration-v2",
				setAt = time(),
				migrated = true,  -- Flag for audit trail
			}
			-- Note: Ledger may not be initialized yet during migration
			-- Audit log will be added in Core.lua after initialization
			self:Debug(string.format("Scope inferred: %s-%s", 
				inferredScope.guildName or "?", inferredScope.realm or "?"))
		end
		
		self.db.global.dbVersion = 2
	end
	
	if currentVersion < 3 then
		self:Debug("Migrating database to version 3 (LWW lastUpdate)...")
		
		-- Add lastUpdate to all existing players for Last-Write-Wins sync
		-- Use GetServerTime if available, fallback to time()
		local serverTime = GetServerTime and GetServerTime() or time()
		local migratedCount = 0
		
		for name, data in pairs(self.db.global.players or {}) do
			if not data.lastUpdate then
				data.lastUpdate = serverTime
				migratedCount = migratedCount + 1
			end
		end
		
		if migratedCount > 0 then
			self:Debug(string.format("Added lastUpdate to %d player records", migratedCount))
		end
		
		self.db.global.dbVersion = 3
	end
	
	if currentVersion < 4 then
		-- v0.8.4: Reset debug mode to false for all users
		-- Previously, officer's debug setting was being synced to raiders accidentally
		-- This migration ensures everyone starts clean with debug OFF
		if self.db.global.config and self.db.global.config.debug then
			self.db.global.config.debug = false
			self:Print("Debug mode has been turned OFF (use /cb debug to re-enable)")
		end
		
		self.db.global.dbVersion = 4
	end
	
	if currentVersion < 5 then
		self:Debug("Migrating database to version 5 (Attendance Tracking)...")
		
		-- Initialize seasonBossKills and seasonID if not present
		if not self.db.global.seasonBossKills then
			self.db.global.seasonBossKills = 0
		end
		if not self.db.global.seasonID then
			self.db.global.seasonID = 0
		end
		
		-- Add coreAdmittedAt to existing Core players
		-- Set to current time so they aren't penalized for pre-feature bosses
		local migratedCount = 0
		for name, data in pairs(self.db.global.players or {}) do
			if not data.coreAdmittedAt and data.status == "Core" then
				data.coreAdmittedAt = time()
				migratedCount = migratedCount + 1
			end
			-- Ensure attendance.full exists
			if data.attendance and not data.attendance.full then
				data.attendance.full = 0
			end
		end
		
		if migratedCount > 0 then
			self:Debug(string.format("Added coreAdmittedAt to %d Core players", migratedCount))
		end
		
		self.db.global.dbVersion = 5
	end
	
	if currentVersion < 6 then
		self:Debug("Migrating database to version 6 (Epoch Tagging)...")
		
		-- Add createdEpoch to all existing players for stale record detection
		-- Records pre-migration are assigned current epoch since they're known-good
		local currentEpoch = self.db.global.resetEpoch or 0
		local migratedCount = 0
		
		for name, data in pairs(self.db.global.players or {}) do
			if not data.createdEpoch then
				data.createdEpoch = currentEpoch
				migratedCount = migratedCount + 1
			end
		end
		
		if migratedCount > 0 then
			self:Debug(string.format("Added createdEpoch to %d player records", migratedCount))
		end
		
		self.db.global.dbVersion = 6
	end
	
	if currentVersion < 7 then
		self:Debug("Migrating database to version 7 (Delta Sync v2)...")
		
		-- Add rev and fieldRev to existing players for field-level sync
		-- rev = lastUpdate (or 0) to seed initial revision
		-- fieldRev = {} (empty, will be populated on first mutation)
		local migratedCount = 0
		
		for name, data in pairs(self.db.global.players or {}) do
			if not data.rev then
				data.rev = data.lastUpdate or 0
				data.fieldRev = {}
				migratedCount = migratedCount + 1
			end
		end
		
		-- Initialize syncState if not present (AceDB handles defaults, but be explicit)
		if not self.db.global.syncState then
			self.db.global.syncState = {
				nextRev = 0,
				nextSeq = 0,
				lastFullSyncAt = 0,
				dirtyPlayers = {},
				dirtyPurgesAt = 0,
				dirtyMplusAt = 0,
				pendingSyncs = {},
				maxPendingSyncs = 5,
			}
		end
		
		if migratedCount > 0 then
			self:Debug(string.format("Added rev/fieldRev to %d player records", migratedCount))
		end
		
		self.db.global.dbVersion = 7
	end
end

-- Infer most common realm from existing player keys
function Cerebro:InferGuildFromPlayers()
	local realmCounts = {}
	local totalPlayers = 0
	
	for playerName, _ in pairs(self.db.global.players or {}) do
		local _, realm = strsplit("-", playerName)
		if realm then
			-- Normalize realm for consistent counting
			local normalizedRealm = self:NormalizeRealmString(realm)
			if normalizedRealm then
				realmCounts[normalizedRealm] = (realmCounts[normalizedRealm] or 0) + 1
				totalPlayers = totalPlayers + 1
			end
		end
	end
	
	if totalPlayers == 0 then return nil end
	
	-- Find most common realm
	local topRealm, topCount = nil, 0
	for realm, count in pairs(realmCounts) do
		if count > topCount then
			topRealm = realm
			topCount = count
		end
	end
	
	-- Get guild name from current character if in guild
	local guildName = nil
	if IsInGuild and IsInGuild() then
		guildName = GetGuildInfo("player")
	end
	
	return {
		guildName = guildName,  -- May be nil if not in guild during migration
		realm = topRealm,       -- Already normalized
	}
end

--[[=============================================
	Player Data Management
===============================================]]

function Cerebro:GetPlayerData(player)
	-- Scope check FIRST: prevent auto-creation of records for out-of-guild alts
	local scopeStatus = self:VerifyGuildScope()
	if scopeStatus == false then
		-- Mismatch - return frozen guest stub
		return GUEST_STUB
	end
	-- Note: scopeStatus == nil (pending) allows through for now; UI will handle retry
	
	player = player or self:GetFullPlayerName(UnitName("player")) or "Unknown"
	-- Ensure full name consistency if short name passed
	if player and not player:find("-") then
		local full = self:GetFullPlayerName(player)
		if full then player = full end
	end

	if not self.db.global.players[player] then
		-- Create new player entry
		-- ALL new entries start as "Pending"
		-- Officers self-correct in AnnounceSelf() after login
		-- Raiders stay Pending until admitted by an officer
		
		self.db.global.players[player] = {
			class = select(2, UnitClass(player)), -- Attempt to get class of the specific player
			lastSeen = time(),
			lastUpdate = GetServerTime and GetServerTime() or time(), -- For LWW sync
			createdEpoch = self.db.global.resetEpoch or 0,  -- For stale record detection (v0.8.18)
			attendance = {
				total = 0,
				onTime = 0,
				prepared = 0,
				full = 0,            -- Sessions with full attendance
			},
			bossKillsPresent = 0,
			coreAdmittedAt = nil,  -- Timestamp when admitted to Core (set on status change)
			-- EPGP Data
			ep = 0,
			gp = 200,
			pr = 0,
			status = "Pending",
			role = nil,
		}
		self:Debug(string.format("Created new player entry: %s (status=Pending)", player))
	end

	-- Ensure bossKillsPresent exists (migration)
	if not self.db.global.players[player].bossKillsPresent then
		self.db.global.players[player].bossKillsPresent = 0
	end

	-- Ensure EPGP fields exist (migration)
	if not self.db.global.players[player].ep then
		self.db.global.players[player].ep = 0
		self.db.global.players[player].gp = self.db.global.config.epgp.baseGP or 200
		self.db.global.players[player].pr = 0
		self.db.global.players[player].status = "Pending" -- Default to Pending
	end

	-- Update last seen
	self.db.global.players[player].lastSeen = time()

	return self.db.global.players[player]
end









function Cerebro:GetAllPlayers()
	local players = {}
	
	-- Calculate total boss kills
	local totalBosses = self.db.global.totalBossKills or 0
    local isOfficer = self:IsOfficer()

	for name, data in pairs(self.db.global.players) do
        -- Filtering: Standard users do NOT see Pending players in the list
        -- Officers see everyone.
        -- "pending" status (lowercase check to be safe)
        local status = data.status or "Core"
        local isPending = (status == "Pending")

        if isOfficer or not isPending then
            -- Calculate percentage
            local pct = 0
            if totalBosses > 0 then
                pct = math.floor(((data.bossKillsPresent or 0) / totalBosses) * 100)
            end
        
            table.insert(players, {
                name = name,
                lifetimeEarned = data.lifetimeEarned or 0,
                lifetimeSpent = data.lifetimeSpent or 0,
                class = data.class,
                lastSeen = data.lastSeen,
                attendance = data.attendance,
                attendancePct = pct, -- 0-100 based on boss kills
                isMain = not data.main,
                -- EPGP Data
                ep = data.ep or 0,
                gp = data.gp or (self.db.global.config.epgp and self.db.global.config.epgp.baseGP) or 200,
                pr = data.pr or 0,
                status = status,
                role = data.role,
                -- Attendance tracking
                bossKillsPresent = data.bossKillsPresent or 0,
                coreAdmittedAt = data.coreAdmittedAt,
                eligibleBossKills = data.eligibleBossKills or 0,
            })
        end
	end

	-- Sort by PR descending
	table.sort(players, function(a, b) return (a.pr or 0) > (b.pr or 0) end)

	return players
end


--[[=============================================
	Backup/Export
===============================================]]

function Cerebro:ExportData()
	-- Serialize entire database for export
	local serialized = self:Serialize(self.db.global)
	return serialized
end

function Cerebro:ImportData(data)
	local success, importedData = self:Deserialize(data)

	if not success then
		self:Print("Failed to import data: Invalid format.")
		return false
	end

	-- Backup current data
	local backup = self:CopyTable(self.db.global)

	-- Import
	self.db.global = importedData

	self:Print("Data imported successfully.")

	return true, backup
end

function Cerebro:BackupDatabase()
	local backup = self:CopyTable(self.db.global)
	local timestamp = date("%Y-%m-%d_%H-%M-%S")
	local backupName = "Cerebro_Backup_" .. timestamp

	-- Store in a separate saved variable (would need to add to TOC)
	if not CerebroBackups then
		CerebroBackups = {}
	end

	CerebroBackups[backupName] = backup

	self:Print("Database backed up as: " .. backupName)

	return backupName
end

--[[=============================================
	Cleanup
===============================================]]

function Cerebro:CleanupInactivePlayers(days)
	days = days or 30
	local threshold = time() - (days * 24 * 60 * 60)
	local removed = 0

	for player, data in pairs(self.db.global.players) do
		if data.lastSeen < threshold then
			self.db.global.players[player] = nil
			removed = removed + 1
		end
	end

	if removed > 0 then
		self:Print("Removed " .. removed .. " inactive players (>30 days).")
	else
		self:Print("No inactive players to remove.")
	end
end

--[[=============================================
	Database Repair
===============================================]]

function Cerebro:RepairDatabase()
	self:Debug("Repairing database...")
	local fixedClasses = 0
	local guildRosterLoaded = (GetNumGuildMembers() > 0)

	-- Pre-scan guild roster for lookup map if available
	local guildMap = {}
	if guildRosterLoaded then
		for i = 1, GetNumGuildMembers() do
			local name, _, _, _, _, _, _, _, _, _, classFileName = GetGuildRosterInfo(i)
			if name then
				-- Normalize name-realm
				guildMap[self:GetFullPlayerName(name)] = classFileName
			end
		end
	end

	-- Scan database
	for player, data in pairs(self.db.global.players) do
		if not data.class then
			local newClass = nil

			-- 1. Try UnitClass (if online/visible)
			if UnitName(player) then -- Only works if in range/party
				newClass = select(2, UnitClass(player))
			end

			-- 2. Try Guild Roster Map
			if not newClass and guildMap[player] then
				newClass = guildMap[player]
			end

			-- 3. Try Raid Roster
			if not newClass and IsInRaid() then
				for i = 1, GetNumGroupMembers() do
					local name, _, _, _, _, fileName = GetRaidRosterInfo(i)
					if name and self:GetFullPlayerName(name) == player then
						newClass = fileName
						break
					end
				end
			end

			if newClass then
				data.class = newClass
				fixedClasses = fixedClasses + 1
				self:Debug("Repaired class for " .. player .. ": " .. newClass)
			end
		end

		-- Fix: Ensure Core/Survivor/Officer players have a coreAdmittedAt timestamp
		-- If missing, they show as "(NEW)" in attendance. Default to 0 to count all history.
		if not data.coreAdmittedAt and (data.status == "Core" or data.status == "Officer" or data.status == "Survivor" or data.status == "Veteran Member" or data.status == "Veteran" or data.status == "Alt") then
			-- Backfill with 1 (Epoch start) to ensure they are eligible for all historical bosses
			data.coreAdmittedAt = 1
			-- Propagate this fix to other clients via LWW sync
			data.lastUpdate = time()
			self:Debug("Repaired coreAdmittedAt for " .. player)
		end
	end

	if fixedClasses > 0 then
		-- Only print to user if we actually fixed something
		self:Print("Repaired " .. fixedClasses .. " missing class entries.")
	else
		self:Debug("Database repair complete. No issues found.")
	end
	
	-- Recalculate eligible boss kills for attendance (officer-side)
	self:RecalculateEligibleBossKills()
end

--[[=============================================
	Eligible Boss Kills Calculation
	
	Pre-computes eligibleBossKills per player by iterating
	db.global.history + currentSession. This value is synced
	to raiders so they can display accurate attendance without
	needing the full session history.
===============================================]]

function Cerebro:RecalculateEligibleBossKills()
	-- Only meaningful on officer clients (they have history)
	local history = self.db.global.history
	if not history then return end
	
	local currentSession = self.db.global.currentSession
	local recalculated = 0
	
	for name, data in pairs(self.db.global.players) do
		local coreAdmittedAt = data.coreAdmittedAt
		if not coreAdmittedAt then
			-- Non-core players have no eligibility
			data.eligibleBossKills = 0
		else
			local count = 0
			
			-- Count from archived sessions
			for _, session in pairs(history) do
				for _, boss in ipairs(session.bosses or {}) do
					if boss.killTime and boss.killTime >= coreAdmittedAt then
						count = count + 1
					end
				end
			end
			
			-- Count from current session
			if currentSession then
				for _, boss in ipairs(currentSession.bosses or {}) do
					if boss.killTime and boss.killTime >= coreAdmittedAt then
						count = count + 1
					end
				end
			end
			
			data.eligibleBossKills = count
			recalculated = recalculated + 1
		end
	end
	
	if recalculated > 0 then
		self:Debug(string.format("Recalculated eligibleBossKills for %d players", recalculated))
	end
end

--[[=============================================
	Guild Scope Functions
===============================================]]

-- Get the current authorized guild scope
function Cerebro:GetAuthorizedGuild()
	return self.db.global.scope
end

-- Set the authorized guild scope (officer-only action)
-- Returns true on success, false on failure
function Cerebro:SetAuthorizedGuild()
	if not IsInGuild() then 
		self:Debug("SetAuthorizedGuild failed: not in guild")
		return false 
	end
	
	local guildName, _, _, _ = GetGuildInfo("player")
	if not guildName then 
		self:Debug("SetAuthorizedGuild failed: GetGuildInfo returned nil")
		return false 
	end
	
	-- Use GetGuildGUID if available (more stable than name)
	local guildGUID = nil
	if GetGuildGUID then
		guildGUID = GetGuildGUID()
	end
	
	-- Use normalized realm
	local normalizedRealm = self:GetNormalizedRealm()
	
	self.db.global.scope = {
		guildGUID = guildGUID,
		guildName = guildName,
		realm = normalizedRealm,
		setBy = self:GetFullPlayerName(UnitName("player")),
		setAt = time(),
		migrated = false,
	}
	
	-- Add audit log
	if self.Ledger and self.Ledger.AddAuditLog then
		self.Ledger:AddAuditLog("SCOPE_SET", 
			string.format("Guild locked to %s-%s", guildName, normalizedRealm))
	end
	
	self:Debug(string.format("Guild scope set: %s-%s", guildName, normalizedRealm))
	return true
end

--[[=============================================
	Purge Ledger Cleanup
===============================================]]

function Cerebro:CleanupPurgeLedger()
	local purgedPlayers = self.db.global.purgedPlayers
	if not purgedPlayers then return end
	
	local cutoff = time() - (30 * 24 * 60 * 60)  -- 30 days
	local removed = 0
	
	for name, purgeTime in pairs(purgedPlayers) do
		if purgeTime < cutoff then
			purgedPlayers[name] = nil
			removed = removed + 1
		end
	end
	
	if removed > 0 then
		self:Debug("Cleaned " .. removed .. " stale purge entries")
	end
end
