--[[
MultiBotSpecUI.lua  – Extension module for the MultiBot addon by TheWarlock aka Wishmaster117
Adds a “Set talents” button on each bot frame and builds a dynamic dropdown list
with all spec builds returned by the command  /w <bot> "talents spec list".
]]--

local function getSpecIcons()
  local dataProvider = MultiBot.GetIconosEntries
  local icons = type(dataProvider) == "function" and dataProvider() or (MultiBot.data and MultiBot.data.iconos)
  if type(icons) ~= "table" then
    return {}
  end
  return icons
end

local icons = getSpecIcons()

local defaultIcon = "Interface\\Icons\\Achievement_Reputation_08"

local function specTip(key, fallback)
  local multiBot = _G["MultiBot"]
  if multiBot and multiBot.L then
    return multiBot.L("tips.spec." .. key, fallback)
  end
  return fallback or ("tips.spec." .. key)
end

-- canonicalisation des petits préfixes
local specCanonical = {
  -- Paladin
  holy        = "Holy",
  ret         = "Ret",
  -- Druid
  balance     = "Balance",
  bear        = "Bear",
  cat         = "Cat",
  resto       = "Restoration",
  -- Shaman
  ele         = "Ele",
  enh         = "Enh",
  -- Warrior
  arms        = "Arms",
  fury        = "Fury",
  prot        = "Protection",
  -- Hunter
  bm          = "BM",
  mm          = "Marksmanship",
  surv        = "Survival",
  -- Priest
  shadow      = "Shadow",
  disc        = "Discipline",
  -- Mage
  arcane      = "Arcane",
  fire        = "Fire",
  frostfire   = "Frostfire",
  frost       = "Frost",
  -- Warlock
  affli       = "Affliction",
  demo        = "Demonology",
  destro      = "Destruction",
  -- DK
  blood       = "Blood",
  unholy      = "Unholy",
  double      = "Double",
  -- Rogue
  as          = "As",
  combat      = "Combat",
  subtlety    = "Subtlety",
}


