-- ============================================================
-- IMAGO — Die Welt hat eine Geschichte.
-- IMAGO.lua — Hauptdatei: Init, Events, Koordination & Scanner
-- ============================================================

IMAGO = {}
IMAGO.VERSION = "1.0.0" 

local defaults = {
    enabled       = true,
    seenZones     = {},
    seenNPCs      = {},
    seenInstances = {},
    viewedNPCs    = {},
    favorites     = {},
    history       = {},
    showOnceOnly  = false,
    noTimerClose  = false,
    minimapPos    = 220, 
    hideMinimap   = false,
    -- Debug: Chat-Ausgabe bei Zonen-Checks (raw vs. aufgelöste uiMapID)
    debugMap      = false,
}

-- ============================================================
-- DAS SCANNER-MODUL
-- ============================================================
IMAGO.Scanner = {}

function IMAGO.Scanner.DiscoverNPC(npcID)
    if not IMAGOSaved.enabled then return false end
    if not IMAGOdb or not IMAGOdb.idToSlug then return false end
    
    local slug = IMAGOdb.idToSlug[npcID]
    if slug then
        local npcData = IMAGOdb.npcs[slug]
        local locale = GetLocale()
        local name = (locale == "deDE") and npcData.displayName_deDE or npcData.displayName_enUS
        local lore = (locale == "deDE") and npcData.lore_deDE or npcData.lore_enUS
        
        if not IMAGOSaved.seenNPCs[slug] then
            IMAGOSaved.seenNPCs[slug] = true
            
            table.insert(IMAGOSaved.history, 1, slug)
            if #IMAGOSaved.history > 50 then table.remove(IMAGOSaved.history) end
            
            local msg = IMAGO.L["CHAT_DISCOVERY"] and string.format(IMAGO.L["CHAT_DISCOVERY"], name) or ("|cFF9370DB[IMAGO]|r Echo gebunden: |cFFFFD700" .. name .. "|r")
            print(msg)
            PlaySound(3175, "Master")
            
            if IMAGO.Display and IMAGO.Display.Show then
                IMAGO.Display.Show(name, lore, "npc", true)
            end
            
            if IMAGO.Chronicle and IMAGO.Chronicle.frame and IMAGO.Chronicle.frame:IsShown() then
                IMAGO.Chronicle.UpdateList()
            end
            
            return true, true
        else
            local msgKnown = IMAGO.L["CHAT_KNOWN"] and string.format(IMAGO.L["CHAT_KNOWN"], name) or ("|cFF888888[IMAGO]|r Archiv-Eintrag abgerufen: |cFFCCCCCC" .. name .. "|r")
            print(msgKnown)
            
            if not IMAGO.Scanner.IsShowOnceOnlyEnabled() then
                if IMAGO.Display and IMAGO.Display.Show then
                    IMAGO.Display.Show(name, lore, "npc", false)
                end
            end
            return true, false
        end
    end
    return false, false
end

local lastNPCID = nil
-- Letzter verarbeiteter Zonen-Schlüssel: "c:<uiMapID>" = IMAGO-Zone, "r:<uiMapID>" = nur Rohtabellen-ID
local lastZoneKey = nil
local zoneCheckNilRetries = 0
local ZONE_CHECK_NIL_MAX = 12

function IMAGO.Scanner.EnsureZoneProgressTables()
    if not IMAGOSaved then return end
    IMAGOSaved.seenZones = IMAGOSaved.seenZones or {}
    -- String-Keys aus älteren Saves in echte uiMapID-Zahlen übernehmen
    local sz = IMAGOSaved.seenZones
    local toMigrate = {}
    for k, v in pairs(sz) do
        if v and type(k) == "string" then
            local n = tonumber(k)
            if n then toMigrate[k] = n end
        end
    end
    for strKey, numId in pairs(toMigrate) do
        sz[strKey] = nil
        sz[numId] = true
    end
    local dz = IMAGOSaved.discoveredZones
    if type(dz) == "table" and dz ~= IMAGOSaved.seenZones then
        for id, v in pairs(dz) do
            if v then
                local n = type(id) == "number" and id or tonumber(id)
                if n then IMAGOSaved.seenZones[n] = true end
            end
        end
    end
    IMAGOSaved.discoveredZones = IMAGOSaved.seenZones
