local addonName, L = ...

L = L or {}

L.TRACKED_DEBUFFS = {
    [57723] = true,
    [57724] = true,
    [80354] = true,
    [95809] = true,
    [160455] = true,
    [264689] = true,
    [390435] = true,
}

L.DB_VERSION = 24
L.PI_COMM_PREFIX = "LPIB1"
L.PI_SPELL_ID = 10060
L.DEFAULT_PI_WHISPER_PHRASE = "Pi me"

L.DEFAULT_SOUND_SOURCE = "custom"
L.DEFAULT_SOUND_KEY = "pedrolust"
L.DEFAULT_SOUND_CHANNEL = "Master"
L.DEFAULT_USE_TTS = false
L.DEFAULT_TTS_TEXT = "Bloodlust"

L.DEFAULT_USE_IMAGE = false
L.DEFAULT_IMAGE_KEY = "labeouf"
L.DEFAULT_IMAGE_SCALE = 1.0
L.DEFAULT_IMAGE_X = 0
L.DEFAULT_IMAGE_Y = -120
L.DEFAULT_IMAGE_WIDTH = 512
L.DEFAULT_IMAGE_HEIGHT = 512
L.DEFAULT_IMAGE_GRAYSCALE = false
L.DEFAULT_IMAGE_DIRECTION = "bottom"
L.DEFAULT_IMAGE_ENTER_EASING = "out"
L.DEFAULT_IMAGE_EXIT_EASING = "in"

L.BLOODLUST_VISUAL_TOTAL = 40.0
L.PI_TRACK_VISUAL_TOTAL = 15.0
L.PI_REQUEST_VISUAL_TOTAL = 6.0
L.DEFAULT_IMAGE_ENTER_DURATION = 0.7
L.DEFAULT_IMAGE_EXIT_DURATION = 0.7
L.DEFAULT_IMAGE_WOBBLE_SCALE = 0.05
L.DEFAULT_IMAGE_WOBBLE_STEP_DURATION = 0.3

L.DEFAULT_START_MOVE_ENABLED = true
L.DEFAULT_START_FADE_ENABLED = false
L.DEFAULT_LOOP_WOBBLE_ENABLED = true
L.DEFAULT_LOOP_SPIN_ENABLED = false
L.DEFAULT_LOOP_SPIN_DIRECTION = "cw"
L.DEFAULT_LOOP_SPIN_DURATION = 2.0
L.DEFAULT_END_MOVE_ENABLED = true
L.DEFAULT_END_FADE_ENABLED = false

L.DEFAULT_USE_TEXT_OVERLAY = false
L.DEFAULT_TEXT_OVERLAY_TEXT = "Bloodlust"
L.DEFAULT_TEXT_COUNTDOWN_ENABLED = false
L.DEFAULT_TEXT_FONT_KEY = "Friz Quadrata TT"
L.DEFAULT_TEXT_SCALE = 1.0
L.DEFAULT_TEXT_X = 0
L.DEFAULT_TEXT_Y = 140
L.DEFAULT_TEXT_COLOR_R = 1
L.DEFAULT_TEXT_COLOR_G = 0
L.DEFAULT_TEXT_COLOR_B = 0
L.DEFAULT_TEXT_COLOR_A = 1
L.DEFAULT_TEXT_ALT_COLOR_R = 1
L.DEFAULT_TEXT_ALT_COLOR_G = 1
L.DEFAULT_TEXT_ALT_COLOR_B = 0
L.DEFAULT_TEXT_ALT_COLOR_A = 1
L.DEFAULT_TEXT_DIRECTION = "top"
L.DEFAULT_TEXT_ENTER_EASING = "out"
L.DEFAULT_TEXT_EXIT_EASING = "in"
L.DEFAULT_TEXT_ENTER_DURATION = 0.7
L.DEFAULT_TEXT_EXIT_DURATION = 0.7
L.DEFAULT_TEXT_START_MOVE_ENABLED = false
L.DEFAULT_TEXT_START_FADE_ENABLED = false
L.DEFAULT_TEXT_LOOP_PULSE_ENABLED = false
L.DEFAULT_TEXT_LOOP_COLOR_CYCLE_ENABLED = false
L.DEFAULT_TEXT_PULSE_SCALE = 0.08
L.DEFAULT_TEXT_PULSE_STEP_DURATION = 0.3
L.DEFAULT_TEXT_COLOR_CYCLE_DURATION = 0.5
L.DEFAULT_TEXT_HOLD_DURATION = 5.0
L.DEFAULT_TEXT_END_MOVE_ENABLED = false
L.DEFAULT_TEXT_END_FADE_ENABLED = false

