
local ADDON_NAME = ...

DeloRaidRemindersDB = DeloRaidRemindersDB or {}

-- =========================================================
-- CONFIG
-- =========================================================

local DEFAULT_SOUND_KEY = "raid_warning"

local MIDNIGHT_RAID_REQUIRED = {
    "The Voidspire",
    "The Dreamrift",
    "March on Quel'Danas",
}

local MIDNIGHT_RAID_NAME_HINTS = {
    "the voidspire",
    "the dreamrift",
    "march on quel'danas",
    "manaforge",
}

local REMINDER_SOUNDS = {
    { key = "none", label = "No Sound" },
    { key = "raid_warning", label = "Raid Warning", kit = "RAID_WARNING", file = "Sound\\Interface\\RaidWarning.ogg" },
    { key = "ready_check", label = "Ready Check", kit = "READY_CHECK", file = "Sound\\Interface\\ReadyCheck.ogg" },
    { key = "alarm_clock", label = "Alarm Clock", kit = "ALARM_CLOCK_WARNING_3", file = "Sound\\Interface\\AlarmClockWarning3.ogg" },
    { key = "tell_message", label = "Tell Message", kit = "TELL_MESSAGE", file = "Sound\\Interface\\TellMessage.ogg" },
    { key = "lfg_role_check", label = "LFG Role Check", kit = "LFG_ROLE_CHECK", file = "Sound\\Interface\\LFG_RoleCheck.ogg" },
    { key = "lfg_recruitment", label = "LFG Recruitment", kit = "LFG_REWARDS", file = "Sound\\Interface\\LFG_Rewards.ogg" },
    { key = "pvp_queue_pop", label = "Queue Pop", kit = "PVP_THROUGH_QUEUE", file = "Sound\\Interface\\PVPThroughQueue.ogg" },
    { key = "ready_for_war", label = "PVP Ready", kit = "PVP_ENTER_QUEUE", file = "Sound\\Interface\\PVPEnterQueue.ogg" },
}

local SOUND_BY_KEY = {}
for _, option in ipairs(REMINDER_SOUNDS) do
    SOUND_BY_KEY[option.key] = option
end

-- =========================================================
-- STATE
-- =========================================================

local app = CreateFrame("Frame")
local db

local state = {
    raids = {},
    selectedEncounterID = nil,
    selectedEncounterName = nil,
    activeEncounterID = nil,
    fightStartTime = nil,
    activeReminders = nil,
    activeReminderForSoundMenu = nil,
    popupUnlocked = false,
}

local ui = {}
local bossButtons = {}
local raidHeaderRows = {}
local reminderRows = {}
local isUILoaded = false

-- =========================================================
-- UTIL
-- =========================================================

local function Trim(value)
    if value == nil then
        return ""
    end
    return tostring(value):match("^%s*(.-)%s*$")
end

local function NormalizeBossName(name)
    local n = Trim(name):lower()
    if n == "" then
        return nil
    end
    return n
end

