local addonName, NS = ...

-- ============================================================================
-- VAULTSKINS: THEME ENGINE
-- ============================================================================

local rawTimerAfter = C_Timer and C_Timer.After
local function ScheduleTask(key, delay, callback)
    if NS.Stability and NS.Stability.Schedule then
        return NS.Stability:Schedule(key, delay, callback)
    end
    if rawTimerAfter then return rawTimerAfter(math.max(0, tonumber(delay) or 0), callback) end
    return callback()
end

-- ============================== REGISTRY STATE ==============================
local Engine = {}
NS.ThemeEngine = Engine

local registry = {}
local orderedThemeIDs = {}
local resolvedCache = {}
local registrationSerial = 0
Engine.apiVersion = 5

local REQUIRED_COLORS = {
    "background", "panel", "card", "unlocked", "reward", "locked",
    "border", "accent", "glow", "text", "muted", "success",
    "metal", "silver", "artTint", "shadow",
}

-- ========================= TABLE AND COLOR HELPERS ==========================
local function IsArray(value)
    if type(value) ~= "table" then
        return false
    end

    local count = 0
    for key in pairs(value) do
        if type(key) ~= "number" then
            return false
        end
        count = count + 1
    end

    return count > 0
end

local function DeepCopy(value, seen)
    if type(value) ~= "table" then
        return value
    end

    seen = seen or {}
    if seen[value] then
        return seen[value]
    end

    local copy = {}
    seen[value] = copy
    for key, child in pairs(value) do
        copy[DeepCopy(key, seen)] = DeepCopy(child, seen)
    end
    return copy
end

local function DeepMerge(destination, source)
    if type(source) ~= "table" then
        return destination
    end

    for key, value in pairs(source) do
        if key ~= "extends" and key ~= "resolver" then
            if type(value) == "table" and not IsArray(value) then
                if type(destination[key]) ~= "table" or IsArray(destination[key]) then
                    destination[key] = {}
                end
                DeepMerge(destination[key], value)
            else
                destination[key] = DeepCopy(value)
            end
        end
    end

    return destination
end

local function Clamp(value, minimum, maximum)
    value = tonumber(value) or minimum
    if value < minimum then
        return minimum
    end
    if value > maximum then
        return maximum
    end
    return value
end

local function NormalizeColor(color, fallback)
    fallback = fallback or { r = 1, g = 1, b = 1, a = 1 }
    color = type(color) == "table" and color or fallback
    return {
        r = Clamp(color.r ~= nil and color.r or fallback.r, 0, 1),
        g = Clamp(color.g ~= nil and color.g or fallback.g, 0, 1),
        b = Clamp(color.b ~= nil and color.b or fallback.b, 0, 1),
        a = Clamp(color.a ~= nil and color.a or fallback.a or 1, 0, 1),
    }
end

local function ClearCache()
    for key in pairs(resolvedCache) do
        resolvedCache[key] = nil
    end
end

local function RefreshPublicOrder()
    local publicOrder = {}
    for _, themeID in ipairs(orderedThemeIDs) do
        local definition = registry[themeID]
        if definition and not definition.hidden then
            table.insert(publicOrder, themeID)
        end
    end
    NS.ThemeOrder = publicOrder
end

local function BuildContext(classFile, faction)
    if not classFile then
        local _, detectedClass = UnitClass("player")
        classFile = detectedClass
    end
    if faction == nil then
        faction = UnitFactionGroup and UnitFactionGroup("player") or nil
    end
    return {
        addonName = addonName,
        classFile = classFile,
        classColor = classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] or nil,
        faction = faction,
    }
end

-- ============================ THEME INHERITANCE =============================
local function ResolveDefinition(themeID, stack)
    local definition = registry[themeID]
    if not definition then
        definition = registry.Midnight or registry.Base
        themeID = definition and definition.id or themeID
    end
    if not definition then
        return nil
    end

    stack = stack or {}
    if stack[themeID] then
        return nil, "Circular theme inheritance detected at " .. tostring(themeID)
    end
    stack[themeID] = true

    local resolved = {}
    if definition.extends then
        local parent, parentError = ResolveDefinition(definition.extends, stack)
        if not parent then
            stack[themeID] = nil
            return nil, parentError or ("Missing parent theme " .. tostring(definition.extends))
        end
        DeepMerge(resolved, parent)
    end

    DeepMerge(resolved, definition)
    resolved.id = definition.id or themeID
    resolved.displayName = definition.displayName or resolved.displayName or resolved.id
    resolved.category = definition.category or resolved.category or "Core"
    resolved.pack = definition.pack or resolved.pack or addonName
    resolved.hidden = definition.hidden and true or false
    resolved.registrationSerial = definition.registrationSerial
    resolved.resolver = definition.resolver

    stack[themeID] = nil
    return resolved
end

