-- Ядро аддона — спільний простір імен і службові функції.
-- Код не залежить від назви аддона: ім'я приходить через ADDON_NAME,
-- глобальна змінна SavedVariables — це ADDON_NAME .. "CaptureDB"
-- (має збігатися з рядком ## SavedVariables: у .toc).

local ADDON_NAME, ns = ...

ns.version = C_AddOns.GetAddOnMetadata(ADDON_NAME, "Version") or "?"
ns.svName = ADDON_NAME .. "CaptureDB"

-- ПУБЛІЧНА vs ТЕСТЕРСЬКА збірка. У dev/тестерській — false (усі збирачі текстів
-- працюють: GetCaptureDB пише в SavedVariables, Diag/лог помилок/детектор taint,
-- секція «Збір текстів», Capture.lua). Публічну збірку для CurseForge генерує
-- pipeline/build_public.py — він ставить це в true й прибирає UALLCaptureDB зі
-- SavedVariables у .toc, тож збір НЕ працює і нічого не зберігається на диск.
-- РЯДОК НИЖЧЕ ПЕРЕПИСУЄ build_public.py — не міняй формат.
ns.PUBLIC = true -- @BUILD-PUBLIC-FLAG@

-- Спільний простір імен для LoadOnDemand-під-аддонів (UALL_Items тощо):
-- вони не отримують той самий `...`-ns, тож читають його з цього глобала.
_G.WoWUkraineNS = ns

local PREFIX = "|cff0057b7" .. ADDON_NAME .. "|r: "

function ns.Print(msg)
    DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. tostring(msg))
end