end

--- WoW-Checkbuttons liefern oft 1/nil statt true/false — für zuverlässige Logik normalisieren.
function IMAGO.Scanner.IsShowOnceOnlyEnabled()
    if not IMAGOSaved then return false end
    local v = IMAGOSaved.showOnceOnly
    return v == true or v == 1
end

function IMAGO.Scanner.IsZoneMarkedSeen(zoneId)
    if not IMAGOSaved or not IMAGOSaved.seenZones then return false end
    local n = tonumber(zoneId) or zoneId
    if type(n) ~= "number" then return false end
    return not not (IMAGOSaved.seenZones[n] or IMAGOSaved.seenZones[tostring(n)])
end

--- Liefert die erste uiMapID auf dem Pfad (inkl. Start) mit Eintrag in IMAGOdb.zones, sonst nil.
function IMAGO.Scanner.ResolveTrackedZoneMapID(uiMapID)
    if not uiMapID or type(uiMapID) ~= "number" or not IMAGOdb or not IMAGOdb.zones then
        return nil
    end
    local id = uiMapID
    local depth = 0
    while id and depth < 24 do
        if IMAGOdb.zones[id] then
            return id
        end
        local info = C_Map.GetMapInfo(id)
        if not info then
            return nil
        end
        id = info.parentMapID
        depth = depth + 1
    end
    return nil
end

function IMAGO.Scanner.CheckNPC()
    if not UnitExists("target") then 
        lastNPCID = nil
        return 
    end

    -- Während Combat Lockdown können Unit-GUIDs als „Secret Strings“ vorliegen;
    -- jede String-Operation (z. B. strsplit) würde dann fehlschlagen. Lore-Popup
    -- ist hier ohnehin nicht zuverlässig aktualisierbar.
    if InCombatLockdown() then
        return
    end

    local guid = UnitGUID("target")
    if not guid then return end

    local ok, creatureType, _, _, _, _, npcIDStr = pcall(strsplit, "-", guid)
    if not ok or not creatureType then
        return
    end

    if creatureType == "Creature" or creatureType == "Vehicle" then
        local npcID = tonumber(npcIDStr)
        if not npcID or npcID == lastNPCID then return end
        lastNPCID = npcID
        IMAGO.Scanner.DiscoverNPC(npcID)
    end
end

function IMAGO.Scanner.CheckZone()
    if not IMAGOSaved or not IMAGOSaved.enabled then return end
    IMAGO.Scanner.EnsureZoneProgressTables()

    local rawMapID = C_Map.GetBestMapForUnit("player")
    if not rawMapID then
        zoneCheckNilRetries = zoneCheckNilRetries + 1
        if zoneCheckNilRetries <= ZONE_CHECK_NIL_MAX then
            C_Timer.After(0.5, IMAGO.Scanner.CheckZone)
        end
        return
    end

    if not C_Map.GetMapInfo(rawMapID) then
        zoneCheckNilRetries = zoneCheckNilRetries + 1
        if zoneCheckNilRetries <= ZONE_CHECK_NIL_MAX then
            C_Timer.After(0.5, IMAGO.Scanner.CheckZone)
        end
        return
    end

    zoneCheckNilRetries = 0

    local canonicalMapID = IMAGO.Scanner.ResolveTrackedZoneMapID(rawMapID)
    local key = canonicalMapID and ("c:" .. tostring(canonicalMapID)) or ("r:" .. tostring(rawMapID))

    -- Nach /reload ist lastZoneKey nil: ohne Seeding würde DiscoverZone erneut laufen.
    -- Wenn die Zone schon in seenZones steht und „nur einmal“ aktiv ist, Zustand angleichen
    -- und kein redundantes Popup auslösen.
    if lastZoneKey == nil and canonicalMapID and IMAGO.Scanner.IsShowOnceOnlyEnabled()
        and IMAGO.Scanner.IsZoneMarkedSeen(canonicalMapID) then
        lastZoneKey = key
    end

    if IMAGOSaved.debugMap then
        local rawInfo = C_Map.GetMapInfo(rawMapID)
        local rawName = rawInfo and rawInfo.name or "?"
        if canonicalMapID then
            local cInfo = C_Map.GetMapInfo(canonicalMapID)
            local cName = cInfo and cInfo.name or "?"
            print(string.format(
                "|cFF9370DB[IMAGO DEBUG]|r Zone-Check: raw uiMapID=%d (%s) -> tracked=%d (%s)",
                rawMapID, rawName, canonicalMapID, cName
            ))
        else
            print(string.format(
                "|cFF9370DB[IMAGO DEBUG]|r Zone-Check: raw uiMapID=%d (%s) -> keine IMAGO-Zone (Parent-Kette)",
                rawMapID, rawName
            ))
        end
    end

    if key == lastZoneKey then
        return
    end
    lastZoneKey = key

    if canonicalMapID then
        IMAGO.Scanner.DiscoverZone(canonicalMapID)
    end