local specIconMap = {
  DeathKnight = {
    Blood  = {
      pve = { icon = icons[5427], tip =  specTip("dkbloodpve") },
      pvp = { icon = icons[5427], tip =  specTip("dkbloodpvp") },
    },
    Frost  = {
      pve = { icon = icons[5438], tip = specTip("dkbfrostpve") },
      pvp = { icon = icons[5438], tip = specTip("dkbfrostpvp") },
    },
    Unholy = {
      pve = { icon = icons[5454], tip = specTip("dkunhopve") },
      pvp = { icon = icons[5454], tip = specTip("dkunhopvp")  },
    },
    Double = {
      pve = { icon = icons[5946], tip = specTip("dkdoublepve") },
    },
  },

  Druid = {
    Balance     = {
      pve = { icon = icons[73], tip = specTip("druidbalpve") },
      pvp = { icon = icons[73], tip = specTip("druidbalpvp") },
    },
    Cat        = {
      pve = { icon = icons[28] , tip = specTip("druidcatpve") },
      pvp = { icon = icons[28], tip = specTip("druidcatpvp") },
    },
    Bear       = {
      pve = { icon = icons[325] , tip = specTip("druidbearpve") }
    },
    Restoration = {
      pve = { icon = icons[5745]  , tip = specTip("druidrestopve") },
      pvp = { icon = icons[5745], tip = specTip("druidrestopvp") },
    },
  },

  Hunter = {
    BM           = {
      pve = { icon = icons[103], tip = specTip("huntbmpve") },
      pvp = { icon = icons[103], tip = specTip("huntbmpvp") },
    },
    Marksmanship = {
      pve = { icon = icons[214], tip = specTip("huntmarkpve") },
      pvp = { icon = icons[214], tip = specTip("huntmarkpvp") },
    },
    Survival     = {
      pve = { icon = icons[181]  , tip = specTip("huntsurvpve") },
      pvp = { icon = icons[181]  , tip = specTip("huntsurvpvp") },
    },
  },

  Mage = {
    Arcane = {
      pve = { icon = icons[5551], tip = specTip("magearcapve")},
      pvp = { icon = icons[5551], tip = specTip("magearcapvp") },
    },
    Fire   = {
      pve = { icon = icons[5494] , tip = specTip("magefirepve") },
      pvp = { icon = icons[5494], tip = specTip("magefirepvp") },
    },
    Frostfire   = {
      pve = { icon = icons[200] , tip = specTip("magefrostfirepve") },
    },
    Frost  = {
      pve = { icon = icons[5521] , tip = specTip("magefrostpve") },
      pvp = { icon = icons[5521], tip = specTip("magefrostpvp")},
    },
  },

  Paladin = {
    Holy = {
      pve = { icon = icons[5608], tip = specTip("paladinholypve")},
      pvp = { icon = icons[5608], tip = specTip("paladinholypvp")},
    },
    Protection = {
      pve = { icon = icons[5578] , tip = specTip("paladinprotpve") },
      pvp = { icon = icons[5578], tip = specTip("paladinprotpvp") },
    },
    Ret   = {
      pve = { icon = icons[300], tip = specTip("paladinretpve") },
      pvp = { icon = icons[300], tip = specTip("paladinretpvp") }
    },
  },

  Priest = {
    Discipline = {
      pve = { icon = icons[5685], tip = specTip("priestdiscipve") },
      pvp = { icon = icons[5685], tip = specTip("priestdiscipvp") },
    },
    Holy = {
      pve = { icon = icons[5601], tip = specTip("priestholypve") },
      pvp = { icon = icons[5601], tip = specTip("priestholypvp") },
    },
    Shadow = {
      pve = { icon = icons[5929], tip = specTip("priestshadowpve") },
      pvp = { icon = icons[5929], tip = specTip("priestshadowpvp") },
    },
  },

  Rogue = {
    As = {
      pve = { icon = icons[346], tip = specTip("rogassapve") },
      pvp = { icon = icons[346], tip = specTip("rogassapvp") },
    },
    Combat = {
      pve = { icon = icons[358], tip = specTip("rogcombatpve") },
      pvp = { icon = icons[358], tip = specTip("rogcombatpvp") },
    },
    Subtlety = {
      pve = { icon = icons[331], tip = specTip("rogsubtipve") },
      pvp = { icon = icons[331], tip = specTip("rogsubtipvp") },
    },
  },

  Shaman = {
    Ele   = {
      pve = { icon = icons[5716], tip = specTip("shamanelempve") },
      pvp = { icon = icons[5716], tip = specTip("shamanelempvp") },
    },
    Enh  = {
      pve = { icon = icons[5755], tip = specTip("shamanenhpve") },
      pvp = { icon = icons[5755], tip = specTip("shamanenhpvp") },
    },
    Restoration = {
      pve = { icon = icons[5756], tip = specTip("shamanrestopve") },
      pvp = { icon = icons[5756], tip = specTip("shamanrestopvp") },
    },
  },

  Warlock = {
    Affliction = {
      pve = { icon = icons[5852], tip = specTip("warlockafflipve") },
      pvp = { icon = icons[5852], tip = specTip("warlockafflipvp") },
    },
    Demonology = {
      pve = { icon = icons[5889], tip = specTip("warlockdemonopve") },
      pvp = { icon = icons[5889], tip = specTip("warlockdemonopvp") },
    },
    Destruction = {
      pve = { icon = icons[5907], tip = specTip("warlockdestrupve") },
      pvp = { icon = icons[5907], tip = specTip("warlockdestrupvp") },
    },
  },

  Warrior = {
    Arms = {
      pve = { icon = icons[436], tip = specTip("warriorarmspve") },
      pvp = { icon = icons[436], tip = specTip("warriorarmspvp") },
    },
    Fury = {
      pve = { icon = icons[480], tip = specTip("warriorfurypve") },
      pvp = { icon = icons[480], tip = specTip("warriorfurypvp") },
    },
    Protection = {
      pve = { icon = icons[4456], tip = specTip("warriorprotecpve") },
      pvp = { icon = icons[4456], tip = specTip("warriorprotecpvp") },
    },
  },
}

-- renvoie le premier champ userdata trouvé dans un wrapper
local function unwrapFrame(obj)
    if type(obj) == "userdata" then return obj end
    if type(obj) == "table" then
        for _, v in pairs(obj) do
            if type(v) == "userdata" then
                return v
            end
        end
    end
    return nil
end


local MultiBot = _G["MultiBot"] or {}
_G["MultiBot"] = MultiBot

local Spec = MultiBot.spec or {}
Spec.currentBuild = {}  -- table pour conserver la build courante de chaque bot (ex : "0-13-58")
Spec.busy = false
Spec.pendingRefresh = nil
MultiBot.spec = Spec

if Spec.initialised then
    return
end
Spec.initialised = true

Spec.pending, Spec.buttons = nil, {}

local SPEC_LEGACY_USAGE_FILTER_TTL = 5