local function ParseTimeToSeconds(value)
    value = Trim(value)
    if value == "" then
        return nil
    end

    if value:find(":", 1, true) then
        local parts = {}
        for part in value:gmatch("[^:]+") do
            parts[#parts + 1] = tonumber(part)
        end
        if #parts == 2 and parts[1] and parts[2] then
            return parts[1] * 60 + parts[2]
        end
        if #parts == 3 and parts[1] and parts[2] and parts[3] then
            return parts[1] * 3600 + parts[2] * 60 + parts[3]
        end
        return nil
    end

    return tonumber(value)
end

local function FormatSeconds(seconds)
    local n = math.max(0, math.floor(seconds or 0))
    return string.format("%02d:%02d", math.floor(n / 60), n % 60)
end

local function SortReminders(list)
    table.sort(list, function(a, b)
        if a.time == b.time then
            return (a.text or "") < (b.text or "")
        end
        return a.time < b.time
    end)
end

local function CreateBackdropPanel(parent, width, height)
    local panel = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    panel:SetSize(width, height)
    panel:SetBackdrop({
        bgFile = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    panel:SetBackdropColor(0.12, 0.14, 0.19, 0.8)
    return panel
end

-- =========================================================
-- DATABASE / SOUND
-- =========================================================

local function EnsureDB()
    if type(DeloRaidRemindersDB) ~= "table" then
        DeloRaidRemindersDB = {}
    end

    db = DeloRaidRemindersDB
    db.remindersByEncounter = db.remindersByEncounter or {}
    db.remindersByBossName = db.remindersByBossName or {}
    db.popupPosition = db.popupPosition or { x = 0, y = 200 }
    db.popupFontSize = tonumber(db.popupFontSize) or 48
    db.defaultSoundKey = db.defaultSoundKey or DEFAULT_SOUND_KEY
    if db.defaultTTSEnabled == nil then
        if db.useVoiceOver ~= nil then
            db.defaultTTSEnabled = db.useVoiceOver and true or false
        else
            db.defaultTTSEnabled = false
        end
    end
    if db.defaultCountdownEnabled == nil then
        db.defaultCountdownEnabled = false
    end
    db.defaultCountdownSeconds = tonumber(db.defaultCountdownSeconds) or 3
    if db.popupUnlocked == nil then
        db.popupUnlocked = false
    end
end

local function GetSoundOptionByKey(key)
    return SOUND_BY_KEY[key]
end

local function IsValidSoundKey(key)
    return GetSoundOptionByKey(key) ~= nil
end

local function GetSoundLabel(key)
    local opt = GetSoundOptionByKey(key)
    return opt and opt.label or "Unknown"
end

local function PlayReminderSound(soundKey)
    local key = soundKey or db.defaultSoundKey or DEFAULT_SOUND_KEY
    local opt = GetSoundOptionByKey(key)
    if not opt or opt.key == "none" then
        return
    end

    if opt.kit and SOUNDKIT and SOUNDKIT[opt.kit] then
        local played = PlaySound(SOUNDKIT[opt.kit], "Master")
        if played then
            return
        end
    end

    if opt.file and PlaySoundFile then
        local played = PlaySoundFile(opt.file, "Master")
        if played then
            return
        end
    end

    local fallback = GetSoundOptionByKey(DEFAULT_SOUND_KEY)
    if fallback and fallback.file and PlaySoundFile then
        PlaySoundFile(fallback.file, "Master")
    end
end

local ttsQueue = {}
local ttsPlaying = false

local function ResolveVoiceID()
    if C_TTSSettings and C_TTSSettings.GetVoiceOptionID and Enum and Enum.TtsVoiceType then
        local id = C_TTSSettings.GetVoiceOptionID(Enum.TtsVoiceType.Standard)
        if id and id ~= 0 then
            return id
        end
    end
    return 0
end

local function ProcessTTSQueue()
    if #ttsQueue == 0 then
        ttsPlaying = false
        return
    end

    local payload = table.remove(ttsQueue, 1)
    local text = payload and payload.text or nil
    if not text or text == "" then
        ttsPlaying = false
        ProcessTTSQueue()
        return
    end

    if not (C_VoiceChat and C_VoiceChat.SpeakText) then
        ttsPlaying = false
        ProcessTTSQueue()
        return
    end

    ttsPlaying = true
    C_VoiceChat.StopSpeakingText()
    C_Timer.After(0.1, function()
        local voiceID = ResolveVoiceID()
        local volume = 100

        local ok = pcall(function()
            C_VoiceChat.SpeakText(voiceID, text, 0, volume, false)
        end)
        if not ok then
            ttsPlaying = false
            ProcessTTSQueue()
            return
        end

        C_Timer.After(2, function()
            ttsPlaying = false
            ProcessTTSQueue()
        end)
    end)
end

local function TrySpeakText(text)
    if not text or text == "" then
        return false
    end

    if C_VoiceChat and C_VoiceChat.SpeakText then
        table.insert(ttsQueue, { text = text })
        if not ttsPlaying then
            ProcessTTSQueue()
        end
        return true
    end

    return false
end

local function SpellNameFromID(spellID)
    local id = tonumber(spellID)
    if not id then
        return nil
    end

    if C_Spell and C_Spell.GetSpellName then
        local name = C_Spell.GetSpellName(id)
        if name and name ~= "" then
            return name
        end
    end

    return nil
end

local function NormalizeImportText(text)
    local trimmed = Trim(text)
    if trimmed == "" then
        return nil
    end

    trimmed = trimmed:gsub("%{spell:(%d+)%}", function(id)
        return SpellNameFromID(id) or ("Spell " .. id)
    end)
    return trimmed:gsub("%s+", " ")
end

local function GetEncounterReminders(encounterID)
    if not encounterID then
        return nil
    end

    local data = db.remindersByEncounter[encounterID]
    if not data then
        data = {}
        db.remindersByEncounter[encounterID] = data
    end
    return data
end

local function SaveBossNameLink(encounterID, encounterName)
    local normalizedName = NormalizeBossName(encounterName)
    if normalizedName and encounterID then
        db.remindersByBossName[normalizedName] = GetEncounterReminders(encounterID)
    end
end

local function GetRemindersForEncounterStart(encounterID, encounterName)
    local byID = db.remindersByEncounter[encounterID] or db.remindersByEncounter[tostring(encounterID)]
    if byID and #byID > 0 then
        return byID
    end

    local normalizedName = NormalizeBossName(encounterName)
    if normalizedName then
        local byName = db.remindersByBossName[normalizedName]
        if byName and #byName > 0 then
            return byName
        end
    end

    return nil
end

local function SyncActiveReminderFromSource(sourceReminder)
    if not sourceReminder or not state.activeReminders then
        return
    end

    local srcTime = tonumber(sourceReminder.time)
    local srcText = tostring(sourceReminder.text or "")

    for _, active in ipairs(state.activeReminders) do
        local sameRef = (active.sourceRef == sourceReminder)
        local sameIdentity = (srcTime ~= nil and tonumber(active.time) == srcTime and tostring(active.text or "") == srcText)
        if sameRef or sameIdentity then
            active.ttsEnabled = sourceReminder.ttsEnabled and true or false
            active.soundKey = sourceReminder.soundKey or active.soundKey
            active.countdownEnabled = sourceReminder.countdownEnabled and true or false
            active.countdownSeconds = tonumber(sourceReminder.countdownSeconds) or active.countdownSeconds or 3
        end
    end
end

-- =========================================================
-- POPUP
-- =========================================================

local function ApplySavedPopupPosition()
    if not (ui.popup and db) then
        return
    end

    local x = 0
    local y = 200
    if type(db.popupPosition) == "table" then
        x = tonumber(db.popupPosition.x) or x
        y = tonumber(db.popupPosition.y) or y
    end

    ui.popup:ClearAllPoints()
    ui.popup:SetPoint("CENTER", UIParent, "CENTER", x, y)
end

local function SavePopupPosition()
    if not (ui.popup and db) then
        return
    end

    local _, _, _, x, y = ui.popup:GetPoint(1)
    db.popupPosition = { x = math.floor((x or 0) + 0.5), y = math.floor((y or 0) + 0.5) }
end

local function ApplyPopupFontSize()
    if not (ui.popup and ui.popup.text and db) then
        return
    end

    local size = math.max(16, math.min(96, tonumber(db.popupFontSize) or 48))
    db.popupFontSize = size
    ui.popup.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE")
    ui.popup.text:SetTextColor(1, 1, 1, 1)
    ui.popup.text:SetShadowOffset(0, 0)
    ui.popup.text:SetShadowColor(0, 0, 0, 0)
    ui.popup:SetSize(math.max(80, ui.popup.text:GetStringWidth() + 24), math.max(30, ui.popup.text:GetStringHeight() + 16))
end

local function SetPopupDragMode(enabled)
    state.popupUnlocked = enabled and true or false
    if db then
        db.popupUnlocked = state.popupUnlocked
    end

    if not ui.popup then
        return
    end

    ui.popup:EnableMouse(state.popupUnlocked)
    if ui.popup.hideTimer then
        ui.popup.hideTimer:Cancel()
        ui.popup.hideTimer = nil
    end

    if state.popupUnlocked then
        ui.popup.text:SetText("Drag to move. Mouse wheel to resize.")
        ApplyPopupFontSize()
        ui.popup:Show()
    else
        SavePopupPosition()
        ui.popup:Hide()
    end

    if ui.movePopupBtn then
        ui.movePopupBtn:SetText(state.popupUnlocked and "Lock Popup" or "Move Popup")
    end
end

local function ShowReminderPopup(text, soundKey, ttsEnabled)
    if not text or text == "" then
        return
    end

    if ui.popup and ui.popup.text then
        ui.popup.text:SetText(text)
        ApplyPopupFontSize()
        ui.popup:Show()
        if ui.popup.hideTimer then
            ui.popup.hideTimer:Cancel()
        end
        if not state.popupUnlocked then
            ui.popup.hideTimer = C_Timer.NewTimer(5, function() ui.popup:Hide() end)
        end
    end

    PlayReminderSound(soundKey)
    if ttsEnabled then
        TrySpeakText(text)
    end
end

-- =========================================================
-- RAID / BOSS DATA
-- =========================================================

local function IsAllowedRaidName(raidName)
    if not raidName then
        return false
    end
    local lower = raidName:lower()
    for _, hint in ipairs(MIDNIGHT_RAID_NAME_HINTS) do
        if lower:find(hint, 1, true) then
            return true
        end
    end
    return false
end

local function BuildCurrentExpansionRaidData()
    local raids = {}
    local byName = {}

    if not (EJ_GetNumTiers and EJ_SelectTier and EJ_GetInstanceByIndex and EJ_SelectInstance and EJ_GetEncounterInfoByIndex) then
        for _, raidName in ipairs(MIDNIGHT_RAID_REQUIRED) do
            raids[#raids + 1] = {
                instanceID = nil,
                name = raidName,
                bosses = { { encounterID = nil, name = "(Bosses unavailable in this client build)", instanceName = raidName } },
            }
        end
        return raids
    end

    for tierIndex = 1, (EJ_GetNumTiers() or 0) do
        EJ_SelectTier(tierIndex)
        local instanceIndex = 1
        while true do
            local instanceID, instanceName = EJ_GetInstanceByIndex(instanceIndex, true)
            if not instanceID then
                break
            end

            if IsAllowedRaidName(instanceName) then
                local raid = byName[instanceName]
                if not raid then
                    raid = { instanceID = instanceID, name = instanceName, bosses = {} }
                    byName[instanceName] = raid
                    raids[#raids + 1] = raid
                end

                EJ_SelectInstance(instanceID)
                local encounterIndex = 1
                while true do
                    local encounterName, _, encounterID = EJ_GetEncounterInfoByIndex(encounterIndex, instanceID)
                    if not encounterName then
                        break
                    end
                    raid.bosses[#raid.bosses + 1] = {
                        encounterID = encounterID,
                        name = encounterName,
                        instanceID = instanceID,
                        instanceName = instanceName,
                    }
                    encounterIndex = encounterIndex + 1
                end
            end

            instanceIndex = instanceIndex + 1
        end
    end

    for _, raidName in ipairs(MIDNIGHT_RAID_REQUIRED) do
        if not byName[raidName] then
            raids[#raids + 1] = {
                instanceID = nil,
                name = raidName,
                bosses = { { encounterID = nil, name = "(Bosses unavailable in this client build)", instanceName = raidName } },
            }
        end
    end

    table.sort(raids, function(a, b) return (a.name or "") < (b.name or "") end)
    return raids
end

-- =========================================================
-- BOSS / REMINDER ROW HELPERS
-- =========================================================

local function HideBossRowsFrom(index)
    for i = index, #bossButtons do
        bossButtons[i]:Hide()
    end
end

local function HideRaidHeaderRowsFrom(index)
    for i = index, #raidHeaderRows do
        raidHeaderRows[i]:Hide()
    end
end

local function EnsureRaidHeaderRow(index)
    if raidHeaderRows[index] then
        return raidHeaderRows[index]
    end

    local row = CreateFrame("Frame", nil, ui.bossScrollChild)
    row:SetHeight(20)
    row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.text:SetPoint("LEFT", row, "LEFT", 4, 0)
    row.text:SetTextColor(0.86, 0.88, 0.94)
    row.text:SetJustifyH("LEFT")
    raidHeaderRows[index] = row
    return row
end

local function EnsureBossRow(index)
    if bossButtons[index] then
        return bossButtons[index]
    end

    local btn = CreateFrame("Button", nil, ui.bossScrollChild, "UIPanelButtonTemplate")
    btn:SetHeight(20)
    btn:SetScript("OnClick", function(self)
        state.selectedEncounterID = self.encounterID
        state.selectedEncounterName = self.encounterName

        ui.selectedBossText:SetText(string.format("Selected: %s", self.encounterName or "None"))
        ui.selectedBossSubText:SetText(self.instanceName or "")

        ui.timeEditBox:SetFocus()
        ui.timeEditBox:HighlightText()
        if ui.importDialogEditBox then
            ui.importDialogEditBox:ClearFocus()
        end

        RefreshReminderRows()
    end)

    bossButtons[index] = btn
    return btn
end

local function RefreshBossList()
    state.raids = BuildCurrentExpansionRaidData()

    local headerRow = 1
    local bossRow = 1
    local y = -8

    for _, raid in ipairs(state.raids) do
        local header = EnsureRaidHeaderRow(headerRow)
        header:ClearAllPoints()
        header:SetPoint("TOPLEFT", ui.bossScrollChild, "TOPLEFT", 8, y)
        header:SetPoint("TOPRIGHT", ui.bossScrollChild, "TOPRIGHT", -8, y)
        header.text:SetText(raid.name or "Raid")
        header:Show()
        y = y - 22
        headerRow = headerRow + 1

        for _, boss in ipairs(raid.bosses) do
            local btn = EnsureBossRow(bossRow)
            btn:SetText(boss.name or "Boss")
            btn.encounterID = boss.encounterID
            btn.encounterName = boss.name
            btn.instanceName = raid.name
            btn:SetEnabled(boss.encounterID ~= nil)
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", ui.bossScrollChild, "TOPLEFT", 14, y)
            btn:SetPoint("TOPRIGHT", ui.bossScrollChild, "TOPRIGHT", -8, y)
            btn:Show()
            y = y - 22
            bossRow = bossRow + 1
        end

        y = y - 8
    end

    HideRaidHeaderRowsFrom(headerRow)
    HideBossRowsFrom(bossRow)
    ui.bossScrollChild:SetHeight(math.max(10, -y + 10))
end

local function EnsureReminderRow(index)
    if reminderRows[index] then
        return reminderRows[index]
    end

    local row = CreateFrame("Frame", nil, ui.reminderListFrame)
    row:SetSize(620, 24)

    row.text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    row.text:SetPoint("LEFT", row, "LEFT", 4, 0)
    row.text:SetPoint("RIGHT", row, "RIGHT", -400, 0)
    row.text:SetJustifyH("LEFT")

    row.ttsLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.ttsLabel:SetText("TTS")

    row.ttsCheck = CreateFrame("CheckButton", nil, row, "UICheckButtonTemplate")
    row.ttsCheck:SetPoint("RIGHT", row, "RIGHT", -292, 0)
    if row.ttsCheck.Text then
        row.ttsCheck.Text:SetText("")
    end
    row.ttsLabel:ClearAllPoints()
    row.ttsLabel:SetPoint("RIGHT", row.ttsCheck, "LEFT", -8, 0)

    row.advanceLabel = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.advanceLabel:SetPoint("RIGHT", row, "RIGHT", -180, 0)
    row.advanceLabel:SetText("Show in Advance")

    row.advanceEditBox = CreateFrame("EditBox", nil, row, "InputBoxTemplate")
    row.advanceEditBox:SetSize(44, 20)
    row.advanceEditBox:SetPoint("RIGHT", row, "RIGHT", -154, 0)
    row.advanceEditBox:SetAutoFocus(false)
    row.advanceEditBox:SetMaxLetters(3)
    row.advanceEditBox:SetJustifyH("CENTER")
    row.advanceLabel:ClearAllPoints()
    row.advanceLabel:SetPoint("RIGHT", row.advanceEditBox, "LEFT", -4, 0)

    row.soundButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.soundButton:SetSize(72, 20)
    row.soundButton:SetPoint("RIGHT", row, "RIGHT", -78, 0)
    row.soundButton:SetText("")

    row.deleteButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.deleteButton:SetSize(70, 20)
    row.deleteButton:SetPoint("RIGHT", row, "RIGHT", -4, 0)
    row.deleteButton:SetText("Delete")

    reminderRows[index] = row
    return row
end

local function HideReminderRowsFrom(index)
    for i = index, #reminderRows do
        reminderRows[i]:Hide()
    end
end

function RefreshReminderRows()
    local encounterID = state.selectedEncounterID
    local reminders = encounterID and GetEncounterReminders(encounterID) or nil

    if encounterID and state.selectedEncounterName then
        SaveBossNameLink(encounterID, state.selectedEncounterName)
    end

    local rowIndex = 1
    local y = -8

    if reminders then
        SortReminders(reminders)
        for i, reminder in ipairs(reminders) do
            if not IsValidSoundKey(reminder.soundKey) then
                reminder.soundKey = db.defaultSoundKey or DEFAULT_SOUND_KEY
            end

            local reminderIndex = i
            local reminderRef = reminder
            local row = EnsureReminderRow(rowIndex)

            if reminderRef.ttsEnabled == nil then
                reminderRef.ttsEnabled = false
            end

            local soundLabel = GetSoundLabel(reminderRef.soundKey)
            row.text:SetText(string.format("%s - %s  [%s]", FormatSeconds(reminderRef.time), reminderRef.text or "(empty)", soundLabel))
            row.soundButton:SetText(soundLabel)
            row.soundButton:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:AddLine("Reminder Sound", 1, 1, 1)
                GameTooltip:AddLine(soundLabel, 0.9, 0.9, 0.9, true)
                GameTooltip:Show()
            end)
            row.soundButton:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)

            row.ttsCheck:SetChecked(reminderRef.ttsEnabled and true or false)
            row.ttsCheck:SetScript("OnClick", function(self)
                reminderRef.ttsEnabled = self:GetChecked() and true or false
                SyncActiveReminderFromSource(reminderRef)
            end)
            row.ttsCheck:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:AddLine("Reminder TTS", 1, 1, 1)
                GameTooltip:AddLine("Enable or disable voice over for this reminder only.", 0.8, 0.8, 0.8, true)
                GameTooltip:Show()
            end)
            row.ttsCheck:SetScript("OnLeave", function() GameTooltip:Hide() end)

            if reminderRef.countdownEnabled and reminderRef.countdownSeconds and reminderRef.countdownSeconds > 0 then
                row.advanceEditBox:SetText(tostring(reminderRef.countdownSeconds))
            else
                row.advanceEditBox:SetText("")
            end
            row.advanceEditBox:SetScript("OnEditFocusLost", function(self)
                local raw = Trim(self:GetText())
                if raw == "" then
                    reminderRef.countdownEnabled = false
                    reminderRef.countdownSeconds = nil
                    SyncActiveReminderFromSource(reminderRef)
                    RefreshReminderRows()
                    return
                end

                local value = math.floor(tonumber(raw) or 0)
                if value <= 0 then
                    reminderRef.countdownEnabled = false
                    reminderRef.countdownSeconds = nil
                    self:SetText("")
                else
                    reminderRef.countdownEnabled = true
                    reminderRef.countdownSeconds = value
                    self:SetText(tostring(value))
                end
                SyncActiveReminderFromSource(reminderRef)
                RefreshReminderRows()
            end)
            row.advanceEditBox:SetScript("OnEnterPressed", function(self)
                self:ClearFocus()
            end)
            row.advanceEditBox:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:AddLine("Show in Advance (seconds)", 1, 1, 1)
                GameTooltip:AddLine("Set per reminder. Empty or 0 disables advance warning.", 0.8, 0.8, 0.8, true)
                GameTooltip:Show()
            end)
            row.advanceEditBox:SetScript("OnLeave", function() GameTooltip:Hide() end)

            row.soundButton:SetScript("OnClick", function()
                state.activeReminderForSoundMenu = reminderRef
                ToggleDropDownMenu(1, nil, ui.rowSoundMenu, row.soundButton, 0, 0)
            end)

            row.deleteButton:SetScript("OnClick", function()
                table.remove(reminders, reminderIndex)
                RefreshReminderRows()
            end)

            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", ui.reminderListFrame, "TOPLEFT", 8, y)
            row:SetPoint("TOPRIGHT", ui.reminderListFrame, "TOPRIGHT", -8, y)
            row:Show()

            y = y - 24
            rowIndex = rowIndex + 1
        end
    end

    HideReminderRowsFrom(rowIndex)
