-- Core.lua -- Main logic and event modules --
-- WanderBook: Forge your path through Azeroth with your own goals. --
-- Made by Blakmane. May 2025. --

local _, addon = ...
WanderBook = addon

WanderBook_UnlockState = WanderBook_UnlockState or {}

if WanderBook.loaded then return end
WanderBook.loaded = true

WanderBook.unlocked = WanderBook_UnlockState.unlocked or false
if type(WanderBook_DefaultOaths) ~= "boolean" then
    WanderBook_DefaultOaths = false
end

WanderBook.settings = WanderBook.settings or {
    reportToChat = true,
    reportFullProgress = false,
    reportPercentageProgress = true,
    scaleIndex = 1,
}

--------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------

-- MAIN FRAME UI CALL --
local f = CreateFrame("Frame", "WanderBookFrame")
local UpdateUI = function()
    if WanderBookUI and WanderBookUI:IsShown() and WanderBookUI.ShowObjectives then
        WanderBookUI:ShowObjectives()
    end
end

-- LOCAL CALLS & DEBUG --
WanderBook.debug = false
local function printDebug(...) if WanderBook.debug then print("[WB DEBUG]", ...) end end

-- VALIDATION TAGS -- type=
local ValidTypes = {
    zone        = { "zone", "subzone" },
    emote       = { "emote", "creature" },
    loot        = { "item" },
    use         = { "spell", "item", "creature" },
    damage      = { "spell", "item", "creature" },
    regen       = { "spell", "item", "creature" },
    crit        = { "spell", "item", "creature" },
    kill        = { "spell", "item", "creature" }
}

local PLAYER_NAME = UnitName("player")
local lastTargetName, lastTargetCreatureType = nil, nil
local lastSpellHitByTarget = {}
local spellHitLog = {}

--------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------
-- OBJECTIVE TOOLS --

WanderBook.objectives = {}
WanderBook.objectivesByType = {}
local objectives, objectivesByType = WanderBook.objectives, WanderBook.objectivesByType
function AddObjective(obj)
    if not objectivesByType[obj.type] then objectivesByType[obj.type] = {} end
    table.insert(objectivesByType[obj.type], obj)
end

-- REINDEX --
function ReindexOathIDs()
    for index, obj in ipairs(objectives) do
        obj.id = index
    end
end