local function specNow()
    if GetTime then
        return GetTime()
    end

    return time and time() or 0
end

local function normalizeSpecAuthorName(author)
    if type(author) ~= "string" then
        return ""
    end

    local name = author
    if Ambiguate then
        name = Ambiguate(author, "none") or author
    end

    name = string.match(name, "^[^-]+") or name
    return string.lower(name or "")
end

local function cleanSpecChatLine(message)
    if type(message) ~= "string" then
        return ""
    end

    return message
        :gsub("|c%x%x%x%x%x%x%x%x", "")
        :gsub("|r", "")
        :gsub("^%s+", "")
        :gsub("%s+$", "")
end

local function extractCurrentTalentSpecLine(message)
    local clean = cleanSpecChatLine(message)
    if clean == "" then
        return ""
    end

    for line in string.gmatch(clean .. "\n", "([^\r\n]+)") do
        line = line:gsub("^%s+", ""):gsub("%s+$", "")
        local lower = string.lower(line)

        if string.find(lower, "current talent spec", 1, true)
            or string.find(lower, "current spec", 1, true) then
            return line
        end
    end

    return ""
end

local function emitPreservedTalentWhisper(author, line)
    if type(line) ~= "string" or line == "" then
        return
    end

    if not DEFAULT_CHAT_FRAME or not DEFAULT_CHAT_FRAME.AddMessage then
        return
    end

    local authorName = author or ""
    if Ambiguate then
        authorName = Ambiguate(authorName, "none") or authorName
    end

    local template = _G.CHAT_WHISPER_GET or "%s whispers: "
    local prefix = string.format(template, "[" .. tostring(authorName) .. "]")
    local color = ChatTypeInfo and ChatTypeInfo["WHISPER"] or nil

    if color then
        DEFAULT_CHAT_FRAME:AddMessage(prefix .. line, color.r, color.g, color.b)
    else
        DEFAULT_CHAT_FRAME:AddMessage(prefix .. line)
    end
end

local function isLegacyTalentUsageLine(message)
    local clean = cleanSpecChatLine(message)
    local lower = string.lower(clean)

    if lower == "warrior" or lower == "paladin" or lower == "hunter" or lower == "rogue"
        or lower == "priest" or lower == "death knight" or lower == "shaman" or lower == "mage"
        or lower == "warlock" or lower == "druid" then
        return true
    end

    if string.find(lower, "talents usage", 1, true) then
        return true
    end

    if string.find(lower, "talents switch", 1, true) and string.find(lower, "talents spec", 1, true) then
        return true
    end

    return false
end

local function ensureSpecLegacyUsageFilter()
    if MultiBot._specLegacyUsageFilterInstalled then
        return true
    end

    if type(ChatFrame_AddMessageEventFilter) ~= "function" then
        return false
    end

    MultiBot._specLegacyUsageFilterInstalled = true
    ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER", function(_, _, message, author, ...)
        local state = MultiBot and MultiBot._specLegacyUsageFilter or nil
        if type(state) ~= "table" then
            return false
        end

        if state.expiresAt and specNow() > state.expiresAt then
            MultiBot._specLegacyUsageFilter = nil
            return false
        end

        if state.botKey and normalizeSpecAuthorName(author) ~= state.botKey then
            return false
        end

        local currentSpecLine = extractCurrentTalentSpecLine(message)
        if currentSpecLine ~= "" then
            if isLegacyTalentUsageLine(message) then
                emitPreservedTalentWhisper(author, currentSpecLine)
                return true
            end

            return false
        end

        if isLegacyTalentUsageLine(message) then
            return true
        end

        return false
    end)

    return true
end

local function suppressNextTalentUsageLines(botName)
    if not botName or botName == "" then
        return
    end

    if not ensureSpecLegacyUsageFilter() then
        return
    end

    MultiBot._specLegacyUsageFilter = {
        botKey = normalizeSpecAuthorName(botName),
        expiresAt = specNow() + SPEC_LEGACY_USAGE_FILTER_TTL,
    }
end