end

-- =========================================================
-- REMINDER ACTIONS
-- =========================================================

local function AddManualReminder()
    if not state.selectedEncounterID then
        print("DeloRaidReminders: Select a boss first.")
        return
    end

    local seconds = ParseTimeToSeconds(ui.timeEditBox:GetText())
    if not seconds or seconds < 0 then
        print("DeloRaidReminders: Invalid time. Use seconds, MM:SS, or HH:MM:SS.")
        return
    end

    local text = NormalizeImportText(ui.noteEditBox:GetText())
    if not text then
        print("DeloRaidReminders: Enter reminder text.")
        return
    end

    local reminders = GetEncounterReminders(state.selectedEncounterID)
    local countdownEnabled = ui.countdownCheck and ui.countdownCheck:GetChecked() or false
    local countdownSeconds = tonumber(ui.countdownSecondsEditBox and ui.countdownSecondsEditBox:GetText())
    countdownSeconds = math.floor(tonumber(countdownSeconds) or db.defaultCountdownSeconds or 3)
    countdownSeconds = math.max(1, countdownSeconds)
    reminders[#reminders + 1] = {
        time = seconds,
        text = text,
        soundKey = ui.selectedSoundKey or db.defaultSoundKey or DEFAULT_SOUND_KEY,
        ttsEnabled = ui.voiceOverCheck and ui.voiceOverCheck:GetChecked() and true or false,
        countdownEnabled = countdownEnabled and true or false,
        countdownSeconds = countdownSeconds,
    }
    SaveBossNameLink(state.selectedEncounterID, state.selectedEncounterName)
    SortReminders(reminders)

    ui.timeEditBox:SetText("")
    ui.noteEditBox:SetText("")
    RefreshReminderRows()

    print(string.format("DeloRaidReminders: Added %s reminder for %s.", FormatSeconds(seconds), state.selectedEncounterName or "boss"))
