-- HDG_HT_W_MultiHouse.lua  (A3)
-- Two account-shared houses (1 Alliance + 1 Horde max). Each card shows:
-- faction icon, neighborhood/house name, level badge, XP fillbar, and a
-- secure "teleport home" button (lifted from VN's MyHomeRow pattern).
--
-- Data layer (own this widget for now):
--  * On Create: register PLAYER_HOUSE_LIST_UPDATED + HOUSE_LEVEL_FAVOR_UPDATED
--  * On Show: call C_Housing.GetPlayerOwnedHouses() to refresh
--  * Cache per-house level/xp in HDG_DB.account.houses[houseGUID] so we can
--    show data for the cross-faction house even when the active char's faction
--    doesn't match (the favor API only returns data for matching faction).

local _, privateDB = ...
privateDB.HDG_HT = privateDB.HDG_HT or {}
privateDB.HDG_HT.Widgets = privateDB.HDG_HT.Widgets or {}
local W = { id = "multiHouse", title = "My Homes" }
privateDB.HDG_HT.Widgets.MultiHouse = W

local CARD_GAP = 8

-- ---------------------------------------------------------------------------
-- SavedVar cache (shared across all instances)
-- ---------------------------------------------------------------------------
local function GetCache()
    HDG_DB = HDG_DB or {}
    HDG_DB.account = HDG_DB.account or {}
    HDG_DB.account.houses = HDG_DB.account.houses or {}
    return HDG_DB.account.houses
end

local function GetFaction(neighborhoodGUID)
    local C = C_Housing
    if not (C and C.DoesFactionMatchNeighborhood and neighborhoodGUID) then return nil end
    local matches  = C.DoesFactionMatchNeighborhood(neighborhoodGUID)
    local pFaction = UnitFactionGroup("player")
    if pFaction == "Alliance" then return matches and "Alliance" or "Horde"
    elseif pFaction == "Horde"  then return matches and "Horde"    or "Alliance" end
    return nil
end

local function StoreHouse(houseInfo)
    if not (houseInfo and houseInfo.houseGUID) then return end
    local cache = GetCache()
    local existing = cache[houseInfo.houseGUID] or {}
    existing.houseGUID         = houseInfo.houseGUID
    existing.neighborhoodGUID  = houseInfo.neighborhoodGUID
    existing.neighborhoodName  = houseInfo.neighborhoodName or existing.neighborhoodName
    existing.houseName         = houseInfo.houseName        or existing.houseName
    existing.plotID            = houseInfo.plotID           or existing.plotID
    existing.faction           = GetFaction(houseInfo.neighborhoodGUID) or existing.faction
    existing.lastSeen          = time()
    cache[houseInfo.houseGUID] = existing
end

-- Max favor per house is global (same for every house) — derived from
-- GetMaxHouseLevel() + GetHouseLevelFavorForLevel(max). Cached at widget
-- create time. HouseLevelFavor payload only carries houseLevel + houseFavor;
-- there is no `maxFavor` field on the struct.
local function StoreLevelFavor(houseGUID, level, currentFavor)
    if not houseGUID then return end
    local cache = GetCache()
    local h = cache[houseGUID]
    if not h then return end  -- ignore if we haven't seen this house yet
    if level then h.level = level end
    if currentFavor then h.xp = currentFavor end
    h.lastSeen = time()
end

-- Resolve max XP (cached per-session). Same value for every house since the
-- progression curve is account-global.
local function GetMaxXP()
    if privateDB.HDG_HT._maxHouseXP then return privateDB.HDG_HT._maxHouseXP end
    if not (C_Housing and C_Housing.GetMaxHouseLevel and C_Housing.GetHouseLevelFavorForLevel) then
        return nil
    end
    local okMax, maxLvl = pcall(C_Housing.GetMaxHouseLevel)
    if not okMax or not maxLvl then return nil end
    local okFav, maxFav = pcall(C_Housing.GetHouseLevelFavorForLevel, maxLvl)
    if okFav and maxFav and maxFav > 0 then
        privateDB.HDG_HT._maxHouseXP = maxFav
        return maxFav
    end
end

-- Returns up to 2 houses, Alliance first, Horde second (matches VN ordering).
local function GetSortedHouses()
    local cache = GetCache()
    local list = {}
    for _, h in pairs(cache) do list[#list + 1] = h end
    table.sort(list, function(a, b)
        local aA = (a.faction == "Alliance") and 0 or 1
        local bA = (b.faction == "Alliance") and 0 or 1
        if aA ~= bA then return aA < bA end
        return (a.lastSeen or 0) > (b.lastSeen or 0)
    end)
    return list
end

-- ---------------------------------------------------------------------------
-- Debug helper. /run HDG_DebugMyHomes() prints the full state.
-- ---------------------------------------------------------------------------
function _G.HDG_DebugMyHomes()
    local cache = GetCache()
    local n = 0
    for _ in pairs(cache) do n = n + 1 end
    print("|cFFfcd34d[HDG MultiHouse]|r cache: " .. n .. " house(s)")
    for guid, h in pairs(cache) do
        print(string.format(
            "  guid=%s  faction=%s  nbhd=%s  house=%s  lvl=%s  xp=%s  lastSeen=%ds ago",
            tostring(guid):sub(-8),
            tostring(h.faction),
            tostring(h.neighborhoodName),
            tostring(h.houseName),
            tostring(h.level),
            tostring(h.xp),
            time() - (h.lastSeen or 0)))
    end
    local instCount = 0
    if privateDB.HDG_HT._multiHouseInstances then
        for _ in pairs(privateDB.HDG_HT._multiHouseInstances) do instCount = instCount + 1 end
    end
    print(string.format("  live instances: %d  | _maxHouseXP cached: %s",
        instCount, tostring(privateDB.HDG_HT._maxHouseXP)))
    -- Probe each instance's card state
    if privateDB.HDG_HT._multiHouseInstances then
        for k, inst in pairs(privateDB.HDG_HT._multiHouseInstances) do
            local c1 = inst.cards and inst.cards[1]
            local c2 = inst.cards and inst.cards[2]
            print(string.format(
                "  inst[%s]: frame.shown=%s  c1.shown=%s (text=%q)  c2.shown=%s (text=%q)",
                tostring(k),
                tostring(inst.frame and inst.frame:IsShown()),
                tostring(c1 and c1:IsShown()), tostring(c1 and c1.nbhdText and c1.nbhdText:GetText() or ""),
                tostring(c2 and c2:IsShown()), tostring(c2 and c2.nbhdText and c2.nbhdText:GetText() or "")))
        end
    end
end

-- ---------------------------------------------------------------------------
-- Shared event listener (one frame across all widget instances)
-- ---------------------------------------------------------------------------
local _eventFrame  = nil
-- Weak-value table — destroyed widget instances (tab close/re-render orphans)
-- get GC'd and disappear from this set automatically. Iterate with pairs(),
-- not ipairs() (sparse). Key is a monotonic counter so existing entries
-- never overwrite each other.
local _instances     = setmetatable({}, { __mode = "v" })
local _nextInstanceK = 0
-- Expose for /run HDG_DebugMyHomes() inspection.
privateDB.HDG_HT._multiHouseInstances = _instances
-- Lock-after-populated: PLAYER_HOUSE_LIST_UPDATED can fire repeatedly during
-- session (login, reload, even random transitions). Once we have the list +
-- a brief window for cross-faction favor responses to land, stop processing
-- events — the cache in HDG_DB.account.houses is the source of truth from
-- there on. Prevents subsequent empty/stale events from clearing the panel.
local _locked         = false
local _lockDeadline   = nil   -- GetTime() after which we ignore further events

local function ensureEventFrame()
    if _eventFrame then return end
    _eventFrame = CreateFrame("Frame")
    _eventFrame:RegisterEvent("PLAYER_HOUSE_LIST_UPDATED")
    _eventFrame:RegisterEvent("HOUSE_LEVEL_FAVOR_UPDATED")
    -- PLAYER_REGEN_ENABLED is NOT subject to the lock — we always need to
    -- re-bind tpBtn after combat ends (SetAttribute was skipped during combat).
    _eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    _eventFrame:SetScript("OnEvent", function(self, event, ...)
        -- PLAYER_REGEN_ENABLED bypasses the lock — re-bind to apply secure
        -- button attributes that were skipped during combat.
        if event == "PLAYER_REGEN_ENABLED" then
            for _, inst in pairs(_instances) do
                if inst.Refresh then inst:Refresh() end
            end
            return
        end
        if _locked then return end

        if event == "PLAYER_HOUSE_LIST_UPDATED" then
            local houseInfos = ...
            if type(houseInfos) == "table" and #houseInfos > 0 then
                for _, info in ipairs(houseInfos) do StoreHouse(info) end
                -- Fan out per-house favor requests (HDG_UI only requests the first)
                if C_Housing and C_Housing.GetCurrentHouseLevelFavor then
                    for _, info in ipairs(houseInfos) do
                        if info.houseGUID then
                            pcall(C_Housing.GetCurrentHouseLevelFavor, info.houseGUID)
                        end
                    end
                end
                -- Schedule the lock 3s out — gives async favor responses time
                -- to land before we stop listening.
                if not _lockDeadline then
                    _lockDeadline = GetTime() + 3
                    C_Timer.After(3, function()
                        _locked = true
                        -- Drop ONLY the housing events; keep PLAYER_REGEN_ENABLED
                        -- registered so post-combat re-bind still fires.
                        -- (UnregisterAllEvents would clobber regen too.)
                        if self.UnregisterEvent then
                            self:UnregisterEvent("PLAYER_HOUSE_LIST_UPDATED")
                            self:UnregisterEvent("HOUSE_LEVEL_FAVOR_UPDATED")
                        end
                    end)
                end
            end
        elseif event == "HOUSE_LEVEL_FAVOR_UPDATED" then
            local payload = ...
            if type(payload) == "table" then
                StoreLevelFavor(payload.houseGUID, payload.houseLevel, payload.houseFavor)
            end
        end

        for _, inst in pairs(_instances) do
            if inst.Refresh then inst:Refresh() end
        end
    end)
end

-- ---------------------------------------------------------------------------
-- Mini-card factory: faction icon + name + level + XP bar + teleport button
-- ---------------------------------------------------------------------------

-- EnableMouse is restricted when the card frame inherits protected-frame
-- status from its SecureActionButtonTemplate child (tpBtn). The call can
-- throw ADDON_ACTION_BLOCKED when our stack has been contaminated by a
-- DIFFERENT addon's taint (combat lockdown also fails; routine addon
-- code is fine). pcall absorbs the rare cross-taint case. Do NOT use
-- issecure() here -- it returns false for any addon-initiated stack,
-- which over-blocks every call. See Reference/MIDNIGHT_SECRET_VALUES.md
-- "Pitfall: issecure() vs InCombatLockdown" for the full story.
local function safeEnableMouse(frame, enabled)
    if not InCombatLockdown() then
        pcall(frame.EnableMouse, frame, enabled)
    end
end

local function CreateHouseCard(parent)
    local Theme  = privateDB.HDG_HT.Theme
    local scheme = Theme.GetCurrentScheme()
    local TP_W   = 70
    local TP_H   = 22

    local card = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    card:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    card:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.6)
    card:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.3)

    -- Left accent (faction-colored stripe)
    card.accent = card:CreateTexture(nil, "ARTWORK")
    card.accent:SetPoint("TOPLEFT", 0, 0)
    card.accent:SetPoint("BOTTOMLEFT", 0, 0)
    card.accent:SetWidth(2)
    card.accent:SetTexture("Interface\\Buttons\\WHITE8x8")

    -- Faction icon (top-left)
    card.factionIcon = card:CreateTexture(nil, "ARTWORK")
    card.factionIcon:SetSize(20, 20)
    card.factionIcon:SetPoint("TOPLEFT", card, "TOPLEFT", 8, -6)

    -- Neighborhood name (header)
    card.nbhdText = card:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    card.nbhdText:SetPoint("TOPLEFT", card.factionIcon, "TOPRIGHT", 6, 0)
    card.nbhdText:SetPoint("RIGHT", card, "RIGHT", -8, 0)
    card.nbhdText:SetJustifyH("LEFT")
    card.nbhdText:SetWordWrap(false)
    card.nbhdText:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b)
    Theme.ApplyFont(card.nbhdText, scheme, "header")

    -- House name (sub-line)
    card.houseText = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    card.houseText:SetPoint("TOPLEFT", card.nbhdText, "BOTTOMLEFT", 0, -2)
    card.houseText:SetPoint("RIGHT", card, "RIGHT", -8, 0)
    card.houseText:SetJustifyH("LEFT")
    card.houseText:SetWordWrap(false)
    card.houseText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
    Theme.ApplyFont(card.houseText, scheme, "small")

    -- XP fillbar (bottom)
    card.bar = CreateFrame("StatusBar", nil, card)
    card.bar:SetHeight(6)
    card.bar:SetPoint("LEFT",  card, "LEFT",  8, 0)
    card.bar:SetPoint("RIGHT", card, "RIGHT", -TP_W - 16, 0)
    card.bar:SetPoint("BOTTOM", card, "BOTTOM", 0, 8)
    card.bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
    card.bar:SetStatusBarColor(scheme.success.r, scheme.success.g, scheme.success.b, 1)
    card.bar.bg = card.bar:CreateTexture(nil, "BACKGROUND")
    card.bar.bg:SetAllPoints()
    card.bar.bg:SetTexture("Interface\\Buttons\\WHITE8x8")
    card.bar.bg:SetVertexColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.85)

    -- Level / XP label — anchored UNDER the address (houseText) and given a
    -- RIGHT bound so it can wrap onto a second line when the widget is
    -- narrow (e.g. third-width). At twoThirds it stays one line.
    card.lvlText = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    card.lvlText:SetPoint("TOPLEFT", card.houseText, "BOTTOMLEFT", 0, -2)
    card.lvlText:SetPoint("RIGHT", card, "RIGHT", -8, 0)
    card.lvlText:SetJustifyH("LEFT")
    card.lvlText:SetJustifyV("TOP")
    card.lvlText:SetWordWrap(true)
    card.lvlText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
    Theme.ApplyFont(card.lvlText, scheme, "small")

    -- Secure teleport button (right)
    card.tpBtn = CreateFrame("Button", nil, card, "SecureActionButtonTemplate, BackdropTemplate")
    card.tpBtn:SetSize(TP_W, TP_H)
    card.tpBtn:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -8, 6)
    card.tpBtn:RegisterForClicks("AnyDown", "AnyUp")
    card.tpBtn:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    card.tpBtn:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.85)
    card.tpBtn:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.5)

    card.tpIcon = card.tpBtn:CreateTexture(nil, "ARTWORK")
    card.tpIcon:SetSize(14, 14)
    card.tpIcon:SetPoint("LEFT", card.tpBtn, "LEFT", 4, 0)
    card.tpIcon:SetAtlas("dashboard-panel-homestone-teleport-button")

    card.tpLabel = card.tpBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    card.tpLabel:SetPoint("LEFT", card.tpIcon, "RIGHT", 4, 0)
    card.tpLabel:SetText("Visit")
    card.tpLabel:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
    Theme.ApplyFont(card.tpLabel, scheme, "small")

    function card:Bind(house)
        -- Don't Show/Hide the card -- it has a SecureActionButtonTemplate child
        -- (tpBtn), so Frame:Show() on the parent is treated as protected and
        -- blocks when our call stack is tainted (BugGrabber report 12.0.5).
        -- SetAlpha is unprotected. EnableMouse on the card is protected
        -- because SecureActionButtonTemplate protection propagates UP from
        -- self.tpBtn. safeEnableMouse() gates on InCombatLockdown and
        -- pcall-absorbs the rare cross-addon-taint failure (see
        -- Reference/MIDNIGHT_SECRET_VALUES.md). Card stays "shown"
        -- perpetually; we toggle visibility + mouse via these paths.
        if not house then
            self:SetAlpha(0)
            safeEnableMouse(self, false)
            if self.tpBtn then
                self.tpBtn:SetAlpha(0)
                safeEnableMouse(self.tpBtn, false)
            end
            return
        end
        self:SetAlpha(1)
        safeEnableMouse(self, true)

        local faction = house.faction
        if faction == "Alliance" then
            self.factionIcon:SetAtlas("communities-create-button-wow-alliance")
            self.factionIcon:Show()
            self.accent:SetVertexColor(0.27, 0.51, 0.78, 1) -- Alliance blue
        elseif faction == "Horde" then
            self.factionIcon:SetAtlas("communities-create-button-wow-horde")
            self.factionIcon:Show()
            self.accent:SetVertexColor(0.78, 0.27, 0.27, 1) -- Horde red
        else
            self.factionIcon:Hide()
            self.accent:SetVertexColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.5)
        end

        self.nbhdText:SetText(house.neighborhoodName or "Unknown")
        self.houseText:SetText(house.houseName or "")

        local lvl, xp = house.level, house.xp
        local maxXP = GetMaxXP()  -- account-global max favor
        if lvl and xp and maxXP and maxXP > 0 then
            self.lvlText:SetText(string.format("Level %d  -  %s / %s",
                lvl, BreakUpLargeNumbers(xp), BreakUpLargeNumbers(maxXP)))
            self.bar:SetMinMaxValues(0, maxXP)
            self.bar:SetValue(math.min(xp, maxXP))
            self.bar:Show()
        elseif lvl then
            self.lvlText:SetText(string.format("Level %d", lvl))
            self.bar:Hide()
        else
            self.lvlText:SetText(Theme.C_Dim() .. "Level -|r")
            self.bar:Hide()
        end

        -- Need ALL three attributes to be non-nil for the secure
        -- "teleporthome" handler — partial info silently fails.
        local canTeleport = house.houseGUID and house.neighborhoodGUID and house.plotID
        -- Secure button visibility: never call Show/Hide on a
        -- SecureActionButtonTemplate frame from inside Refresh — it's
        -- protected and blocks when execution is tainted (other addon's
        -- event handler somewhere up the call stack). SetAlpha +
        -- EnableMouse are unprotected and give us the same visual/click
        -- behaviour without taint risk. The button frame stays in the
        -- "shown" state perpetually; its parent card hides it via the
        -- parent-Hide path at line 311.
        -- SetAttribute on SecureActionButton can throw ADDON_ACTION_BLOCKED
        -- from a cross-addon-tainted call stack. Defense in depth:
        --  1. HasSecretAspect(Enum.SecretAspect.Attributes) -- if the
        --     button has already been marked with the Attributes aspect
        --     by prior tainted SetAttribute writes, further writes would
        --     compound the contamination; skip cleanly.
        --  2. InCombatLockdown -- routine protection.
        --  3. pcall -- absorbs the rare cross-addon-taint case that the
        --     aspect check can't predict.
        -- HasSecretAspect takes the Enum.SecretAspect integer value, NOT
        -- a string name (the doc table is verbose about Name= but the
        -- API itself wants EnumValue).
        -- Do NOT use issecure() here -- it returns false for all addon
        -- stacks and would skip every bind.
        -- See Reference/MIDNIGHT_SECRET_VALUES.md SecretAspect table.
        local attrsAspect = Enum and Enum.SecretAspect and Enum.SecretAspect.Attributes
        local attrsAlreadySecret = attrsAspect and self.tpBtn.HasSecretAspect
            and self.tpBtn:HasSecretAspect(attrsAspect) or false
        if canTeleport and not InCombatLockdown() and not attrsAlreadySecret then
            local ok = pcall(function()
                self.tpBtn:SetAttribute("type", "teleporthome")
                self.tpBtn:SetAttribute("house-neighborhood-guid", house.neighborhoodGUID)
                self.tpBtn:SetAttribute("house-guid", house.houseGUID)
                self.tpBtn:SetAttribute("house-plot-id", house.plotID)
            end)
            if ok then
                self.tpBtn:SetAlpha(1)
                safeEnableMouse(self.tpBtn, true)
            else
                self.tpBtn:SetAlpha(0)
                safeEnableMouse(self.tpBtn, false)
            end
        else
            self.tpBtn:SetAlpha(0)
            safeEnableMouse(self.tpBtn, false)
        end
    end

    return card