function Spec:RequestList(bot, wrapper)
    if self.busy then
        return                  --    on ignore le clic
    end
    local frame = unwrapFrame(wrapper)
    if type(frame) ~= "userdata" then
        return
    end

    self:HideDropdown()

    self.pending = {
        bot     = bot,
        anchor  = frame,
        wrapper = wrapper,
        specs   = {},
        builds  = {},
        indices = {},
    }
    self.activeWrapper = wrapper

    local comm = MultiBot.Comm or nil
    local bridgeApplyCapable = comm
        and comm.IsTalentSpecApplyCapable
        and comm.IsTalentSpecApplyCapable()

    -- The current active slot/build is returned by TALENT_SPEC_CURRENT on a
    -- TALENT_SPEC_APPLY_V1 bridge. Keep the old "talents" whisper only as an
    -- explicitly enabled legacy fallback.
    if not bridgeApplyCapable and MultiBot.allowLegacyChatFallback == true then
        suppressNextTalentUsageLines(bot)
        SendChatMessage("talents", "WHISPER", nil, bot)
    end

    if comm and comm.RequestTalentSpecList and comm.RequestTalentSpecList(bot) then
        return
    end

    if MultiBot.allowLegacyChatFallback == true then
        SendChatMessage("talents spec list", "WHISPER", nil, bot)
    end
end


local function short(name)
    local dash = name:find("-", 1, true)
    return dash and name:sub(1, dash-1) or name
end

local SPEC_DROPDOWN_MIGRATION_VERSION = 1
local SPEC_DROPDOWN_MIGRATION_KEY = "specDropdownPositionsVersion"

local function getLegacySpecDropdownStore(createIfMissing)
    local save = _G.MultiBotSave
    if type(save) ~= "table" then
        if not createIfMissing then
            return nil
        end

        save = {}
        _G.MultiBotSave = save
    end

    local store = save.SpecDropdown
    if type(store) ~= "table" then
        if not createIfMissing then
            return nil
        end

        store = {}
        save.SpecDropdown = store
    end

    return store
end

local function cleanupLegacySpecDropdownStoreIfEmpty()
    local save = _G.MultiBotSave
    if type(save) ~= "table" then
        return
    end

    local store = save.SpecDropdown
    if type(store) == "table" and next(store) == nil then
        save.SpecDropdown = nil
    end
end

local function getSpecDropdownStore()
    if MultiBot.Store and MultiBot.Store.EnsureUIChildStore then
        local store = MultiBot.Store.EnsureUIChildStore("specDropdownPositions")
        if store then
            return store
        end
    end

    local profile = MultiBot.db and MultiBot.db.profile
    if profile then
        profile.ui = profile.ui or {}
        profile.ui.specDropdownPositions = profile.ui.specDropdownPositions or {}
        return profile.ui.specDropdownPositions
    end

    return getLegacySpecDropdownStore(true)
end

local function migrateLegacySpecDropdownPositionsIfNeeded(store)
    if not MultiBot.GetProfileMigrationStore() then
        return
    end

    if not MultiBot.ShouldSyncLegacyState(SPEC_DROPDOWN_MIGRATION_KEY, SPEC_DROPDOWN_MIGRATION_VERSION) then
        return
    end

    local legacyStore = getLegacySpecDropdownStore(false)
    for charKey, pos in pairs(legacyStore or {}) do
        if store[charKey] == nil and pos ~= nil then
            store[charKey] = pos
        end
    end

    MultiBot.MarkLegacyStateMigrated(SPEC_DROPDOWN_MIGRATION_KEY, SPEC_DROPDOWN_MIGRATION_VERSION)

    -- Purge migrated legacy payload to avoid stale duplicate persistence.
    if type(legacyStore) == "table" then
        if wipe then
            wipe(legacyStore)
        else
            for key in pairs(legacyStore) do
                legacyStore[key] = nil
            end
        end
    end

    cleanupLegacySpecDropdownStoreIfEmpty()
end

local function getSpecDropdownPosition(charKey)
    if type(charKey) ~= "string" or charKey == "" then
        return nil
    end

    local store = getSpecDropdownStore()
    migrateLegacySpecDropdownPositionsIfNeeded(store)

    local pos = store[charKey]
    if pos ~= nil then
        return pos
    end

    local shouldSyncLegacy = MultiBot.ShouldSyncLegacyState(SPEC_DROPDOWN_MIGRATION_KEY, SPEC_DROPDOWN_MIGRATION_VERSION)
    if shouldSyncLegacy then
        local legacyStore = getLegacySpecDropdownStore(false)
        pos = legacyStore and legacyStore[charKey]
        if pos ~= nil then
            store[charKey] = pos
        end
    end

    return pos
end