L.DEFAULT_PI_REQUEST_SPELLS = {
    { spellID = 453326, enabled = true },
    { spellID = 228260, enabled = true },
    { spellID = 375087, enabled = true },
    { spellID = 255937, enabled = true },
    { spellID = 1249625, enabled = true },
    { spellID = 1250646, enabled = true },
    { spellID = 288613, enabled = true },
    { spellID = 19574, enabled = true },
    { spellID = 1217605, enabled = true },
    { spellID = 191427, enabled = true },
    { spellID = 279302, enabled = true },
    { spellID = 205180, enabled = true },
    { spellID = 265187, enabled = true },
    { spellID = 1122, enabled = true },
    { spellID = 121471, enabled = true },
    { spellID = 107574, enabled = true },
    { spellID = 102558, enabled = true },
    { spellID = 389539, enabled = true },
    { spellID = 132578, enabled = true },
    { spellID = 212084, enabled = true },
}

L.ALERT_PROFILE_DEFS = {
    bloodlust = {
        key = "bloodlust",
        label = "Bloodlust",
        dbPrefix = nil,
        duration = L.BLOODLUST_VISUAL_TOTAL,
        defaultSelectedSoundSource = "custom",
        defaultSelectedSoundKey = "pedrolust",
        defaultSoundSource = "custom",
        defaultSoundKey = "pedrolust",
        defaultUseImageOverlay = true,
        defaultSelectedImageKey = "pedrolust",
        defaultImageKey = "pedrolust",
        defaultImageScale = 0.5,
        defaultImageX = 0,
        defaultImageY = 900,
        defaultStartMoveEnabled = false,
        defaultStartFadeEnabled = true,
        defaultEndMoveEnabled = false,
        defaultEndFadeEnabled = true,
        defaultUseTextOverlay = true,
        defaultTextCountdownEnabled = true,
        defaultSelectedFontKey = "Expressway",
        defaultSelectedFontFallbackKey = "Arial Narrow",
        defaultTextScale = 0.65,
        defaultTextX = 0,
        defaultTextY = 620,
        defaultTextColorR = 0.2,
        defaultTextColorG = 0.8235294118,
        defaultTextColorB = 1,
        defaultTextColorA = 1,
        defaultTTSText = L.DEFAULT_TTS_TEXT,
        defaultTextOverlayText = L.DEFAULT_TEXT_OVERLAY_TEXT,
    },
    piTrack = {
        key = "piTrack",
        label = "Power Infusion Tracking",
        dbPrefix = "piTrack",
        duration = L.PI_TRACK_VISUAL_TOTAL,
        defaultSoundSource = "custom",
        defaultSoundKey = "bikehorn",
        defaultTTSText = "Power Infusion",
        defaultTextOverlayText = "Power Infusion from {pi_giver}!",
        defaultTextCountdownEnabled = true,
        defaultSelectedFontKey = "Expressway",
        defaultSelectedFontFallbackKey = "Arial Narrow",
        defaultTextScale = 0.5,
        defaultTextX = 0,
        defaultTextY = 300,
        defaultTextColorR = 1,
        defaultTextColorG = 0.35,
        defaultTextColorB = 0.35,
        defaultTextColorA = 1,
        defaultTextAltColorR = 1,
        defaultTextAltColorG = 1,
        defaultTextAltColorB = 0,
        defaultTextAltColorA = 1,
        defaultTextLoopColorCycleEnabled = true,
    },
    piRequest = {
        key = "piRequest",
        label = "Power Infusion Request",
        dbPrefix = "piRequest",
        duration = L.PI_REQUEST_VISUAL_TOTAL,
        defaultSoundSource = "custom",
        defaultSoundKey = "empowered",
        defaultTTSText = "Power Infusion request by {pi_requester}",
        defaultTextOverlayText = "Power Infusion request by {pi_requester}",
        defaultSelectedFontKey = "Expressway",
        defaultSelectedFontFallbackKey = "Arial Narrow",
        defaultTextScale = 0.5,
        defaultTextX = 0,
        defaultTextY = 240,
        defaultTextColorR = 1,
        defaultTextColorG = 0.35,
        defaultTextColorB = 0.35,
        defaultTextColorA = 1,
        defaultTextAltColorR = 1,
        defaultTextAltColorG = 1,
        defaultTextAltColorB = 0,
        defaultTextAltColorA = 1,
        defaultTextLoopColorCycleEnabled = true,
    },
}

