-- ============================================================================
-- RuntimeNPCNames: Discovers NPC names at runtime via mouseover/target
-- Persists to SavedVariables so names accumulate across sessions.
-- Merges into MapperBridge nameTable so BossLootTooltip can display them.
--
-- v12.37.1 changes:
--   - SavedVariables schema migrated from {[npcID]=name} to {[npcID]={n,t}}
--     where n=name and t=lastSeen timestamp (via time()).
--   - Size cap of 5000 entries with hysteresis to 4500 on trim.
--   - Auto-migrates old-format data on first load of this version.
--   - Re-encounters touch timestamps to keep active NPCs hot.
-- ============================================================================

local ADDON_NAME, NS = ...
local ETB = NS.ETB

local RuntimeNPC = {}
ETB.RuntimeNPCNames = RuntimeNPC

-- SavedVariable schema (v2): ETB_RuntimeNPCNames = { [npcID] = {n="Name", t=timestamp}, ... }
-- Legacy schema (v1):        ETB_RuntimeNPCNames = { [npcID] = "Name", ... }
-- Declared in .toc SavedVariables line.

-- Size cap + hysteresis. Trimming down to 4500 when we cross 5000 prevents
-- thrashing (we don't trim-every-save once we reach the cap; we let it
-- refill by 500 before re-trimming).
local MAX_ENTRIES = 5000
local TRIM_TO    = 4500

local cache = {}         -- live session cache: [npcID] = "Name" (strings only)
local nameTableRef = nil -- reference to MapperBridge's nameTable.npc

-- ============================================================================
-- GUID → npcID extraction (same logic as BossLootTooltip)
-- ============================================================================

local function GetNPCIDFromGUID(guid)
    if not guid then return nil end
    local unitType, _, _, _, _, npcID = strsplit("-", guid)
    if unitType == "Creature" or unitType == "Vehicle" then
        return tonumber(npcID)
    end
    return nil
end

-- ============================================================================
-- SavedVariables read/write helpers — hide the v1/v2 schema difference
-- ============================================================================

-- Get name from a SV entry, handling both old (string) and new (table) formats.
local function _svGetName(entry)
    if type(entry) == "string" then return entry end
    if type(entry) == "table" then return entry.n end
    return nil
end

-- Write a name to the SV in v2 format, stamping current time.
local function _svWrite(npcID, name)
    if not ETB_RuntimeNPCNames then return end
    local existing = ETB_RuntimeNPCNames[npcID]
    if type(existing) == "table" then
        -- Refresh timestamp without reallocating the table.
        existing.n = name
        existing.t = time()
    else
        ETB_RuntimeNPCNames[npcID] = { n = name, t = time() }
    end
end

-- ============================================================================
-- Core: resolve and cache an NPC name from a unit token
-- ============================================================================

