--[[
	Cerebro - Config Schema (Modernized)
	
	PURPOSE:
	Single source of truth for ALL configuration options. This schema drives:
	- Blizzard Interface Options UI generation (via Core:SetupOptions)
	- Validation and normalization of all config writes
	- RBAC gating for officer-only settings
	
	DESIGN DECISIONS:
	1. DB Path Semantics: Keys use implicit "db.global" prefix (e.g., "config.epgp.baseGP")
	2. RBAC Gating: officerOnly settings are HIDDEN by default (not just disabled)
	3. Validation: min/max violations are CLAMPED (not rejected)
	4. isPercent: Display 0–100, storage format declared per-item (normalized=0–1 or raw=0–100)
	5. Output: Deterministic sorting by category→group→item order
]]--

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

local ConfigSchema = {}

--[[=============================================
	SCHEMA DEFINITION
	
	Each item:
	- id: unique stable identifier
	- key: db path (e.g., "config.epgp.baseGP")
	- label: display name
	- type: "bool" | "number" | "string" | "select"
	- category: top-level tab name
	- order: numeric sort key
	
	Optional:
	- group: subsection header
	- desc: tooltip text
	- min, max, step: numeric bounds
	- isPercent: if true, display as 0–100
	- percentStorage: "normalized" (0–1) or "raw" (0–100)
	- officerOnly: requires officer rank
	- officerOnlyMode: "hidden" (default) or "disabled"
	- namespace: "global" (default) or "profile"
	- choices: {value=label} for select type
	- advanced: hide in advanced section
===============================================]]--

