local addonName, L = ...

local frame = CreateFrame("Frame")

L.activeDebuffs = {}
L.initialized = false
L.trackedDebuffPresent = false
L.debuffLossGraceUntil = 0
L.debuffLossRescanScheduled = false
L.activeSlashAlertTestProfileKey = nil
L.activeSlashAlertTestEndsAt = 0
L.activeSlashAlertTestTimer = nil

local DEBUFF_LOSS_GRACE_SECONDS = 2.0

function L:ScheduleDebuffLossRescan(delaySeconds)
    -- Zone transitions and aura cache refreshes can briefly hide debuffs.
    -- Coalesce those follow-up scans so we do not spam duplicate timers.
    if self.debuffLossRescanScheduled then
        return
    end

    self.debuffLossRescanScheduled = true
    C_Timer.After(delaySeconds, function()
        self.debuffLossRescanScheduled = false
        if self.initialized then
            self:ScanPlayerDebuffs(false)
        end
    end)
end

function L:BeginDebuffLossGracePeriod()
    -- Keep an already-active alert alive across short "no aura data yet" gaps,
    -- especially while loading screens or world transfers are still settling.
    if not self.trackedDebuffPresent then
        return
    end

    self.debuffLossGraceUntil = GetTime() + DEBUFF_LOSS_GRACE_SECONDS
    self:ScheduleDebuffLossRescan(DEBUFF_LOSS_GRACE_SECONDS)
end

function L:TriggerAlert(profileKey, context, options)
    profileKey = profileKey or "bloodlust"
    options = options or {}

    self:PlayCurrentAlert(profileKey, context)

    if not options.audioOnly then
        self:StartOverlayPlayback("full", false, profileKey)
        self:StartTextOverlayPlayback("full", false, profileKey, context)
    end
end

function L:OnLustStarted()
    self:TriggerAlert("bloodlust")
end

function L:OnLustEnded()
    if self:IsOverlayShown("bloodlust") then
        self:HideOverlay("bloodlust")
    end
    if self:IsTextOverlayShown("bloodlust") then
        self:HideTextOverlay("bloodlust")
    end
end

local TEST_PI_TRACK_CONTEXT = {
    piGiverName = "Priest",
    senderName = "Priest",
}

local TEST_PI_REQUEST_CONTEXT = {
    requesterName = "Requester",
}

function L:IsSlashAlertTestActive(profileKey)
    self:RefreshSlashAlertTestState()

    if profileKey then
        return self.activeSlashAlertTestProfileKey == profileKey
    end

    return self.activeSlashAlertTestProfileKey ~= nil
end

function L:RefreshSlashAlertTestState()
    local profileKey = self.activeSlashAlertTestProfileKey
    if not profileKey then
        return
    end

    local testExpired = (self.activeSlashAlertTestEndsAt or 0) > 0 and GetTime() >= self.activeSlashAlertTestEndsAt
    if not testExpired then
        return
    end

    self.activeSlashAlertTestProfileKey = nil
    self.activeSlashAlertTestEndsAt = 0
    if self.activeSlashAlertTestTimer then
        self.activeSlashAlertTestTimer:Cancel()
        self.activeSlashAlertTestTimer = nil
    end

    if self.NotifySettingsChanged then
        self:NotifySettingsChanged("slash_test_stopped", profileKey)
    end
end

function L:ScheduleSlashAlertTestRefresh(profileKey, durationSeconds)
    if self.activeSlashAlertTestTimer then
        self.activeSlashAlertTestTimer:Cancel()
        self.activeSlashAlertTestTimer = nil
    end

    self.activeSlashAlertTestTimer = C_Timer.NewTimer(math.max(0.05, durationSeconds or 0), function()
        self.activeSlashAlertTestTimer = nil
        self:RefreshSlashAlertTestState()
    end)
end