-- v15.8.8: throttle SavedVariables timestamp writes per NPC. Previously
-- every mouseover (even multi-fire bursts from Blizzard's compare UI)
-- wrote to ETB_RuntimeNPCNames[npcID].t with the current time. In a city
-- with dozens of NPCs the user mouses over while running, that's a SV
-- write storm. Throttling to once per 60s per NPC preserves the "active
-- recently" signal for eviction without writing on every hover.
local SV_WRITE_THROTTLE_SEC = 60
local _lastSVWrite = {}  -- npcID -> last write time

local function CacheUnitName(unit)
    local guid = UnitGUID(unit)
    local npcID = GetNPCIDFromGUID(guid)
    if not npcID or npcID == 0 then return end

    local name = UnitName(unit)
    if not name or name == "" or name == UNKNOWNOBJECT then return end

    if cache[npcID] then
        -- v15.8.8: only refresh timestamp if we haven't written in the
        -- last SV_WRITE_THROTTLE_SEC. Cheap table lookup instead of an
        -- SV table mutation on every NPC mouseover.
        local now = time()
        if (now - (_lastSVWrite[npcID] or 0)) < SV_WRITE_THROTTLE_SEC then
            return
        end
        _lastSVWrite[npcID] = now

        if ETB_RuntimeNPCNames then
            local entry = ETB_RuntimeNPCNames[npcID]
            if type(entry) == "table" then
                entry.t = now
            else
                -- Still in legacy string format (shouldn't happen post-Initialize,
                -- but be defensive) — upgrade in place.
                _svWrite(npcID, name)
            end
        end
        return
    end

    -- New NPC: populate live cache, SV, and nameTable (if registered).
    cache[npcID] = name
    _lastSVWrite[npcID] = time()  -- v15.8.8: stamp at insert time
    _svWrite(npcID, name)
    if nameTableRef and not nameTableRef[npcID] then
        nameTableRef[npcID] = name
    end
end

-- ============================================================================
-- Event handlers
-- ============================================================================

local frame = CreateFrame("Frame")

frame:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
frame:RegisterEvent("PLAYER_TARGET_CHANGED")

frame:SetScript("OnEvent", function(self, event)
    if event == "UPDATE_MOUSEOVER_UNIT" then
        CacheUnitName("mouseover")
    elseif event == "PLAYER_TARGET_CHANGED" then
        CacheUnitName("target")
    end
end)

-- ============================================================================
-- Migration: legacy v1 (string values) → v2 ({n, t} values)
-- ============================================================================

local function _migrateIfNeeded()
    if not ETB_RuntimeNPCNames then return 0 end
    local migrated = 0
    local now = time()
    for npcID, entry in pairs(ETB_RuntimeNPCNames) do
        if type(entry) == "string" then
            ETB_RuntimeNPCNames[npcID] = { n = entry, t = now }
            migrated = migrated + 1
        end
    end
    return migrated
end

-- ============================================================================
-- Trim: enforce size cap. Called from Initialize after migration.
-- Sorts by timestamp ascending, drops oldest until count == TRIM_TO.
-- O(n log n) on startup only — not on the hot path.
-- ============================================================================

local function _trimIfNeeded()
    if not ETB_RuntimeNPCNames then return 0, 0 end

    -- Count current size
    local count = 0
    for _ in pairs(ETB_RuntimeNPCNames) do count = count + 1 end
    if count <= MAX_ENTRIES then return count, 0 end

    -- Build sortable array of [{npcID, t}, ...]
    local entries = {}
    for npcID, entry in pairs(ETB_RuntimeNPCNames) do
        local t = (type(entry) == "table" and entry.t) or 0
        entries[#entries + 1] = { id = npcID, t = t }
    end
    table.sort(entries, function(a, b)
        -- Guard against mixed-type comparisons (timestamps should all be
        -- numbers post-migration, but stay defensive).
        return (tonumber(a.t) or 0) < (tonumber(b.t) or 0)
    end)

    -- Drop oldest until count == TRIM_TO
    local toRemove = count - TRIM_TO
    local removed = 0
    for i = 1, toRemove do
        local e = entries[i]
        if e then
            ETB_RuntimeNPCNames[e.id] = nil
            removed = removed + 1
        end
    end
    return count, removed
end

-- ============================================================================
-- Initialization: load SavedVariables (with migration + trim), populate
-- live cache. Called from Init.lua phase 1 (login+1s).
-- ============================================================================

function RuntimeNPC:Initialize()
    ETB_RuntimeNPCNames = ETB_RuntimeNPCNames or {}

    -- Step 1: migrate legacy v1 entries to v2 schema.
    local migrated = _migrateIfNeeded()
    if migrated > 0 and ETB.debug then
        ETB:Debug("RuntimeNPC", string.format(
            "Migrated %d entries from v1 (string) to v2 ({n,t}) schema",
            migrated))
    end

    -- Step 2: trim if we're over the cap.
    local preTrimCount, removed = _trimIfNeeded()
    if removed > 0 then
        ETB:Debug("RuntimeNPC", string.format(
            "Trimmed %d NPC names (was %d, now %d)",
            removed, preTrimCount, preTrimCount - removed))
    end

    -- Step 3: populate the live session cache (strings only).
    local loaded = 0
    for npcID, entry in pairs(ETB_RuntimeNPCNames) do
        local name = _svGetName(entry)
        if name then
            cache[npcID] = name
            loaded = loaded + 1
        end
    end

    if loaded > 0 then
        ETB:Debug("RuntimeNPC", string.format(
            "Loaded %d cached NPC names from SavedVariables", loaded))
    end
end

-- ============================================================================
-- Merge all cached names into nameTable.npc (call after MapperBridge init)
-- ============================================================================

function RuntimeNPC:MergeIntoNameTable(npcNameTable)
    if not npcNameTable then return 0 end
    nameTableRef = npcNameTable  -- keep reference for live updates

    local merged = 0
    for npcID, name in pairs(cache) do
        if not npcNameTable[npcID] then
            npcNameTable[npcID] = name
            merged = merged + 1
        end
    end
    return merged
end

-- ============================================================================
-- Public API
-- ============================================================================

function RuntimeNPC:GetName(npcID)
    return cache[npcID]
end

function RuntimeNPC:GetCount()
    local count = 0
    for _ in pairs(cache) do count = count + 1 end
    return count
end

-- ============================================================================
-- Stats: /etb npcnames
-- Reports count, SV persistence, and age range of the cached entries.
-- ============================================================================

function RuntimeNPC:PrintStats()
    local svCount = 0
    local oldestT, newestT
    if ETB_RuntimeNPCNames then
        for _, entry in pairs(ETB_RuntimeNPCNames) do
            svCount = svCount + 1
            local t = (type(entry) == "table" and entry.t) or nil
            if t then
                if not oldestT or t < oldestT then oldestT = t end
                if not newestT or t > newestT then newestT = t end
            end
        end
    end

    ETB:Print(string.format("RuntimeNPCNames: %d cached (%d in SavedVariables)",
        RuntimeNPC:GetCount(), svCount))

    if oldestT and newestT then
        local now = time()
        local oldestDays = math.floor((now - oldestT) / 86400)
        local newestHours = math.floor((now - newestT) / 3600)
        ETB:Print(string.format("  oldest entry: %d days ago   newest: %d hours ago",
            oldestDays, newestHours))
        ETB:Print(string.format("  cap: %d (trim to %d)", MAX_ENTRIES, TRIM_TO))
    end
end