local schema = {
	-- GENERAL CATEGORY
	{
		id = "debug_mode",
		key = "config.debug",
		label = "Debug Mode",
		desc = "Enable debug messages in chat",
		type = "bool",
		category = "General",
		order = 1,
		officerOnly = false,
	},
	{
		id = "use_new_ui",
		key = "config.ui.useNewUI",
		label = "Use New UI (Experimental)",
		desc = "Enable the redesigned UI with updated visuals. Disable to revert to the classic interface.",
		type = "bool",
		category = "General",
		order = 2,
		officerOnly = false,
	},
	
	-- EPGP CATEGORY - General Settings
	{
		id = "epgp_baseGP",
		key = "config.epgp.baseGP",
		label = "Base GP",
		desc = "Minimum GP floor for all players",
		type = "number",
		category = "EP/GP System",
		group = "General Settings",
		order = 10,
		min = 0,
		max = 10000,
		step = 10,
		officerOnly = true,
	},
	{
		id = "epgp_decayRate",
		key = "config.epgp.decayRate",
		label = "Weekly Decay Rate",
		desc = "Percentage to decay EP and GP each week",
		type = "number",
		category = "EP/GP System",
		group = "General Settings",
		order = 11,
		min = 0,
		max = 0.50,
		step = 0.01,
		isPercent = true,
		percentStorage = "normalized", -- stored as 0–1
		officerOnly = true,
	},
	
	-- EPGP CATEGORY - EP Awards
	{
		id = "epgp_onTimeGracePeriod",
		key = "config.epgp.onTimeGracePeriod",
		label = "On-Time Grace Period",
		desc = "Minutes after session start where arrivals count as on-time",
		type = "number",
		category = "EP/GP System",
		group = "EP Awards",
		order = 19,
		min = 0,
		max = 30,
		step = 1,
		officerOnly = true,
	},
	{
		id = "epgp_epOnTime",
		key = "config.epgp.epOnTime",
		label = "On Time Bonus",
		desc = "EP awarded for being on time to raid",
		type = "number",
		category = "EP/GP System",
		group = "EP Awards",
		order = 20,
		min = 0,
		max = 1000,
		step = 10,
		officerOnly = true,
	},
	{
		id = "epgp_epBossKill",
		key = "config.epgp.epBossKill",
		label = "Boss Kill",
		desc = "EP awarded per boss kill",
		type = "number",
		category = "EP/GP System",
		group = "EP Awards",
		order = 21,
		min = 0,
		max = 1000,
		step = 10,
		officerOnly = true,
	},
	{
		id = "epgp_epFullAttendance",
		key = "config.epgp.epFullAttendance",
		label = "Full Attendance",
		desc = "EP awarded for full raid attendance",
		type = "number",
		category = "EP/GP System",
		group = "EP Awards",
		order = 22,
		min = 0,
		max = 1000,
		step = 10,
		officerOnly = true,
	},
	{
		id = "epgp_epProgressionHourly",
		key = "config.epgp.epProgressionHourly",
		label = "Progression (Hourly)",
		desc = "EP awarded per hour of progression",
		type = "number",
		category = "EP/GP System",
		group = "EP Awards",
		order = 23,
		min = 0,
		max = 1000,
		step = 10,
		officerOnly = true,
	},
	{
		id = "epgp_benchMultiplier",
		key = "config.epgp.benchMultiplier",
		label = "Bench Multiplier",
		desc = "EP multiplier for benched raiders",
		type = "number",
		category = "EP/GP System",
		group = "EP Awards",
		order = 24,
		min = 0,
		max = 1.0,
		step = 0.05,
		isPercent = true,
		percentStorage = "normalized",
		officerOnly = true,
	},
	
	-- EPGP CATEGORY - GP Modifiers
	{
		id = "epgp_offspecMultiplier",
		key = "config.epgp.offspecMultiplier",
		label = "Off-Spec Multiplier",
		desc = "GP multiplier for off-spec items",
		type = "number",
		category = "EP/GP System",
		group = "GP Modifiers",
		order = 30,
		min = 0,
		max = 1.0,
		step = 0.05,
		isPercent = true,
		percentStorage = "normalized",
		officerOnly = true,
	},
	{
		id = "epgp_minorUpgradeMultiplier",
		key = "config.epgp.minorUpgradeMultiplier",
		label = "Minor Upgrade Multiplier",
		desc = "GP multiplier for minor upgrades",
		type = "number",
		category = "EP/GP System",
		group = "GP Modifiers",
		order = 31,
		min = 0,
		max = 1.0,
		step = 0.05,
		isPercent = true,
		percentStorage = "normalized",
		officerOnly = true,
	},
	{
		id = "epgp_minorUpgradeIlvlDelta",
		key = "config.epgp.minorUpgradeIlvlDelta",
		label = "Minor Upgrade iLvl Delta",
		desc = "Max iLvl difference to consider as minor upgrade",
		type = "number",
		category = "EP/GP System",
		group = "GP Modifiers",
		order = 32,
		min = 0,
		max = 50,
		step = 1,
		officerOnly = true,
	},
	
	-- MYTHIC+ CATEGORY
	{
		id = "mplus_base",
		key = "config.epgp.mplusBase",
		label = "M+ Base EP",
		desc = "Base EP for completing a Mythic+ dungeon",
		type = "number",
		category = "Mythic+",
		order = 40,
		min = 0,
		max = 1000,
		step = 10,
		officerOnly = true,
	},
	{
		id = "mplus_perLevel",
		key = "config.epgp.mplusPerLevel",
		label = "EP per Level",
		desc = "Additional EP per keystone level",
		type = "number",
		category = "Mythic+",
		order = 41,
		min = 0,
		max = 100,
		step = 1,
		officerOnly = true,
	},
	{
		id = "mplus_maxLevel",
		key = "config.epgp.mplusMaxLevel",
		label = "Max Level Cap",
		desc = "Maximum keystone level for EP scaling",
		type = "number",
		category = "Mythic+",
		order = 42,
		min = 1,
		max = 50,
		step = 1,
		officerOnly = true,
	},
	{
		id = "mplus_guildBonus",
		key = "config.epgp.mplusGuildBonus",
		label = "Guild Bonus %",
		desc = "Bonus EP percentage for all-guild groups",
		type = "number",
		category = "Mythic+",
		order = 43,
		min = 0,
		max = 100,
		step = 5,
		isPercent = true,
		percentStorage = "raw", -- stored as 0–100
		officerOnly = true,
	},
	{
		id = "mplus_readinessCap",
		key = "config.epgp.readinessCap",
		label = "Readiness Cap",
		desc = "Maximum readiness EP per week",
		type = "number",
		category = "Mythic+",
		order = 44,
		min = 0,
		max = 10000,
		step = 100,
		officerOnly = true,
	},
	{
		id = "season_start_override",
		key = "config.epgp.seasonStartOverride",
		label = "Season Start Override",
		desc = "Override season start date (YYYY-MM-DD). Stored as UTC timestamp. Clear to use hardcoded defaults.",
		type = "string",
		category = "Mythic+",
		order = 45,
		officerOnly = true,
		-- Note: Custom validation handled in ValidateAndNormalize for this id
	},
	
	-- PREPARATION CATEGORY
	{
		id = "prep_minIlvl",
		key = "config.prep.minIlvl",
		label = "Min iLevel",
		desc = "Minimum item level requirement",
		type = "number",
		category = "Preparation",
		order = 50,
		min = 0,
		max = 1000,
		step = 10,
		officerOnly = true,
	},
	{
		id = "prep_awardAmount",
		key = "config.prep.prepAwardAmount",
		label = "Prep Award EP",
		desc = "EP awarded for raid preparation",
		type = "number",
		category = "Preparation",
		order = 51,
		min = 0,
		max = 500,
		step = 10,
		officerOnly = true,
	},
	{
		id = "prep_requireGems",
		key = "config.prep.requireGems",
		label = "Require Gems",
		desc = "Require gems for readiness check",
		type = "bool",
		category = "Preparation",
		order = 52,
		officerOnly = true,
	},
	{
		id = "prep_requireEnchants",
		key = "config.prep.requireEnchants",
		label = "Require Enchants",
		desc = "Require enchants for readiness check",
		type = "bool",
		category = "Preparation",
		order = 53,
		officerOnly = true,
	},
	{
		id = "prep_requireConsumables",
		key = "config.prep.requireConsumables",
		label = "Require Consumables",
		desc = "Require consumables for readiness check",
		type = "bool",
		category = "Preparation",
		order = 54,
		officerOnly = true,
	},
	
	-- ADMIN CATEGORY - Destructive Command Permissions
	{
		id = "admin_allowOfficerFactoryReset",
		key = "config.admin.allowOfficerFactoryReset",
		label = "Allow Officer Factory Reset",
		desc = "If enabled, officers can run /cb factoryreset (subject to 72h backup protection). If disabled, only Guild Master can reset.",
		type = "bool",
		category = "Admin",
		group = "Destructive Commands",
		order = 100,
		officerOnly = true,
	},
}