-- SavedVariables-таблиця збору текстів (створюється за потреби).
function ns.GetCaptureDB()
    if ns.PUBLIC then
        -- Публічна збірка: збирачів немає. Віддаємо «ВСЕ ВЖЕ ЗІБРАНО»-таблицю:
        -- читання БУДЬ-ЯКОГО ключа → true, тож колекторний патерн «if not db.x[key]
        -- then db.x[key]=v; notify(...)» СКРІЗЬ бачить key як наявний → нічого не
        -- пише й НЕ сповіщає в чат; записи ігноруються (нічого не накопичується в
        -- пам'яті, UALLCaptureDB немає у SavedVariables → нічого на диск). Один
        -- центральний гейт замість правок у 23 хуках; жодних падінь на db.x.y=...
        if not ns.__nullDB then
            local allTrue = setmetatable({}, {
                __index = function() return true end, __newindex = function() end })
            ns.__nullDB = setmetatable({}, {
                __index = function() return allTrue end, __newindex = function() end })
        end
        return ns.__nullDB
    end
    local db = _G[ns.svName]
    if not db then
        db = {}
        _G[ns.svName] = db
    end
    db.quests = db.quests or {}
    db.meta = db.meta or {}
    return db
end

-- Діагностичний лог у SavedVariables — читається з диска без скріншотів.
-- Тримаємо останні 40 записів, щоб файл не розпухав.
function ns.Diag(msg)
    if ns.PUBLIC then return end
    local db = ns.GetCaptureDB()
    db.diag = db.diag or {}
    table.insert(db.diag, date("%H:%M:%S ") .. tostring(msg))
    if #db.diag > 40 then table.remove(db.diag, 1) end
end

-- ОКРЕМИЙ постійний лог ПОМИЛОК — його НЕ витісняє звичайна діагностика.
-- Сюди потрапляє кожна Lua-помилка, навіть якщо трекер щось спамить.
function ns.LogError(msg)
    local db = ns.GetCaptureDB()
    db.errors = db.errors or {}
    table.insert(db.errors, date("%H:%M:%S ") .. tostring(msg))
    if #db.errors > 30 then table.remove(db.errors, 1) end
end

-- Безпечне читання тексту віджета. У WoW 11.0+ частина текстів трекера
-- та захищеного UI — «СЕКРЕТНІ ЗНАЧЕННЯ»: їх НЕ МОЖНА порівнювати чи
-- читати з аддона (інакше "attempt to compare a secret string value" і
-- лавина помилок/краш — саме це валило аддон у делвах/сценаріях).
-- Повертає звичайний непорожній рядок АБО nil (секретне/порожнє/нема).
function ns.SafeText(v)
    if v == nil then return nil end
    local ok, isEmpty = pcall(function() return v == "" end)
    if not ok then return nil end -- секретне значення — не чіпаємо
    if isEmpty then return nil end
    return v
end

-- ШВИДКА перевірка кирилиці (байтова, без декодування UTF-8): провідні байти
-- кириличних літер — 0xD0/0xD1 (а-я,і,ї,є) і 0xD2 (ґ). Клієнт enUS, тож БУДЬ-ЯКА
-- кирилиця в тексті означає, що це ВЖЕ НАШ переклад → не перекладати повторно
-- й не збирати назад (інакше марні цикли + засмічення бази укр. рядками).
function ns.HasCyrillic(s)
    return type(s) == "string" and s:find("[\208\209\210]") ~= nil
end

-- ЄДИНИЙ реєстр УСІХ наших підмін тексту: fontString -> {en, ua}. КОЖНЕ місце,
-- де ми замінюємо англійський текст на український, реєструє пару сюди.
-- Це робить «показати оригінал» element-agnostic: аддону БАЙДУЖЕ, що за віджет
-- (моб/предмет/квест/діалог) — Shift просто тимчасово повертає збережений
-- оригінал УСЮДИ одразу, відпускання — назад на переклад. Слабкі ключі — без
-- витоків (віджети, яких уже нема, прибираються GC).
ns.replacements = setmetatable({}, { __mode = "k" })
-- solo=true — елемент НЕ з пулу (єдиний, напр. SimpleHTML книги), і не має
-- читабельного GetText; ToggleOriginal свопатиме його напряму за станом Shift.
function ns.Remember(fs, en, ua, solo)
    if fs and type(en) == "string" and type(ua) == "string" and en ~= ua then
        ns.replacements[fs] = { en = en, ua = ua, solo = solo }
    end
end

-- Свопає зареєстровані віджети між оригіналом і перекладом за станом Shift.
-- ОБОРОТНО: на «показати оригінал» запам'ятовуємо, які саме віджети МИ змінили
-- ua→en, і на відпусканні відновлюємо ЛИШЕ їх. НЕ можна на відпусканні тупо
-- ставити ua всім англ. парам — інакше Shift насильно українізує навіть те, що
-- вимкнене в опціях і має лишатися англійським (баг: «вимкнув усе, а Shift робить
-- квести укр.»). Чіпаємо лише ті, чий ПОТОЧНИЙ текст саме наша пара (пули).
function ns.ToggleOriginal()
    local show = ns.ShowOriginal()
    local flipped = ns.__uaFlipped
    if not flipped then flipped = setmetatable({}, { __mode = "k" }); ns.__uaFlipped = flipped end
    for fs, p in pairs(ns.replacements) do
        if p.solo then
            -- solo (SimpleHTML книги): показуємо en лише поки тримається Shift, інакше ua
            -- (книга видима ЛИШЕ коли її модуль активний, тож тут насильної укр. нема).
            if fs.SetText then pcall(fs.SetText, fs, show and p.en or p.ua) end
        elseif fs.GetText and fs.SetText then
            local ok, cur = pcall(fs.GetText, fs)
            cur = ok and ns.SafeText(cur) or nil
            if show then
                -- показати оригінал: en лише для тих, що ЗАРАЗ українською (наш переклад)
                if cur == p.ua then pcall(fs.SetText, fs, p.en); flipped[fs] = true end
            else
                -- відновити ua ЛИШЕ для тих, що ми самі щойно перевели на en
                if flipped[fs] and cur == p.en then pcall(fs.SetText, fs, p.ua) end
                flipped[fs] = nil
            end
        end
    end
end

-- Гаряча клавіша-ПЕРЕМИКАЧ «показати оригінал». На відміну від утримання
-- модифікатора (Shift/Ctrl/Alt часто зайняті в гравця → переклад блимає в кожному
-- комбо), це ФІКСОВАНИЙ стан: натиснув раз — оригінал, ще раз — переклад. Клавішу
-- гравець призначає сам у «Гра → Призначення клавіш» (можна акорд: Shift+T).
-- ns.originalToggled враховує ns.ShowOriginal(), тож охоплює весь реєстр підмін.
-- Виклики: клавіша з «Гра → Призначення клавіш → WoWUkraine» (Bindings.xml), слеш
-- «/uall toggle» або макро «/run UALL_ToggleOriginalHotkey()». (Прив'язку відновлено:
--  Bindings.xml + глобалі BINDING_HEADER/NAME нижче — без заголовка розділ був безіменний
--  і його не знаходили, звідси хибний висновок «не працює» у f1401c5.)
function UALL_ToggleOriginalHotkey()
    ns.originalToggled = not ns.originalToggled
    -- ПЕРСИСТИМО стан перемикача між сесіями (db.showOriginal). Це НЕЗАЛЕЖНИЙ від
    -- майстра «Увімкнути переклад» шлях показу оригіналу — окрема клавіша-перемикач
    -- у «Призначенні клавіш» (чекбокс-дубль прибрано; ShowOriginal зважає й на майстер).
    local db = ns.GetSettingsDB and ns.GetSettingsDB()
    if db then db.showOriginal = ns.originalToggled end
    if C_Timer and C_Timer.After then
        C_Timer.After(0, ns.ToggleOriginal) -- поза стеком натискання — taint-safe
    else
        ns.ToggleOriginal()
    end
end

-- Заголовки/підписи дій на екрані «Гра → Призначення клавіш» (Bindings.xml з
-- category="ADDONS" → секція «Аддони», як BtWQuests). Це НАШІ власні глобалі
-- (Blizzard їх не має), тож secure-код їх не читає — тейнту немає (на відміну від
-- override чужих BINDING_* [[globalstrings-interface-taint]]).
_G.BINDING_HEADER_UALL = "WoWUkraine"
_G.BINDING_NAME_UALL_TOGGLE_ORIGINAL = "Вимкнути весь переклад (перемикач)"
_G.BINDING_NAME_UALL_SHOW_ORIGINAL_HOLD = "Показати оригінал (утримання)"

-- УТРИМАННЯ оригіналу на ДОВІЛЬНІЙ клавіші (переназначувана заміна модифікатора
-- Shift/Ctrl/Alt). WoW не дає addon-біндингу події key-up, тож ловимо клавішу самі:
-- кадр із EnableKeyboard + PropagateKeyboardInput(true) СПОСТЕРІГАЄ натиск/відпускання
-- (клавішу НЕ їсть — рух/дії гравця цілі), і коли натиснута клавіша = та, що гравець
-- призначив біндингу UALL_SHOW_ORIGINAL_HOLD, вмикаємо ns.holdOriginal (ShowOriginal
-- враховує його). Поки редагується текст (чат) — не спрацьовує (кадр не отримує клавіш).
do
    local hf = CreateFrame("Frame", nil, UIParent)
    hf:EnableKeyboard(true)
    hf:SetPropagateKeyboardInput(true)
    local function boundKey()
        return GetBindingKey and GetBindingKey("UALL_SHOW_ORIGINAL_HOLD")
    end
    local function setHold(on)
        if ns.holdOriginal == on then return end
        ns.holdOriginal = on
        if C_Timer and C_Timer.After then C_Timer.After(0, ns.ToggleOriginal)
        else ns.ToggleOriginal() end
    end
    hf:SetScript("OnKeyDown", function(_, key)
        local bk = boundKey()
        if bk and key == bk then setHold(true) end
    end)
    hf:SetScript("OnKeyUp", function(_, key)
        local bk = boundKey()
        if bk and key == bk then setHold(false) end
    end)
end

-- ОПЦІЯ «оригінальні імена»: міняє кириличні імена персонажів (усі відмінки)
-- на латинський оригінал. ns.nameForms = { ["Вратіон"]="Wrathion", ... } —
-- стем ловить ПОЧАТОК слова, кириличне відмінкове закінчення відкидається
-- (латиниця лишається без відмінка: «Щоденник Wrathion»). Текст зберігається
-- ОДИН раз — дублювати не треба, заміна йде на льоту перед показом.
local nameCache = {}
local nameCacheN = 0
function ns.ApplyNames(text)
    if type(text) ~= "string" or not ns.nameForms then return text end
    if not ns.IsEnabled("names.original") then return text end
    local cached = nameCache[text]
    if cached ~= nil then return cached end
    local result = text:gsub("%S+", function(tok)
        -- відрізати ASCII-пунктуацію спереду/ззаду (кирилиця/«» — не %p)
        local lead, core, trail = tok:match("^(%p*)(.-)(%p*)$")
        if not core or core == "" then return nil end
        for stem, eng in pairs(ns.nameForms) do
            if core:sub(1, #stem) == stem then
                return lead .. eng .. trail
            end
        end
        return nil -- слово без імені — без змін
    end)
    if nameCacheN > 4000 then nameCache = {}; nameCacheN = 0 end
    nameCache[text] = result
    nameCacheN = nameCacheN + 1
    return result
end

-- СТАТЬ ГРАВЦЯ: токен $p(чол:жін) у наших перекладах розгортається за UnitSex
-- (2=чол, 3=жін, 1=невідомо→чол). REST API прибрав Blizzard-$g, тож кодуємо рід
-- гравця цим токеном у місцях звертання («ти зібра$p(в:ла)», «$p(друже:подруго)»).
-- Display-only, без тейнту. Викликати на КОЖЕН наш рядок перед показом.
function ns.ApplyPlayerGender(text)
    if type(text) ~= "string" then return text end
    if not text:find("$p(", 1, true) then return text end   -- швидкий вихід
    local female = (UnitSex and UnitSex("player")) == 3
    return (text:gsub("%$p%(([^:%)]*):([^%)]*)%)", function(m, f)
        return female and f or m
    end))
end

-- GameTooltip:SetOwner ПЕРЕПАРЕНТОВУЄ тултіп у фрейм-власник → БУДЬ-ЯКИЙ обхід
-- дітей відкритого вікна може дістатись рядків тултіпа і SetText-нути їх, тейнтячи
-- secret-шари layout (LayoutFrame:491, SharedTooltipTemplates:213). Тому КОЖНА
-- дитяча рекурсія зобов'язана питати цей заслін [[tooltip-reparent-walk-leak]].
function ns.IsTooltipFrame(f)
    if f == GameTooltip then return true end
    if type(f) == "table" and f.GetObjectType then
        local ok, t = pcall(f.GetObjectType, f)
        return ok and t == "GameTooltip"
    end
    return false
end

-- Повертає таблицю перекладу квесту за questID або nil.
function ns.GetQuestTranslation(questID)
    return questID and ns.quests and ns.quests[questID] or nil
end

-- Підрахунок записів у таблиці з нечисловими/розрідженими ключами.
local function CountKeys(t)
    local n = 0
    if t then
        for _ in pairs(t) do n = n + 1 end
    end
    return n
end

-- Слеш-команда /<назва аддона маленькими літерами> — статистика і керування.
local SLASH_KEY = ADDON_NAME:upper()
local slashCommand = "/" .. ADDON_NAME:lower()
_G["SLASH_" .. SLASH_KEY .. "1"] = slashCommand
_G["SLASH_" .. SLASH_KEY .. "2"] = "/wu"   -- короткі аліаси
_G["SLASH_" .. SLASH_KEY .. "3"] = "/wuk"
SlashCmdList[SLASH_KEY] = function(arg)
    arg = (arg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
    if arg == "stats" or arg == "" then
        local translated = CountKeys(ns.quests)
        if ns.PUBLIC then
            ns.Print(("Перекладено квестів: %d."):format(translated))
        else
            local captured = CountKeys(ns.GetCaptureDB().quests)
            ns.Print(("Перекладено квестів: %d. Зібрано для перекладу: %d."):format(translated, captured))
        end
    elseif arg == "options" or arg == "opt" then
        ns.OpenOptions()
    elseif arg == "toggle" or arg == "orig" or arg == "оригінал" then
        UALL_ToggleOriginalHotkey()
        ns.Print("Перемкнуто: " .. (ns.originalToggled and "ОРИГІНАЛ" or "переклад"))
    elseif arg == "welcome" or arg == "info" or arg == "про" then
        if ns.ShowWelcome then ns.ShowWelcome() else ns.Print("Вікно недоступне.") end
    elseif not ns.PUBLIC and arg == "debug" then
        ns.debug = not ns.debug
        ns.Print("Діагностика: " .. (ns.debug and "УВІМКНЕНО (наведи на спел і дивись чат)" or "вимкнено"))
        ns.Print(("Хук тултіпів: %s | спелів у базі: %d | spells.name: %s | enabled: %s"):format(
            tostring(ns.spellHooksRegistered), CountKeys(ns.spells),
            tostring(ns.IsEnabled("spells.name")), tostring(ns.IsEnabled("enabled"))))
        ns.Print(("Описів у базі: %d шаблонів / %d спелів | spells.description: %s%s"):format(
            CountKeys(ns.descTemplates), CountKeys(ns.spellDescIndex),
            tostring(ns.IsEnabled("spells.description")),
            CountKeys(ns.descTemplates) == 0
                and " | БАЗА ОПИСІВ НЕ ЗАВАНТАЖЕНА — потрібен ПОВНИЙ перезапуск гри (не /reload)!" or ""))
    elseif not ns.PUBLIC and (arg == "helptip" or arg == "підказка") then
        -- ТЕСТ HelpTip-хука: показати підказку з відомим рядком (є в uiMisc) →
        -- має зʼявитись українською. Перевірка без скидання туторіалів.
        if HelpTip and HelpTip.Show then
            local info = {
                text = "Click to view Mission Details.",
                buttonStyle = HelpTip.ButtonStyle and HelpTip.ButtonStyle.Close,
                targetPoint = HelpTip.Point and HelpTip.Point.TopEdgeCenter,
            }
            pcall(HelpTip.Show, HelpTip, UIParent, info, UIParent)
            ns.Print("Тестова підказка показана по центру екрана — має бути українською.")
        else
            ns.Print("HelpTip недоступний у цьому клієнті.")
        end
    elseif not ns.PUBLIC and arg == "dumpgs" then
        -- Дамп GlobalStrings (інтерфейс гри) у SavedVariables для перекладу.
        -- Проходимо _G, беремо глобали ВЕЛИКИМИ-літерами зі строковим значенням —
        -- це і є рядки інтерфейсу (ADD_FRIEND, SEND_MESSAGE, ...). Авторитетно для
        -- ЦІЄЇ версії гри. Екстракт: pipeline/extract_globalstrings.py.
        local db = ns.GetCaptureDB()
        db.globalStrings = {}
        local n = 0
        for k, v in pairs(_G) do
            if type(k) == "string" and type(v) == "string"
               and #v >= 1 and #v <= 400
               and k:match("^[A-Z][A-Z0-9_]*$") and not ns.HasCyrillic(v) then
                db.globalStrings[k] = v
                n = n + 1
            end
        end
        ns.Print(("GlobalStrings зібрано: %d. Зроби /reload, потім надішли файл UALL.lua."):format(n))
    elseif not ns.PUBLIC and arg:match("^find") then
        -- Діагностика: знайти ВСІ видимі написи з даним текстом і вивести їх
        -- фрейм-ланцюжок (щоб зрозуміти, який фрейм рендерить неперекладене ім'я).
        local needle = arg:gsub("^find%s*", "")
        if needle == "" then
            ns.Print("Вжиток: " .. slashCommand .. " find <текст> (напр. fidoficus)")
            return
        end
        local function chain(f)
            local s, d = {}, 0
            while f and d < 8 do
                s[#s + 1] = (f.GetName and f:GetName())
                    or (f.GetObjectType and f:GetObjectType()) or "?"
                f = f.GetParent and f:GetParent(); d = d + 1
            end
            return table.concat(s, " < ")
        end
        local count = 0
        local f = EnumerateFrames()
        while f do
            if f.GetRegions and (not f.IsShown or f:IsShown()) then
                for _, r in ipairs({ f:GetRegions() }) do
                    if r.IsObjectType and r:IsObjectType("FontString") then
                        local okT, raw = pcall(r.GetText, r)
                        local t = okT and ns.SafeText(raw) or nil -- nil для secret
                        if t and t:lower():find(needle, 1, true) then
                            count = count + 1
                            ns.Print(("[%d] «%s» @ %s"):format(count, t:sub(1, 45), chain(r:GetParent())))
                        end
                    end
                end
            end
            f = EnumerateFrames(f)
        end
        ns.Print(("Знайдено написів «%s»: %d"):format(needle, count))
    elseif not ns.PUBLIC and arg == "clear" then
        local db = ns.GetCaptureDB()
        db.quests = {}
        db.errors = nil; db.diag = nil; db.uierr = nil -- скинути журнали для чистої перевірки
        db.ejdebug = nil; db.ejdebugN = nil; db.ejtext = nil -- діагностика журналу
        db.ejloot = nil; db.ejlootN = nil; db.ejhdr = nil
        ns.Print("Зібрані тексти й журнали (errors/diag/ej) очищено.")
    else
        ns.Print(("Команди: %s stats — статистика, %s options — що перекладати, %s clear — очистити зібране."):format(
            slashCommand, slashCommand, slashCommand))
    end
end

-- ЗБИРАЧ ПОМИЛОК/TAINT — лише dev/тестерська. У публічній НЕ перехоплюємо
-- глобальний обробник помилок і НЕ слухаємо ADDON_ACTION_* (щоб не конфліктувати
-- з BugSack/іншими та не збирати нічого на диск).
if not ns.PUBLIC then
-- Кожна Lua-помилка з нашого аддона автоматично пишеться в діагностичний
-- лог (SavedVariables) — її можна прочитати з диска без скріншотів.
-- Чужі помилки передаємо стандартному обробнику без змін.
local origHandler = geterrorhandler()
local errorsLogged = 0
seterrorhandler(function(err)
    pcall(function()
        -- Логуємо БУДЬ-ЯКУ помилку (не лише з нашим іменем у тексті): крах
        -- у делві міг блеймити Blizzard-файл через taint, але причина — ми.
        -- АЛЕ позначаємо походження: якщо в тексті немає «WoWUkraine» — це
        -- найімовірніше ЧУЖИЙ аддон/Blizzard (наш taint гра підписує нашим
        -- іменем). Префікс «[?]» рятує від хибних діагнозів (як було з
        -- SetCooldown — він не наш, а ми гналися за привидом у шрифтах).
        if errorsLogged < 30 then
            errorsLogged = errorsLogged + 1
            local s = tostring(err)
            local tag = s:find("WoWUkraine", 1, true) and "" or "[?чужа] "
            ns.LogError(tag .. s:sub(1, 400))
        end
    end)
    if origHandler then return origHandler(err) end
end)

-- ДЕТЕКТОР TAINT: попап «вимкнути аддони / кнопки не тиснуться» = НЕ Lua-помилка
-- (seterrorhandler його НЕ ловить, тому лог порожній), а ADDON_ACTION_BLOCKED/
-- FORBIDDEN — аддон зачепив захищену/заборонену дію. Логуємо ІМ'Я аддона +
-- функцію: якщо винні МИ (WoWUkraine), у db.errors буде точна назва дії — і
-- видно, що саме виправляти, без здогадок.
local taintWatch = CreateFrame("Frame")
taintWatch:RegisterEvent("ADDON_ACTION_BLOCKED")
taintWatch:RegisterEvent("ADDON_ACTION_FORBIDDEN")
taintWatch:SetScript("OnEvent", function(_, event, addon, func)
    pcall(function()
        local mark = (tostring(addon):find("WoWUkraine", 1, true)) and "‼НАШ" or "[?чужа]"
        ns.LogError(("%s %s: %s -> %s"):format(mark, event,
            tostring(addon), tostring(func)))
    end)
end)
end -- if not ns.PUBLIC (збирач помилок/taint)

-- ЄДИНИЙ обробник Shift «показати оригінал» для ВСЬОГО аддона (раніше було 6
-- розрізнених у модулях). Логіка проста й element-agnostic: тимчасово прибрати
-- НАШУ кирилицю (ns.ToggleOriginal свопає весь реєстр підмін) і показати оригінал.
local modWatcher = CreateFrame("Frame")
modWatcher:RegisterEvent("MODIFIER_STATE_CHANGED")
modWatcher:SetScript("OnEvent", function(_, _, key)
    local chosen = ns.GetSetting and ns.GetSetting("originalKey")
    if not chosen or chosen == "NONE" then return end
    -- key приходить як LSHIFT/RSHIFT/LCTRL/... — звіряємо за входженням.
    if not key:find(chosen, 1, true) then return end
    -- ВІДКЛАДАЄМО на наступний кадр: Shift — модифікатор ЗАХИЩЕНИХ дій
    -- (лут/клік/каст). Синхронна UI-робота тут заражала їх ("Interface action
    -- failed because of an AddOn"). C_Timer.After(0) виносить роботу в чистий
    -- кадр, поза стеком захищеної дії — taint не поширюється.
    -- ОДИН своп усіх наших підмін (моби/предмети/квести/діалоги/…). Цього
    -- ДОСТАТНЬО: усі переклади зареєстровані в ns.replacements.
    -- НЕ перевикликаємо OnEnter власника тултіпа: для ЗАХИЩЕНИХ фреймів (кнопки
    -- дій) виклик їхнього OnEnter з нашого коду ЗАРАЖАЄ їх — звідси «кнопки не
    -- тиснуться» і попап «вимкнути аддони». Своп тексту цього не робить.
    C_Timer.After(0, ns.ToggleOriginal)
end)

-- LoadOnDemand: великі необов'язкові бази вантажимо ЛИШЕ коли потрібні (економія RAM).
-- Предмети (UALL_Items) — лише коли увімкнено будь-яку опцію items.*.
-- Перемикання опції ON застосується на наступний /reload (тоді й вантажиться).
function ns.LoadItemsIfNeeded()
    if not (C_AddOns and C_AddOns.LoadAddOn) then return end
    if ns.items and next(ns.items) then return end -- уже завантажено
    if ns.IsEnabled("items.name") or ns.IsEnabled("items.flavor")
       or ns.IsEnabled("items.effects") or ns.IsEnabled("items.statsSlots") then
        pcall(C_AddOns.LoadAddOn, "UALL_Items")
    end
end

-- Імена NPC (UALL_NpcNames) — лише коли увімкнено world.npcNames (типово ВИМК,
-- тож для більшості гравців ця база (~1.4M) взагалі не вантажиться).
function ns.LoadNpcNamesIfNeeded()
    if not (C_AddOns and C_AddOns.LoadAddOn) then return end
    if ns.npcNames and next(ns.npcNames) then return end
    if ns.IsEnabled("world.npcNames") then
        pcall(C_AddOns.LoadAddOn, "UALL_NpcNames")
        -- Якщо опція ввімкнена, а база так і не завантажилась — під-аддон вимкнено
        -- у списку або щойно доданий (новий аддон реєструється лише після ПОВНОГО
        -- перезапуску гри). Підкажемо гравцеві ОДИН раз, щоб не гадав, чому імена англ.
        if not (ns.npcNames and next(ns.npcNames)) and not ns._npcNamesWarned then
            ns._npcNamesWarned = true
            ns.Print("|cffff6060Імена NPC увімкнено, але базу «WoWUkraine (Імена NPC)» "
                .. "не завантажено. Увімкни її у списку аддонів і ПЕРЕЗАЙДИ в гру "
                .. "(новий аддон реєструється лише після повного перезапуску, не /reload).|r")
        end
    end
end

local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:SetScript("OnEvent", function(self, event, name)
    if name ~= ADDON_NAME then return end
    ns.GetCaptureDB()
    ns.LoadItemsIfNeeded()    -- предмети-LoD за опцією
    ns.LoadNpcNamesIfNeeded() -- імена NPC-LoD за опцією (Journal — при відкритті EJ)
    -- (Прибрано вітальний рядок у чат при кожному вході — зайвий шум.
    -- Статистика доступна за «/uall stats», версія — у вітальному вікні.)
    ns.Diag(("завантаження: v%s | квестів у базі %d | enabled=%s"):format(
        ns.version, CountKeys(ns.quests), tostring(ns.IsEnabled("enabled"))))
    self:UnregisterEvent("ADDON_LOADED")
end)