-- ============================= REGISTRATION API =============================
function Engine.Color(r, g, b, a)
    return {
        r = Clamp(r, 0, 1),
        g = Clamp(g, 0, 1),
        b = Clamp(b, 0, 1),
        a = Clamp(a == nil and 1 or a, 0, 1),
    }
end

function Engine:ClearCache()
    ClearCache()
end

function Engine:RegisterTheme(themeID, definition)
    if type(themeID) ~= "string" or themeID == "" or type(definition) ~= "table" then
        return false, "Theme registration requires a non-empty ID and definition table."
    end

    local isNew = registry[themeID] == nil
    registrationSerial = registrationSerial + 1

    local stored = DeepCopy(definition)
    stored.id = themeID
    stored.registrationSerial = registrationSerial
    registry[themeID] = stored

    if isNew then
        table.insert(orderedThemeIDs, themeID)
    end

    ClearCache()
    RefreshPublicOrder()

    if NS.Options and NS.Options.Frame and NS.Options.Frame:IsShown() then
        ScheduleTask("theme-engine-options-refresh", 0, function()
            if NS.Options then
                NS.Options:Refresh()
            end
        end)
    end

    return true
end

function Engine:RegisterThemePack(packName, themes)
    if type(packName) ~= "string" or packName == "" or type(themes) ~= "table" then
        return false, "Theme pack registration requires a name and themes table."
    end

    local registered = 0
    for _, entry in ipairs(themes) do
        if type(entry) == "table" and type(entry.id) == "string" then
            local definition = DeepCopy(entry)
            definition.id = nil
            definition.pack = definition.pack or packName
            local ok = self:RegisterTheme(entry.id, definition)
            if ok then
                registered = registered + 1
            end
        end
    end

    return registered > 0, registered
end

function Engine:IsRegistered(themeID)
    return registry[themeID] ~= nil and not registry[themeID].hidden
end

function Engine:GetThemeIDs(includeHidden)
    local result = {}
    for _, themeID in ipairs(orderedThemeIDs) do
        local definition = registry[themeID]
        if definition and (includeHidden or not definition.hidden) then
            table.insert(result, themeID)
        end
    end
    return result
end

function Engine:GetThemeMetadata(themeID)
    local definition = registry[themeID]
    if not definition then
        return nil
    end
    return {
        id = themeID,
        displayName = definition.displayName or themeID,
        category = definition.category or "Core",
        pack = definition.pack or addonName,
        description = definition.description,
        hidden = definition.hidden and true or false,
    }
end

-- ============================== CUSTOM PALETTE ==============================
local CUSTOM_COLOR_KEYS = {
    "background", "panel", "locked", "reward", "unlocked",
    "cardBorder", "selectionAccent", "frameBorder", "frameAccent",
    "outerGlow", "glow", "artTint", "scenePrimary", "sceneSecondary",
}

local function ApplyCustomPalette(theme)
    if not (VaultSkinsDB and VaultSkinsDB.customThemeEnabled and type(VaultSkinsDB.customColors) == "table") then
        return theme
    end

    local colors = VaultSkinsDB.customColors
    local fallbacks = {
        background = theme.background,
        panel = theme.panel,
        locked = theme.locked or theme.card,
        reward = theme.reward,
        unlocked = theme.unlocked or theme.card,
        cardBorder = theme.border,
        selectionAccent = theme.accent,
        frameBorder = theme.border,
        frameAccent = theme.accent,
        outerGlow = theme.accent,
        glow = theme.glow,
        artTint = theme.artTint or theme.text,
        scenePrimary = theme.glow,
        sceneSecondary = theme.accent,
    }
    for _, key in ipairs(CUSTOM_COLOR_KEYS) do
        if type(colors[key]) == "table" then
            theme[key] = NormalizeColor(colors[key], fallbacks[key] or theme.accent)
        elseif fallbacks[key] then
            theme[key] = NormalizeColor(theme[key], fallbacks[key])
        end
    end

    theme.customPaletteActive = true
    theme.scenePrimary = NormalizeColor(theme.scenePrimary, theme.glow)
    theme.sceneSecondary = NormalizeColor(theme.sceneSecondary, theme.accent)
    theme.cardBorder = NormalizeColor(theme.cardBorder, theme.border)
    theme.selectionAccent = NormalizeColor(theme.selectionAccent, theme.accent)
    theme.frameBorder = NormalizeColor(theme.frameBorder, theme.border)
    theme.frameAccent = NormalizeColor(theme.frameAccent, theme.accent)
    theme.outerGlow = NormalizeColor(theme.outerGlow, theme.frameAccent or theme.accent)

    theme.frameEffectProfile = type(theme.frameEffectProfile) == "table" and theme.frameEffectProfile or {}
    theme.frameEffectProfile.crestColor = "frameAccent"
    theme.frameEffectProfile.dividerColor = "frameAccent"
    theme.frameEffectProfile.topColor = "glow"
    theme.frameEffectProfile.sideColor = "scenePrimary"
    theme.frameEffectProfile.bottomColor = "frameAccent"
    theme.frameEffectProfile.cornerColor = "frameBorder"
    theme.frameEffectProfile.particlePrimary = "scenePrimary"
    theme.frameEffectProfile.particleSecondary = "sceneSecondary"

    theme.artworkProfile = type(theme.artworkProfile) == "table" and theme.artworkProfile or {}
    theme.artworkProfile.primaryColor = "artTint"
    theme.artworkProfile.secondaryColor = "artTint"
    theme.artworkProfile.tertiaryColor = "artTint"

    return theme