--[[=============================================
	SCHEMA HELPERS
===============================================]]--

-- Get all schema items sorted for deterministic output
function ConfigSchema:GetItemsSorted()
	local sorted = {}
	for _, item in ipairs(schema) do
		table.insert(sorted, item)
	end
	
	-- Sort by: category order, group order, item order, label
	table.sort(sorted, function(a, b)
		if a.category ~= b.category then
			return (a.category or "") < (b.category or "")
		end
		if (a.group or "") ~= (b.group or "") then
			return (a.group or "") < (b.group or "")
		end
		if a.order ~= b.order then
			return a.order < b.order
		end
		return (a.label or "") < (b.label or "")
	end)
	
	return sorted
end

-- Get unique categories sorted by first item order
function ConfigSchema:GetCategoriesSorted()
	local categories = {}
	local seen = {}
	
	for _, item in ipairs(self:GetItemsSorted()) do
		if not seen[item.category] then
			seen[item.category] = true
			table.insert(categories, item.category)
		end
	end
	
	return categories
end

-- Get items for a specific category/group
function ConfigSchema:GetItemsForCategory(category, group)
	local items = {}
	for _, item in ipairs(self:GetItemsSorted()) do
		if item.category == category then
			if not group or item.group == group then
				table.insert(items, item)
			end
		end
	end
	return items
end