end

function IMAGO.Scanner.CheckInstance() end
function IMAGO.Scanner.CheckEncounter(id) end

-- ============================================================
-- ZONEN-SCANNER & AUTO-POPUP
-- ============================================================

function IMAGO.Scanner.DiscoverZone(mapID)
    if not IMAGOSaved.enabled then return false end
    IMAGO.Scanner.EnsureZoneProgressTables()
    if not IMAGOdb or not IMAGOdb.zones then return false end

    local zoneData = IMAGOdb.zones[mapID]
    if not zoneData then return false end -- Zone nicht in unserer Lore-Datenbank

    local trackedID = tonumber(mapID) or mapID
    if type(trackedID) ~= "number" then return false end

    local locale = GetLocale()
    local name = (locale == "deDE") and zoneData.name_deDE or zoneData.name_enUS
    local lore = (locale == "deDE") and zoneData.lore_deDE or zoneData.lore_enUS

    local wasSeen = IMAGO.Scanner.IsZoneMarkedSeen(trackedID)
    local isNew = not wasSeen

    -- Immer persistieren sobald die Zone gültig ist (einheitlicher numerischer Key für SavedVariables)
    IMAGOSaved.seenZones[trackedID] = true
    IMAGOSaved.discoveredZones = IMAGOSaved.seenZones

    if isNew then
        table.insert(IMAGOSaved.history, 1, {type="zone", id=trackedID, time=time()})
        if #IMAGOSaved.history > 50 then table.remove(IMAGOSaved.history) end
        print("|cFF9370DB[IMAGO]|r Neue Region betreten: |cFFFFD700" .. name .. "|r")
    end

    local showOnceOnly = IMAGO.Scanner.IsShowOnceOnlyEnabled()
    if IMAGO.Display and IMAGO.Display.Show then
        if isNew or not showOnceOnly then
            IMAGO.Display.Show(name, lore, "zone", isNew)
        end
    end

    -- Chronik updaten, falls sie gerade offen ist
    if IMAGO.Chronicle and IMAGO.Chronicle.frame and IMAGO.Chronicle.frame:IsShown() then
        IMAGO.Chronicle.UpdateList()
    end

    return true
end