end

local function ImportLorrgsText(blob)
    if not state.selectedEncounterID then
        print("DeloRaidReminders: Select a boss first.")
        return
    end

    blob = Trim(blob)
    if blob == "" then
        print("DeloRaidReminders: Paste import text first.")
        return
    end

    local imported = {}
    for line in blob:gmatch("[^\r\n]+") do
        local h, m, s = line:match("%{time:(%d+):(%d+):(%d+)%}")
        local mm, ss = line:match("%{time:(%d+):(%d+)%}")

        local seconds
        if h and m and s then
            seconds = tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s)
        elseif mm and ss then
            seconds = tonumber(mm) * 60 + tonumber(ss)
        end

        if seconds then
            local rhs = line:match("%-%s*(.+)$") or ""
            local text = NormalizeImportText(rhs)
            if text and text ~= "" then
                local countdownEnabled = ui.countdownCheck and ui.countdownCheck:GetChecked() or false
                local countdownSeconds = tonumber(ui.countdownSecondsEditBox and ui.countdownSecondsEditBox:GetText())
                countdownSeconds = math.floor(tonumber(countdownSeconds) or db.defaultCountdownSeconds or 3)
                countdownSeconds = math.max(1, countdownSeconds)
                imported[#imported + 1] = {
                    time = seconds,
                    text = text,
                    soundKey = ui.selectedSoundKey or db.defaultSoundKey or DEFAULT_SOUND_KEY,
                    ttsEnabled = ui.voiceOverCheck and ui.voiceOverCheck:GetChecked() and true or false,
                    countdownEnabled = countdownEnabled and true or false,
                    countdownSeconds = countdownSeconds,
                }
            end
        end
    end

    if #imported == 0 then
        print("DeloRaidReminders: No valid lines found. Expected format like {time:00:03} - {spell:12345}.")
        return
    end

    local reminders = GetEncounterReminders(state.selectedEncounterID)
    SaveBossNameLink(state.selectedEncounterID, state.selectedEncounterName)
    for _, r in ipairs(imported) do
        reminders[#reminders + 1] = r
    end
    SortReminders(reminders)
    RefreshReminderRows()

    print(string.format("DeloRaidReminders: Imported %d reminders for %s.", #imported, state.selectedEncounterName or "boss"))
end

-- =========================================================
-- UI
-- =========================================================

local function BuildUI()
    local frameWidth = 1060
    local frameHeight = 640
    local leftWidth = 320
    local gap = 14
    local rightX = 14 + leftWidth + gap
    local rightWidth = frameWidth - rightX - 14

    ui.frame = CreateFrame("Frame", "DeloRaidRemindersMainFrame", UIParent, "BasicFrameTemplateWithInset")
    ui.frame:SetSize(frameWidth, frameHeight)
    ui.frame:SetPoint("CENTER")
    ui.frame:SetMovable(true)
    ui.frame:EnableMouse(true)
    ui.frame:RegisterForDrag("LeftButton")
    ui.frame:SetScript("OnDragStart", ui.frame.StartMoving)
    ui.frame:SetScript("OnDragStop", ui.frame.StopMovingOrSizing)
    ui.frame:Hide()

    ui.frame.TitleBg:SetVertexColor(0.18, 0.21, 0.28, 0.95)
    ui.frame.title = ui.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    ui.frame.title:SetPoint("LEFT", ui.frame.TitleBg, "LEFT", 8, 0)
    ui.frame.title:SetText("DeloRaidReminders")

    ui.leftPanel = CreateBackdropPanel(ui.frame, leftWidth, frameHeight - 70)
    ui.leftPanel:SetPoint("TOPLEFT", ui.frame, "TOPLEFT", 12, -44)

    local leftTitle = ui.leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    leftTitle:SetPoint("TOPLEFT", ui.leftPanel, "TOPLEFT", 12, -10)
    leftTitle:SetText("Raids")

    ui.bossScrollFrame = CreateFrame("ScrollFrame", "DeloRaidRemindersBossScrollFrame", ui.leftPanel, "UIPanelScrollFrameTemplate")
    ui.bossScrollFrame:SetPoint("TOPLEFT", ui.leftPanel, "TOPLEFT", 10, -30)
    ui.bossScrollFrame:SetSize(leftWidth - 38, frameHeight - 110)

    ui.bossScrollChild = CreateFrame("Frame", nil, ui.bossScrollFrame)
    ui.bossScrollChild:SetSize(leftWidth - 58, 10)
    ui.bossScrollFrame:SetScrollChild(ui.bossScrollChild)

    local bossScrollBar = _G["DeloRaidRemindersBossScrollFrameScrollBar"]
    if bossScrollBar then
        bossScrollBar:ClearAllPoints()
        bossScrollBar:SetPoint("TOPRIGHT", ui.leftPanel, "TOPRIGHT", -8, -36)
        bossScrollBar:SetPoint("BOTTOMRIGHT", ui.leftPanel, "BOTTOMRIGHT", -8, 14)
    end

    ui.selectedBossText = ui.frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    ui.selectedBossText:SetPoint("TOPLEFT", ui.frame, "TOPLEFT", rightX, -50)
    ui.selectedBossText:SetWidth(rightWidth)
    ui.selectedBossText:SetJustifyH("LEFT")
    ui.selectedBossText:SetWordWrap(false)
    ui.selectedBossText:SetText("Selected: None")
    ui.selectedBossText:SetTextColor(0.95, 0.92, 0.68)

    ui.selectedBossSubText = ui.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    ui.selectedBossSubText:SetPoint("TOPLEFT", ui.selectedBossText, "BOTTOMLEFT", 0, -4)
    ui.selectedBossSubText:SetWidth(rightWidth)
    ui.selectedBossSubText:SetJustifyH("LEFT")
    ui.selectedBossSubText:SetWordWrap(false)
    ui.selectedBossSubText:SetText("")

    ui.editorPanel = CreateBackdropPanel(ui.frame, rightWidth, 168)
    ui.editorPanel:SetPoint("TOPLEFT", ui.frame, "TOPLEFT", rightX - 6, -84)
    ui.editorPanel:SetBackdropColor(0.15, 0.17, 0.23, 0.82)

    local timeLabel = ui.editorPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    timeLabel:SetPoint("TOPLEFT", ui.editorPanel, "TOPLEFT", 12, -14)
    timeLabel:SetText("Time")

    ui.timeEditBox = CreateFrame("EditBox", nil, ui.editorPanel, "InputBoxTemplate")
    ui.timeEditBox:SetSize(120, 30)
    ui.timeEditBox:SetPoint("TOPLEFT", timeLabel, "BOTTOMLEFT", 0, -4)
    ui.timeEditBox:SetAutoFocus(false)
    ui.timeEditBox:SetMaxLetters(16)
    ui.timeEditBox:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Time", 1, 1, 1)
        GameTooltip:AddLine("Use seconds or MM:SS (e.g. 90 or 01:30).", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    ui.timeEditBox:SetScript("OnLeave", function() GameTooltip:Hide() end)

    local noteLabel = ui.editorPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    noteLabel:SetPoint("TOPLEFT", ui.editorPanel, "TOPLEFT", 148, -14)
    noteLabel:SetText("Reminder")

    ui.noteEditBox = CreateFrame("EditBox", nil, ui.editorPanel, "InputBoxTemplate")
    ui.noteEditBox:SetSize(290, 30)
    ui.noteEditBox:SetPoint("TOPLEFT", noteLabel, "BOTTOMLEFT", 0, -4)
    ui.noteEditBox:SetAutoFocus(false)
    ui.noteEditBox:SetMaxLetters(180)
    ui.noteEditBox:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Reminder", 1, 1, 1)
        GameTooltip:AddLine("Text shown in the popup and spoken by voice over if enabled.", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    ui.noteEditBox:SetScript("OnLeave", function() GameTooltip:Hide() end)

    local soundLabel = ui.editorPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    soundLabel:SetPoint("TOPLEFT", ui.editorPanel, "TOPLEFT", 450, -14)
    soundLabel:SetText("Sound")

    ui.soundDropdown = CreateFrame("Frame", "DeloRaidRemindersSoundDropdown", ui.editorPanel, "UIDropDownMenuTemplate")
    ui.soundDropdown:SetPoint("TOPLEFT", soundLabel, "BOTTOMLEFT", -16, -6)
    UIDropDownMenu_SetWidth(ui.soundDropdown, 190)
    UIDropDownMenu_Initialize(ui.soundDropdown, function(_, level)
        for _, option in ipairs(REMINDER_SOUNDS) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = option.label
            info.value = option.key
            info.checked = (ui.selectedSoundKey == option.key)
            info.func = function()
                ui.selectedSoundKey = option.key
                db.defaultSoundKey = option.key
                UIDropDownMenu_SetSelectedValue(ui.soundDropdown, option.key)
                UIDropDownMenu_SetText(ui.soundDropdown, option.label)
                PlayReminderSound(option.key)
            end
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    ui.selectedSoundKey = db.defaultSoundKey
    if not IsValidSoundKey(ui.selectedSoundKey) then
        ui.selectedSoundKey = DEFAULT_SOUND_KEY
    end
    UIDropDownMenu_SetSelectedValue(ui.soundDropdown, ui.selectedSoundKey)
    UIDropDownMenu_SetText(ui.soundDropdown, GetSoundLabel(ui.selectedSoundKey))
    ui.soundDropdown:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Sound", 1, 1, 1)
        GameTooltip:AddLine("Plays when the reminder fires.", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    ui.soundDropdown:SetScript("OnLeave", function() GameTooltip:Hide() end)

    ui.voiceOverCheck = CreateFrame("CheckButton", "DeloRaidRemindersVoiceOverCheck", ui.editorPanel, "UICheckButtonTemplate")
    ui.voiceOverCheck:SetPoint("TOPLEFT", ui.editorPanel, "TOPLEFT", 12, -70)
    ui.voiceOverCheck:SetChecked(db.defaultTTSEnabled and true or false)
    ui.voiceOverCheck:SetScript("OnClick", function(self)
        db.defaultTTSEnabled = self:GetChecked() and true or false
    end)
    ui.voiceOverCheck:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Use Voice Over", 1, 1, 1)
        GameTooltip:AddLine("Only works if you have 'Combat Audio Alerts' enabled in Options > Accessibility > Audio Assist.", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    ui.voiceOverCheck:SetScript("OnLeave", function() GameTooltip:Hide() end)

    ui.voiceOverCheck.text = ui.voiceOverCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    ui.voiceOverCheck.text:SetPoint("LEFT", ui.voiceOverCheck, "RIGHT", 2, 0)
    ui.voiceOverCheck.text:SetText("Use Voice Over/TTS")

    ui.countdownCheck = CreateFrame("CheckButton", "DeloRaidRemindersCountdownCheck", ui.editorPanel, "UICheckButtonTemplate")
    ui.countdownCheck:SetPoint("TOPLEFT", ui.editorPanel, "TOPLEFT", 450, -70)
    ui.countdownCheck:SetChecked(db.defaultCountdownEnabled and true or false)
    ui.countdownCheck:SetScript("OnClick", function(self)
        db.defaultCountdownEnabled = self:GetChecked() and true or false
    end)
    ui.countdownCheck:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Show in Advance", 1, 1, 1)
        GameTooltip:AddLine("Shows and speaks (if enabled) a reminder before the set main time. ReminderTime - Show in Advance seconds", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    ui.countdownCheck:SetScript("OnLeave", function() GameTooltip:Hide() end)

    ui.countdownCheck.text = ui.countdownCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    ui.countdownCheck.text:SetPoint("LEFT", ui.countdownCheck, "RIGHT", 2, 0)
    ui.countdownCheck.text:SetText("Show in Advance")

    ui.countdownSecondsEditBox = CreateFrame("EditBox", nil, ui.editorPanel, "InputBoxTemplate")
    ui.countdownSecondsEditBox:SetSize(56, 26)
    ui.countdownSecondsEditBox:SetPoint("LEFT", ui.countdownCheck.text, "RIGHT", 6, 0)
    ui.countdownSecondsEditBox:SetAutoFocus(false)
    ui.countdownSecondsEditBox:SetMaxLetters(3)
    ui.countdownSecondsEditBox:SetText(tostring(db.defaultCountdownSeconds or 3))
    ui.countdownSecondsEditBox:SetScript("OnEditFocusLost", function(self)
        local value = math.floor(tonumber(self:GetText()) or db.defaultCountdownSeconds or 3)
        value = math.max(1, value)
        db.defaultCountdownSeconds = value
        self:SetText(tostring(value))
    end)
    ui.countdownSecondsEditBox:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Show in Advance Seconds", 1, 1, 1)
        GameTooltip:AddLine("How many seconds before the reminder to trigger the show in advance.", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    ui.countdownSecondsEditBox:SetScript("OnLeave", function() GameTooltip:Hide() end)

    local addBtn = CreateFrame("Button", nil, ui.editorPanel, "UIPanelButtonTemplate")
    addBtn:SetSize(138, 26)
    addBtn:SetPoint("BOTTOMLEFT", ui.editorPanel, "BOTTOMLEFT", 12, 10)
    addBtn:SetText("Add Reminder")
    addBtn:SetScript("OnClick", AddManualReminder)

    local importBtn = CreateFrame("Button", nil, ui.editorPanel, "UIPanelButtonTemplate")
    importBtn:SetSize(160, 26)
    importBtn:SetPoint("LEFT", addBtn, "RIGHT", 8, 0)
    importBtn:SetText("Import (lorrgs)")

    ui.movePopupBtn = CreateFrame("Button", nil, ui.editorPanel, "UIPanelButtonTemplate")
    ui.movePopupBtn:SetSize(120, 26)
    ui.movePopupBtn:SetPoint("LEFT", importBtn, "RIGHT", 8, 0)
    ui.movePopupBtn:SetText("Move Popup")
    ui.movePopupBtn:SetScript("OnClick", function() SetPopupDragMode(not state.popupUnlocked) end)

    local reminderListTitle = ui.frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    reminderListTitle:SetPoint("TOPLEFT", ui.editorPanel, "BOTTOMLEFT", 6, -10)
    reminderListTitle:SetText("Saved reminders")

    ui.reminderListFrame = CreateBackdropPanel(ui.frame, rightWidth, frameHeight - 292)
    ui.reminderListFrame:SetPoint("TOPLEFT", reminderListTitle, "BOTTOMLEFT", 0, -6)

    ui.rowSoundMenu = CreateFrame("Frame", "DeloRaidRemindersRowSoundMenu", UIParent, "UIDropDownMenuTemplate")
    UIDropDownMenu_Initialize(ui.rowSoundMenu, function(_, level)
        local reminder = state.activeReminderForSoundMenu
        if not reminder then return end
        for _, option in ipairs(REMINDER_SOUNDS) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = option.label
            info.value = option.key
            info.checked = (reminder.soundKey == option.key)
            info.func = function()
                reminder.soundKey = option.key
                SyncActiveReminderFromSource(reminder)
                PlayReminderSound(option.key)
                RefreshReminderRows()
            end
            UIDropDownMenu_AddButton(info, level)
        end
    end, "MENU")

    ui.importDialog = CreateFrame("Frame", "DeloRaidRemindersImportDialog", UIParent, "BasicFrameTemplateWithInset")
    ui.importDialog:SetSize(700, 390)
    ui.importDialog:SetPoint("CENTER")
    ui.importDialog:SetFrameStrata("DIALOG")
    ui.importDialog:SetMovable(true)
    ui.importDialog:EnableMouse(true)
    ui.importDialog:RegisterForDrag("LeftButton")
    ui.importDialog:SetScript("OnDragStart", ui.importDialog.StartMoving)
    ui.importDialog:SetScript("OnDragStop", ui.importDialog.StopMovingOrSizing)
    ui.importDialog:Hide()

    ui.importDialog.title = ui.importDialog:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    ui.importDialog.title:SetPoint("LEFT", ui.importDialog.TitleBg, "LEFT", 8, 0)
    ui.importDialog.title:SetText("Import from lorrgs.io")

    ui.importDialogScroll = CreateFrame("ScrollFrame", nil, ui.importDialog, "UIPanelScrollFrameTemplate")
    ui.importDialogScroll:SetPoint("TOPLEFT", ui.importDialog, "TOPLEFT", 14, -36)
    ui.importDialogScroll:SetSize(660, 280)

    ui.importDialogEditBox = CreateFrame("EditBox", nil, ui.importDialogScroll)
    ui.importDialogEditBox:SetMultiLine(true)
    ui.importDialogEditBox:SetAutoFocus(false)
    ui.importDialogEditBox:SetFontObject(ChatFontNormal)
    ui.importDialogEditBox:SetPoint("TOPLEFT", ui.importDialogScroll, "TOPLEFT", 4, -4)
    ui.importDialogEditBox:SetWidth(632)
    ui.importDialogEditBox:SetHeight(280)
    ui.importDialogEditBox:SetScript("OnEscapePressed", function(self) self:ClearFocus(); ui.importDialog:Hide() end)
    ui.importDialogEditBox:SetScript("OnCursorChanged", function(_, _, y) ui.importDialogScroll:SetVerticalScroll(y) end)
    ui.importDialogEditBox:SetScript("OnTextChanged", function(self)
        self:SetHeight(math.max(280, (self:GetNumberOfLines() + 1) * 14))
    end)
    ui.importDialogScroll:SetScrollChild(ui.importDialogEditBox)

    local cancelImportBtn = CreateFrame("Button", nil, ui.importDialog, "UIPanelButtonTemplate")
    cancelImportBtn:SetSize(120, 24)
    cancelImportBtn:SetPoint("BOTTOMRIGHT", ui.importDialog, "BOTTOMRIGHT", -14, 14)
    cancelImportBtn:SetText("Cancel")
    cancelImportBtn:SetScript("OnClick", function() ui.importDialog:Hide() end)

    local acceptImportBtn = CreateFrame("Button", nil, ui.importDialog, "UIPanelButtonTemplate")
    acceptImportBtn:SetSize(150, 24)
    acceptImportBtn:SetPoint("RIGHT", cancelImportBtn, "LEFT", -8, 0)
    acceptImportBtn:SetText("Import Timeline")
    acceptImportBtn:SetScript("OnClick", function()
        ImportLorrgsText(ui.importDialogEditBox:GetText())
        ui.importDialog:Hide()
    end)

    importBtn:SetScript("OnClick", function()
        if not state.selectedEncounterID then
            print("DeloRaidReminders: Select a boss first.")
            return
        end
        ui.importDialog:Show()
        ui.importDialogEditBox:SetFocus()
        ui.importDialogEditBox:HighlightText()
    end)

    ui.popup = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    ui.popup:SetSize(440, 90)
    ui.popup:SetFrameStrata("DIALOG")
    ui.popup:SetMovable(true)
    ui.popup:EnableMouse(false)
    ui.popup:EnableMouseWheel(true)
    ui.popup:RegisterForDrag("LeftButton")
    ui.popup:SetClampedToScreen(true)
    ui.popup:SetScript("OnDragStart", function(self) if state.popupUnlocked then self:StartMoving() end end)
    ui.popup:SetScript("OnDragStop", function(self) self:StopMovingOrSizing(); SavePopupPosition() end)
    ui.popup:SetScript("OnMouseWheel", function(_, delta)
        if not state.popupUnlocked then return end
        db.popupFontSize = math.max(16, math.min(96, (tonumber(db.popupFontSize) or 48) + (delta > 0 and 2 or -2)))
        ApplyPopupFontSize()
    end)
    ui.popup:Hide()

    ui.popup.text = ui.popup:CreateFontString(nil, "OVERLAY")
    ui.popup.text:SetPoint("CENTER")
    ApplyPopupFontSize()
    ui.popup.text:SetText("")
    ApplySavedPopupPosition()
    SetPopupDragMode(db and db.popupUnlocked)

    isUILoaded = true
end

-- =========================================================
-- RUNTIME / EVENTS
-- =========================================================

local function StartEncounter(encounterID, encounterName)
    state.activeEncounterID = encounterID
    state.fightStartTime = GetTime()
    state.activeReminders = nil

    local source = GetRemindersForEncounterStart(encounterID, encounterName)
    if source and #source > 0 then
        state.activeReminders = {}
        for _, r in ipairs(source) do
            local soundKey = r.soundKey or db.defaultSoundKey or DEFAULT_SOUND_KEY
            if not IsValidSoundKey(soundKey) then soundKey = DEFAULT_SOUND_KEY end
            state.activeReminders[#state.activeReminders + 1] = {
                time = tonumber(r.time) or 0,
                text = r.text,
                soundKey = soundKey,
                ttsEnabled = (r.ttsEnabled == nil) and false or (r.ttsEnabled and true or false),
                sourceRef = r,
                fired = false,
                countdownEnabled = r.countdownEnabled and true or false,
                countdownSeconds = tonumber(r.countdownSeconds) or (db and db.defaultCountdownSeconds) or 3,
                countdownFired = false,
            }
        end
        SortReminders(state.activeReminders)
    end
end

local function StopEncounter()
    state.activeEncounterID = nil
    state.fightStartTime = nil
    state.activeReminders = nil
end

local accumulator = 0
local function OnUpdate(_, elapsed)
    if not state.activeEncounterID or not state.fightStartTime or not state.activeReminders then return end
    accumulator = accumulator + elapsed
    if accumulator < 0.1 then return end
    accumulator = 0

    local now = GetTime() - state.fightStartTime
    for _, reminder in ipairs(state.activeReminders) do
        if reminder.countdownEnabled and not reminder.countdownFired then
            local seconds = tonumber(reminder.countdownSeconds) or 0
            local triggerTime = reminder.time - seconds
            if seconds > 0 and triggerTime >= 0 and now >= triggerTime then
                reminder.countdownFired = true
                local countdownText = string.format("%s in %d", reminder.text or "Reminder", seconds)
                ShowReminderPopup(countdownText, reminder.soundKey, reminder.ttsEnabled)
            end
        end
        if not reminder.fired and now >= reminder.time then
            reminder.fired = true
            ShowReminderPopup(reminder.text, reminder.soundKey, reminder.ttsEnabled)
        end
    end
end

local function EnsureUI()
    if isUILoaded and ui.frame then return true end
    local ok, err = pcall(BuildUI)
    if not ok then
        print("DeloRaidReminders: UI initialization failed.")
        print(tostring(err))
        return false
    end
    return true
end

local function ToggleMainWindow()
    if not EnsureUI() or not ui.frame then return end

    if ui.frame:IsShown() then
        ui.frame:Hide()
        if ui.importDialog and ui.importDialog:IsShown() then
            ui.importDialog:Hide()
        end
    else
        RefreshBossList()
        RefreshReminderRows()
        ui.frame:Show()
    end
end

local function HandleSlash(msg)
    msg = Trim(msg):lower()
    if msg == "" or msg == "open" or msg == "show" then
        ToggleMainWindow()
        return
    end
    print("DeloRaidReminders command: /deloraid or /deloraidreminders")
end

app:SetScript("OnEvent", function(_, event, ...)
    if event == "ADDON_LOADED" then
        local name = ...
        if name ~= ADDON_NAME then return end

        EnsureDB()
        app:SetScript("OnUpdate", OnUpdate)

        SLASH_DELORAIDREMINDERS1 = "/deloraid"
        SLASH_DELORAIDREMINDERS2 = "/deloraidreminders"
        SlashCmdList.DELORAIDREMINDERS = HandleSlash

        EnsureUI()
        print("DeloRaidReminders loaded. Use /deloraid to open.")
        return
    end

    if event == "ENCOUNTER_START" then
        local encounterID, encounterName = ...
        StartEncounter(encounterID, encounterName)
        return
    end

    if event == "ENCOUNTER_END" then
        StopEncounter()
        return
    end

    if event == "PLAYER_ENTERING_WORLD" and ui.frame and ui.frame:IsShown() then
        RefreshBossList()
        RefreshReminderRows()
    end
end)

app:RegisterEvent("ADDON_LOADED")
app:RegisterEvent("ENCOUNTER_START")
app:RegisterEvent("ENCOUNTER_END")
app:RegisterEvent("PLAYER_ENTERING_WORLD")