local pluginName, pluginShared = ...
local Lockouts = pluginShared.Lockouts

local guarded = pluginShared.guarded
-- Favorites.lua defines this first in the load order: the star, the addon's name, then the message,
-- out as an emote or printed depending on the Emote box. The fallback keeps this file usable on its own.
local say = pluginShared.say or print
local tell = pluginShared.tell or print
local L = LibStub("AceLocale-3.0"):GetLocale("AppearanceMatic")

SLASH_APPEARANCEMATIC1 = "/appearancematic"
SLASH_APPEARANCEMATIC2 = "/am"
SlashCmdList["APPEARANCEMATIC"] = function(msg)
    local arg = type(msg) == "string" and msg:lower():match("^%s*(%S*)") or ""
    if arg == "settings" or arg == "options" or arg == "config" then
        if pluginShared.OpenSettings then
            pluginShared.OpenSettings()
        end
        return
    end
    if arg == "changelog" then
        if pluginShared.ShowChangeLog then
            pluginShared.ShowChangeLog()
        end
        return
    end
    if arg == "test" or arg == "preview" then
        local alert = pluginShared.Alert
        if alert and alert.Preview then
            local kind = type(msg) == "string" and msg:lower():match("^%s*%S+%s+(%S+)") or nil
            if kind == "mount" then
                alert:Preview("mount")
            elseif kind == "appearance" then
                alert:Preview("appearance")
            else
                alert:Preview("appearance")
                if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
                    C_Timer.After(0.6, function() alert:Preview("mount") end)
                else
                    alert:Preview("mount")
                end
            end
        else
            print(L["lock_alert_engine_missing"])
        end
        return
    end
    if arg == "profile" then
        local profiler = pluginShared.Profiler
        if not profiler then
            return
        end
        local what = type(msg) == "string" and msg:lower():match("^%s*%S+%s+(%S+)") or nil
        if what == "reset" then
            local running = profiler:Running()
            profiler:Reset()
            print(running and L["profile_reset"] or L["profile_reset_idle"])
            return
        end
        if what == "report" then
            profiler:Report()
            return
        end
        if profiler:Running() then
            profiler:Report()
            profiler:Stop()
            print(L["profile_stopped"])
        else
            print(string.format(L["profile_started_fmt"], profiler:Start()))
        end
        return
    end
    if arg == "stop" then
        -- the route keeps its entries; the guide rests until the switch, a route change or the next login
        Lockouts:PauseRoute()
        tell(L["lock_routing_stopped"])
        return
    end
    if arg == "reset" then
        -- fresh read: no route, no guide, nothing tracked by Blizzard, no remembered guide state
        if pluginShared.Route then
            pluginShared.Route:Stop()
        end
        Lockouts:StopGuide()
        local cleared = pluginShared.Tracking and pluginShared.Tracking.StopAll and pluginShared.Tracking:StopAll() or 0
        if type(C_SuperTrack) == "table" and type(C_SuperTrack.ClearSuperTrackedContent) == "function" then
            pcall(C_SuperTrack.ClearSuperTrackedContent)
        end
        if type(C_Map) == "table" and type(C_Map.ClearUserWaypoint) == "function" then
            pcall(C_Map.ClearUserWaypoint)
        end
        if AppearanceMaticDB then
            AppearanceMaticDB.guide = nil
            AppearanceMaticDB.guideDismissed = nil
            AppearanceMaticDB.routeTarget = nil
        end
        if pluginShared.Route and pluginShared.Route.InvalidateGateways then
            pluginShared.Route:InvalidateGateways()
        end
        Lockouts.visualBySource = nil
        Lockouts:SetRoutePaused(true) -- the route keeps its entries; it starts guiding again when it changes
        Lockouts:ScheduleRebuild()
        print(string.format(L["lock_guide_reset"], cleared))
        return
    end
    if arg == "here" then
        local okMap, uiMapID = pcall(C_Map.GetBestMapForUnit, "player")
        local okPos, pos = okMap and pcall(C_Map.GetPlayerMapPosition, uiMapID, "player")
        local okInfo, info = okMap and pcall(C_Map.GetMapInfo, uiMapID)
        if okPos and type(pos) == "table" then
            local x, y = pos.x, pos.y
            if type(pos.GetXY) == "function" then x, y = pos:GetXY() end
            print(string.format("AM here: map %s (%s) x=%.4f y=%.4f  (/way #%s %.2f %.2f)", tostring(uiMapID),
                okInfo and type(info) == "table" and tostring(info.name) or "?", x, y, tostring(uiMapID), x * 100, y * 100))
        else
            print("AM here: no map position available here.")
        end
        return
    end
    local probeMap = arg == "probemap" and type(msg) == "string" and msg:match("(%d+)") or nil
    if probeMap then
        local mapID = tonumber(probeMap)
        print("AM probe: map " .. mapID)
        if type(C_EncounterJournal) == "table" and type(C_EncounterJournal.GetEncountersOnMap) == "function" then
            local ok, list = pcall(C_EncounterJournal.GetEncountersOnMap, mapID)
            local out = {}
            for _, e in ipairs(ok and type(list) == "table" and list or {}) do
                out[#out + 1] = tostring(e.encounterID) .. "@" .. string.format("%.2f,%.2f", e.mapX or -1, e.mapY or -1)
            end
            print("AM probe: encounters=" .. #out .. " [" .. table.concat(out, "; ") .. "]")
        end
        if type(C_AreaPoiInfo) == "table" and type(C_AreaPoiInfo.GetAreaPOIForMap) == "function"
            and type(C_AreaPoiInfo.GetAreaPOIInfo) == "function" then
            local ok, ids = pcall(C_AreaPoiInfo.GetAreaPOIForMap, mapID)
            local out = {}
            for _, id in ipairs(ok and type(ids) == "table" and ids or {}) do
                local okI, info = pcall(C_AreaPoiInfo.GetAreaPOIInfo, mapID, id)
                if okI and type(info) == "table" then
                    local px, py = -1, -1
                    if type(info.position) == "table" then
                        px, py = info.position.x or -1, info.position.y or -1
                    end
                    out[#out + 1] = id .. ":" .. tostring(info.name) .. "@" .. string.format("%.2f,%.2f", px, py)
                        .. (info.atlasName and ("(" .. tostring(info.atlasName) .. ")") or "")
                end
            end
            print("AM probe: areapois=" .. #out .. " [" .. table.concat(out, "; ") .. "]")
        end
        if type(C_TaskQuest) == "table" and type(C_TaskQuest.GetQuestsOnMap) == "function" then
            local ok, list = pcall(C_TaskQuest.GetQuestsOnMap, mapID)
            local out = {}
            for _, q in ipairs(ok and type(list) == "table" and list or {}) do
                local title
                if type(C_TaskQuest.GetQuestInfoByQuestID) == "function" then
                    local okT, name = pcall(C_TaskQuest.GetQuestInfoByQuestID, q.questID)
                    title = okT and name or nil
                end
                out[#out + 1] = tostring(q.questID) .. ":" .. tostring(title) .. " tag=" .. tostring(q.questTagType)
            end
            print("AM probe: taskquests=" .. #out .. " [" .. table.concat(out, "; ") .. "]")
        end
        if type(C_VignetteInfo) == "table" and type(C_VignetteInfo.GetVignettes) == "function" then
            local ok, guids = pcall(C_VignetteInfo.GetVignettes)
            local out = {}
            for _, guid in ipairs(ok and type(guids) == "table" and guids or {}) do
                local okI, info = pcall(C_VignetteInfo.GetVignetteInfo, guid)
                local pos = ""
                if type(C_VignetteInfo.GetVignettePosition) == "function" then
                    local okP, p = pcall(C_VignetteInfo.GetVignettePosition, guid, mapID)
                    if okP and type(p) == "table" then
                        pos = "@" .. string.format("%.2f,%.2f", p.x or -1, p.y or -1)
                    end
                end
                out[#out + 1] = (okI and type(info) == "table" and tostring(info.name) or "?") .. pos
                    .. (okI and type(info) == "table" and info.onWorldMap and "(wm)" or "")
            end
            print("AM probe: vignettes=" .. #out .. " [" .. table.concat(out, "; ") .. "]")
        end
        return
    end
    if arg == "diag" then
        local route, arrow, tracking = pluginShared.Route, pluginShared.GuideArrow, pluginShared.Tracking
        local function fmt(v) if type(v) == "table" then return string.format("map %s @ %.3f,%.3f %s", tostring(v.mapID), v.x or -1, v.y or -1, tostring(v.text or v.title or "")) end return tostring(v) end
        print("AM diag: timeline: " .. Lockouts:Timeline() .. " settling=" .. tostring(Lockouts:IsSettling()))
        local d = AppearanceMaticDB and AppearanceMaticDB.diag or {}
        local routeNames = {}
        for _, entry in ipairs(Lockouts.routeOrder or {}) do
            routeNames[#routeNames + 1] = (entry.raid or ((entry.isMount and "m" or "a") .. tostring(entry.id))) .. (entry.done and "(done)" or "")
        end
        local guidedRef = Lockouts:GuidedRef()
        print("AM diag: panel: routeRows=" .. tostring(Lockouts.routeRows) .. " order=[" .. table.concat(routeNames, ", ") .. "]"
            .. " guided=" .. (guidedRef and (tostring(guidedRef.type) .. ":" .. tostring(guidedRef.id) .. " raid=" .. tostring(guidedRef.raidName)) or "nil")
            .. " paused=" .. tostring(Lockouts.routePaused) .. " saved=" .. tostring(AppearanceMaticDB and AppearanceMaticDB.routePaused)
            .. " switch=[" .. table.concat(Lockouts.switchLog or {}, "; ") .. "]"
            .. " settled=" .. tostring(Lockouts.routeSettled) .. " incomplete=" .. tostring(Lockouts.dropsIncomplete)
            .. " rebuildError=" .. tostring(d.rebuildError) .. " atLine=" .. tostring(d.rebuildErrorLine) .. " lastTryOn=" .. tostring(Lockouts.lastTryOn))
        local acq = pluginShared.Acquisition
        if acq then
            local names = AppearanceMaticDB and AppearanceMaticDB.acquisition and AppearanceMaticDB.acquisition.npcNames
            local n = 0
            for _ in pairs(names or {}) do n = n + 1 end
            local static = pluginShared.AcquisitionData and pluginShared.AcquisitionData.vendors
            local sample = static and static[233949]
            print("AM diag: npcNames cached=" .. n .. " lastMiss=" .. tostring(acq.lastNpcMiss) .. " static[233949]=" .. (sample and ("npc " .. tostring(sample.npcID) .. " cost=" .. tostring(acq:FormatCost(sample.costRows))) or "nil"))
        end
        if route then
            local playerContinent = Lockouts:PlayerWorld()
            local okMap, playerMap = pcall(C_Map.GetBestMapForUnit, "player")
            local gateways = route:Gateways()
            local here, destContinent = 0, nil
            for _, g in ipairs(gateways or {}) do if g.continentID == playerContinent then here = here + 1 end end
            if route.target and route.target.mapID then
                destContinent = route.worldFromMap(route.target.mapID, route.target.x or 0.5, route.target.y or 0.5)
            end
            local toward = (playerContinent and destContinent) and #route:GatewaysToward(gateways, playerContinent, destContinent) or "n/a"
            local discovered = route.DiscoverGateways and route:DiscoverGateways() or {}
            local hereNames = {}
            for _, g in ipairs(gateways or {}) do
                if g.continentID == playerContinent and g.mapID == playerMap then hereNames[#hereNames + 1] = g.label end
            end
            local locked, lockedHere = 0, {}
            for _, g in ipairs(gateways or {}) do
                if g.locked then
                    locked = locked + 1
                    if g.continentID == playerContinent then lockedHere[#lockedHere + 1] = g.label end
                end
            end
            print("AM diag: continents: player map " .. tostring(okMap and playerMap) .. " continent " .. tostring(playerContinent)
                .. " gatewaysHere=" .. here .. " destContinent=" .. tostring(destContinent) .. " toward=" .. tostring(toward)
                .. " discovered=" .. #discovered .. " learned=" .. (function() local n = 0 for _ in pairs(AppearanceMaticDB and AppearanceMaticDB.learnedGateways or {}) do n = n + 1 end return n end)()
                .. " onThisMap=" .. table.concat(hereNames, "; "))
            local RD = pluginShared.RouteData or {}
            local gs, es = RD.GATEWAYS_STAMP, RD.ENTRANCES_STAMP
            local okBuild, _, _, _, build = pcall(GetBuildInfo)
            print("AM diag: data: gateways from TravelGuide " .. tostring(gs and gs.sha) .. " (" .. tostring(gs and gs.date) .. ")"
                .. " entrances for build " .. tostring(es and es.wowBuild) .. " client " .. tostring(okBuild and build)
                .. " locked=" .. locked .. " lockedHere=" .. table.concat(lockedHere, "; ")
                .. " unresolved=" .. table.concat(route and route.unresolvedGateways or {}, "; "))
        end
        print("AM diag: cover: revealed=" .. tostring(Lockouts.panelRevealed) .. " since=" .. tostring(Lockouts.coverSince)
            .. " ready=" .. tostring(Lockouts.dropsReady) .. " pending=" .. tostring(Lockouts.dropsPending)
            .. " log: " .. table.concat(Lockouts.coverLog or {}, " | "))
        print("AM diag: draws: " .. table.concat(Lockouts.drawLog or {}, " | "))
        local trackedAge = (route and route.trackedAskedAt and type(GetTime) == "function") and string.format("%.1fs ago", GetTime() - route.trackedAskedAt) or "never"
        local pinCount = 0
        for _ in pairs(Lockouts.encounterPins or {}) do pinCount = pinCount + 1 end
        print("AM diag: route.target=" .. fmt(route and route.target) .. " route.lastHop=" .. fmt(route and route.lastHop) .. " ticker=" .. tostring(route and route.ticker ~= nil)
            .. " tracked=" .. fmt(route and route.trackedHop) .. " asked=" .. trackedAge
            .. " trackErr=" .. tostring(pluginShared.Tracking and pluginShared.Tracking.lastStartError)
            .. " pins=" .. pinCount)
        if route and route.HearthDiagText then
            print("AM diag: hearth: " .. route:HearthDiagText())
        end
        local network = pluginShared.Network
        if network and network.DiagText then
            print("AM diag: network: " .. network:DiagText())
        end
        if Lockouts.ActiveWorldBossSet then
            Lockouts:ActiveWorldBossSet()
        end
        local wbQuests = Lockouts.activeBossQuests or {}
        local wbTitles = {}
        for i = 1, math.min(#wbQuests, 6) do
            wbTitles[#wbTitles + 1] = tostring(wbQuests[i].title or wbQuests[i].questID) .. "@" .. tostring(wbQuests[i].mapID)
        end
        local wbRaid
        for _, entry in ipairs(Lockouts.routeOrder or {}) do
            if not entry.done then
                wbRaid = entry.raid or entry.raidName
                break
            end
        end
        local wbm, wbx, wby, wbName
        if wbRaid and Lockouts.WorldBossSpot then
            wbm, wbx, wby, wbName = Lockouts:WorldBossSpot(wbRaid)
        end
        local vignNames = {}
        for _, v in ipairs(Lockouts.activeBossVignettes or {}) do
            vignNames[#vignNames + 1] = v.name .. (v.mapID and ("@" .. v.mapID) or "")
        end
        print("AM diag: worldboss: quests=" .. #wbQuests .. "/" .. tostring(Lockouts.activeBossQuestTotal)
            .. " vign=" .. #vignNames .. " [" .. table.concat(vignNames, "; ") .. "] [" .. table.concat(wbTitles, "; ") .. "]"
            .. " group=" .. tostring(wbRaid) .. " spot=" .. tostring(wbm) .. "@" .. tostring(wbx) .. "," .. tostring(wby)
            .. " boss=" .. tostring(wbName))
        local gRow = wbRaid and Lockouts.GateRow and Lockouts:GateRow(wbRaid) or nil
        local gState = wbRaid and Lockouts.GateState and Lockouts:GateState(wbRaid) or nil
        local gq = {}
        if gRow and type(C_QuestLog) == "table" then
            for _, q in ipairs(gRow.quests or {}) do
                local c, a
                if type(C_QuestLog.IsQuestFlaggedCompleted) == "function" then
                    local okQ, v = pcall(C_QuestLog.IsQuestFlaggedCompleted, q)
                    c = okQ and v
                end
                if type(C_QuestLog.IsQuestFlaggedCompletedOnAccount) == "function" then
                    local okQ, v = pcall(C_QuestLog.IsQuestFlaggedCompletedOnAccount, q)
                    a = okQ and v
                end
                gq[#gq + 1] = q .. " char=" .. tostring(c) .. " acc=" .. tostring(a)
            end
        end
        local liveCount = 0
        for _ in pairs(Lockouts.liveContinentBosses or {}) do liveCount = liveCount + 1 end
        print("AM diag: gate: group=" .. tostring(wbRaid) .. " kind=" .. tostring(gRow and gRow.kind)
            .. " state=" .. tostring(gState and gState.state) .. " [" .. table.concat(gq, "; ") .. "] liveCont=" .. liveCount)
        local pinsDump = {}
        for encID, pin in pairs(Lockouts.encounterPins or {}) do
            pinsDump[#pinsDump + 1] = tostring(encID) .. "@" .. tostring(pin.mapID)
        end
        table.sort(pinsDump)
        local encDump = {}
        local wbEntry = wbRaid and Lockouts.BuildBossOrdinalIndex and Lockouts:BuildBossOrdinalIndex()[wbRaid:lower()] or nil
        for name, encID in pairs(wbEntry and wbEntry.bossEncounters or {}) do
            local hit = (Lockouts.encounterPins or {})[encID] and "*" or ""
            encDump[#encDump + 1] = name .. "=" .. tostring(encID) .. hit
        end
        table.sort(encDump)
        print("AM diag: wbindex: pins=[" .. table.concat(pinsDump, " ") .. "]")
        print("AM diag: wbindex: bosses=[" .. table.concat(encDump, " ") .. "] (* = pinned)")
        local gt = Lockouts.guideTarget
        print("AM diag: guideTarget=" .. (gt and (gt.type .. ":" .. gt.id) or "nil") .. " raid=" .. tostring(gt and gt.raidName) .. " fallback=" .. (gt and gt.fallback and (tostring(gt.fallback.mapID) .. "@" .. tostring(gt.fallback.x) .. "," .. tostring(gt.fallback.y)) or "nil") .. " guideRouted=" .. tostring(Lockouts.guideRouted) .. " guideHop=" .. fmt(Lockouts.guideHop) .. " ticker=" .. tostring(Lockouts.guideTicker ~= nil))
        if gt and gt.raidName then
            local index = Lockouts:BuildEntranceIndex()
            local count, byInst = 0, 0
            for k, v in pairs(index) do if k ~= "byInstance" then count = count + 1 end end
            for _ in pairs(index.byInstance or {}) do byInst = byInst + 1 end
            local bossEntry = Lockouts:BuildBossOrdinalIndex()[gt.raidName:lower()]
            local entry = Lockouts:LegacyEntrance(gt.raidName)
            print("AM diag: entrance: index names=" .. count .. " byInstance=" .. byInst .. " raidKey=" .. tostring(index[gt.raidName:lower()] ~= nil)
                .. " originalInstanceID=" .. tostring(bossEntry and bossEntry.originalInstanceID) .. " seasonInstanceID=" .. tostring(bossEntry and bossEntry.seasonInstanceID)
                .. " -> " .. (entry and fmt(entry) or "nil") .. " retryAt=" .. tostring(Lockouts.entranceRetryAt))
            local okMap, uiMapID = pcall(C_Map.GetBestMapForUnit, "player")
            local zoneMap = pluginShared.Route and pluginShared.Route.ZoneMap and pluginShared.Route:ZoneMap(uiMapID) or uiMapID
            if C_EncounterJournal and C_EncounterJournal.GetDungeonEntrancesForMap then
                local names = {}
                local okE, list = pcall(C_EncounterJournal.GetDungeonEntrancesForMap, zoneMap)
                for _, e in ipairs(okE and type(list) == "table" and list or {}) do names[#names + 1] = tostring(e.name) .. "#" .. tostring(e.journalInstanceID) end
                print("AM diag: entrances on zone map " .. tostring(zoneMap) .. ": " .. (#names > 0 and table.concat(names, ", ") or "none"))
            end
        end
        if gt and tracking then
            local okMap, uiMapID = pcall(C_Map.GetBestMapForUnit, "player")
            local ok, result, info = pcall(C_ContentTracking.GetNextWaypointForTrackable, gt.type, gt.id, uiMapID)
            print("AM diag: tracking=" .. tostring(tracking:IsTracking(gt.type, gt.id)) .. " nextWaypoint result=" .. tostring(result) .. " info=" .. (type(info) == "table" and string.format("%.3f,%.3f %s", info.x or -1, info.y or -1, tostring(info.waypointText)) or tostring(info)))
            local zoneMap = pluginShared.Route and pluginShared.Route.ZoneMap and pluginShared.Route:ZoneMap(uiMapID) or nil
            local okZ, resultZ, zInfo = pcall(C_ContentTracking.GetNextWaypointForTrackable, gt.type, gt.id, zoneMap or uiMapID)
            print("AM diag: zoneMap=" .. tostring(zoneMap) .. " result=" .. tostring(resultZ) .. " info=" .. (type(zInfo) == "table" and string.format("%.3f,%.3f %s", zInfo.x or -1, zInfo.y or -1, tostring(zInfo.waypointText)) or tostring(zInfo)))
            local okT, tMap, tx, ty, tText = pcall(tracking.NextWaypoint, tracking, gt.type, gt.id)
            print("AM diag: Tracking:NextWaypoint -> map " .. tostring(tMap) .. " @ " .. tostring(tx) .. "," .. tostring(ty) .. " " .. tostring(tText))
        end
        if type(C_SuperTrack) == "table" then
            local okMap, playerMap = pcall(C_Map.GetBestMapForUnit, "player")
            local canSet = okMap and C_Map.CanSetUserWaypointOnMap and C_Map.CanSetUserWaypointOnMap(playerMap)
            local hasPin = C_Map.HasUserWaypoint and C_Map.HasUserWaypoint()
            local pin = hasPin and C_Map.GetUserWaypoint and C_Map.GetUserWaypoint()
            local px, py = pin and pin.position and pin.position.x, pin and pin.position and pin.position.y
            print(string.format("AM diag: playerMap=%s canSetPin=%s hasPin=%s pin=%s@%s,%s superAny=%s superPin=%s superContent=%s",
                tostring(playerMap), tostring(canSet), tostring(hasPin), tostring(pin and pin.uiMapID), tostring(px), tostring(py),
                tostring(C_SuperTrack.IsSuperTrackingAnything and C_SuperTrack.IsSuperTrackingAnything()),
                tostring(C_SuperTrack.IsSuperTrackingUserWaypoint and C_SuperTrack.IsSuperTrackingUserWaypoint()),
                tostring(C_SuperTrack.IsSuperTrackingContent and C_SuperTrack.IsSuperTrackingContent())))
            if okMap and C_SuperTrack.GetNextWaypointForMap then
                local okHop, hx, hy, htext = pcall(C_SuperTrack.GetNextWaypointForMap, playerMap)
                print("AM diag: gameNextHop=" .. tostring(okHop and hx) .. "," .. tostring(okHop and hy) .. " " .. tostring(okHop and htext))
            end
            print("AM diag: pinRefused=" .. tostring(AppearanceMaticDB and AppearanceMaticDB.diag and AppearanceMaticDB.diag.pinRefused))
        end
        local pins = LibStub and LibStub("HereBeDragons-Pins-2.0", true)
        print("AM diag: arrow hides: " .. tostring(arrow and arrow.HideLog and arrow:HideLog() or "n/a") .. " shellShown=" .. tostring(_G.AppearanceMaticGuideArrowShell and _G.AppearanceMaticGuideArrowShell:IsShown()) .. " combat=" .. tostring(type(InCombatLockdown) == "function" and InCombatLockdown()))
        print("AM diag: arrow active=" .. tostring(arrow and arrow:IsActive()) .. " frameShown=" .. tostring(_G.AppearanceMaticGuideArrow and _G.AppearanceMaticGuideArrow:IsShown()) .. " alpha=" .. tostring(_G.AppearanceMaticGuideArrow and _G.AppearanceMaticGuideArrow:GetAlpha()) .. " HBDPins=" .. tostring(pins ~= nil))
        if arrow and arrow.CurrentTaxiDestination then
            print("AM diag: taxiDest=" .. tostring(arrow:CurrentTaxiDestination()) .. " flightMap=" .. tostring(_G.FlightMapFrame ~= nil))
        end
        if arrow and arrow.DebugVector then
            local bearing, yards = arrow:DebugVector()
            print("AM diag: vector bearing=" .. tostring(bearing) .. " yards=" .. tostring(yards))
        end
        return
    end
    if arg == "help" then
        local startup = pluginShared.Core and pluginShared.Core.startup
        if startup and startup.PrintHelp then
            startup:PrintHelp("AppearanceMatic")
        end
        return
    end
    Lockouts:ToggleWidget()
end

if pluginShared.Favorites then
    pluginShared.Favorites:AddListener(function()
        Lockouts:OnFavoritesChanged()
    end)
end

local watcher = CreateFrame("Frame")
watcher:RegisterEvent("ADDON_LOADED")
watcher:RegisterEvent("PLAYER_ENTERING_WORLD")
watcher:RegisterEvent("BOSS_KILL")
watcher:RegisterEvent("UPDATE_INSTANCE_INFO")
watcher:RegisterEvent("CONTENT_TRACKING_UPDATE")
watcher:RegisterEvent("TRANSMOG_COLLECTION_UPDATED")
watcher:RegisterEvent("ZONE_CHANGED_NEW_AREA")
watcher:RegisterEvent("PLAYER_REGEN_ENABLED")
-- what unlocks a gateway can change mid-session: quests turn in, levels go up, spells and standings change
watcher:RegisterEvent("QUEST_TURNED_IN")
watcher:RegisterEvent("PLAYER_LEVEL_UP")
watcher:RegisterEvent("SPELLS_CHANGED")
watcher:RegisterEvent("TAXIMAP_OPENED")
watcher:RegisterEvent("UPDATE_FACTION")
-- access gates: map pins and PvP zone control change, quests turn in
watcher:RegisterEvent("AREA_POIS_UPDATED")
watcher:RegisterEvent("PVP_WORLDSTATE_UPDATE")
watcher:RegisterEvent("QUEST_DATA_LOAD_RESULT")
-- the try-on model wears the player's own gear: what they are wearing and what it looks like are both
-- inputs to it
watcher:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
watcher:RegisterEvent("TRANSMOGRIFY_SUCCESS")
local LOCK_EVENTS = { QUEST_TURNED_IN = true, PLAYER_LEVEL_UP = true, SPELLS_CHANGED = true, UPDATE_FACTION = true }
local GATE_EVENTS = { QUEST_TURNED_IN = true, AREA_POIS_UPDATED = true, PVP_WORLDSTATE_UPDATE = true, QUEST_DATA_LOAD_RESULT = true }
watcher:SetScript("OnEvent", function(_, event, name, ...)
    if event == "TAXIMAP_OPENED" then
        -- a flight point learned at this master changes what the route may use
        local route = pluginShared.Route
        if route and route.InvalidateTaxiNodes then
            route:InvalidateTaxiNodes()
        end
        return
    end
    if event == "PLAYER_EQUIPMENT_CHANGED" or event == "TRANSMOGRIFY_SUCCESS" then
        Lockouts:ForgetHoverPreviews()
        return
    end
    if event == "QUEST_DATA_LOAD_RESULT" then
        -- only an answer that brought data changes what a gate line can say; a refusal (removed or
        -- unknown quest) changes nothing, and redrawing on it was half of a once-a-second loop
        local success = ...
        if success ~= true then
            return
        end
    end
    if LOCK_EVENTS[event] or GATE_EVENTS[event] then
        local route = pluginShared.Route
        if LOCK_EVENTS[event] and route and route.MarkLocksDirty then
            route:MarkLocksDirty()
        end
        local favorites = pluginShared.Favorites
        if favorites and favorites.InvalidateTooltipCache then
            favorites:InvalidateTooltipCache()
        end
        if GATE_EVENTS[event] and Lockouts.gateStates then
            -- the answers recompute right here (cheap, a handful of gates); the panel redraws only
            -- when one truly changed, not for every pin list a flight streams past
            if Lockouts:RefreshGates() and Lockouts.panel and Lockouts.panel:IsShown() and not Lockouts.minimized then
                Lockouts:ScheduleRebuild()
            end
        end
        return
    end
    if event == "ADDON_LOADED" then
        if name == "Blizzard_Collections" then
            Lockouts:InstallHeaderButton()
        elseif name == "Blizzard_EncounterJournal" then
            Lockouts:InvalidateIndexes()
            Lockouts:ForgetNamelessItems()
            Lockouts:ScheduleRebuild()
        end
        return
    end
    if event == "ZONE_CHANGED_NEW_AREA" then
        Lockouts:MarkSettling(3)
        Lockouts:InvalidateGates()
        if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
            C_Timer.After(3.5, function()
                if Lockouts.guideTarget then
                    Lockouts:RouteGuideToTomTom(0)
                end
                -- out of a raid that is now done this lockout: the route moves on; a route with no
                -- guide picks one up
                Lockouts:RouteAdvanceIfDone()
            end)
        end
        if Lockouts.panel and Lockouts.panel:IsShown() and not Lockouts.minimized then
            Lockouts:ScheduleRebuild()
        end
        return
    end
    if event == "PLAYER_REGEN_ENABLED" then
        -- out of combat the game hands over what it withheld mid-fight, so anything the panel had to
        -- leave out is worked out again and the list fills itself in
        if Lockouts.combatHidden then
            Lockouts.combatHidden = nil
            Lockouts:InvalidateDropCache()
            Lockouts:ForgetNamelessItems()
            if pluginShared.Favorites and pluginShared.Favorites.InvalidateMountScan then
                pluginShared.Favorites:InvalidateMountScan()
            end
            if Lockouts.panel and Lockouts.panel:IsShown() and not Lockouts.minimized then
                Lockouts:ScheduleRebuild()
            end
        end
        return
    end
    if event == "TRANSMOG_COLLECTION_UPDATED" then
        Lockouts:InvalidateDropCache()
        Lockouts:ForgetNamelessItems()
        local favorites = pluginShared.Favorites
        if favorites and favorites.InvalidateMountScan then
            favorites:InvalidateMountScan()
        end
        if Lockouts.panel and Lockouts.panel:IsShown() and not Lockouts.minimized then
            Lockouts:ScheduleRebuild()
        end
        return
    end
    if event == "CONTENT_TRACKING_UPDATE" then
        local trackType, id, isTracked = name, ...
        local target = Lockouts.guideTarget
        if target and isTracked == false and target.type == trackType and target.id == id and not Lockouts:IsSettling() then
            Lockouts.trackMisses = (Lockouts.trackMisses or 0) + 1
        end
        if Lockouts.panel and Lockouts.panel:IsShown() and not Lockouts.minimized then
            Lockouts:ScheduleRebuild()
        end
        return
    end
    if event == "UPDATE_INSTANCE_INFO" then
        if AppearanceMaticDB then
            Lockouts:SnapshotCharacter()
            if Lockouts.panel and Lockouts.panel:IsShown() and not Lockouts.minimized then
                Lockouts:ScheduleRebuild()
            end
            if Lockouts.initialized then
                Lockouts:RouteAdvanceIfDone()
            end
        end
        return
    end
    if event == "PLAYER_ENTERING_WORLD" then
        Lockouts:MarkSettling(5)
        local isInitialLogin, isReloadingUi = name, ...
        local function resume()
            local route = pluginShared.Route
            local savedRoute = AppearanceMaticDB and AppearanceMaticDB.routeTarget
            if route and route.Resume and type(savedRoute) == "table" and savedRoute.mapID then
                -- a saved route is the one guide; a Blizzard-tracked guide left running beside it
                -- would put a second hop on the arrow (portal transitions re-enter here too)
                if Lockouts.guideTarget and not Lockouts.guideRouted then
                    Lockouts:StopGuide()
                end
                if not route.target then
                    route:Resume()
                end
            end
            -- the saved guide re-arms Blizzard's tracking (pin, objective, collected alert) and its
            -- ticker beside the resumed route; it adopts nothing new while a route is running
            if Lockouts.ResumeGuide then
                Lockouts:ResumeGuide()
            end
            -- a route with nothing guided starts guiding once the collection has settled
            if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
                C_Timer.After(6, function() Lockouts:RouteAdvanceIfDone() end)
            end
        end
        -- /reload and portal transitions have warm state: the route, arrow and pin come back on the
        -- spot (the settle window already holds the game queries back). A six-second wait here left
        -- them dead after every reload. Only a cold login waits briefly for the client to stand up.
        if isInitialLogin and not isReloadingUi and type(C_Timer) == "table" and type(C_Timer.After) == "function" then
            C_Timer.After(2, resume)
        else
            resume()
        end
    end
    if event == "PLAYER_ENTERING_WORLD" and AppearanceMaticDB and not Lockouts.initialized then
        Lockouts.initialized = true
        AppearanceMaticDB.previewTest = nil
        AppearanceMaticDB.previewYaw = nil
        Lockouts:Init()
        if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
            -- collection data settles a few seconds after login
            C_Timer.After(10, function() Lockouts:PruneCollectedFavorites() end)
        end
    end
    if type(RequestRaidInfo) == "function" then
        RequestRaidInfo()
    end
end)

local isLoaded = C_AddOns and C_AddOns.IsAddOnLoaded or IsAddOnLoaded
if type(isLoaded) == "function" then
    local ok, loaded = guarded(isLoaded, "Blizzard_Collections")
    if ok and loaded then
        Lockouts:InstallHeaderButton()
    end
end