-- ============================================================
-- FEATURE: MINIMAP BUTTON
-- ============================================================
function IMAGO.CreateMinimapButton()
    local dragFrame = CreateFrame("Button", "IMAGOMinimapButton", Minimap)
    dragFrame:SetSize(31, 31)
    dragFrame:SetFrameStrata("MEDIUM")
    dragFrame:SetFrameLevel(Minimap:GetFrameLevel() + 5)
    dragFrame:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")

    dragFrame.icon = dragFrame:CreateTexture(nil, "BACKGROUND")
    dragFrame.icon:SetTexture("Interface\\Icons\\INV_Misc_Book_09") 
    dragFrame.icon:SetSize(20, 20)
    dragFrame.icon:SetPoint("CENTER", 0, 0)
    
    dragFrame.border = dragFrame:CreateTexture(nil, "OVERLAY")
    dragFrame.border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    dragFrame.border:SetSize(53, 53)
    dragFrame.border:SetPoint("TOPLEFT", 0, 0)
    
    local function UpdatePosition()
        local angle = math.rad(IMAGOSaved.minimapPos or 220)
        -- HIER DER FIX: Radius dynamisch berechnen (Minimap-Breite / 2 + Puffer)
        local radius = (Minimap:GetWidth() / 2) + 5 
        local x = math.cos(angle) * radius
        local y = math.sin(angle) * radius
        dragFrame:SetPoint("CENTER", Minimap, "CENTER", x, y)
    end
    
    dragFrame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    dragFrame:SetScript("OnClick", function(self, button)
        if button == "LeftButton" then
            if IMAGO.Chronicle then IMAGO.Chronicle.Toggle() end
        end
    end)
    
    dragFrame:RegisterForDrag("LeftButton")
    dragFrame:SetScript("OnDragStart", function()
        dragFrame:SetScript("OnUpdate", function()
            local mx, my = Minimap:GetCenter()
            local cx, cy = GetCursorPosition()
            local scale = Minimap:GetEffectiveScale()
            cx, cy = cx / scale, cy / scale
            local angle = math.deg(math.atan2(cy - my, cx - mx))
            if angle < 0 then angle = angle + 360 end
            IMAGOSaved.minimapPos = angle
            UpdatePosition()
        end)
    end)
    dragFrame:SetScript("OnDragStop", function()
        dragFrame:SetScript("OnUpdate", nil)
    end)
    
    -- Reagiert auf UI-Skalierung und Änderungen am Edit-Mode
    dragFrame:SetScript("OnEvent", UpdatePosition)
    UpdatePosition()
end

