-- ============================================================================
-- pom.lua
-- Plate-o-Matic main entry
-- ============================================================================

local ADDON_NAME, ns = ...

-- Always unify namespace (prevents split-table bugs across files)
ns = _G[ADDON_NAME] or ns
_G[ADDON_NAME] = ns

-- Make the addon table globally reachable (other modules / RobUI integration expects this)
_G.PlateOMatic = _G.PlateOMatic or ns

-- =========================================================
-- Plate-o-Matic (pom.lua)
-- Main entry file
-- =========================================================

local DB_VERSION = 4
local CURRENT_DB_VERSION = 7

-- =========================================================
-- Skins
-- =========================================================
ns.SKINS = {
    { name = "Base",               texture = "Interface\\AddOns\\pom\\skins\\base.tga", ignoreColor = false },
    { name = "Yellow",             texture = "Interface\\AddOns\\pom\\skins\\yellow.tga", ignoreColor = true },
    { name = "Cool Blue",          texture = "Interface\\AddOns\\pom\\skins\\coolblue.tga", ignoreColor = true },
    { name = "Gray",               texture = "Interface\\AddOns\\pom\\skins\\gray.tga", ignoreColor = true },
    { name = "Statusbar 1",        texture = "Interface\\AddOns\\pom\\skins\\statusbar1.tga", ignoreColor = true },
    { name = "MiniBoss",           texture = "Interface\\AddOns\\pom\\skins\\miniboss1.tga", ignoreColor = true },

    { name = "Status: Hostile",    texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_hostile_256x32.tga", ignoreColor = true },
    { name = "Status: Friendly",   texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_friendly_256x32.tga", ignoreColor = true },
    { name = "Status: Neutral",    texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_neutral_256x32.tga", ignoreColor = true },

    { name = "Class: Warrior",      texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_warrior_256x32.tga",      ignoreColor = true },
    { name = "Class: Paladin",      texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_paladin_256x32.tga",      ignoreColor = true },
    { name = "Class: Hunter",       texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_hunter_256x32.tga",       ignoreColor = true },
    { name = "Class: Rogue",        texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_rogue_256x32.tga",        ignoreColor = true },
    { name = "Class: Priest",       texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_priest_256x32.tga",       ignoreColor = true },
    { name = "Class: Death Knight", texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_deathknight_256x32.tga",  ignoreColor = true },
    { name = "Class: Shaman",       texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_shaman_256x32.tga",       ignoreColor = true },
    { name = "Class: Mage",         texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_mage_256x32.tga",         ignoreColor = true },
    { name = "Class: Warlock",      texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_warlock_256x32.tga",      ignoreColor = true },
    { name = "Class: Monk",         texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_monk_256x32.tga",         ignoreColor = true },
    { name = "Class: Druid",        texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_druid_256x32.tga",        ignoreColor = true },
    { name = "Class: Demon Hunter", texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_demonhunter_256x32.tga",  ignoreColor = true },
    { name = "Class: Evoker",       texture = "Interface\\AddOns\\pom\\skins\\robui_statusbar_evoker_256x32.tga",       ignoreColor = true },

    { name = "Flat (Plater)",      texture = "Interface\\Buttons\\WHITE8x8",            ignoreColor = false },
    { name = "Smooth",             texture = "Interface\\TargetingFrame\\UI-StatusBar", ignoreColor = false },
    { name = "Blizzard Raid",      texture = "Interface\\RaidFrame\\Raid-Bar-Hp-Fill",  ignoreColor = false },
}

-- =========================================================
-- Key compatibility
-- =========================================================
local LEGACY_TO_MODERN = {
    highlevel = "boss",
    caster    = "eliteCaster",
    melee     = "eliteMelee",
}

local MODERN_TO_LEGACY = {
    boss        = "highlevel",
    eliteCaster = "caster",
    eliteMelee  = "melee",
}

local function CopyScalarAlias(tbl, oldKey, newKey)
    if type(tbl) ~= "table" then return end

    if tbl[newKey] == nil and tbl[oldKey] ~= nil then
        tbl[newKey] = tbl[oldKey]
    elseif tbl[oldKey] == nil and tbl[newKey] ~= nil then
        tbl[oldKey] = tbl[newKey]
    end
end

local function CopyColorAlias(tbl, oldKey, newKey)
    if type(tbl) ~= "table" then return end

    local oldV = tbl[oldKey]
    local newV = tbl[newKey]

    if type(newV) ~= "table" and type(oldV) == "table" then
        tbl[newKey] = CopyTable(oldV)
    elseif type(oldV) ~= "table" and type(newV) == "table" then
        tbl[oldKey] = CopyTable(newV)
    end
end

local function CopyBoolAlias(tbl, oldKey, newKey)
    if type(tbl) ~= "table" then return end

    if tbl[newKey] == nil and tbl[oldKey] ~= nil then
        tbl[newKey] = tbl[oldKey] and true or false
    elseif tbl[oldKey] == nil and tbl[newKey] ~= nil then
        tbl[oldKey] = tbl[newKey] and true or false
    end
end

local function SyncLegacyModernKeys(db)
    if type(db) ~= "table" then return end

    db.skins      = type(db.skins) == "table" and db.skins or {}
    db.heights    = type(db.heights) == "table" and db.heights or {}
    db.baseColors = type(db.baseColors) == "table" and db.baseColors or {}

    for oldKey, newKey in pairs(LEGACY_TO_MODERN) do
        CopyScalarAlias(db.skins, oldKey, newKey)
        CopyScalarAlias(db.heights, oldKey, newKey)
        CopyColorAlias(db.baseColors, oldKey, newKey)
    end

    -- Keep old bool alias alive, but tank-specific toggle is no longer a surfaced UI option.
    CopyBoolAlias(db, "alwaysShowRoleBaseColors", "useRoleBaseColors")
    CopyBoolAlias(db, "alwaysShowRoleBaseColorsForTanks", "useRoleBaseColorsForTanks")
end

-- =========================================================
-- Defaults (SavedVariables)
-- =========================================================
ns.DEFAULTS = {
    dbVersion = DB_VERSION,

    skins = {
        normal      = 23,
        plusOne     = 23,
        boss        = 23,
        eliteCaster = 23,
        eliteMelee  = 23,

        -- Legacy compatibility
        highlevel   = 23,
        caster      = 23,
        melee       = 23,
    },

    heights = {
        normal      = 16,
        plusOne     = 18,
        boss        = 20,
        eliteCaster = 18,
        eliteMelee  = 18,

        -- Legacy compatibility
        highlevel   = 20,
        caster      = 18,
        melee       = 18,
    },

    fontPath    = "Fonts\\FRIZQT__.TTF",
    fontSize    = 10,
    fontOutline = "OUTLINE",

    hideClassIcon    = true,
    alwaysTargetGlow = true,
    targetScale      = 1.15,
    showTargetArrow  = true,
    arrowPointingOut = false,

    showQuestIcon  = true,
    showQuestProg  = true,

    useBaseColorsSolo = false,

    -- DPS/Healer fallback is enabled by default.
    -- Tanks are controlled by threat logic, not by config UI.
    useRoleBaseColors = true,
    useRoleBaseColorsForTanks = false,

    -- Legacy aliases kept alive
    alwaysShowRoleBaseColors = true,
    alwaysShowRoleBaseColorsForTanks = false,

    nameAnchor  = "LEFT",
    globalScale = 1.0,

    buffSize   = 20,
    debuffSize = 20,

    targetBorderColor = { r=1.0, g=0.82, b=0.0, a=1.0 },
}

local BASECOLORS_DEFAULTS = {
    normal      = { r=0.8, g=0.8, b=0.8 },
    plusOne     = { r=1.0, g=0.5, b=0.0 },
    boss        = { r=0.8, g=0.0, b=1.0 },
    eliteCaster = { r=1.0, g=0.4, b=0.8 },
    eliteMelee  = { r=0.0, g=0.6, b=1.0 },

    -- Legacy compatibility
    highlevel   = { r=0.8, g=0.0, b=1.0 },
    caster      = { r=1.0, g=0.4, b=0.8 },
    melee       = { r=0.0, g=0.6, b=1.0 },

    neutral     = { r=0.9, g=0.8, b=0.3 },

    -- Role colors used by the restored fallback
    healer      = { r=0.2, g=1.0, b=0.2 },
    dps         = { r=1.0, g=0.2, b=0.2 },
    gray        = { r=0.5, g=0.5, b=0.5 },
}

local THREAT_DEFAULTS = {
    enabled = true,

    colorBarByThreat = true,
    flashAggro       = true,
    flashDuration    = 0,

    showThreatPerc   = true,
    pctTextMin       = 1,

    gradientStart    = 60,
    gradientEnd      = 100,

    tankModeEnabled  = true,
    checkOffTank     = true,

    groupTintEnabled = false,

    colors = {
        high   = { r=1.0, g=0.0, b=0.0 },
        medium = { r=1.0, g=1.0, b=0.0 },
        low    = { r=0.5, g=0.5, b=0.5 },

        safe   = { r=0.2, g=0.8, b=0.2 },
        ot     = { r=0.6, g=0.2, b=1.0 },

        group  = { r=0.2, g=0.7, b=1.0, a=1.0 },
    },
}

-- =========================================================
-- Locals
-- =========================================================
local U      = ns.util
local Mixins = ns.mixins
local Config = ns.config

-- =========================================================
-- Mouseover highlight
-- =========================================================
local BORDER_THICKNESS = 2
local HIGHLIGHT_ALPHA  = 0.40

local function GetMouseoverAnchor(frame)
    if frame and frame.POM_Mixin and frame.POM_Mixin.healthBar then
        return frame.POM_Mixin.healthBar
    end
    if frame and frame.healthBar then return frame.healthBar end
    if frame and frame.HealthBarsContainer and frame.HealthBarsContainer.healthBar then
        return frame.HealthBarsContainer.healthBar
    end
    return frame
end

local function CreateHighlightEdges(parent)
    local t = {}
    for _, pos in ipairs({"top", "bottom", "left", "right"}) do
        local tex = parent:CreateTexture(nil, "OVERLAY")
        tex:SetColorTexture(1, 1, 1, 1)
        tex:SetBlendMode("ADD")
        tex:Hide()
        t[pos] = tex
    end
    return t
end

local function LayoutEdgesTight(e, anchor, thickness)
    e.top:ClearAllPoints()
    e.top:SetPoint("TOPLEFT", anchor, "TOPLEFT", 0, 0)
    e.top:SetPoint("TOPRIGHT", anchor, "TOPRIGHT", 0, 0)
    e.top:SetHeight(thickness)

    e.bottom:ClearAllPoints()
    e.bottom:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, 0)
    e.bottom:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", 0, 0)
    e.bottom:SetHeight(thickness)

    e.left:ClearAllPoints()
    e.left:SetPoint("TOPLEFT", anchor, "TOPLEFT", 0, -thickness)
    e.left:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, thickness)
    e.left:SetWidth(thickness)

    e.right:ClearAllPoints()
    e.right:SetPoint("TOPRIGHT", anchor, "TOPRIGHT", 0, -thickness)
    e.right:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", 0, thickness)
    e.right:SetWidth(thickness)
end

local function SetMouseoverFX(frame, on)
    if not frame or not frame.__pom_mo_fx then return end
    local border = frame.__pom_mo_border
    local overlay = frame.__pom_mo_overlay
    if not border or not overlay then return end

    local isTarget = false
    if frame.unit and UnitExists("target") then
        isTarget = U.PlainIsUnit(frame.unit, "target")
    end

    if on and not isTarget then
        local c = PlateOMaticDB and PlateOMaticDB.targetBorderColor or nil
        local r = (c and c.r) or 1.0
        local g = (c and c.g) or 0.82
        local b = (c and c.b) or 0.0
        local a = 1.0

        border.top:SetColorTexture(r, g, b, a)
        border.bottom:SetColorTexture(r, g, b, a)
        border.left:SetColorTexture(r, g, b, a)
        border.right:SetColorTexture(r, g, b, a)

        overlay:SetColorTexture(r, g, b, HIGHLIGHT_ALPHA)

        border.top:Show()
        border.bottom:Show()
        border.left:Show()
        border.right:Show()
        overlay:Show()
    else
        border.top:Hide()
        border.bottom:Hide()
        border.left:Hide()
        border.right:Hide()
        overlay:Hide()
    end
end

local function EnsureMouseoverFX(frame)
    if not frame or frame:IsForbidden() then return end
    if frame.__pom_mo_fx then return end
    frame.__pom_mo_fx = true

    local anchor = GetMouseoverAnchor(frame)
    if not anchor or anchor:IsForbidden() then return end

    local border = CreateHighlightEdges(anchor)
    LayoutEdgesTight(border, anchor, BORDER_THICKNESS)

    local overlay = anchor:CreateTexture(nil, "OVERLAY")
    overlay:SetColorTexture(1, 1, 1, 1)
    overlay:SetBlendMode("ADD")
    overlay:SetAllPoints(anchor)
    overlay:Hide()

    frame.__pom_mo_anchor = anchor
    frame.__pom_mo_border = border
    frame.__pom_mo_overlay = overlay
end

-- =========================================================
-- Aura tooltips
-- =========================================================
local function POM_SetAuraTooltipScripts(auraIcon, unit)
    if not auraIcon or auraIcon:IsForbidden() then return end
    if auraIcon.__pomAuraTooltip then return end
    auraIcon.__pomAuraTooltip = true

    auraIcon:EnableMouse(true)

    auraIcon:SetScript("OnEnter", function(self)
        if not unit or not UnitExists(unit) then return end

        local auraInstanceID =
            self.auraInstanceID
            or self._auraInstanceID
            or (self.auraInfo and self.auraInfo.auraInstanceID)
            or (self.auraData and self.auraData.auraInstanceID)

        if not auraInstanceID then return end

        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")

        if GameTooltip.SetUnitAuraByAuraInstanceID then
            GameTooltip:SetUnitAuraByAuraInstanceID(unit, auraInstanceID)
            GameTooltip:Show()
        else
            GameTooltip:Hide()
        end
    end)

    auraIcon:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
end

local function POM_AttachAuraTooltips(frame)
    if not frame or frame:IsForbidden() or not frame.unit then return end

    if frame.BuffFrame and not frame.BuffFrame:IsForbidden() then
        if type(frame.BuffFrame.auraFrames) == "table" then
            for _, icon in ipairs(frame.BuffFrame.auraFrames) do
                POM_SetAuraTooltipScripts(icon, frame.unit)
            end
        else
            local n = frame.BuffFrame:GetNumChildren()
            for i = 1, n do
                local child = select(i, frame.BuffFrame:GetChildren())
                POM_SetAuraTooltipScripts(child, frame.unit)
            end
        end
    end

    if frame.DebuffFrame and not frame.DebuffFrame:IsForbidden() then
        if type(frame.DebuffFrame.auraFrames) == "table" then
            for _, icon in ipairs(frame.DebuffFrame.auraFrames) do
                POM_SetAuraTooltipScripts(icon, frame.unit)
            end
        else
            local n = frame.DebuffFrame:GetNumChildren()
            for i = 1, n do
                local child = select(i, frame.DebuffFrame:GetChildren())
                POM_SetAuraTooltipScripts(child, frame.unit)
            end
        end
    end
end

-- =========================================================
-- Mouseover refresh
-- =========================================================
local function UpdateAllMouseoverHighlights()
    local plates = C_NamePlate.GetNamePlates and C_NamePlate.GetNamePlates() or nil
    if not plates then return end

    local hasMO = UnitExists("mouseover")

    for i = 1, #plates do
        local plate = plates[i]
        local uf = plate and plate.UnitFrame
        if uf and not uf:IsForbidden() and uf.unit and U.IsNameplateUnit(uf.unit) then
            EnsureMouseoverFX(uf)

            local on = false
            if hasMO then
                on = U.PlainIsUnit("mouseover", uf.unit)
            end

            SetMouseoverFX(uf, on)
        end
    end
end

local POM_MouseTracker = CreateFrame("Frame")
POM_MouseTracker:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
POM_MouseTracker:RegisterEvent("PLAYER_TARGET_CHANGED")
POM_MouseTracker:SetScript("OnEvent", function()
    UpdateAllMouseoverHighlights()
end)

-- =========================================================
-- Deep merge
-- =========================================================
local function DeepMergeMissing(dst, src)
    if not dst or not src then return end
    for k, v in pairs(src) do
        if dst[k] == nil then
            dst[k] = (type(v) == "table") and CopyTable(v) or v
        else
            if type(v) == "table" and type(dst[k]) == "table" then
                DeepMergeMissing(dst[k], v)
            end
        end
    end
end

-- =========================================================
-- Plate helpers
-- =========================================================
local function EnsureMixin(frame)
    if not frame or frame:IsForbidden() then return nil end
    if not U.IsNameplateUnit(frame.unit) then return nil end

    if not frame.POM_Mixin then
        frame.POM_Mixin = Mixins:New(frame, ns.SKINS)
    end

    return frame.POM_Mixin
end

local function RefreshMouseoverForFrame(frame)
    EnsureMouseoverFX(frame)

    local isMouseOver = false
    if frame and frame.unit and UnitExists("mouseover") then
        isMouseOver = U.PlainIsUnit("mouseover", frame.unit)
    end

    SetMouseoverFX(frame, isMouseOver)
end

-- =========================================================
-- Apply styling to one plate
-- =========================================================
local function Apply(frame)
    if not frame or frame:IsForbidden() then return end
    if not U.IsNameplateUnit(frame.unit) then return end

    local mixin = EnsureMixin(frame)
    if not mixin then return end

    U.KillBlizzard(frame, PlateOMaticDB)

    mixin:UpdateVisuals()
    mixin:UpdateQuest()
    mixin:UpdateName()
    mixin:UpdateHealth()

    if mixin.UpdateHeight then
        mixin:UpdateHeight()
    end

    if U.IsSafeUnit(frame.unit) then
        mixin:UpdateState()
    end

    if ns.pvp and ns.pvp.Apply then
        ns.pvp:Apply(frame)
    end

    if ns.raidmarkers then
        ns.raidmarkers:Apply(frame)
    end

    POM_AttachAuraTooltips(frame)
    RefreshMouseoverForFrame(frame)
end

-- =========================================================
-- Light refresh
-- =========================================================
local function ApplyLight(frame)
    if not frame or frame:IsForbidden() then return end
    if not U.IsNameplateUnit(frame.unit) then return end

    local mixin = EnsureMixin(frame)
    if not mixin then return end

    U.KillBlizzard(frame, PlateOMaticDB)

    mixin:UpdateVisuals()
    mixin:UpdateName()
    mixin:UpdateHealth()

    -- Important: role/threat/gray/base-color state must still refresh
    -- on light updates or colors can get stuck / disappear.
    if U.IsSafeUnit(frame.unit) then
        mixin:UpdateState()
    end

    POM_AttachAuraTooltips(frame)
    RefreshMouseoverForFrame(frame)
end

-- =========================================================
-- DB MIGRATIONS
-- =========================================================
local MIGRATIONS = {}

MIGRATIONS[1] = function(db) end

MIGRATIONS[2] = function(db)
    db.baseColors = db.baseColors or {}
    db.threat     = db.threat or {}
end

MIGRATIONS[3] = function(db)
    db.baseColors = db.baseColors or {}
    db.threat     = db.threat or {}

    DeepMergeMissing(db.baseColors, BASECOLORS_DEFAULTS)
    DeepMergeMissing(db.threat, THREAT_DEFAULTS)

    if db.colors and type(db.colors) == "table" then
        local old = db.colors

        local mapBase = { "normal", "plusOne", "highlevel", "caster", "melee", "neutral" }
        for _, k in ipairs(mapBase) do
            if type(old[k]) == "table" and type(db.baseColors[k]) == "table" then
                local c = old[k]
                if c.r ~= nil then db.baseColors[k].r = c.r end
                if c.g ~= nil then db.baseColors[k].g = c.g end
                if c.b ~= nil then db.baseColors[k].b = c.b end
                if c.a ~= nil then db.baseColors[k].a = c.a end
            end
        end

        local tc = db.threat.colors or {}
        db.threat.colors = tc

        local function pull(oldKey, newKey)
            if type(old[oldKey]) == "table" and type(tc[newKey]) == "table" then
                if old[oldKey].r ~= nil then tc[newKey].r = old[oldKey].r end
                if old[oldKey].g ~= nil then tc[newKey].g = old[oldKey].g end
                if old[oldKey].b ~= nil then tc[newKey].b = old[oldKey].b end
                if old[oldKey].a ~= nil then tc[newKey].a = old[oldKey].a end
            end
        end

        pull("threatHigh",     "high")
        pull("threatMedium",   "medium")
        pull("threatLow",      "low")
        pull("threatTankSafe", "safe")
        pull("threatOffTank",  "ot")
        pull("groupAggro",     "group")
    end

    local t = db.threat
    if db.flashAggro ~= nil then t.flashAggro = db.flashAggro and true or false end
    if db.flashDuration ~= nil then t.flashDuration = tonumber(db.flashDuration) or t.flashDuration end
    if db.showThreatPerc ~= nil then t.showThreatPerc = db.showThreatPerc and true or false end
    if db.colorBarByThreat ~= nil then t.colorBarByThreat = db.colorBarByThreat and true or false end
    if db.tankModeEnabled ~= nil then t.tankModeEnabled = db.tankModeEnabled and true or false end
    if db.checkOffTank ~= nil then t.checkOffTank = db.checkOffTank and true or false end
    if db.groupAggroColorEnabled ~= nil then t.groupTintEnabled = db.groupAggroColorEnabled and true or false end
end

MIGRATIONS[4] = function(db)
    if db.__migratedFromPerChar then return end
    if not PomcaDB or type(PomcaDB) ~= "table" then return end

    local looksEmpty = true
    for k, _ in pairs(db) do
        if k ~= "__version" and k ~= "dbVersion" and k ~= "__migratedFromPerChar" then
            looksEmpty = false
            break
        end
    end

    if not looksEmpty then
        db.__migratedFromPerChar = true
        return
    end

    DeepMergeMissing(db, PomcaDB)
    db.baseColors = db.baseColors or {}
    db.threat     = db.threat or {}

    db.__migratedFromPerChar = true
end

-- Rename legacy bucket keys to modern names, but keep aliases
MIGRATIONS[5] = function(db)
    db.skins      = type(db.skins) == "table" and db.skins or {}
    db.heights    = type(db.heights) == "table" and db.heights or {}
    db.baseColors = type(db.baseColors) == "table" and db.baseColors or {}

    SyncLegacyModernKeys(db)
end

-- Restore role-based color fallback settings + aliases
MIGRATIONS[6] = function(db)
    SyncLegacyModernKeys(db)

    if db.useRoleBaseColors == nil and db.alwaysShowRoleBaseColors == nil then
        db.useRoleBaseColors = true
        db.alwaysShowRoleBaseColors = true
    end

    if db.useRoleBaseColorsForTanks == nil and db.alwaysShowRoleBaseColorsForTanks == nil then
        db.useRoleBaseColorsForTanks = false
        db.alwaysShowRoleBaseColorsForTanks = false
    end

    db.baseColors = db.baseColors or {}
    DeepMergeMissing(db.baseColors, BASECOLORS_DEFAULTS)
end

-- =========================================================
-- DB init + Migrations + Defaults
-- =========================================================
local function InitDB()
    PlateOMaticDB = PlateOMaticDB or {}
    _G.PlateOMaticDB = PlateOMaticDB
    local db = PlateOMaticDB

    local v = tonumber(db.__version)
    if not v then
        if type(db.threat) == "table" or type(db.baseColors) == "table" then
            v = 4
        else
            v = 1
        end
        db.__version = v
    end

    while v < CURRENT_DB_VERSION do
        local step = MIGRATIONS[v]
        if step then
            pcall(step, db)
        end
        v = v + 1
        db.__version = v
    end

    db.baseColors = db.baseColors or {}
    db.threat     = db.threat or {}
    db.pvp        = db.pvp or {}
    db.skins      = db.skins or {}
    db.heights    = db.heights or {}

    -- Removed feature (target-of-NPC): Blizzard makes compound unit tokens
    -- secret on addon-restricted maps, so drop any leftover saved settings.
    db.targetOf   = nil

    DeepMergeMissing(db, ns.DEFAULTS)
    DeepMergeMissing(db.baseColors, BASECOLORS_DEFAULTS)
    DeepMergeMissing(db.threat, THREAT_DEFAULTS)

    -- Keep both modern and legacy keys alive during transition
    SyncLegacyModernKeys(db)

    if ns.pvp_db and ns.pvp_db.DEFAULTS then
        DeepMergeMissing(db.pvp, ns.pvp_db.DEFAULTS)
    end
end

-- =========================================================
-- Menu
-- =========================================================
local function CreateMenu()
    if ns._menu then
        ns._menu:SetShown(not ns._menu:IsShown())
        return
    end

    local f = CreateFrame("Frame", "POM_MenuFrame", UIParent, "BasicFrameTemplateWithInset")
    f:SetSize(1020, 760)
    f:SetPoint("CENTER")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)

    if f.TitleText then
        f.TitleText:SetText("Plate-o-Matic")
    end

    local function applyAll()
        U.RefreshAllPlates(Apply)
        if ns.preview and ns.preview.frame and ns.preview.frame.Refresh then
            ns.preview.frame:Refresh()
        end
        UpdateAllMouseoverHighlights()
    end

    Config:Build(f, ns.SKINS, applyAll)

    ns._menu = f
    f:Show()
end

-- =========================================================
-- Hooks - WoW 12.0.5 / Midnight safe
-- =========================================================
local function POM_TryHookGlobal(funcName, callback)
    if type(funcName) ~= "string" or type(callback) ~= "function" then
        return false
    end

    if type(_G[funcName]) ~= "function" then
        return false
    end

    local ok, err = pcall(hooksecurefunc, funcName, callback)
    if not ok then
        print("|cFFFF0000Plate-o-Matic:|r Failed to hook " .. funcName .. ": " .. tostring(err))
        return false
    end

    return true
end

local function POM_UpdateAuraFrames(frame)
    if not frame or frame:IsForbidden() then return end
    if not frame.unit or not U.IsNameplateUnit(frame.unit) then return end

    local db = PlateOMaticDB or ns.DEFAULTS or {}

    local buffSize = tonumber(db.buffSize) or 20
    local debuffSize = tonumber(db.debuffSize) or 20

    local bScale = buffSize / 18
    local dScale = debuffSize / 18

    if frame.BuffFrame and not frame.BuffFrame:IsForbidden() then
        frame.BuffFrame:SetScale(bScale)
    end

    if frame.DebuffFrame and not frame.DebuffFrame:IsForbidden() then
        frame.DebuffFrame:SetScale(dScale)
    end

    POM_AttachAuraTooltips(frame)
end

POM_TryHookGlobal("CompactUnitFrame_UpdateAll", Apply)
POM_TryHookGlobal("CompactUnitFrame_UpdateHealth", ApplyLight)
POM_TryHookGlobal("CompactUnitFrame_UpdateName", ApplyLight)
POM_TryHookGlobal("CompactUnitFrame_UpdateStatusText", ApplyLight)

POM_TryHookGlobal("CompactUnitFrame_UpdateHealthBorder", function(frame)
    if frame and not frame:IsForbidden() and U.IsNameplateUnit(frame.unit) then
        U.KillBlizzard(frame, PlateOMaticDB)
    end
end)

POM_TryHookGlobal("CompactUnitFrame_UpdateSelectionHighlight", function(frame)
    if frame and not frame:IsForbidden() and U.IsNameplateUnit(frame.unit) then
        if frame.selectionHighlight then
            U.LockAlpha(frame.selectionHighlight)
        end
    end
end)

-- 12.0.5 safe:
-- This existed in older builds, but may be removed/renamed after Blizzard compact-frame cleanup.
-- If missing, POM still works because Apply/ApplyLight already refresh aura tooltip attachment.
POM_TryHookGlobal("CompactUnitFrame_UpdateAuras", POM_UpdateAuraFrames)

-- =========================================================
-- Events
-- =========================================================
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_LOGIN")
ev:RegisterEvent("NAME_PLATE_UNIT_ADDED")
ev:RegisterEvent("GROUP_ROSTER_UPDATE")
ev:RegisterEvent("PLAYER_ROLES_ASSIGNED")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:RegisterEvent("UNIT_FACTION")
ev:RegisterEvent("PLAYER_FLAGS_CHANGED")
ev:RegisterEvent("PLAYER_TARGET_CHANGED")

ev:SetScript("OnEvent", function(_, event, unit)
    if event == "PLAYER_LOGIN" then
        InitDB()

        if ns and ns.UpdateTankCache then
            ns:UpdateTankCache()
        end

        print("|cFF00FF00Plate-o-Matic:|r Loaded.")
        UpdateAllMouseoverHighlights()

    elseif event == "NAME_PLATE_UNIT_ADDED" then
        local plate = C_NamePlate.GetNamePlateForUnit(unit)
        if plate and plate.UnitFrame then
            Apply(plate.UnitFrame)
        end
        UpdateAllMouseoverHighlights()

    elseif event == "PLAYER_TARGET_CHANGED" then
        U.RefreshAllPlates(Apply)
        UpdateAllMouseoverHighlights()

    else
        if ns and ns.UpdateTankCache then
            ns:UpdateTankCache()
        end

        U.RefreshAllPlates(Apply)
        UpdateAllMouseoverHighlights()
    end
end)

-- =========================================================
-- Slash command
-- =========================================================
SLASH_PLATEOMATIC1 = "/pom"
SLASH_PLATEOMATIC2 = "/pompvp"
SLASH_POMSTACKTEST1 = "/pomst"

SlashCmdList["PLATEOMATIC"] = function(msg)
    msg = (msg or ""):lower()

    if msg == "reset" then
        PlateOMaticDB = nil
        _G.PlateOMaticDB = nil
        C_UI.Reload()
        return
    end

    if msg == "pvp" or msg == "pvpsettings" then
        if ns.pvp_ui and ns.pvp_ui.Toggle then
            ns.pvp_ui:Toggle()
        else
            print("|cFFFF0000Plate-o-Matic:|r PvP UI not loaded (pvp_ui.lua missing in TOC?)")
        end
        return
    end

    if msg == "st" or msg == "stack" or msg == "stacking" then
        if ns.stacktest and ns.stacktest.Toggle then
            ns.stacktest:Toggle()
        else
            print("|cFFFF0000Plate-o-Matic:|r Stacking test UI not loaded.")
        end
        return
    end

    CreateMenu()
end

SlashCmdList["POMSTACKTEST"] = function()
    if ns.stacktest and ns.stacktest.Toggle then
        ns.stacktest:Toggle()
    else
        print("|cFFFF0000Plate-o-Matic:|r Stacking test UI not loaded.")
    end
end

SlashCmdList["PLATEOMATIC2"] = function()
    if ns.pvp_ui and ns.pvp_ui.Toggle then
        ns.pvp_ui:Toggle()
    end
end