L.settingsChangeListeners = L.settingsChangeListeners or {}

function L:RegisterSettingsChangeListener(listenerKey, callback)
    if type(listenerKey) ~= "string" or listenerKey == "" then
        return false
    end
    if type(callback) ~= "function" then
        self.settingsChangeListeners[listenerKey] = nil
        return false
    end

    self.settingsChangeListeners[listenerKey] = callback
    return true
end

function L:UnregisterSettingsChangeListener(listenerKey)
    if type(listenerKey) ~= "string" or listenerKey == "" then
        return
    end

    self.settingsChangeListeners[listenerKey] = nil
end

function L:NotifySettingsChanged(reason, profileKey)
    for _, callback in pairs(self.settingsChangeListeners) do
        callback(reason, profileKey)
    end
end

local PROFILE_FIELD_DEFAULTS = {
    -- Field defaults live in one table so new alert profiles can reuse the same
    -- persistence logic without copy/pasting full default blocks per profile.
    selectedSoundSource = function(self, profile)
        return profile.defaultSoundSource or self.DEFAULT_SOUND_SOURCE
    end,
    selectedSoundKey = function(self, profile)
        return profile.defaultSoundKey or self.DEFAULT_SOUND_KEY
    end,
    soundChannel = function(self)
        return self.DEFAULT_SOUND_CHANNEL
    end,
    useTTS = function(self)
        return self.DEFAULT_USE_TTS
    end,
    ttsText = function(self, profile)
        return profile.defaultTTSText or self.DEFAULT_TTS_TEXT
    end,
    ttsVoiceID = function()
        return nil
    end,
    useImageOverlay = function(self)
        return self.DEFAULT_USE_IMAGE
    end,
    selectedImageKey = function(self)
        return self.DEFAULT_IMAGE_KEY
    end,
    imageScale = function(self)
        return self.DEFAULT_IMAGE_SCALE
    end,
    imageX = function(self)
        return self.DEFAULT_IMAGE_X
    end,
    imageY = function(self)
        return self.DEFAULT_IMAGE_Y
    end,
    imageWidth = function(self)
        return self.DEFAULT_IMAGE_WIDTH
    end,
    imageHeight = function(self)
        return self.DEFAULT_IMAGE_HEIGHT
    end,
    imageEnterDuration = function(self)
        return self.DEFAULT_IMAGE_ENTER_DURATION
    end,
    imageExitDuration = function(self)
        return self.DEFAULT_IMAGE_EXIT_DURATION
    end,
    imageWobbleScale = function(self)
        return self.DEFAULT_IMAGE_WOBBLE_SCALE
    end,
    imageWobbleStepDuration = function(self)
        return self.DEFAULT_IMAGE_WOBBLE_STEP_DURATION
    end,
    imageGrayscale = function(self)
        return self.DEFAULT_IMAGE_GRAYSCALE
    end,
    imageDirection = function(self)
        return self.DEFAULT_IMAGE_DIRECTION
    end,
    imageEnterEasing = function(self)
        return self.DEFAULT_IMAGE_ENTER_EASING
    end,
    imageExitEasing = function(self)
        return self.DEFAULT_IMAGE_EXIT_EASING
    end,
    startMoveEnabled = function(self)
        return self.DEFAULT_START_MOVE_ENABLED
    end,
    startFadeEnabled = function(self)
        return self.DEFAULT_START_FADE_ENABLED
    end,
    loopWobbleEnabled = function(self)
        return self.DEFAULT_LOOP_WOBBLE_ENABLED
    end,
    loopSpinEnabled = function(self)
        return self.DEFAULT_LOOP_SPIN_ENABLED
    end,
    loopSpinDirection = function(self)
        return self.DEFAULT_LOOP_SPIN_DIRECTION
    end,
    loopSpinDuration = function(self)
        return self.DEFAULT_LOOP_SPIN_DURATION
    end,
    endMoveEnabled = function(self)
        return self.DEFAULT_END_MOVE_ENABLED
    end,
    endFadeEnabled = function(self)
        return self.DEFAULT_END_FADE_ENABLED
    end,
    useTextOverlay = function(self)
        return self.DEFAULT_USE_TEXT_OVERLAY
    end,
    textOverlayText = function(self, profile)
        return profile.defaultTextOverlayText or self.DEFAULT_TEXT_OVERLAY_TEXT
    end,
    textCountdownEnabled = function(self)
        return self.DEFAULT_TEXT_COUNTDOWN_ENABLED
    end,
    selectedFontKey = function(self)
        return self.DEFAULT_TEXT_FONT_KEY
    end,
    textScale = function(self)
        return self.DEFAULT_TEXT_SCALE
    end,
    textX = function(self)
        return self.DEFAULT_TEXT_X
    end,
    textY = function(self)
        return self.DEFAULT_TEXT_Y
    end,
    textColorR = function(self)
        return self.DEFAULT_TEXT_COLOR_R
    end,
    textColorG = function(self)
        return self.DEFAULT_TEXT_COLOR_G
    end,
    textColorB = function(self)
        return self.DEFAULT_TEXT_COLOR_B
    end,
    textColorA = function(self)
        return self.DEFAULT_TEXT_COLOR_A
    end,
    textAltColorR = function(self)
        return self.DEFAULT_TEXT_ALT_COLOR_R
    end,
    textAltColorG = function(self)
        return self.DEFAULT_TEXT_ALT_COLOR_G
    end,
    textAltColorB = function(self)
        return self.DEFAULT_TEXT_ALT_COLOR_B
    end,
    textAltColorA = function(self)
        return self.DEFAULT_TEXT_ALT_COLOR_A
    end,
    textDirection = function(self)
        return self.DEFAULT_TEXT_DIRECTION
    end,
    textEnterEasing = function(self)
        return self.DEFAULT_TEXT_ENTER_EASING
    end,
    textExitEasing = function(self)
        return self.DEFAULT_TEXT_EXIT_EASING
    end,
    textEnterDuration = function(self)
        return self.DEFAULT_TEXT_ENTER_DURATION
    end,
    textExitDuration = function(self)
        return self.DEFAULT_TEXT_EXIT_DURATION
    end,
    textStartMoveEnabled = function(self)
        return self.DEFAULT_TEXT_START_MOVE_ENABLED
    end,
    textStartFadeEnabled = function(self)
        return self.DEFAULT_TEXT_START_FADE_ENABLED
    end,
    textLoopPulseEnabled = function(self)
        return self.DEFAULT_TEXT_LOOP_PULSE_ENABLED
    end,
    textLoopColorCycleEnabled = function(self)
        return self.DEFAULT_TEXT_LOOP_COLOR_CYCLE_ENABLED
    end,
    textPulseScale = function(self)
        return self.DEFAULT_TEXT_PULSE_SCALE
    end,
    textPulseStepDuration = function(self)
        return self.DEFAULT_TEXT_PULSE_STEP_DURATION
    end,
    textColorCycleDuration = function(self)
        return self.DEFAULT_TEXT_COLOR_CYCLE_DURATION
    end,
    textHoldDuration = function(self)
        return self.DEFAULT_TEXT_HOLD_DURATION
    end,
    textEndMoveEnabled = function(self)
        return self.DEFAULT_TEXT_END_MOVE_ENABLED
    end,
    textEndFadeEnabled = function(self)
        return self.DEFAULT_TEXT_END_FADE_ENABLED
    end,
}