-- Resolve DB path to table reference and key
function ConfigSchema:ResolveDbPath(itemOrKey)
	local item = itemOrKey
	
	-- Handle legacy string key format
	if type(itemOrKey) == "string" then
		item = {key = itemOrKey, namespace = "global"}
	end
	
	local key = item.key
	if not key then
		return nil, nil, "No key provided"
	end
	
	local namespace = item.namespace or "global"
	
	-- Split path into components
	local keys = {}
	for part in key:gmatch("[^.]+") do
		table.insert(keys, part)
	end
	
	-- Start from appropriate namespace
	local root = namespace == "profile" and Cerebro.db.profile or Cerebro.db.global
	
	-- Navigate to parent table
	local current = root
	for i = 1, #keys - 1 do
		local k = keys[i]
		if not current[k] then
			current[k] = {}
		end
		current = current[k]
	end
	
	-- Return (parentTable, finalKey)
	return current, keys[#keys]
end

-- Validate and normalize a value according to schema item
function ConfigSchema:ValidateAndNormalize(item, value)
	if item.type == "bool" then
		return true, not not value, nil
	end
	
	if item.type == "number" then
		-- Coerce to number
		local num = tonumber(value)
		if not num then
			return false, nil, "Invalid number"
		end
		
		-- Handle percent conversion (display → storage)
		if item.isPercent and item.percentStorage == "normalized" then
			-- If input is 0–100 range, normalize to 0–1
			if num > 1 then
				num = num / 100
			end
		end
		
		-- Clamp to min/max
		if item.min and num < item.min then
			num = item.min
		end
		if item.max and num > item.max then
			num = item.max
		end
		
		return true, num, nil
	end
	
	if item.type == "string" then
		-- Custom validator for season_start_override
		if item.id == "season_start_override" then
			-- Handle empty/nil (clear override)
			if not value or value == "" then
				return true, nil, nil  -- Clear the override
			end
			
			-- Parse YYYY-MM-DD
			local y, m, d = value:match("^(%d%d%d%d)-(%d%d)-(%d%d)$")
			if not y then
				return false, nil, "Invalid format (use YYYY-MM-DD)"
			end
			
			-- Convert to UTC noon (avoids DST edge cases)
			local ts = time({year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=12, min=0, sec=0})
			
			-- Validate date exists (leap day check)
			local check = date("*t", ts)
			if check.day ~= tonumber(d) or check.month ~= tonumber(m) then
				return false, nil, "Invalid date (does not exist)"
			end
			
			-- Range validation
			if ts > time() then
				return false, nil, "Future dates not allowed"
			end
			if ts < 1577836800 then  -- Jan 1, 2020 UTC
				return false, nil, "Date too old (before 2020)"
			end
			
			return true, ts, nil  -- Return Unix timestamp (number)
		end
		
		return true, tostring(value), nil
	end
	
	if item.type == "select" then
		-- Validate against choices
		if item.choices and not item.choices[value] then
			return false, nil, "Invalid choice"
		end
		return true, value, nil
	end
	
	return true, value, nil
end

-- Legacy alias for ValidateAndNormalize (Weights tab uses this name)
function ConfigSchema:Validate(item, value)
	return self:ValidateAndNormalize(item, value)
end

-- Get value from DB
-- Returns the raw stored value. UI should handle display conversion separately.
function ConfigSchema:GetValue(itemOrKey)
	local item = itemOrKey
	
	-- Handle legacy string key format
	if type(itemOrKey) == "string" then
		-- Find the item by key for full metadata
		for _, schemaItem in ipairs(self:GetItemsSorted()) do
			if schemaItem.key == itemOrKey then
				item = schemaItem
				break
			end
		end
		-- If not found, create minimal item
		if type(item) == "string" then
			item = {key = itemOrKey, namespace = "global"}
		end
	end
	
	local parent, key = self:ResolveDbPath(item)
	if not parent then return nil end
	
	return parent[key]
end

-- Set value to DB (with validation)
function ConfigSchema:SetValue(itemOrKey, value)
	local item = itemOrKey
	
	-- Handle legacy string key format
	if type(itemOrKey) == "string" then
		-- Find the item by key for full metadata
		for _, schemaItem in ipairs(self:GetItemsSorted()) do
			if schemaItem.key == itemOrKey then
				item = schemaItem
				break
			end
		end
		-- If not found, create minimal item
		if type(item) == "string" then
			item = {key = itemOrKey, namespace = "global", type = "string"}
		end
	end
	
	-- RBAC check
	if item.officerOnly and not Cerebro:IsOfficerSafe() then
		Cerebro:Print("Officer permissions required to modify this setting.")
		return false
	end
	
	-- Capture old value for logging
	local oldValue = self:GetValue(item)
	
	-- Validate and normalize
	local ok, normalizedValue, err = self:ValidateAndNormalize(item, value)
	if not ok then
		Cerebro:Print("Validation error: " .. (err or "unknown"))
		return false
	end
	
	-- Skip if no change
	if oldValue == normalizedValue then
		return true -- No change, no log
	end
	
	-- Write to DB
	local parent, key = self:ResolveDbPath(item)
	parent[key] = normalizedValue
	
	Cerebro:Debug("ConfigSchema: Set " .. item.key .. " = " .. tostring(normalizedValue))
	
	-- Log to System Logs if this is an officer-only setting (Weights tab)
	-- Note: We do NOT log to History (transactions) - History is for EP/GP adjustments only
	if item.officerOnly and Cerebro.Ledger and Cerebro.Ledger.AddAuditLog then
		local label = item.label or item.key or "Unknown"
		local logDetails = string.format("Changed [%s] from %s to %s", label, tostring(oldValue), tostring(normalizedValue))
		Cerebro.Ledger:AddAuditLog("WEIGHT_CHANGE", logDetails)
	end
	
	return true
end

-- Legacy compatibility (for existing code that uses key strings)
function ConfigSchema:GetValueByKey(key)
	for _, item in ipairs(schema) do
		if item.key == key then
			return self:GetValue(item)
		end
	end
	return nil
end

function ConfigSchema:SetValueByKey(key, value)
	for _, item in ipairs(schema) do
		if item.key == key then
			return self:SetValue(item, value)
		end
	end
	return false
end

-- Legacy API compatibility (Weights tab expects old format)
-- Old format: [{category: "Name", items: [...]}, ...]
-- New format: flat array of items with category field
function ConfigSchema:GetCategories()
	local categories = {}
	local categoryMap = {}
	
	-- Group items by category
	for _, item in ipairs(self:GetItemsSorted()) do
		if not categoryMap[item.category] then
			categoryMap[item.category] = {
				category = item.category,
				items = {}
			}
			table.insert(categories, categoryMap[item.category])
		end
		table.insert(categoryMap[item.category].items, item)
	end
	
	return categories
end

-- Make globally accessible
_G.UI_ConfigSchema = ConfigSchema