function L:TriggerSlashAlertTest(profileKey, context)
    profileKey = profileKey or "bloodlust"
    self:StopSlashAlertTests()
    local testDuration = math.max(
        tonumber(self:GetProfileDuration(profileKey)) or 0,
        tonumber(self.GetTextOverlaySequenceTotalDuration and self:GetTextOverlaySequenceTotalDuration(profileKey)) or 0
    ) + 0.1
    self.activeSlashAlertTestProfileKey = profileKey
    self.activeSlashAlertTestEndsAt = GetTime() + testDuration
    self:ScheduleSlashAlertTestRefresh(profileKey, testDuration)
    self:PlayCurrentAlert(profileKey, context, { isPreview = true })
    self:StartOverlayPlayback("full", true, profileKey)
    self:StartTextOverlayPlayback("full", true, profileKey, context)
    if self.NotifySettingsChanged then
        self:NotifySettingsChanged("slash_test_started", profileKey)
    end
end

function L:StopSlashAlertTests()
    local activeTestProfileKey = self.activeSlashAlertTestProfileKey
    self.activeSlashAlertTestProfileKey = nil
    self.activeSlashAlertTestEndsAt = 0
    if self.activeSlashAlertTestTimer then
        self.activeSlashAlertTestTimer:Cancel()
        self.activeSlashAlertTestTimer = nil
    end

    self:StopAllAudioIfPossible()

    if self.StopOverlaySequence then
        self:StopOverlaySequence(true, true)
    end
    if self.StopTextOverlaySequence then
        self:StopTextOverlaySequence(true, true)
    end
    if self.StopOverlayPreview then
        self:StopOverlayPreview()
    end
    if self.StopTextOverlayPreview then
        self:StopTextOverlayPreview()
    end

    for _, profileKey in ipairs({ "bloodlust", "piTrack", "piRequest" }) do
        if self.IsOverlayShown and self:IsOverlayShown(profileKey) then
            self:HideOverlay(profileKey)
        end
        if self.IsTextOverlayShown and self:IsTextOverlayShown(profileKey) then
            self:HideTextOverlay(profileKey)
        end
    end

    if activeTestProfileKey and self.NotifySettingsChanged then
        self:NotifySettingsChanged("slash_test_stopped", activeTestProfileKey)
    end
end

local function GetPlayerDebuffSpellId(index)
    local aura = C_UnitAuras.GetAuraDataByIndex("player", index, "HARMFUL")
    if not aura then
        return nil, nil
    end

    local auraInstanceID = aura.auraInstanceID
    if auraInstanceID and C_Secrets and C_Secrets.ShouldUnitAuraInstanceBeSecret then
        if C_Secrets.ShouldUnitAuraInstanceBeSecret("player", auraInstanceID) then
            return nil, true
        end
    end

    return aura.spellId, true
end

function L:ScanPlayerDebuffs(playSoundOnNew)
    -- Track debuffs strictly by spell ID so renamed/localized auras or duplicate
    -- spell names never affect alert detection.
    local currentlyActive = {}
    local foundNewTrackedDebuff = false
    local hasAnyTracked = false

    local index = 1
    while true do
        local spellId, hasAura = GetPlayerDebuffSpellId(index)
        if not hasAura then
            break
        end

        if spellId and self.TRACKED_DEBUFFS[spellId] then
            hasAnyTracked = true
            currentlyActive[spellId] = true
            if playSoundOnNew and not self.activeDebuffs[spellId] then
                foundNewTrackedDebuff = true
            end
        end

        index = index + 1
    end

    local wasTracked = self.trackedDebuffPresent

    -- If the last tracked debuff vanished only inside the grace window, defer the
    -- shutdown decision until we have rescanned after the window expires.
    if wasTracked and not hasAnyTracked and GetTime() < (self.debuffLossGraceUntil or 0) then
        local delaySeconds = math.max(0.05, self.debuffLossGraceUntil - GetTime())
        self:ScheduleDebuffLossRescan(delaySeconds)
        return
    end

    self.trackedDebuffPresent = hasAnyTracked
    self.activeDebuffs = currentlyActive

    if hasAnyTracked then
        self.debuffLossGraceUntil = 0
    end

    if foundNewTrackedDebuff and not wasTracked then
        self:OnLustStarted()
    end

    if wasTracked and not hasAnyTracked then
        self:OnLustEnded()
    end
end