-- ============================================================
-- FEATURE: "WUSSTEST DU SCHON?"
-- ============================================================
local function ShowLoginFact()
    if not IMAGOSaved.enabled then return end
    
    local discovered = {}
    for slug, isSeen in pairs(IMAGOSaved.seenNPCs) do
        if isSeen and IMAGOdb.npcs[slug] then
            table.insert(discovered, slug)
        end
    end
    
    local locale = GetLocale()
    
    if #discovered > 0 then
        local randomSlug = discovered[math.random(1, #discovered)]
        local npcData = IMAGOdb.npcs[randomSlug]
        local name = (locale == "deDE") and npcData.displayName_deDE or npcData.displayName_enUS
        local lore = (locale == "deDE") and npcData.lore_deDE or npcData.lore_enUS
        local firstSentence = lore:match("^(.-%.%s)") or lore
        
        print(string.format("|cFF9370DB[IMAGO]|r |cFFFFD700%s|r (|cFFCCCCCC%s|r) - %s", IMAGO.L["LOGIN_DID_YOU_KNOW"], name, firstSentence))
    else
        print("|cFF9370DB[IMAGO]|r |cFF888888" .. IMAGO.L["LOGIN_EMPTY_CHRONICLE"] .. "|r")
    end
end

-- ============================================================
-- EVENTS & INITIALISIERUNG
-- ============================================================
local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("ADDON_LOADED")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
initFrame:RegisterEvent("PLAYER_TARGET_CHANGED")

initFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = ...
        if addonName == "IMAGO" then IMAGO.Init() end
    elseif event == "PLAYER_ENTERING_WORLD" then
        IMAGO.Scanner.EnsureZoneProgressTables()
        C_Timer.After(4, ShowLoginFact)
        C_Timer.After(0.5, function()
            IMAGO.Scanner.CheckZone()
        end)
        C_Timer.After(2, function()
            IMAGO.Scanner.CheckZone()
            IMAGO.Scanner.CheckInstance()
        end)
    elseif event == "ZONE_CHANGED_NEW_AREA" then
        C_Timer.After(1, function() IMAGO.Scanner.CheckZone() end)
    elseif event == "PLAYER_TARGET_CHANGED" then
        IMAGO.Scanner.CheckNPC()
    end
end)

function IMAGO.Init()
    IMAGOSaved = IMAGOSaved or {}
    for key, value in pairs(defaults) do
        if IMAGOSaved[key] == nil then IMAGOSaved[key] = value end
    end
    IMAGOSaved.viewedNPCs = IMAGOSaved.viewedNPCs or {}
    IMAGOSaved.showOnceOnly = (IMAGOSaved.showOnceOnly == true or IMAGOSaved.showOnceOnly == 1)
    IMAGO.Scanner.EnsureZoneProgressTables()

    if IMAGO.BuildReverseLookup then IMAGO.BuildReverseLookup() end
    if IMAGO.Options and IMAGO.Options.Init then IMAGO.Options.Init() end
    
    if IMAGO.Display and IMAGO.Display.CreateFrame then IMAGO.Display.CreateFrame() end
    if IMAGO.UnitContextMenu and IMAGO.UnitContextMenu.Init then IMAGO.UnitContextMenu.Init() end
    IMAGO.CreateMinimapButton()

    if TooltipDataProcessor and TooltipDataProcessor.AddTooltipPostCall then
        TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, function(tooltip, data)
            if not IMAGOSaved.enabled then return end
            -- TooltipDataProcessor-Callbacks können tainted laufen; geschützte APIs
            -- (z. B. UnitGUID) mit Secret-Unit-Tokens sind im Kampf / bei Taint problematisch.
            if InCombatLockdown() then return end

            local _, unit = tooltip:GetUnit()
            if not unit then return end
            if type(unit) ~= "string" or unit == "" then return end

            local ok, guid = pcall(UnitGUID, unit)
            if not ok or not guid or guid == "" then return end

            local type, _, _, _, _, npcIDStr = strsplit("-", guid)
            if type == "Creature" or type == "Vehicle" then
                local npcID = tonumber(npcIDStr)
                if IMAGOdb and IMAGOdb.idToSlug then
                    local slug = IMAGOdb.idToSlug[npcID]
                    if slug then
                        tooltip:AddLine(" ") 
                        if IMAGOSaved.seenNPCs[slug] then
                            tooltip:AddLine(IMAGO.L["TOOLTIP_KNOWN"] or "IMAGO: In Chronik verzeichnet")
                        else
                            tooltip:AddLine(IMAGO.L["TOOLTIP_UNKNOWN"] or "IMAGO: Schicksal verborgen")
                        end
                    end
                end
            end
        end)
    end

    SLASH_IMAGO1 = "/imago"
    SlashCmdList["IMAGO"] = function(msg)
        msg = msg:lower():trim()
        
        if msg == "" then
            if IMAGO.Chronicle then IMAGO.Chronicle.Toggle() end
        elseif msg == "settings" or msg == "config" then
            if Settings and Settings.OpenToCategory and IMAGO.settingsCategory then
                Settings.OpenToCategory(IMAGO.settingsCategory:GetID())
            end
        elseif msg == "help" then
            print("|cFF9370DBIMAGO Slash Commands:|r")
            print("|cFFFFD700/imago|r - " .. (IMAGO.L["CMD_HELP_OPEN_DESC"] or "Öffnet oder schließt die Chronik"))
            print("|cFFFFD700/imago settings|r - " .. (IMAGO.L["CMD_HELP_SETTINGS_DESC"] or "Öffnet die Addon-Einstellungen"))
            print("|cFFFFD700/imago help|r - " .. (IMAGO.L["CMD_HELP_HELP_DESC"] or "Zeigt diese Hilfe an"))
        elseif msg == "debugmap" then
            IMAGOSaved.debugMap = not IMAGOSaved.debugMap
            print(string.format(
                "|cFFFFD700IMAGO:|r Zonen-Debug %s (/imago debugmap zum Umschalten)",
                IMAGOSaved.debugMap and "|cFF00FF00AN|r" or "|cFFFF0000AUS|r"
            ))
        elseif msg == "reset" then
            IMAGOSaved.seenZones     = {}
            IMAGOSaved.discoveredZones = IMAGOSaved.seenZones
            IMAGOSaved.seenNPCs      = {}
            IMAGOSaved.seenInstances = {}
            IMAGOSaved.viewedNPCs    = {}
            IMAGOSaved.favorites     = {}
            IMAGOSaved.history       = {}
            print("|cFFFFD700IMAGO:|r " .. (IMAGO.L["RESET_DONE"] or "Historie zurückgesetzt."))
            if IMAGO.Chronicle and IMAGO.Chronicle.frame and IMAGO.Chronicle.frame:IsShown() then
                IMAGO.Chronicle.UpdateList()
            end
            
        elseif msg == "map" then
            -- DAS ULTIMATIVE DEV-TOOL FÜR ZONEN-IDS
            local mapID = C_Map.GetBestMapForUnit("player")
            if mapID then
                local mapInfo = C_Map.GetMapInfo(mapID)
                local mapName = mapInfo and mapInfo.name or "Unbekannt"
                print(string.format("|cFFFFD700[IMAGO DEV]|r Aktuelle Map ID: |cFF00FF00%d|r (%s)", mapID, mapName))
                print("|cFFCCCCCCTrage exakt diese ID in deine zones.lua ein!|r")
            else
                print("|cFFFF0000[IMAGO DEV]|r Konnte Map ID nicht ermitteln.")
            end
            
        elseif msg == "validate" then
            print(IMAGO.L["VAL_START"] or "|cFFFFD700[IMAGO]|r Starte Datenbank-Validierung...")
            local count, missingIDs, missingLore = 0, 0, 0
            local locale = GetLocale()
            for slug, data in pairs(IMAGOdb.npcs) do
                count = count + 1
                if not data.displayID and not data.ids then
                    print(string.format(IMAGO.L["VAL_ERR_ID"] or "|cFFFF0000Fehler:|r %s hat weder displayID noch ids-Array!", slug))
                    missingIDs = missingIDs + 1
                end
                local loreText = (locale == "deDE") and data.lore_deDE or data.lore_enUS
                if not loreText or loreText == "" then
                    print(string.format(IMAGO.L["VAL_WARN_LORE"] or "|cFFFF8C00Warnung:|r %s hat keine Lore in der aktuellen Sprache!", slug))
                    missingLore = missingLore + 1
                end
            end
            print(string.format(IMAGO.L["VAL_DONE"] or "Validierung beendet. %d NPCs geprüft. %d kritische Fehler, %d Warnungen.", count, missingIDs, missingLore))
        
        elseif msg == "unlockall" then
            local count = 0
            
            -- 1. NPCs freischalten
            for slug, _ in pairs(IMAGOdb.npcs or {}) do
                if not IMAGOSaved.seenNPCs[slug] then
                    IMAGOSaved.seenNPCs[slug] = true
                    IMAGOSaved.viewedNPCs[slug] = true -- Verhindert Cinematic-Spam
                    count = count + 1
                end
            end
            
            -- 2. Zonen freischalten
            for mapID, _ in pairs(IMAGOdb.zones or {}) do
                if not IMAGOSaved.seenZones[mapID] then
                    IMAGOSaved.seenZones[mapID] = true
                    count = count + 1
                end
            end
            
            -- Output im Chat
            local successMsg = IMAGO.L["CMD_UNLOCKALL_SUCCESS"] and string.format(IMAGO.L["CMD_UNLOCKALL_SUCCESS"], count) or string.format("|cFF9370DB[IMAGO]|r Alle Archive geöffnet. %d neue Einträge entschlüsselt.", count)
            print(successMsg)
            
            -- UI sofort aktualisieren, falls offen
            if IMAGO.Chronicle and IMAGO.Chronicle.frame and IMAGO.Chronicle.frame:IsShown() then
                IMAGO.Chronicle.UpdateList()
            end

        elseif msg:match("^scan %d+") then
            local id = tonumber(msg:match("^scan (%d+)"))
            local isRelevant = IMAGO.Scanner.DiscoverNPC(id)
            if not isRelevant then print("|cFFFF0000IMAGO:|r ID " .. id .. " ist nicht in der Datenbank.") end
        end
    end
end