local function setSpecDropdownPosition(charKey, position)
    if type(charKey) ~= "string" or charKey == "" or type(position) ~= "table" then
        return
    end

    local store = getSpecDropdownStore()
    migrateLegacySpecDropdownPositionsIfNeeded(store)
    store[charKey] = position

    local shouldSyncLegacy = MultiBot.ShouldSyncLegacyState(SPEC_DROPDOWN_MIGRATION_KEY, SPEC_DROPDOWN_MIGRATION_VERSION)
    if shouldSyncLegacy then
        local legacyStore = getLegacySpecDropdownStore(true)
        legacyStore[charKey] = position
    end
end

local function getSpecDropdownCharacterKey()
    return (UnitName("player") or "Player") .. "-" .. (GetRealmName() or "")
end


--------------------------------------------------------------
-- Whisper handler routed by central event dispatcher.
--------------------------------------------------------------
function MultiBot.HandleSpecWhisper(msg, sender)
    if type(msg) ~= "string" or type(sender) ~= "string" then
        return
    end

    ----------------------------------------------------------------
    -- 1) Basic cleanup for the incoming line
    ----------------------------------------------------------------
    local clean = msg
        :gsub("|c%x%x%x%x%x%x%x%x", "")
        :gsub("|r", "")
        :gsub("\r?\n", " ")

    ----------------------------------------------------------------
    -- 2) "My current talent spec is: … (x/x/x)"
    ----------------------------------------------------------------
    if clean:match("^%s*My current talent spec is:") then
        local inside = clean:match("%(([^%)]+)%)")
        if inside then
            local a, b, c = inside:match("(%d+)[^%d]*(%d+)[^%d]*(%d+)")
            if a and b and c then
                local key = short(sender):lower()
                Spec.currentBuild[key] = a.."-"..b.."-"..c
            end
        end

        ----------------------------------------------------------------
        -- 2-bis) pending refresh
        ----------------------------------------------------------------
        if Spec.pendingRefresh and short(sender) == short(Spec.pendingRefresh) then
            local unit = MultiBot.toUnit(sender)
            if unit then
                if not MultiBot.talent:IsShown() then
                    local _, token = UnitClass(unit)
                    MultiBot.talent.name  = sender
                    MultiBot.talent.class = MultiBot.toClass(token)
                end
                MultiBot.TimerAfter(0.6, function()
                    MultiBot.auto.talent = true
                    InspectUnit(unit)
                    if InspectFrame then HideUIPanel(InspectFrame) end
                    MultiBot.TimerAfter(0.1, function()
                        if MultiBot.talent:IsShown() then MultiBot.talent:Hide() end
                    end)
                end)
            end
            Spec.pendingRefresh = nil
            Spec.busy           = false
        end
        return
    end

    ----------------------------------------------------------------
    -- 3)  Spec list lines: "1) Feral pve (0-60-11)" … "Total …"
    ----------------------------------------------------------------
    local P = Spec.pending
    if not P or short(sender) ~= short(P.bot) then return end

    local name  = clean:match("^%d+[%.%)]?%s*([^%(]+)%s*%(")
                or clean:match("^([^%(]+)%s*%(")
    if name then
        local build = clean:match("%((%d+%-%d+%-%d+)%)")
        tinsert(P.specs,  strtrim(name))
        tinsert(P.builds, strtrim(build))
        return
    end

    local plain = clean:lower()
    if plain:find("total") and plain:find("spec") then
        Spec:BuildDropdown()
        Spec.pending = nil
    end
end

function MultiBot.ApplyBridgeTalentSpecBegin(botName, token)
    local pending = Spec.pending
    if not pending or short(botName) ~= short(pending.bot) then
        return false
    end

    pending.bridgeToken = token
    pending.specs = {}
    pending.builds = {}
    pending.indices = {}
    return true
end

function MultiBot.ApplyBridgeTalentSpecItem(botName, token, entry)
    local pending = Spec.pending
    if not pending or short(botName) ~= short(pending.bot) then
        return false
    end

    if pending.bridgeToken and token and pending.bridgeToken ~= token then
        return false
    end

    if type(entry) ~= "table" or type(entry.name) ~= "string" or entry.name == "" then
        return false
    end

    tinsert(pending.specs, strtrim(entry.name))
    tinsert(pending.builds, strtrim(entry.build or ""))
    tinsert(pending.indices, tonumber(entry.index) or -1)
    return true
end