function L:DebugBasic()
    local db = self:GetDB()
    self:Print("----- General Debug -----")
    self:Print("Available sounds: " .. tostring(#self.availableSounds))
    self:Print("Available TTS voices: " .. tostring(#self.availableVoices))
    self:Print("Available images: " .. tostring(#self.availableImages))
    self:Print("Available fonts: " .. tostring(#self.availableFonts))
    self:Print("Use TTS: " .. tostring(self:IsUsingTTS("bloodlust")))
    self:Print("Sound channel: " .. tostring(db.soundChannel))
    self:Print("Use image overlay: " .. tostring(db.useImageOverlay))
    self:Print("Selected image key: " .. tostring(db.selectedImageKey))
    self:Print("Image position: " .. tostring(db.imageX) .. ", " .. tostring(db.imageY))
    self:Print("Image scale: " .. tostring(db.imageScale))
    self:Print("Image enter duration: " .. tostring(db.imageEnterDuration))
    self:Print("Image exit duration: " .. tostring(db.imageExitDuration))
    self:Print("Image wobble scale: " .. tostring(db.imageWobbleScale))
    self:Print("Image wobble interval: " .. tostring(db.imageWobbleStepDuration))
    self:Print("Image grayscale: " .. tostring(db.imageGrayscale))
    self:Print("Image direction: " .. tostring(db.imageDirection))
    self:Print("Image enter easing: " .. tostring(db.imageEnterEasing))
    self:Print("Image exit easing: " .. tostring(db.imageExitEasing))
    self:Print("Start move: " .. tostring(db.startMoveEnabled))
    self:Print("Start fade: " .. tostring(db.startFadeEnabled))
    self:Print("Loop wobble: " .. tostring(db.loopWobbleEnabled))
    self:Print("Loop spin: " .. tostring(db.loopSpinEnabled))
    self:Print("Loop spin direction: " .. tostring(db.loopSpinDirection))
    self:Print("Loop spin duration: " .. tostring(db.loopSpinDuration))
    self:Print("End move: " .. tostring(db.endMoveEnabled))
    self:Print("End fade: " .. tostring(db.endFadeEnabled))
    self:Print("Use text overlay: " .. tostring(db.useTextOverlay))
    self:Print("Text value: " .. tostring(db.textOverlayText))
    self:Print("Text countdown: " .. tostring(db.textCountdownEnabled))
    self:Print("Text font: " .. tostring(db.selectedFontKey))
    self:Print("Text position: " .. tostring(db.textX) .. ", " .. tostring(db.textY))
    self:Print("Text scale: " .. tostring(db.textScale))
    self:Print("Text color cycle: " .. tostring(db.textLoopColorCycleEnabled))
    self:Print("Image edit mode: " .. tostring(self:IsEditModeEnabled("bloodlust")))
    self:Print("Text edit mode: " .. tostring(self:IsTextEditModeEnabled("bloodlust")))
    self:Print("Tracked debuff present: " .. tostring(self.trackedDebuffPresent))
    self:Print("Overlay state: " .. tostring(self:GetOverlaySequenceState()))
    self:Print("Text overlay state: " .. tostring(self:GetTextOverlaySequenceState()))
    self:Print("TTS playback state: " .. tostring(self.ttsPlaybackState))
    self:Print("Experimental PI enabled: " .. tostring(db.enableExperimentalPI))
    self:Print("Experimental PI debug: " .. tostring(db.debugExperimentalPI))
end

frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_LOGOUT")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:RegisterEvent("UNIT_AURA")
frame:RegisterEvent("VOICE_CHAT_TTS_VOICES_UPDATE")
frame:RegisterEvent("VOICE_CHAT_TTS_PLAYBACK_STARTED")
frame:RegisterEvent("VOICE_CHAT_TTS_PLAYBACK_FINISHED")
frame:RegisterEvent("VOICE_CHAT_TTS_PLAYBACK_FAILED")

frame:SetScript("OnEvent", function(_, event, unit)
    if event == "PLAYER_LOGIN" then
        -- Build runtime indexes once after the saved variables are guaranteed to exist.
        L:EnsureDB()
        if L.InitializeLauncher then
            L:InitializeLauncher()
        end
        L:RebuildSoundList()
        L:RebuildVoiceList()
        L:RebuildImageList()
        L:RebuildFontList()
        L:EnsureValidSelectedSound("bloodlust")
        L:EnsureValidSelectedVoice("bloodlust")
        L:EnsureValidSelectedImage("bloodlust")
        L:EnsureValidSelectedFont("bloodlust")
        L:EnsureValidSelectedSound("piTrack")
        L:EnsureValidSelectedVoice("piTrack")
        L:EnsureValidSelectedImage("piTrack")
        L:EnsureValidSelectedFont("piTrack")
        L:EnsureValidSelectedSound("piRequest")
        L:EnsureValidSelectedVoice("piRequest")
        L:EnsureValidSelectedImage("piRequest")
        L:EnsureValidSelectedFont("piRequest")
        L:EnsureOverlayFrame()
        L:ApplyOverlayVisuals("bloodlust")
        L:EnsureTextOverlayFrame()
        L:ApplyTextOverlayVisuals(L.BLOODLUST_VISUAL_TOTAL, "bloodlust")
        if L.CreateTabbedSettingsPanels then
            L:CreateTabbedSettingsPanels()
        end

        L:ScanPlayerDebuffs(false)
        L.initialized = true
        L:Print("Loaded " .. #L.availableSounds .. " sounds, " .. #L.availableVoices .. " TTS voices, " .. #L.availableImages .. " images and " .. #L.availableFonts .. " fonts.")
        return
    end

    if event == "PLAYER_LOGOUT" then
        if L.StopSlashAlertTests then
            L:StopSlashAlertTests()
        elseif L.StopAllAudioIfPossible then
            L:StopAllAudioIfPossible()
        end

        if L.StopAllSettingsTransientState then
            L:StopAllSettingsTransientState()
        end

        return
    end

    if event == "VOICE_CHAT_TTS_VOICES_UPDATE" then
        L:RebuildVoiceList()
        L:EnsureValidSelectedVoice("bloodlust")
        L:EnsureValidSelectedVoice("piTrack")
        L:EnsureValidSelectedVoice("piRequest")
        if L.NotifySettingsChanged then
            L:NotifySettingsChanged("tts_voices_update")
        end
        return
    end

    if event == "VOICE_CHAT_TTS_PLAYBACK_STARTED" then
        L.ttsPlaybackState = "started"
        L:RestoreTemporaryTTSCVar()
        if L.NotifySettingsChanged then
            L:NotifySettingsChanged("tts_playback_started")
        end
        return
    end

    if event == "VOICE_CHAT_TTS_PLAYBACK_FINISHED" then
        L.ttsPlaybackState = "finished"
        L:RestoreTemporaryTTSCVar()
        if L.NotifySettingsChanged then
            L:NotifySettingsChanged("tts_playback_finished")
        end
        return
    end

    if event == "VOICE_CHAT_TTS_PLAYBACK_FAILED" then
        L.ttsPlaybackState = "failed"
        L:RestoreTemporaryTTSCVar()
        if L.NotifySettingsChanged then
            L:NotifySettingsChanged("tts_playback_failed")
        end
        L:Print("TTS playback failed.")
        return
    end

    if event == "PLAYER_ENTERING_WORLD" and L.initialized then
        -- Entering world is the most common time for aura snapshots to lie briefly.
        L:BeginDebuffLossGracePeriod()
        L:ScanPlayerDebuffs(false)
        return
    end

    if event == "PLAYER_REGEN_ENABLED" and L.initialized then
        if L.OpenQueuedSettingsCategoryIfPossible then
            L:OpenQueuedSettingsCategoryIfPossible()
        end
        return
    end

    if event == "UNIT_AURA" and unit == "player" and L.initialized then
        L:ScanPlayerDebuffs(true)
    end
end)

SLASH_LUSTOMIZER1 = "/lustomizer"
SLASH_LUSTOMIZER2 = "/lusto"
SLASH_LUSTOMIZER3 = "/lust"

local function PrintSlashCommandHelp()
    L:Print("Commands: /lustomizer, /lustomizer help, /lustomizer image, /lustomizer text, /lustomizer pi, /lustomizer pitrack, /lustomizer pirequest, /lustomizer pireq, /lustomizer test, /lustomizer testpi, /lustomizer testpireq, /lustomizer stop, /lustomizer testsound, /lustomizer testtts, /lustomizer showimage, /lustomizer hideimage, /lustomizer editimage, /lustomizer showtext, /lustomizer hidetext, /lustomizer edittext, /lustomizer minimap, /lustomizer debug")
end

SlashCmdList["LUSTOMIZER"] = function(msg)
    msg = L:Trim(msg:lower())

    if msg == "" or msg == "options" or msg == "config" or msg == "gui" then
        L:OpenSettingsCategory("main")
    elseif msg == "help" then
        PrintSlashCommandHelp()
    elseif msg == "image" or msg == "overlay" then
        L:OpenSettingsCategory("image")
    elseif msg == "text" or msg == "textoverlay" then
        L:OpenSettingsCategory("text")
    elseif msg == "pisettings" or msg == "piconfig" or msg == "pi" then
        if L.OpenPISettingsCategory then
            L:OpenPISettingsCategory()
        end
    elseif msg == "pitrack" or msg == "pisound" then
        if L.OpenSettingsCategory then
            L:OpenSettingsCategory("piTrack")
        end
    elseif msg == "pirequest" or msg == "pireqsettings" then
        if L.OpenSettingsCategory then
            L:OpenSettingsCategory("piRequest")
        end
    elseif msg == "pireq" or msg == "requestpi" then
        if L.SendPIRequest then
            L:SendPIRequest()
        end
    elseif msg == "test" then
        L:TriggerSlashAlertTest("bloodlust")
        L:Print("Bloodlust test alert triggered.")
    elseif msg == "testpi" then
        L:TriggerSlashAlertTest("piTrack", TEST_PI_TRACK_CONTEXT)
        L:Print("Power Infusion receiver test alert triggered.")
    elseif msg == "testpireq" or msg == "testrequestpi" then
        L:TriggerSlashAlertTest("piRequest", TEST_PI_REQUEST_CONTEXT)
        L:Print("Power Infusion request test alert triggered.")
    elseif msg == "stop" then
        L:StopSlashAlertTests()
        L:Print("Active test alerts stopped.")
    elseif msg == "testsound" then
        L:PlaySelectedSound("bloodlust")
        L:Print("Test sound triggered.")
    elseif msg == "testtts" then
        L:PlaySelectedTTS("bloodlust")
        L:Print("Test TTS triggered.")
    elseif msg == "showimage" then
        L:ShowOverlayPreview("bloodlust")
        L:Print("Image preview shown.")
    elseif msg == "hideimage" then
        L:HideOverlay("bloodlust")
        L:Print("Image hidden.")
    elseif msg == "editimage" then
        L:SetEditMode(not L:IsEditModeEnabled("bloodlust"), "bloodlust")
        L:Print("Image edit mode: " .. tostring(L:IsEditModeEnabled("bloodlust")))
        if L.NotifySettingsChanged then
            L:NotifySettingsChanged("slash_edit_image", "bloodlust")
        end
    elseif msg == "showtext" then
        L:ShowTextOverlayPreview("bloodlust")
        L:Print("Text preview shown.")
    elseif msg == "hidetext" then
        L:HideTextOverlay("bloodlust")
        L:Print("Text hidden.")
    elseif msg == "edittext" then
        L:SetTextEditMode(not L:IsTextEditModeEnabled("bloodlust"), "bloodlust")
        L:Print("Text edit mode: " .. tostring(L:IsTextEditModeEnabled("bloodlust")))
        if L.NotifySettingsChanged then
            L:NotifySettingsChanged("slash_edit_text", "bloodlust")
        end
    elseif msg == "debug" then
        L:RebuildSoundList()
        L:RebuildVoiceList()
        L:RebuildImageList()
        L:RebuildFontList()
        L:DebugBasic()
    elseif msg == "minimap" or msg == "icon" then
        if L.ToggleMinimapIcon then
            local isShown = L:ToggleMinimapIcon()
            if isShown then
                L:Print("Minimap icon shown.")
            else
                L:Print("Minimap icon hidden. Use /lustomizer minimap to show it again.")
            end
        end
    else
        PrintSlashCommandHelp()
    end
end