end

-- =========================== THEME RESOLUTION API ===========================
-- Hot path: scalar cache keys avoid allocating a context table on cache hits.
function Engine:ResolveTheme(themeID, context, ignoreCustom)
    themeID = self:IsRegistered(themeID) and themeID or "Midnight"

    local classFile, faction
    if context then
        classFile = context.classFile
        faction = context.faction
    else
        local _, detectedClass = UnitClass("player")
        classFile = detectedClass
        faction = UnitFactionGroup and UnitFactionGroup("player") or nil
    end

    local classKey = classFile or "NONE"
    local factionKey = faction or "NONE"
    local paletteKey = ignoreCustom and "BASE" or ((VaultSkinsDB and VaultSkinsDB.customThemeEnabled) and "CUSTOM" or "NORMAL")
    local themeCache = resolvedCache[themeID]
    local classCache = themeCache and themeCache[classKey]
    local factionCache = classCache and classCache[factionKey]
    local cached = factionCache and factionCache[paletteKey]
    if cached then
        return cached
    end

    context = context or BuildContext(classFile, faction)
    local resolved, resolveError = ResolveDefinition(themeID)
    if not resolved then
        if themeID ~= "Midnight" then
            return self:ResolveTheme("Midnight", context)
        end
        return nil, resolveError
    end

    if type(resolved.resolver) == "function" then
        local ok, result = pcall(resolved.resolver, resolved, context, self)
        if ok and type(result) == "table" then
            resolved = result
        end
    end
    resolved.resolver = nil

    if not ignoreCustom then
        resolved = ApplyCustomPalette(resolved)
    end

    local fallback = registry.Base and ResolveDefinition("Base") or nil
    for _, key in ipairs(REQUIRED_COLORS) do
        resolved[key] = NormalizeColor(resolved[key], fallback and fallback[key] or nil)
    end

    resolved.frameStyle = type(resolved.frameStyle) == "table" and resolved.frameStyle or {}
    resolved.rowStyle = type(resolved.rowStyle) == "table" and resolved.rowStyle or {}
    resolved.cardStyle = type(resolved.cardStyle) == "table" and resolved.cardStyle or {}
    resolved.animationStyle = type(resolved.animationStyle) == "table" and resolved.animationStyle or {}
    resolved.frameEffectProfile = type(resolved.frameEffectProfile) == "table" and resolved.frameEffectProfile or {}

    themeCache = themeCache or {}
    resolvedCache[themeID] = themeCache
    classCache = classCache or {}
    themeCache[classKey] = classCache
    factionCache = factionCache or {}
    classCache[factionKey] = factionCache
    factionCache[paletteKey] = resolved
    return resolved
end

function Engine:ResolveBaseTheme(themeID, context)
    return self:ResolveTheme(themeID, context, true)
end

function Engine:NotifyCustomPaletteChanged()
    ClearCache()
end

-- ======================= DIAGNOSTICS AND EXTERNAL API =======================
function Engine:GetDiagnostics()
    local visible = 0
    local packs = {}
    for _, themeID in ipairs(orderedThemeIDs) do
        local definition = registry[themeID]
        if definition and not definition.hidden then
            visible = visible + 1
            local resolved = ResolveDefinition(themeID)
            packs[(resolved and resolved.pack) or definition.pack or addonName] = true
        end
    end

    local packCount = 0
    for _ in pairs(packs) do
        packCount = packCount + 1
    end

    return {
        registered = #orderedThemeIDs,
        visible = visible,
        packs = packCount,
    }
end


-- Optional theme packs register through this stable external API.
_G.VaultSkinsThemeAPI = {
    apiVersion = Engine.apiVersion,
    Color = Engine.Color,
    RegisterTheme = function(themeID, definition)
        return Engine:RegisterTheme(themeID, definition)
    end,
    RegisterThemePack = function(packName, themes)
        return Engine:RegisterThemePack(packName, themes)
    end,
    IsRegistered = function(themeID)
        return Engine:IsRegistered(themeID)
    end,
    GetThemeIDs = function()
        return Engine:GetThemeIDs(false)
    end,
}

-- ============================ NAMESPACE WRAPPERS ============================
function NS:GetTheme(themeID)
    return Engine:ResolveTheme(themeID)
end

function NS:GetBaseTheme(themeID)
    return Engine:ResolveBaseTheme(themeID)
end

function NS:GetThemeOrder()
    return Engine:GetThemeIDs(false)
end