end

-- ---------------------------------------------------------------------------
-- Widget
-- ---------------------------------------------------------------------------
function W.Create(parent, _)
    ensureEventFrame()
    local self = { cards = {} }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)

    self.cards[1] = CreateHouseCard(self.frame)
    self.cards[2] = CreateHouseCard(self.frame)

    _nextInstanceK = _nextInstanceK + 1
    _instances[_nextInstanceK] = self

    function self:Refresh(_)
        local houses = GetSortedHouses()
        for i = 1, 2 do
            local card = self.cards[i]
            local house = houses[i]
            card:Bind(house)
        end
    end

    -- Re-bind from cache every time the widget shows. This makes tab-out then
    -- tab-back-in a no-event repaint — robust to whatever the event flow does.
    self.frame:SetScript("OnShow", function() self:Refresh() end)

    -- Initial fetch on first Create only — kicks the housing API so events
    -- arrive and populate the cache. Subsequent Creates (e.g. tab switch)
    -- skip this; cache is the source of truth from there on.
    if not privateDB.HDG_HT._multiHouseFetched then
        privateDB.HDG_HT._multiHouseFetched = true
        if C_Housing and C_Housing.GetPlayerOwnedHouses then
            pcall(C_Housing.GetPlayerOwnedHouses)
        end
    end

    function self:Resize(w, h)
        local cardW = math.max(120, math.floor((w - CARD_GAP) / 2))
        for i = 1, 2 do
            local card = self.cards[i]
            card:SetSize(cardW, h)
            card:ClearAllPoints()
            card:SetPoint("TOPLEFT", self.frame, "TOPLEFT", (i - 1) * (cardW + CARD_GAP), 0)
        end
    end

    return self
end

return W