-- LOAD OATHS TABLE --
local function LoadSavedOaths()
    if type(WanderBook_SavedOaths) == "table" then
        for _, obj in ipairs(WanderBook_SavedOaths) do
            table.insert(WanderBook.objectives, obj)
            AddObjective(obj)
            ReindexOathIDs()
        end
        --print("Loaded", #WanderBook.objectives, "objectives.")
    else
        --print("No saved oaths found.")
    end
end

local function LoadDefaultOaths()
    if WanderBook_DefaultOaths then return end  -- already loaded flag check

    --print("Attempting to load default oaths...")
    --print("WanderBook_DefaultOathList type:", type(WanderBook_DefaultOathList))
    --print("WanderBook_DefaultOathList length:", #(WanderBook_DefaultOathList or {}))
    
    if type(WanderBook_DefaultOathList) == "table" then
        WanderBook_DefaultOaths = true  -- set before loop to avoid re-entry
        for _, obj in ipairs(WanderBook_DefaultOathList) do
            --print("Adding default objective:", obj.name or "Unnamed")
            table.insert(WanderBook.objectives, obj)
            AddObjective(obj)
            ReindexOathIDs()
        end
        --print("Default oaths loaded.")
    else
        --print("Failed to load default oath list.")
    end
end

-- REFRESH AFTER PLAYER_LOGIN (ensures SVs are injected)
local loadFrame = CreateFrame("Frame")
loadFrame:RegisterEvent("PLAYER_LOGIN")
loadFrame:SetScript("OnEvent", function()
    LoadDefaultOaths()
    LoadSavedOaths()
        
    if WanderBook_UnlockState and WanderBook_UnlockState.unlocked then
        print("|cfffe35eeWanderBook in hand. A book of silent oaths.")
    end
    if WanderBookUI and WanderBookUI.ShowObjectives then
        WanderBookUI:ShowObjectives()
    end
end)


-- COMPLETITION LOGIC --
local function PrintSingleProgress(obj)
    local progressText = obj.isCounter and string.format("%d", obj.progress)
        or string.format("%d/%d", obj.progress, obj.goal)
    local meta = obj.completed and string.format(" Completed by %s %s", obj.completedBy or "?", obj.completedOn or "?") or ""
    print("|cff99e699" .. string.format("%s: %s%s", obj.name, progressText, meta))

    UpdateUI()
end
local function MarkObjectiveComplete(obj)
    obj.progress = obj.goal
    obj.completed = true
    obj.completedBy = PLAYER_NAME
    obj.completedOn = date("%d.%m.%y")
    PrintSingleProgress(obj)
    UpdateUI()
    
    if WanderBookToast and WanderBookToast.Show then
        WanderBookToast.Show(obj)
    end
end

-- PROGRESS UPDATE HELPER --
local function UpdateProgress(obj, delta)
    obj.progress = obj.progress + delta
    obj.lastReportedPercent = obj.lastReportedPercent or 0
    obj.lastReportedCount = obj.lastReportedCount or 0

    local reported = false

    if WanderBook.settings.reportToChat then
        -- DETAILED: every single progress step
        if WanderBook.settings.reportFullProgress then
            if obj.isCounter then
                print("|cff99e699" .. string.format("%s: %d", obj.name, obj.progress))
            else
                print("|cff99e699" .. string.format("%s: %d/%d", obj.name, obj.progress, obj.goal))
            end
            reported = true

        -- COMPACT: milestone-only
        elseif WanderBook.settings.reportPercentageProgress then
            if obj.isCounter then
                -- Determine increment size
                local increment = 100
                if obj.type == "damage" or obj.type == "regen" then
                    increment = 10000
                end

                -- Only report at next multiple
                if obj.progress >= obj.lastReportedCount + increment then
                    obj.lastReportedCount = math.floor(obj.progress / increment) * increment
                    print("|cff99e699" .. string.format("%s: %d", obj.name, obj.progress))
                    reported = true
                end

            else
                -- Percentage milestones
                if obj.goal and obj.goal > 0 then
                    local percent = math.floor((obj.progress / obj.goal) * 100)
                    if percent >= 100 and obj.lastReportedPercent < 100 then
                        -- Let MarkObjectiveComplete handle the reporting
                        obj.lastReportedPercent = 100
                        MarkObjectiveComplete(obj)
                        reported = true
                    elseif percent >= 75 and obj.lastReportedPercent < 75 then
                        print("|cff99e699" .. string.format("%s: 75%% %d/%d", obj.name, obj.progress, obj.goal))
                        obj.lastReportedPercent = 75
                        reported = true
                    elseif percent >= 50 and obj.lastReportedPercent < 50 then
                        print("|cff99e699" .. string.format("%s: 50%% %d/%d", obj.name, obj.progress, obj.goal))
                        obj.lastReportedPercent = 50
                        reported = true
                    elseif percent >= 25 and obj.lastReportedPercent < 25 then
                        print("|cff99e699" .. string.format("%s: 25%% %d/%d", obj.name, obj.progress, obj.goal))
                        obj.lastReportedPercent = 25
                        reported = true
                    end
                end
            end
        end
    end

    -- Handle auto-complete if not already triggered by % logic
    if not obj.isCounter and obj.progress >= obj.goal and not obj.completed then
        MarkObjectiveComplete(obj)
        reported = true
    end

    -- Update UI silently if no report
    if not reported then
        UpdateUI()
    end
end

-- SHARED SPELL ID MATCHING LOGIC --
local function MatchesSpellID(eventID, objSpellID)
    if not objSpellID or not eventID then return false end
    if eventID == objSpellID then return true end

    local objInfo = C_Spell.GetSpellInfo(objSpellID)
    local eventInfo = C_Spell.GetSpellInfo(eventID)
    if objInfo and eventInfo and objInfo.name == eventInfo.name then
        return true
    end

    -- Check IDCatcherDB for any stored ID with that name
    if objInfo and IDCatcherDB and IDCatcherDB.spells then
        for sid, name in pairs(IDCatcherDB.spells) do
            if name == objInfo.name and sid == eventID then
                return true
            end
        end
    end

    -- Check spellNameCache for any stored ID with that name
    if objInfo and spellNameCache and spellNameCache[objInfo.name:lower()] then
        for _, sid in ipairs(spellNameCache[objInfo.name:lower()]) do
            if sid == eventID then
                return true
            end
        end
    end

    return false
end

-- SHARED ITEM ID MATCHING LOGIC --
local function MatchesItemID(eventSpellID, objItemID)
    if not objItemID or not eventSpellID then return false end

    -- Direct match via item spell
    local _, itemSpellID = GetItemSpell(objItemID)
    if itemSpellID and itemSpellID == eventSpellID then
        return true
    end

    -- Match via item name
    local itemName = C_Item.GetItemNameByID(objItemID)
    local eventInfo = C_Spell.GetSpellInfo(eventSpellID)
    local eventSpellName = eventInfo and eventInfo.name or nil

    if itemName and eventSpellName and eventSpellName == itemName then
        return true
    end

    -- Check IDCatcherDB for any item ID that has that name and event ID
    if itemName and IDCatcherDB and IDCatcherDB.items then
        for iid, name in pairs(IDCatcherDB.items) do
            if name == itemName then
                local _, linkedSpellID = GetItemSpell(iid)
                if linkedSpellID and linkedSpellID == eventSpellID then
                    return true
                end
            end
        end
    end

    return false
end

-- DUPLICATED OATH BLOCK --
local function IsDuplicateObjective(newObj)
    for _, obj in ipairs(WanderBook.objectives) do
        if obj.type == newObj.type and
           obj.npcID == newObj.npcID and
           obj.npcName == newObj.npcName and
           obj.goal == newObj.goal and
           obj.name == newObj.name and
           --obj.desc == newObj.desc and
           obj.icon == newObj.icon and
           obj.itemID == newObj.itemID and
           obj.spellID == newObj.spellID and
           obj.creatureType == newObj.creatureType and
           obj.zoneName == newObj.zoneName and
           obj.isCounter == newObj.isCounter then
            return true
        end
    end
    return false
end

-- LIST ORDER OPTIONS HELPER --
function WanderBook.SortObjectivesBy(mode)
    if mode == "alpha" then
        table.sort(WanderBook.objectives, function(a, b)
            return (a.name or ""):lower() < (b.name or ""):lower()
        end)

    elseif mode == "progress" then
        local allZero = true
        for _, obj in ipairs(WanderBook.objectives) do
            if obj.progress and obj.progress > 0 then
                allZero = false
                break
            end
        end

        if allZero then
            -- fallback to alphabetical
            table.sort(WanderBook.objectives, function(a, b)
                return (a.name or ""):lower() < (b.name or ""):lower()
            end)
        else
            table.sort(WanderBook.objectives, function(a, b)
                if a.isCounter ~= b.isCounter then
                    return not a.isCounter
                end
                local aVal = a.isCounter and a.progress or (a.progress / (a.goal or 1))
                local bVal = b.isCounter and b.progress or (b.progress / (b.goal or 1))
                return aVal > bVal
            end)
        end

    elseif mode == "type" then
        local typeOrder = {
            emote = 1, use = 2, loot = 3, kill = 4,
            killwith = 5, crit = 6, regen = 7, damage = 8
        }
        table.sort(WanderBook.objectives, function(a, b)
            return (typeOrder[a.type] or 99) < (typeOrder[b.type] or 99)
        end)
    end

    ReindexOathIDs()
    UpdateUI()
end

--------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------
-- EVENT HANDLERS --

-- ZONE EVENT TABLE AND HANDLER --
local ZONE_EVENTS = { PLAYER_ENTERING_WORLD = true, ZONE_CHANGED = true, ZONE_CHANGED_INDOORS = true, ZONE_CHANGED_NEW_AREA = true }
local function HandleZoneEvent()
    local mapID = C_Map.GetBestMapForUnit("player")
    local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
    local subzoneName = GetSubZoneText()

--[[    for _, obj in ipairs(objectivesByType["zone"] or {}) do
        if not obj.completed and obj.zoneName == zoneName then
            MarkObjectiveComplete(obj)
        end
    end]]
    for _, obj in ipairs(objectivesByType["zone"] or {}) do
        if not obj.completed and (obj.zoneName == zoneName or obj.zoneName == subzoneName) then
            MarkObjectiveComplete(obj)
        end
    end

    for _, obj in ipairs(objectivesByType["subzone"] or {}) do
        if not obj.completed and obj.zoneName == zoneName and obj.subzoneName == subzoneName then
            MarkObjectiveComplete(obj)
        end
    end
end

-- EMOTE EVENT HANDLER --
local emoteList = {
  "agree", "amaze", "applaud", "awe", "bye", "cheer", "clap", "cower", "cringe",
  "cry", "dance", "drink", "facepalm", "flex", "healme", "hello", "hug", "insult",
  "kiss", "kneel", "laugh", "nod", "no", "pet", "point", "pounce", "roar", "rofl",
  "salute", "shoo", "shrug", "sit", "sleep", "thank", "train", "wave"
}

local function HandleEmoteEvent(emoteName)
    if not emoteName or UnitName("player") ~= PLAYER_NAME then return end
    emoteName = emoteName:lower()

    local isValid = false
    for _, e in ipairs(emoteList) do
        if emoteName == e then
            isValid = true
            break
        end
    end
    if not isValid then return end

    for _, obj in ipairs(objectivesByType["emote"] or {}) do
        if not obj.completed then
            local passed = (obj.target or ""):lower() == emoteName

            if passed then
                if obj.npcID or obj.creatureType then
                    if UnitExists("target") then
                        local guid = UnitGUID("target")
                        local _, _, _, _, _, id = strsplit("-", guid or "")
                        local npcID = tonumber(id)
                        local ctype = UnitCreatureType("target")
                        if obj.npcID and npcID ~= obj.npcID then passed = false end
                        if obj.creatureType and ctype ~= obj.creatureType then passed = false end
                    else
                        passed = false
                    end
                end

                if passed and obj.zoneName then
                    local mapID = C_Map.GetBestMapForUnit("player")
                    local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                    if obj.zoneName ~= zoneName then passed = false end
                end

                if passed then
                    UpdateProgress(obj, 1)
                    UpdateUI()
                end
            end
        end
    end
end
hooksecurefunc("DoEmote", function(emoteName)
    --print("|cff00ff00[DEBUG] DoEmote called with:", emoteName)
    HandleEmoteEvent(emoteName)
end)

-- LOOT ITEM EVENT HANDLER --
local function HandleLootEvent(msg)
    local linkMatch = msg:match("|Hitem:(%d+):")
    local quantity = tonumber(msg:match("x(%d+)")) or 1

    for _, obj in ipairs(objectivesByType["loot"] or {}) do
        if not obj.completed and obj.itemID and tonumber(linkMatch) == obj.itemID then
            local passed = true

            if obj.zoneName then
                local mapID = C_Map.GetBestMapForUnit("player")
                local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                if obj.zoneName ~= zoneName then passed = false end
            end

            if passed then
                UpdateProgress(obj, quantity)
                UpdateUI()
            end
        end
    end
end

-- USE EVENT HANDLER (spells or items)
local function HandleUseEvent(unit, _, spellID)
    if unit ~= "player" then return end

    for _, obj in ipairs(objectivesByType["use"] or {}) do
        if not obj.completed then
            local matched = MatchesSpellID(spellID, obj.spellID) or MatchesItemID(spellID, obj.itemID)
            if matched then
                local passed = true

                if obj.npcID or obj.creatureType then
                    if UnitExists("target") then
                        local guid = UnitGUID("target")
                        local _, _, _, _, _, id = strsplit("-", guid or "")
                        local npcID = tonumber(id)
                        local ctype = UnitCreatureType("target")
                        if obj.npcID and npcID ~= obj.npcID then passed = false end
                        if obj.creatureType and ctype ~= obj.creatureType then passed = false end
                    else
                        passed = false
                    end
                end

                if passed and obj.zoneName then
                    local mapID = C_Map.GetBestMapForUnit("player")
                    local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                    if obj.zoneName ~= zoneName then passed = false end
                end

                if passed then
                    UpdateProgress(obj, 1)
                    UpdateUI()
                end
            end
        end
    end
end

-- USE EVENT HANDLER (auras: procs, trinkets, dots, etc)
local function HandleUseAuraApplied(spellID, destGUID)
    if destGUID ~= UnitGUID("player") then return end

    for _, obj in ipairs(objectivesByType["use"] or {}) do
        if not obj.completed and MatchesSpellID(spellID, obj.spellID) then
            local passed = true

            -- Optional zone check
            if obj.zoneName then
                local mapID = C_Map.GetBestMapForUnit("player")
                local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                if obj.zoneName ~= zoneName then
                    passed = false
                end
            end

            if passed then
                UpdateProgress(obj, 1)
                UpdateUI()
            end
        end
    end
end

-- SPELL EVENTS HANDLERS --
local function HandleCombatLogEvent(...)
    local _, subEvent, _, 
          sourceGUID, sourceName, _, _, 
          destGUID, destName, _, _, 
          spellID, spellName, spellSchool, 
          amount, overkill, school, resisted, blocked, absorbed, critical =
          CombatLogGetCurrentEventInfo()

    if subEvent == "SPELL_CAST_SUCCESS" or 
       subEvent == "SPELL_AURA_APPLIED" or 
       subEvent == "SPELL_DAMAGE" or 
       subEvent == "SPELL_PERIODIC_DAMAGE" or 
       subEvent == "SWING_DAMAGE" or 
       subEvent == "RANGE_DAMAGE" then
        lastSpellHitByTarget[destGUID] = spellID
        spellHitLog[destGUID] = spellHitLog[destGUID] or {}
        spellHitLog[destGUID][spellID] = true
    end
    
    if subEvent == "SPELL_AURA_APPLIED" then
        HandleUseAuraApplied(spellID, destGUID)
    end
    
    if sourceGUID ~= UnitGUID("player") then return end

    -- DAMAGE (from spells or items)
    for _, obj in ipairs(objectivesByType["damage"] or {}) do
        if not obj.completed and (
                subEvent == "SPELL_DAMAGE" or 
                subEvent == "SPELL_PERIODIC_DAMAGE"
            ) and amount then
            local matched = MatchesSpellID(spellID, obj.spellID) or MatchesItemID(spellID, obj.itemID)
            if matched then
                local passed = true

                local npcID, ctype

                -- From target
                if UnitExists("target") then
                    local guid = UnitGUID("target")
                    local _, _, _, _, _, id = strsplit("-", guid or "")
                    npcID = tonumber(id)
                    ctype = UnitCreatureType("target")
                -- Fallback: if player is being damaged
                elseif destGUID == UnitGUID("player") then
                    local guid = UnitGUID("player")
                    local _, _, _, _, _, id = strsplit("-", guid or "")
                    npcID = tonumber(id)
                    ctype = UnitCreatureType("player")
                else
                    passed = false
                end

                -- Creature/NPC checks
                if obj.npcID and npcID ~= obj.npcID then passed = false end
                if obj.creatureType and ctype ~= obj.creatureType then passed = false end

                -- Zone check
                if passed and obj.zoneName then
                    local mapID = C_Map.GetBestMapForUnit("player")
                    local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                    if obj.zoneName ~= zoneName then passed = false end
                end

                if passed then
                    UpdateProgress(obj, amount)
                    UpdateUI()
                end
            end
        end
    end

    -- REGEN (healing or resource gain from spells or items)
    for _, obj in ipairs(objectivesByType["regen"] or {}) do
        if not obj.completed and (
            subEvent == "SPELL_HEAL" or
            subEvent == "SPELL_PERIODIC_HEAL" or
            subEvent == "SPELL_ENERGIZE" or
            subEvent == "SPELL_PERIODIC_ENERGIZE"
        ) and amount then

            local matched = MatchesSpellID(spellID, obj.spellID) or MatchesItemID(spellID, obj.itemID)
            if matched then
                local passed = true

                local npcID, ctype

                -- From target
                if UnitExists("target") then
                    local guid = UnitGUID("target")
                    local _, _, _, _, _, id = strsplit("-", guid or "")
                    npcID = tonumber(id)
                    ctype = UnitCreatureType("target")
                -- If self-healing/regen
                elseif destGUID == UnitGUID("player") then
                    local guid = UnitGUID("player")
                    local _, _, _, _, _, id = strsplit("-", guid or "")
                    npcID = tonumber(id)
                    ctype = UnitCreatureType("player")
                else
                    passed = false
                end

                if obj.npcID and npcID ~= obj.npcID then passed = false end
                if obj.creatureType and ctype ~= obj.creatureType then passed = false end

                if passed and obj.zoneName then
                    local mapID = C_Map.GetBestMapForUnit("player")
                    local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                    if obj.zoneName ~= zoneName then passed = false end
                end

                if passed then
                    UpdateProgress(obj, amount)
                    UpdateUI()
                end
            end
        end
    end

    -- SPELL CRIT (damage or healing)
    for _, obj in ipairs(objectivesByType["crit"] or {}) do
        if not obj.completed and (
                subEvent == "SPELL_DAMAGE" or 
                subEvent == "SPELL_PERIODIC_DAMAGE" or 
                subEvent == "SPELL_HEAL" or 
                subEvent == "SPELL_PERIODIC_HEAL"
            ) and critical then

            local matched = MatchesSpellID(spellID, obj.spellID) or MatchesItemID(spellID, obj.itemID)
            if matched then
                local passed = true

                -- Optional: creatureType or npcID
                if obj.npcID or obj.creatureType then
                    local npcID, ctype
                    if destGUID then
                        local _, _, _, _, _, id = strsplit("-", destGUID or "")
                        npcID = tonumber(id)
                        ctype = UnitCreatureType("target") -- fallback, or derive differently if needed
                    end

                    if obj.npcID and npcID ~= obj.npcID then passed = false end
                    if obj.creatureType and ctype ~= obj.creatureType then passed = false end
                end

                -- Optional: zone
                if passed and obj.zoneName then
                    local mapID = C_Map.GetBestMapForUnit("player")
                    local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                    if obj.zoneName ~= zoneName then passed = false end
                end

                if passed then
                    UpdateProgress(obj, 1)
                    UpdateUI()
                end
            end
        end
    end

    -- KILL (by NPC ID or Creature Type)
    if subEvent == "PARTY_KILL" then
        local _, _, _, _, _, npcID = strsplit("-", destGUID)
        npcID = tonumber(npcID)

        -- Standard kill objective
        for _, obj in ipairs(objectivesByType["kill"] or {}) do
            if not obj.completed then
                local passed = true

                -- If objective specifies npcID, check it
                if obj.npcID and obj.npcID ~= npcID then
                    passed = false
                end

                -- If objective specifies creatureType, check it
                if passed and obj.creatureType then
                    local ctype = UnitCreatureType("target")
                    -- If we have no target, fallback: consider destGUID (you could build a helper to resolve ctype from GUID if desired)
                    if not ctype then ctype = "Unknown" end
                    if ctype ~= obj.creatureType then
                        passed = false
                    end
                end

                -- Optional: zone
                if passed and obj.zoneName then
                    local mapID = C_Map.GetBestMapForUnit("player")
                    local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                    if obj.zoneName ~= zoneName then passed = false end
                end

                if passed then
                    UpdateProgress(obj, 1)
                    UpdateUI()
                end
            end
        end

        -- Killwith triggers if the spell hit at any point
        for _, obj in ipairs(objectivesByType["killwith"] or {}) do
            
            local matched = false
            if spellHitLog[destGUID] then
                if obj.spellID then
                    for hitID in pairs(spellHitLog[destGUID]) do
                        if MatchesSpellID(hitID, obj.spellID) then
                            matched = true
                            break
                        end
                    end
                end
                if not matched and obj.itemID then
                    for hitID in pairs(spellHitLog[destGUID]) do
                        if MatchesItemID(hitID, obj.itemID) then
                            matched = true
                            break
                        end
                    end
                end
            end
                
            local passed = true
            if obj.npcID then
                local _, _, _, _, _, id = strsplit("-", destGUID or "")
                local matchID = tonumber(id)
                if matchID ~= obj.npcID then passed = false end
            end

            if passed and obj.creatureType then
                if lastTargetCreatureType ~= obj.creatureType or destName ~= lastTargetName then
                    passed = false
                end
            end

            if passed and obj.zoneName then
                local mapID = C_Map.GetBestMapForUnit("player")
                local zoneName = mapID and C_Map.GetMapInfo(mapID).name or ""
                if obj.zoneName ~= zoneName then passed = false end
            end

            if matched and passed then
                UpdateProgress(obj, 1)
            end
        end

        -- Clean up
        spellHitLog[destGUID] = nil
        lastSpellHitByTarget[destGUID] = nil
    end
end

-- TARGET CHANGE EVENT HANDLER --
local function HandleTargetChanged()
    if UnitExists("target") then
        lastTargetName = UnitName("target")
        lastTargetCreatureType = UnitCreatureType("target")
    else
        lastTargetName = nil
        lastTargetCreatureType = nil
    end
end

--------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------
-- MAIN EVENT TRIGGER --

f:SetScript("OnEvent", function(self, event, ...)
    local msg, unit, subEvent, spellID, destGUID, destName, amount, critical

    if ZONE_EVENTS[event] then
        HandleZoneEvent()
        return
    end
    if event == "CHAT_MSG_LOOT" then
        HandleLootEvent(...)
        return
    end
    if event == "CHAT_MSG_TEXT_EMOTE" then
        HandleEmoteEvent(...)
        return
    end
    if event == "PLAYER_TARGET_CHANGED" then
        HandleTargetChanged()
        return
    end
    if event == "UNIT_SPELLCAST_SUCCEEDED" then
        HandleUseEvent(...)
        return
    end
    if event == "COMBAT_LOG_EVENT_UNFILTERED" then
        HandleCombatLogEvent()
        return
    end
end)

--------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------
-- VALIDATIONS AND USER COMMANDS --

-- PARSER FOR /MAKEOATH COMMAND --
-- Improved parser: supports quoted values with spaces and avoids overwriting
local function ParseArgs(msg)
    --printDebug("Raw input string:", msg)
    local args = {}

    -- Step 1: Match quoted strings
    for k, v in string.gmatch(msg, '(%w+)%s*=%s*"([^"]*)"') do
        --printDebug("Quoted match:", k, v)
        args[k:lower()] = v
    end

    -- Step 2: Match unquoted (only if not already matched)
    for k, v in string.gmatch(msg, '(%w+)%s*=%s*([%w_%-%.]+)') do
        --printDebug("Unquoted match:", k, v)
        if not args[k:lower()] then
            args[k:lower()] = v
        end
    end
    return args
end

--[[local function ParseArgs(msg)
    local args = {}
    for k, v in msg:gmatch("(%w+)%s*=%s*\"([^\"]+)\"") do args[k:lower()] = v end
    for k, v in msg:gmatch("(%w+)%s*=%s*([^%s\"]+)") do if not args[k:lower()] then args[k:lower()] = v end end
    return args
end]]

-- SLASH COMMAND STRING/ALIAS NORMALIZER --
function WanderBook.NormalizeType(t)
    local map = {
        damage        = "damage",
        regen         = "regen",
        spellcast     = "use",
        itemuse       = "use",
        usetarget     = "use",
        emotetarget   = "emote",
        spellcrit     = "crit",
        crittarget    = "crit",
        itemloot      = "loot",
        killwith      = "killwith",
    }
    return map[t] or t
end

-- COMBO VALIDATION --
local function IsValidCombo(main, required)
    local rules = ValidCombos[main]
    if not rules then return false end
    for k in pairs(required) do
        if k ~= "zone" and k ~= "subzone" and not rules[k] then
            return false
        end
    end
    return true
end

-- CREATE:MAKEOATH COMMAND --
SLASH_MAKEOATH1 = "/makeoath"
SlashCmdList["MAKEOATH"] = function(input)
    local args = ParseArgs(input)
    --printDebug("Parsed args.npcname:", args.npcname)
    --printDebug("Parsed args.npc:", args.npc)
    if not args.type or not args.title then
        print("Usage: /makeoath type=<type> title=<title> [optional: spell, npc, item, creature, emote, zone, goal]")
        return
    end

    local objType = WanderBook.NormalizeType(args.type:lower())
    --printDebug("Creating objective with type:", objType)
    
    -- Unified target parser
    local npcOrCreature = args.npc or args.creature
    local npcID = tonumber(npcOrCreature)
    local npcName = args.npcname -- <- prioritize provided npcName
    --printDebug("Initial npcName value:", npcName)


    -- Only attempt resolution from target if npcName not provided
    if not npcName and npcID and UnitExists("target") then
        local guid = UnitGUID("target")
        if guid then
            local _, _, _, _, _, id = strsplit("-", guid)
            if tonumber(id) == npcID then
                npcName = UnitName("target")
            end
        end
    end

    -- Fallback if still nil
    if not npcName and npcID then
        npcName = "NPC " .. tostring(npcID)
    end
    --printDebug("Final resolved npcName:", npcName)


    local creatureType = (not npcID and npcOrCreature) or nil
    
        -- If unified 'token id' is provided, map to either item or spell based on resolution
    if args.token and not args.item and not args.spell then
        local raw = args.token
        local testID = tonumber(raw)
        if testID then
            local name = C_Item.GetItemNameByID(testID)
            if name then
                args.item = raw
            else
                name = C_Spell.GetSpellInfo(testID)
                if name then
                    args.spell = raw
                end
            end
        else
            -- Try by name
            local itemID = IDCatcher_ResolveItemIDByName and IDCatcher_ResolveItemIDByName(raw)
            if itemID then
                args.item = raw
            else
                local spellID = IDCatcher_ResolveSpellIDByName and IDCatcher_ResolveSpellIDByName(raw)
                if spellID then
                    args.spell = raw
                end
            end
        end
    end

    -- Resolve spell ID using live scanner
    local spellID = tonumber(args.spell)
    if not spellID and args.spell and IDCatcher_ResolveSpellIDByName then
        spellID = IDCatcher_ResolveSpellIDByName(args.spell)
        --print("Live resolved spell '" .. args.spell .. "' to ID: " .. tostring(spellID))
    end


    -- Resolve item ID using live scanner
    local itemID = tonumber(args.item)
    if not itemID and args.item and IDCatcher_ResolveItemIDByName then
        itemID = IDCatcher_ResolveItemIDByName(args.item)
        --print("Live resolved item '" .. args.item .. "' to ID: " .. tostring(itemID))
    end

    -- objective table builder
    local obj = {
        id = #objectives + 1,
        name = args.title,
        desc = args.desc or ("A new goal: " .. args.title),
        icon = args.icon and tonumber(args.icon) or nil,
        type = objType,
        progress = 0,
        goal = args.goal == "counter" and 0 or tonumber(args.goal) or 1,
        isCounter = args.goal == "counter" or nil,
        completed = false,
        zoneName = args.zone,
        subzoneName = args.subzone,
        npcID = npcID,
        npcName = npcName,
        creatureType = creatureType,
        spellID = spellID,
        itemID = itemID,
        target = args.emote or args.target
    }
    if IsDuplicateObjective(obj) then
        print("|cffff5555The book already has an identical oath.|r")
        return
    end
    --print("DEBUG: Resolved item input '" .. tostring(args.item) .. "' to itemID: " .. tostring(itemID))
    table.insert(objectives, obj)

    if not objectivesByType[obj.type] then
        objectivesByType[obj.type] = {}
    end
    table.insert(objectivesByType[obj.type], obj)

    ReindexOathIDs()
    UpdateUI()
    
    -- Play Blizzard sound
    PlaySound(SOUNDKIT.UI_BATTLEGROUND_COUNTDOWN_TIMER)
    
    local whispers = {
        "...The oath is sealed.",
        "...It is written.",
        "...The WanderBook remembers.",
        "...The vow is sealed.",
    }
    local msg = whispers[math.random(#whispers)]
    print("|cfffe35eeYou pledged to:|r " .. obj.name .. " |cfffe35ee" .. msg .. "|r")
end

-- LIST:LISTOATHS COMMAND --
SLASH_LISTOATHS1 = "/listoaths"
SlashCmdList["LISTOATHS"] = function()
    print("Active Oaths:")
    for _, obj in ipairs(objectives) do
        local status = obj.completed and "Completed" or (obj.progress .. "/" .. obj.goal)
        print(string.format("[%d] %s - %s", obj.id, obj.name, status))
    end
end

-- RESET:RENEWOATH COMMAND --
SLASH_RENEWOATH1 = "/renewoath"
SlashCmdList["RENEWOATH"] = function(msg)
    local id = tonumber(msg:match("id=(%d+)"))
    if not id or not objectives[id] then
        print("Usage: /renewoath id=1")
        return
    end
    local obj = objectives[id]
    obj.progress = 0
    obj.completed = false
    obj.completedBy = nil
    obj.completedOn = nil
    obj.lastReportedPercent = 0
    obj.lastReportedCount = 0
    
    -- Play Blizzard sound
    PlaySound(SOUNDKIT.ALARM_CLOCK_WARNING_3)
    
    print("|cfffe35eeOath renewed: " .. obj.name .. "|r")
end

-- DELETE:DISMISSOATH COMMAND --
SLASH_DISMISSOATH1 = "/dismissoath"
SlashCmdList["DISMISSOATH"] = function(msg)
    local id = tonumber(msg:match("id=(%d+)"))
    if not id or not objectives[id] then
        print("Usage: /dismissoath id=1")
        return
    end

    local removed = table.remove(objectives, id)

    -- Also remove from objectivesByType
    if removed and removed.type and objectivesByType[removed.type] then
        for i, obj in ipairs(objectivesByType[removed.type]) do
            if obj == removed then
                table.remove(objectivesByType[removed.type], i)
                break
            end
        end
    end

    ReindexOathIDs()
    -- Play Blizzard sound
    PlaySound(SOUNDKIT.UI_REFORGING_REFORGE)    
    
    print("|cfffe35eeOath dismissed: " .. removed.name .. "|r")
end

-- DEBUG: SHOW RAW OBJECTIVE TABLE
SLASH_OATHTABLE1 = "/oathtable"
SlashCmdList["OATHTABLE"] = function(arg)
    local index = tonumber(arg)
    if not index then
        print("Usage: /oathtable <objectiveIndex>")
        return
    end

    local obj = WanderBook.objectives and WanderBook.objectives[index]
    if not obj then
        print("No objective found at index " .. index)
        return
    end

    print("Raw objective table:")
    for k, v in pairs(obj) do
        print("  " .. tostring(k) .. " = " .. tostring(v))
    end
end

-- EXPORT COMMAND
SLASH_EXPORTOATH1 = "/exportoath"
SlashCmdList["EXPORTOATH"] = function(arg)
    local index = tonumber(arg)
    if not index then
        print("Usage: /exportoath <objectiveIndex>")
        return
    end

    local obj = WanderBook.objectives and WanderBook.objectives[index]
    if not obj then
        print("No objective found at index " .. index)
        return
    end

    local cmd = "/makeoath"
    cmd = cmd .. " type=" .. (obj.type or "none")

    if obj.type == "emote" and (obj.target or obj.emote) then
        cmd = cmd .. " emote=" .. (obj.target or obj.emote)
    end
    if obj.spellID then
        cmd = cmd .. " spell=" .. obj.spellID
    end
    if obj.itemID then
        cmd = cmd .. " item=" .. obj.itemID
    end
    if obj.creatureType then
        cmd = cmd .. " creature=" .. obj.creatureType
    end
    if obj.npcID then
        cmd = cmd .. " npc=" .. obj.npcID
    end
    if obj.npcName then
        cmd = cmd .. string.format(' npcName="%s"', obj.npcName)
    end
    if obj.zoneName then
        cmd = cmd .. " zone=\"" .. obj.zoneName .. "\""
    end
    if obj.subzoneName then
        cmd = cmd .. " subzone=\"" .. obj.subzoneName .. "\""
    end
    if obj.isCounter then
        cmd = cmd .. " goal=counter"
    elseif tonumber(obj.goal) and obj.goal > 0 then
        cmd = cmd .. " goal=" .. obj.goal
    end
    if obj.name then
        cmd = cmd .. " title=\"" .. obj.name .. "\""
    end
    if obj.desc then
        cmd = cmd .. " desc=\"" .. obj.desc .. "\""
    end
    if obj.icon then
        cmd = cmd .. " icon=" .. obj.icon
    end

    -- Create popup if not exists
    if not StaticPopupDialogs["WANDERBOOK_EXPORT"] then
        StaticPopupDialogs["WANDERBOOK_EXPORT"] = {
            text = "WanderBook Export Command",
            button1 = "Close",
            hasEditBox = true,
            editBoxWidth = 350,
            timeout = 0,
            whileDead = true,
            hideOnEscape = true,
            preferredIndex = 3,
            OnShow = function(self)
                self.editBox:SetText(WanderBook._exportCmd or "")
                self.editBox:SetFocus()
                self.editBox:HighlightText()
            end,
            OnAccept = function() end,
        }
    end

    -- Set the value to show
    WanderBook._exportCmd = cmd
    StaticPopup_Show("WANDERBOOK_EXPORT")
end

-- TOGGLE: TRACKING REPORT MODE
SLASH_WBTRACK1 = "/wbtrack"
SlashCmdList["WBTRACK"] = function(arg)
    arg = (arg or ""):lower()
    WanderBook.settings = WanderBook.settings or {}

    if arg == "detailed" then
        WanderBook.settings.reportToChat = true
        WanderBook.settings.reportFullProgress = true
        WanderBook.settings.reportPercentageProgress = false
        print("[WanderBook] Progress reporting set to show every increment.")
    elseif arg == "compact" then
        WanderBook.settings.reportToChat = true
        WanderBook.settings.reportFullProgress = false
        WanderBook.settings.reportPercentageProgress = true
        print("[WanderBook] Progress reporting set to show milestones only.")
    elseif arg == "off" then
        WanderBook.settings.reportToChat = false
        WanderBook.settings.reportFullProgress = false
        WanderBook.settings.reportPercentageProgress = false
        print("[WanderBook] Progress reporting off.")
    else
        print("Usage: /wbtrack detailed | compact | off")
    end
end

StaticPopupDialogs["WANDERBOOK_CONFIRM_RESET"] = {
    text = "Are you sure you want to completely reset the WanderBook? This will erase all data and restart from the intro quest.",
    button1 = "Reset",
    button2 = "Cancel",
    OnAccept = function()
        WanderBook_SavedOaths = nil
        WanderBook_DefaultOaths = nil
        WanderBook.objectives = {}
        WanderBook_UnlockState = nil
        WanderBook_Settings = nil

        if WanderBookUI then
            WanderBookUI:ClearAllPoints()
            WanderBookUI:SetPoint("CENTER", UIParent, "CENTER")
        end

        ReloadUI()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3
}

StaticPopupDialogs["WANDERBOOK_CONFIRM_WIPE"] = {
    text = "Wipe all current active oaths? This cannot be undone.",
    button1 = "Wipe",
    button2 = "Cancel",
    OnAccept = function()
        print("|cffff4444[WanderBook]|r Wiping all objectives...")

        WanderBook.objectives = {}
        WanderBook_SavedOaths = {}

        if WanderBookUI and WanderBookUI.ShowObjectives then
            WanderBookUI:ShowObjectives()
        end

        print("|cffff4444[WanderBook]|r All objectives cleared.")
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3
}
SLASH_WANDERBOOKRESET1 = "/wbreset"
SlashCmdList["WANDERBOOKRESET"] = function()
    StaticPopup_Show("WANDERBOOK_CONFIRM_RESET")
end

SLASH_WBWIPETEMP1 = "/wbwipe"
SlashCmdList["WBWIPETEMP"] = function()
    StaticPopup_Show("WANDERBOOK_CONFIRM_WIPE")
end


SLASH_WBCOMMANDS1 = "/wbc"
SlashCmdList["WBCOMMANDS"] = function()
    print("|cffe0b030WanderBook Commands:")
    print("|cff88ff88/wb|r: Open the WanderBook.")
    print("|cff88ff88/wbo|r: Open the WanderBook without cover.")
    print("|cff88ff88/wbn|r: Open the New Oath Panel right away.")
    print(" ")
    print("|cff88ff88/wbtrack|r: Chat reporting. detailed, compact or off.")
    print("|cff88ff88/listoaths|r: Show a list of all active and completed oaths.")
    print("|cff88ff88/wbskip|r: Skip the inicial intro quest.")
    print(" ")
    print("|cffff5555Warning: Once your data is wiped, it can't be undone.")
    print("|cffff8888/wbwipe|r: Wipe all the current active oaths.")
    print("|cffff8888/wbreset|r: Completely reset the WanderBook up to the intro quest.")
end


--------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------
-- REGISTER EVENTS --

for _, ev in ipairs({
    "PLAYER_ENTERING_WORLD", 
    "ZONE_CHANGED_NEW_AREA", 
    "ZONE_CHANGED_INDOORS", 
    "ZONE_CHANGED",
    "CHAT_MSG_TEXT_EMOTE", 
    "CHAT_MSG_LOOT",
    "PLAYER_TARGET_CHANGED",
    "COMBAT_LOG_EVENT_UNFILTERED",
    "UNIT_SPELLCAST_SUCCEEDED"
}) do f:RegisterEvent(ev) end

--------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------

-- SAVE ON LOGOUT
local saveFrame = CreateFrame("Frame")
saveFrame:RegisterEvent("PLAYER_LOGOUT")
saveFrame:SetScript("OnEvent", function()
    WanderBook_SavedOaths = WanderBook.objectives
    WanderBook_Settings = WanderBook.settings
    print("Saved", #WanderBook.objectives, "objectives.")
end)