function MultiBot.ApplyBridgeTalentSpecCurrent(botName, token, slot, tree0, tree1, tree2)
    local pending = Spec.pending
    if not pending or short(botName) ~= short(pending.bot) then
        return false
    end

    if pending.bridgeToken and token and pending.bridgeToken ~= token then
        return false
    end

    slot = tonumber(slot)
    tree0 = tonumber(tree0)
    tree1 = tonumber(tree1)
    tree2 = tonumber(tree2)
    if (slot ~= 1 and slot ~= 2) or not tree0 or not tree1 or not tree2 then
        return false
    end

    Spec.currentBuild[short(botName):lower()] =
        tostring(tree0) .. "-" .. tostring(tree1) .. "-" .. tostring(tree2)
    pending.currentSlot = slot
    return true
end
function MultiBot.ApplyBridgeTalentSpecEnd(botName, token)

    local pending = Spec.pending
    if not pending or short(botName) ~= short(pending.bot) then
        return false
    end

    if pending.bridgeToken and token and pending.bridgeToken ~= token then
        return false
    end

    Spec:BuildDropdown()
    Spec.pending = nil
    return true
end

local function getAceGUI()
    if type(LibStub) ~= "table" then
        return nil
    end

    local ok, lib = pcall(LibStub.GetLibrary, LibStub, "AceGUI-3.0", true)
    if ok and type(lib) == "table" and type(lib.Create) == "function" then
        return lib
    end

    return nil
end

local function debugSpecPath(path)
    -- Debug volontairement désactivé : helper conservé pour ne pas toucher les callsites.
    return path
end

local function disableSetTalentsToggle(wrapper)
    if type(wrapper) ~= "table" then
        return
    end

    if type(wrapper.setDisable) == "function" then
        wrapper:setDisable()
    end
end

function Spec:HideDropdown()
    disableSetTalentsToggle(self.activeWrapper)
    self.activeWrapper = nil

    if self.dropdownWidget and type(self.dropdownWidget.Release) == "function" then
        self.dropdownWidget:Release()
        self.dropdownWidget = nil
    end

    if self.dropdown then
        self.dropdown:Hide()
        self.dropdown:SetParent(nil)
        self.dropdown = nil
    end

    for _, b in ipairs(self.buttons) do
        b:Hide()
        b:SetParent(nil)
    end
    wipe(self.buttons)
end

local function ensureDropdownFrame(specObject, parentFrame, isEmbedded)
    local frame = specObject.dropdown
    if frame then
        return frame
    end

    frame = CreateFrame("Frame", nil, parentFrame or UIParent)

    if isEmbedded then
        local strataLevel = MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()
        if strataLevel then
            frame:SetFrameStrata(strataLevel)
        end
        frame:SetFrameLevel((parentFrame and parentFrame:GetFrameLevel() or 1) + 5)
        frame:SetAllPoints(parentFrame)
    else
        local strataLevel = MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()
        if strataLevel then
            frame:SetFrameStrata(strataLevel)
        end
        if frame.SetBackdrop then
            frame:SetBackdrop({
                bgFile   = "Interface/Tooltips/UI-Tooltip-Background",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile     = true, tileSize = 16,
                edgeSize = 16,
                insets   = { left = 4, right = 4, top = 4, bottom = 4 },
            })
            frame:SetBackdropColor(0, 0, 0, 0.8)
        end

        if not frame._mb_movable_init then
            frame:SetMovable(true)
            frame:EnableMouse(true)
            frame:RegisterForDrag("LeftButton")
            frame:SetClampedToScreen(true)
            frame:SetScript("OnDragStart", frame.StartMoving)
            frame:SetScript("OnDragStop", function(activeFrame)
                activeFrame:StopMovingOrSizing()
                local cx, cy = activeFrame:GetCenter()
                local ux, uy = UIParent:GetCenter()
                local dx, dy = (cx - ux), (cy - uy)
                local charKey = getSpecDropdownCharacterKey()
                setSpecDropdownPosition(charKey, { point = "CENTER", x = dx, y = dy })
            end)
            frame._mb_movable_init = true
        end
    end

    specObject.dropdown = frame
    return frame
end

local function applyDropdownPosition(frame, anchor, isEmbedded)
    if isEmbedded then
        return
    end

    frame:ClearAllPoints()

    local restored = false
    local charKey = getSpecDropdownCharacterKey()
    local pos = getSpecDropdownPosition(charKey)
    if type(pos) == "table" and pos.point then
        frame:SetPoint(pos.point, UIParent, pos.point, pos.x or 0, pos.y or 0)
        restored = true
    end

    if not restored then
        frame:SetPoint("TOP", anchor, "BOTTOM", 0, -4)
    end
end