local PROFILE_BOOLEAN_FIELDS = {
    useTTS = true,
    useImageOverlay = true,
    imageGrayscale = true,
    startMoveEnabled = true,
    startFadeEnabled = true,
    loopWobbleEnabled = true,
    loopSpinEnabled = true,
    endMoveEnabled = true,
    endFadeEnabled = true,
    useTextOverlay = true,
    textCountdownEnabled = true,
    textStartMoveEnabled = true,
    textStartFadeEnabled = true,
    textLoopPulseEnabled = true,
    textLoopColorCycleEnabled = true,
    textEndMoveEnabled = true,
    textEndFadeEnabled = true,
}

local PROFILE_STRING_FIELDS = {
    selectedSoundSource = true,
    selectedSoundKey = true,
    soundChannel = true,
    ttsText = true,
    selectedImageKey = true,
    imageDirection = true,
    imageEnterEasing = true,
    imageExitEasing = true,
    loopSpinDirection = true,
    textOverlayText = true,
    selectedFontKey = true,
    textDirection = true,
    textEnterEasing = true,
    textExitEasing = true,
}

local PROFILE_POSITIVE_NUMBER_FIELDS = {
    imageScale = true,
    imageWidth = true,
    imageHeight = true,
    imageEnterDuration = true,
    imageExitDuration = true,
    imageWobbleStepDuration = true,
    loopSpinDuration = true,
    textScale = true,
    textEnterDuration = true,
    textExitDuration = true,
    textPulseStepDuration = true,
    textColorCycleDuration = true,
    textHoldDuration = true,
}

local PROFILE_NONNEGATIVE_NUMBER_FIELDS = {
    imageWobbleScale = true,
    textPulseScale = true,
}

local PROFILE_NUMBER_FIELDS = {
    imageX = true,
    imageY = true,
    textX = true,
    textY = true,
    textColorR = true,
    textColorG = true,
    textColorB = true,
    textColorA = true,
    textAltColorR = true,
    textAltColorG = true,
    textAltColorB = true,
    textAltColorA = true,
}

local function UpperFirst(value)
    return value:sub(1, 1):upper() .. value:sub(2)
end

function L:Print(msg)
    if DEFAULT_CHAT_FRAME then
        DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99Lustomizer:|r " .. tostring(msg))
    end
end

function L:Trim(value)
    return (tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end

function L:Clamp(value, minValue, maxValue)
    if value < minValue then
        return minValue
    end
    if value > maxValue then
        return maxValue
    end
    return value
end

function L:Lerp(a, b, t)
    return a + (b - a) * t
end

function L:EaseLinear(t)
    return t
end

function L:EaseOutQuad(t)
    return 1 - (1 - t) * (1 - t)
end

function L:EaseInQuad(t)
    return t * t
end

function L:EaseInOutSine(t)
    return -(math.cos(math.pi * t) - 1) / 2
end

function L:ApplyEasing(mode, t)
    t = self:Clamp(t, 0, 1)

    if mode == "linear" then
        return self:EaseLinear(t)
    elseif mode == "in" then
        return self:EaseInQuad(t)
    elseif mode == "out" then
        return self:EaseOutQuad(t)
    elseif mode == "inout" then
        return self:EaseInOutSine(t)
    end

    return self:EaseLinear(t)
end

function L:MakeUniqueID(source, key)
    return tostring(source) .. "::" .. tostring(key)
end

function L:SplitUniqueID(uid)
    local source, key = tostring(uid or ""):match("^(.-)::(.*)$")
    return source, key
end

function L:ParsePositiveNumber(value, fallback)
    local n = tonumber(value)
    if not n or n <= 0 then
        return fallback
    end
    return n
end

function L:BuildDefaultPIRequestSpellEntries()
    local entries = {}

    for _, entry in ipairs(self.DEFAULT_PI_REQUEST_SPELLS) do
        entries[#entries + 1] = {
            spellID = tonumber(entry.spellID) or 0,
            enabled = entry.enabled ~= false,
        }
    end

    return entries
end

function L:NormalizePIRequestSpellEntry(entry)
    local spellID = 0
    local enabled = true

    if type(entry) == "table" then
        spellID = math.max(0, math.floor(tonumber(entry.spellID) or 0))
        enabled = entry.enabled ~= false
    end

    return {
        spellID = spellID,
        enabled = enabled,
    }
end

function L:SanitizePIRequestSpellEntries(entries)
    if type(entries) ~= "table" then
        return self:BuildDefaultPIRequestSpellEntries()
    end

    local sanitized = {}
    for _, entry in ipairs(entries) do
        sanitized[#sanitized + 1] = self:NormalizePIRequestSpellEntry(entry)
    end

    return sanitized
end

function L:GetPIRequestSpellEntries()
    local db = self:GetDB()
    return self:SanitizePIRequestSpellEntries(db and db.piRequestSpells)
end

function L:SetPIRequestSpellEntries(entries)
    local db = self:GetDB()
    if not db then
        return
    end

    db.piRequestSpells = self:SanitizePIRequestSpellEntries(entries)
end

function L:ResetPIRequestSpellEntriesToDefaults()
    self:SetPIRequestSpellEntries(self:BuildDefaultPIRequestSpellEntries())
end

function L:GetDB()
    return LustomizerDB
end

function L:GetAlertProfileDefinition(profileKey)
    return self.ALERT_PROFILE_DEFS[profileKey or "bloodlust"] or self.ALERT_PROFILE_DEFS.bloodlust
end

function L:GetProfileDBKey(profileKey, field)
    local profile = self:GetAlertProfileDefinition(profileKey)
    if not profile.dbPrefix then
        return field
    end
    return profile.dbPrefix .. UpperFirst(field)
end

function L:GetProfileDBValue(profileKey, field)
    local db = self:GetDB()
    return db[self:GetProfileDBKey(profileKey, field)]
end

function L:SetProfileDBValue(profileKey, field, value)
    local db = self:GetDB()
    db[self:GetProfileDBKey(profileKey, field)] = value
end

function L:GetProfileDuration(profileKey)
    local profile = self:GetAlertProfileDefinition(profileKey)
    return tonumber(profile.duration) or self.BLOODLUST_VISUAL_TOTAL
end

local function GetProfileFieldDefault(self, profile, field)
    local profileOverrideKey = "default" .. UpperFirst(field)
    if profile and profile[profileOverrideKey] ~= nil then
        return profile[profileOverrideKey]
    end

    local resolver = PROFILE_FIELD_DEFAULTS[field]
    if type(resolver) == "function" then
        return resolver(self, profile)
    end
    return resolver
end

local function EnsureProfileField(self, db, profileKey, field)
    -- Normalize each persisted field into the type the UI/runtime expect.
    -- This keeps old DB versions and manual SavedVariables edits from breaking.
    local profile = self:GetAlertProfileDefinition(profileKey)
    local dbKey = self:GetProfileDBKey(profileKey, field)
    local value = db[dbKey]
    local defaultValue = GetProfileFieldDefault(self, profile, field)

    if PROFILE_BOOLEAN_FIELDS[field] then
        if type(value) ~= "boolean" then
            db[dbKey] = defaultValue
        end
        return
    end

    if PROFILE_STRING_FIELDS[field] then
        if type(value) ~= "string" or value == "" then
            db[dbKey] = defaultValue
        end
        return
    end

    if field == "ttsVoiceID" then
        if value ~= nil then
            db[dbKey] = tonumber(value)
        end
        return
    end

    if PROFILE_POSITIVE_NUMBER_FIELDS[field] then
        if type(value) ~= "number" or value <= 0 then
            db[dbKey] = defaultValue
        end
        return
    end

    if PROFILE_NONNEGATIVE_NUMBER_FIELDS[field] then
        if type(value) ~= "number" or value < 0 then
            db[dbKey] = defaultValue
        end
        return
    end

    if PROFILE_NUMBER_FIELDS[field] then
        if type(value) ~= "number" then
            db[dbKey] = defaultValue
        end
        return
    end
end

function L:EnsureDB()
    if type(LustomizerDB) ~= "table" then
        LustomizerDB = {}
    end

    local db = LustomizerDB
    local oldVersion = tonumber(db.dbVersion) or 0

    -- Run every field through the same normalizer so all profiles stay structurally
    -- aligned even when new settings are added in later addon versions.
    for profileKey, _ in pairs(self.ALERT_PROFILE_DEFS) do
        for field, _ in pairs(PROFILE_FIELD_DEFAULTS) do
            EnsureProfileField(self, db, profileKey, field)
        end
    end

    if type(db.enableExperimentalPI) ~= "boolean" then
        db.enableExperimentalPI = false
    end
    if type(db.debugExperimentalPI) ~= "boolean" then
        db.debugExperimentalPI = false
    end
    if type(db.piTrackPlayOnSelfCast) ~= "boolean" then
        db.piTrackPlayOnSelfCast = false
    end
    if type(db.piRequestRespectCooldown) ~= "boolean" then
        db.piRequestRespectCooldown = true
    end
    if type(db.piRequestAllowFocus) ~= "boolean" then
        db.piRequestAllowFocus = true
    end
    if type(db.piRequestAllowedPlayerName) ~= "string" then
        db.piRequestAllowedPlayerName = ""
    end
    if type(db.piRequestAcceptSpellCasts) ~= "boolean" then
        db.piRequestAcceptSpellCasts = true
    end
    if type(db.piRequestAcceptWhispers) ~= "boolean" then
        db.piRequestAcceptWhispers = false
    end
    if type(db.piRequestWhisperPhrase) ~= "string" or db.piRequestWhisperPhrase == "" then
        db.piRequestWhisperPhrase = self.DEFAULT_PI_WHISPER_PHRASE
    end
    if type(db.piRequestCooldownReplyEnabled) ~= "boolean" then
        db.piRequestCooldownReplyEnabled = false
    end
    if type(db.piRequestCooldownReplyText) ~= "string" or db.piRequestCooldownReplyText == "" then
        db.piRequestCooldownReplyText = "Power Infusion ready in {time_left}s."
    end
    if type(db.piRequestGracePeriodEnabled) ~= "boolean" then
        db.piRequestGracePeriodEnabled = false
    end
    if type(db.piRequestGracePeriodSeconds) ~= "number" or db.piRequestGracePeriodSeconds < 0 then
        db.piRequestGracePeriodSeconds = 5
    end
    if type(db.piRequestTimeoutSeconds) ~= "number" or db.piRequestTimeoutSeconds <= 0 then
        db.piRequestTimeoutSeconds = 8
    end
    db.piRequestSpells = self:SanitizePIRequestSpellEntries(db.piRequestSpells)
    if type(db.minimapIcon) ~= "table" then
        db.minimapIcon = {}
    end
    if type(db.minimapIcon.hide) ~= "boolean" then
        db.minimapIcon.hide = false
    end
    if type(db.minimapIcon.lock) ~= "boolean" then
        db.minimapIcon.lock = false
    end
    if type(db.minimapIcon.minimapPos) ~= "number" then
        db.minimapIcon.minimapPos = 225
    end
    if type(db.minimapIcon.showInCompartment) ~= "boolean" then
        db.minimapIcon.showInCompartment = true
    end

    if oldVersion < 14 then
        -- Preserve user intent while updating older wording to the current PI labels.
        if db.piRequestTtsText == "{pi_requester} requests PI" then
            db.piRequestTtsText = "Power Infusion request by {pi_requester}"
        end
        if db.piRequestTextOverlayText == "{pi_requester} requests PI" then
            db.piRequestTextOverlayText = "Power Infusion request by {pi_requester}"
        end
    end

    if oldVersion < self.DB_VERSION then
        db.dbVersion = self.DB_VERSION
    end
end

function L:ResetAllSettingsToDefaults()
    -- Tear down previews/edit modes first so frames do not keep rendering stale
    -- state while the saved variables table is being recreated from scratch.
    if self.StopPreviewVisibilityMonitor then
        self:StopPreviewVisibilityMonitor()
    end
    if self.StopOverlayPreview then
        self:StopOverlayPreview()
    end
    if self.StopTextOverlayPreview then
        self:StopTextOverlayPreview()
    end

    if self.SetEditMode then
        self:SetEditMode(false, "bloodlust")
        self:SetEditMode(false, "piTrack")
    end
    if self.SetTextEditMode then
        self:SetTextEditMode(false, "bloodlust")
        self:SetTextEditMode(false, "piTrack")
        self:SetTextEditMode(false, "piRequest")
    end

    LustomizerDB = {}
    self:EnsureDB()

    self:EnsureValidSelectedSound("bloodlust")
    self:EnsureValidSelectedVoice("bloodlust")
    self:EnsureValidSelectedImage("bloodlust")
    self:EnsureValidSelectedFont("bloodlust")
    self:EnsureValidSelectedSound("piTrack")
    self:EnsureValidSelectedVoice("piTrack")
    self:EnsureValidSelectedImage("piTrack")
    self:EnsureValidSelectedFont("piTrack")
    self:EnsureValidSelectedSound("piRequest")
    self:EnsureValidSelectedVoice("piRequest")
    self:EnsureValidSelectedImage("piRequest")
    self:EnsureValidSelectedFont("piRequest")

    self.trackedDebuffPresent = false
    self.activeDebuffs = {}

    if self.ApplyOverlayVisuals then
        self:ApplyOverlayVisuals("bloodlust")
        self:ApplyOverlayVisuals("piTrack")
    end
    if self.ApplyTextOverlayVisuals then
        self:ApplyTextOverlayVisuals(self.BLOODLUST_VISUAL_TOTAL, "bloodlust")
        self:ApplyTextOverlayVisuals(self.PI_TRACK_VISUAL_TOTAL, "piTrack")
        self:ApplyTextOverlayVisuals(self:GetTextOverlaySequenceTotalDuration("piRequest"), "piRequest")
    end

    if self.NotifySettingsChanged then
        self:NotifySettingsChanged("reset_defaults")
    end

    self:Print("All Lustomizer settings were reset to defaults.")
end