local function refreshSpecTalentInspection(bot, className)
    local unit = MultiBot.toUnit(bot)
    if not unit then
        return
    end

    if not MultiBot.talent:IsShown() then
        MultiBot.talent.name = bot
        MultiBot.talent.class = className
    end

    MultiBot.TimerAfter(0.2, function()
        MultiBot.auto.talent = true
        InspectUnit(unit)

        if InspectFrame then
            HideUIPanel(InspectFrame)
        end

        MultiBot.TimerAfter(0.1, function()
            if MultiBot.talent:IsShown() then
                MultiBot.talent:Hide()
            end
        end)
    end)
end

local function runLegacySpecSelection(bot, slot, spec, className)
    SendChatMessage("stopcasting", "WHISPER", nil, bot)
    SendChatMessage("talents switch " .. slot, "WHISPER", nil, bot)

    MultiBot.TimerAfter(0.4, function()
        SendChatMessage("talents spec " .. spec, "WHISPER", nil, bot)
    end)

    Spec.pendingRefresh = bot
    MultiBot.TimerAfter(1.3, function()
        if Spec.pendingRefresh and Spec.pendingRefresh == bot then
            refreshSpecTalentInspection(bot, className)
            Spec.pendingRefresh = nil
            Spec.busy = false
        end
    end)
end

local function bindSpecSelection(button, spec, specIndex, build, tip, bot, className, currentBuild)
    if build == currentBuild then
        button:SetAlpha(0.4)
        local tex = button:GetNormalTexture()
        if tex and tex.SetDesaturated then
            tex:SetDesaturated(true)
        end
        button:SetScript("OnClick", nil)
    else
        button:SetAlpha(1)
        button:SetScript("OnClick", function(_, btn)
            if Spec.busy then
                return
            end
            Spec.busy = true

            local slot = (btn == "RightButton") and 2 or 1
            local comm = MultiBot.Comm or nil
            local request

            if comm and comm.RunTalentSpecApply then
                request = comm.RunTalentSpecApply(bot, slot, specIndex, spec, function(result)
                    Spec.busy = false
                    if type(result) ~= "table" then
                        return
                    end

                    if result.status == "ok" then
                        local points = result.treePoints or {}
                        if points[1] ~= nil and points[2] ~= nil and points[3] ~= nil then
                            Spec.currentBuild[short(bot):lower()] =
                                tostring(points[1]) .. "-" .. tostring(points[2]) .. "-" .. tostring(points[3])
                        end

                        if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
                            DEFAULT_CHAT_FRAME:AddMessage(
                                string.format(MultiBot.L("talent.spec.apply.success"), result.specName or spec, bot),
                                1, 1, 1
                            )
                        end
                        refreshSpecTalentInspection(bot, className)
                    else
                        if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
                            DEFAULT_CHAT_FRAME:AddMessage(
                                string.format(
                                    MultiBot.L("talent.spec.apply.failed"),
                                    result.specName or spec,
                                    bot,
                                    tostring(result.reason or "UNKNOWN")
                                ),
                                1, 0.2, 0.2
                            )
                        end
                    end
                end)
            end

            Spec:HideDropdown()

            if request then
                return
            end

            if MultiBot.allowLegacyChatFallback == true then
                runLegacySpecSelection(bot, slot, spec, className)
                return
            end

            Spec.busy = false
            if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
                local reason = comm and MultiBot.bridge and MultiBot.bridge.lastError or "BRIDGE_UNAVAILABLE"
                DEFAULT_CHAT_FRAME:AddMessage(
                    string.format(MultiBot.L("talent.spec.apply.failed"), spec, bot, tostring(reason or "UNKNOWN")),
                    1, 0.2, 0.2
                )
            end
        end)
    end

    button:SetScript("OnEnter", function(activeButton)
        GameTooltip:SetOwner(activeButton, "ANCHOR_NONE")
        GameTooltip:ClearAllPoints()
        GameTooltip:SetPoint("LEFT", activeButton, "RIGHT", 30, 0)
        GameTooltip:SetText(tip, nil, nil, nil, nil, true)
        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", GameTooltip_Hide)
end

function Spec:BuildDropdown()
    if self.busy then
        return
    end

    local pending = self.pending
    if not pending or #pending.specs == 0 then
        return
    end

    local botKey = short(pending.bot):lower()
    local currentBuild = Spec.currentBuild[botKey]

    local aceGUI = getAceGUI()
    local dropdownFrame
    local embeddedInWindow = false

    if aceGUI then
        local widget = aceGUI:Create("Window")
        if widget and widget.frame and widget.content then
            local title = "Set talents"
            if MultiBot and type(MultiBot.L) == "function" then
                local localized = MultiBot.L("spec.list.title", title)
                if localized and localized ~= "" and localized ~= "spec.list.title" then
                    title = localized
                end
            end

            widget:SetTitle(title)
            widget:SetWidth(92)
            widget:SetHeight((#pending.specs * 37) + 40)
            widget:EnableResize(false)
            local strataLevel = MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()
            if strataLevel then
                widget.frame:SetFrameStrata(strataLevel)
            end

            local function saveWindowPosition()
                local cx, cy = widget.frame:GetCenter()
                local ux, uy = UIParent:GetCenter()
                if cx and cy and ux and uy then
                    setSpecDropdownPosition(getSpecDropdownCharacterKey(), {
                        point = "CENTER",
                        x = cx - ux,
                        y = cy - uy,
                    })
                end
            end

            if widget.title and type(widget.title.HookScript) == "function" then
                widget.title:HookScript("OnMouseUp", saveWindowPosition)
            end

            widget:SetCallback("OnClose", function()
                saveWindowPosition()
                Spec:HideDropdown()
            end)
            self.dropdownWidget = widget

            local frame = ensureDropdownFrame(self, widget.content, true)
            frame:ClearAllPoints()
            frame:SetPoint("TOPLEFT", widget.content, "TOPLEFT", 0, 0)
            frame:SetPoint("BOTTOMRIGHT", widget.content, "BOTTOMRIGHT", 0, 0)

            local pos = getSpecDropdownPosition(getSpecDropdownCharacterKey())
            widget.frame:ClearAllPoints()
            if type(pos) == "table" and pos.point then
                widget.frame:SetPoint(pos.point, UIParent, pos.point, pos.x or 0, pos.y or 0)
            else
                widget.frame:SetPoint("TOP", pending.anchor, "BOTTOM", 0, -4)
            end

            dropdownFrame = frame
            embeddedInWindow = true
            debugSpecPath("AceGUI")
        end
    end

	if not dropdownFrame then
        dropdownFrame = ensureDropdownFrame(self, UIParent, false)
        debugSpecPath("legacy")
    end

    applyDropdownPosition(dropdownFrame, pending.anchor, embeddedInWindow)

    local step = 37
    local needed = #pending.specs
    while #self.buttons < needed do
        local button = CreateFrame("Button", nil, dropdownFrame)
        button:RegisterForClicks("AnyUp")
        button:SetFrameLevel(dropdownFrame:GetFrameLevel() + 1)
        button:SetSize(32, 32)
        button:SetPushedTexture("Interface\\Buttons\\UI-Quickslot-Depress")
        button:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
        local strataLevel = MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()
        if strataLevel then
            button:SetFrameStrata(strataLevel)
        end
        table.insert(self.buttons, button)
    end

    local alreadyMarked = false
    for index, button in ipairs(self.buttons) do
        if index <= needed then
            local specName = pending.specs[index]
            local specIndex = pending.indices[index]
            local build = pending.builds[index]

            button:ClearAllPoints()
            if index == 1 then
                button:SetPoint("TOP", dropdownFrame, "TOP", 0, -4)
            else
                button:SetPoint("TOP", self.buttons[index - 1], "BOTTOM", 0, -4)
            end

            local mode = specName:lower():find("pvp") and "pvp" or "pve"
            local prefix = (specName:match("^[^%s]+") or ""):lower()
            local canonicalSpec = specCanonical[prefix] or prefix:gsub("^%l", string.upper)
            local className = pending.wrapper:getClass():gsub("^%l", string.upper)
            local entry = (((specIconMap[className] or {})[canonicalSpec] or {})[mode]) or {}
            local icon = entry.icon or defaultIcon
            local tip = entry.tip or specName

            button:SetNormalTexture(icon)
            button:SetDisabledTexture(icon)
            button:Show()

            if (not alreadyMarked) and build == currentBuild then
                alreadyMarked = true
                bindSpecSelection(button, specName, specIndex, build, tip, pending.bot, className, currentBuild)
            else
                bindSpecSelection(button, specName, specIndex, build, tip, pending.bot, className, nil)
            end
        else
            button:Hide()
        end
    end

    dropdownFrame:SetWidth(40)
    dropdownFrame:SetHeight(needed * step)
    if self.dropdownWidget and type(self.dropdownWidget.SetHeight) == "function" then
        self.dropdownWidget:SetHeight((needed * step) + 40)
    end
    dropdownFrame:Show()
end