---@class AddonPrivate
local Private = select(2, ...)

---@class RareAlerts
local rares = {}
Private.RareAlerts = rares
rares.active = {}      -- vignetteGUID -> rare data currently shown
rares.dismissed = {}   -- vignetteGUID -> true, hidden by the player until it despawns
rares.rows = {}

local const = Private.constants

local WIDTH      = 260
local ROW_HEIGHT = 24
local TITLE_H    = 20
local MAX_ROWS   = 6

local FrameBackdrop = {
    bgFile   = "interface\\achievementframe\\ui-guildachievement-parchment",
    edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
    edgeSize = 16,
    insets   = { left = 4, right = 4, top = 4, bottom = 4 },
}
local RARE_ICON = "Interface\\Icons\\INV_Misc_Head_Dragon_01"

-- Classify a vignette by its icon atlas into just the three kinds we care about.
-- Anything else (bonus objectives, events, quest icons, etc.) is ignored.
--   rare     -> skull/kill vignettes  (Target + Track)
--   treasure -> loot chests           (Track only)
--   glyph    -> Dragon Glyph pickups   (Track only)
local function classifyVignette(info)
    local atlas = (info.atlasName or ""):lower()
    local name  = (info.name or ""):lower()
    if atlas:find("kill") then return "rare" end
    if atlas:find("loot") or atlas:find("chest") or atlas:find("treasure")
        or (Enum.VignetteType and info.type == Enum.VignetteType.Treasure) then
        return "treasure"
    end
    if atlas:find("glyph") or name:find("glyph") then return "glyph" end
    return nil
end

-- Collect the alive rare / treasure / glyph vignettes on the current map.
local function collectRares(mapID)
    local out = {}
    if not (C_VignetteInfo and C_VignetteInfo.GetVignettes) then return out end
    for _, guid in ipairs(C_VignetteInfo.GetVignettes()) do
        local info = C_VignetteInfo.GetVignetteInfo(guid)
        if info and info.name and not info.isDead then
            local kind = classifyVignette(info)
            if kind then
                local pos = C_VignetteInfo.GetVignettePosition and C_VignetteInfo.GetVignettePosition(guid, mapID)
                out[guid] = {
                    guid = guid,
                    name = info.name,
                    atlas = info.atlasName,
                    kind = kind,
                    mapID = mapID,
                    x = pos and pos.x and pos.x * 100,
                    y = pos and pos.y and pos.y * 100,
                }
            end
        end
    end
    return out
end

function rares:IsEnabled()
    local s = Private.Settings and Private.Settings:Rares()
    return s and s.enabled
end

function rares:SetEnabled(enabled)
    local s = Private.Settings and Private.Settings:Rares()
    if s then s.enabled = enabled and true or false end
    if not enabled then
        if self.frame then self.frame:Hide() end
    else
        self:Refresh()
    end
end

function rares:Toggle()
    self:SetEnabled(not self:IsEnabled())
end

-- Refresh the header filter button's label to reflect the current mode.
function rares:UpdateFilterLabel()
    if not (self.frame and self.frame.filter) then return end
    local s = Private.Settings and Private.Settings:Rares()
    local on = s and s.onlyRelevant
    self.frame.filter.check:SetShown(on and true or false)
    self.frame.filter.text:SetText(on and "|cff00ff00Meta only|r" or "|cff9a9a9aMeta only|r")
end

local function AcquireRow(self, index)
    if self.rows[index] then return self.rows[index] end
    local f = self.frame
    local row = CreateFrame("Button", nil, f)
    row:SetSize(WIDTH - 16, ROW_HEIGHT)

    -- dark card panel + gold hairline (matches the planner rows)
    local bg = row:CreateTexture(nil, "BACKGROUND", nil, -2)
    bg:SetPoint("TOPLEFT", 1, 0)
    bg:SetPoint("BOTTOMRIGHT", -1, 1)
    bg:SetColorTexture(0.05, 0.05, 0.06, 0.55)
    local sep = row:CreateTexture(nil, "BORDER")
    sep:SetHeight(1)
    sep:SetPoint("BOTTOMLEFT", 3, 0)
    sep:SetPoint("BOTTOMRIGHT", -3, 0)
    sep:SetColorTexture(0.83, 0.66, 0.28, 0.11)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(18, 18)
    row.icon:SetPoint("LEFT", 2, 0)
    row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    row.label = row:CreateFontString(nil, "OVERLAY")
    row.label:SetFontObject(const.FONT_OBJECTS.SMALL)
    row.label:SetPoint("LEFT", row.icon, "RIGHT", 4, 0)
    row.label:SetJustifyH("LEFT")
    row.label:SetWordWrap(false)

    -- dismiss (X)
    row.close = CreateFrame("Button", nil, row)
    row.close:SetSize(16, 16)
    row.close:SetPoint("RIGHT", -2, 0)
    row.close:SetNormalTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up")
    row.close:SetPushedTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Down")
    row.close:SetScript("OnClick", function()
        if row.guid then self.dismissed[row.guid] = true end
        self:Render()
    end)

    -- track
    row.track = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.track:SetSize(46, 18)
    row.track:SetPoint("RIGHT", row.close, "LEFT", -3, 0)
    row.track:SetText("Track")
    row.track:SetScript("OnClick", function()
        local r = row.rare
        if r and r.x and r.y and Private.WaypointsUtil then
            Private.WaypointsUtil:TrackPoint(r.mapID, r.x, r.y, r.name)
        end
    end)

    -- target (SECURE: targeting a unit by name is protected, so this must be a
    -- secure /target macro button; its macrotext is only set out of combat).
    -- NOTE: SecureActionButtonTemplate must be listed LAST so its OnClick (which
    -- runs the macro) wins over UIPanelButtonTemplate's OnClick.
    row.target = CreateFrame("Button", nil, row, "UIPanelButtonTemplate, SecureActionButtonTemplate")
    row.target:SetSize(50, 18)
    row.target:SetPoint("RIGHT", row.track, "LEFT", -3, 0)
    row.target:SetText("Target")
    row.target:RegisterForClicks("AnyUp", "AnyDown")

    row.label:SetPoint("RIGHT", row.target, "LEFT", -4, 0)

    row:SetScript("OnEnter", function(b)
        local r = b.rare
        if not r then return end
        local header = ({ rare = "Rare nearby", treasure = "Treasure nearby",
            glyph = "Dragon Glyph nearby" })[r.kind] or "Nearby"
        GameTooltip:SetOwner(b, "ANCHOR_LEFT")
        GameTooltip:AddLine(header, 1, 0.82, 0)
        GameTooltip:AddLine(r.name, 1, 1, 1)
        if r.relevant then
            GameTooltip:AddLine("Needed for: " .. (r.relAch and r.relAch.name or "a tracked meta"), 0, 1, 0, true)
        end
        if r.x and r.y then
            GameTooltip:AddLine(string.format("%.0f, %.0f  \226\128\148  Track to set a waypoint.", r.x, r.y), 0.7, 0.7, 0.7)
        else
            GameTooltip:AddLine("Location unavailable for a waypoint.", 0.8, 0.5, 0.5)
        end
        if r.kind == "rare" then
            GameTooltip:AddLine("Target to /target the rare (if in range).", 0.5, 0.8, 1)
        end
        GameTooltip:Show()
    end)
    row:SetScript("OnLeave", function() GameTooltip:Hide() end)

    self.rows[index] = row
    return row
end

function rares:Render()
    if not self.frame then self:Initialize() end
    -- Rows parent a SECURE /target button; showing, hiding or moving them is combat-
    -- protected (was throwing ADDON_ACTION_BLOCKED). Defer the whole re-render until
    -- combat ends -- PLAYER_REGEN_ENABLED re-fires Refresh, which re-renders cleanly.
    if InCombatLockdown() then return end
    local f = self.frame

    -- prune dismissed entries whose vignette is gone
    for guid in pairs(self.dismissed) do
        if not self.active[guid] then self.dismissed[guid] = nil end
    end

    -- ordered list: relevant first, then by name
    local list = {}
    for guid, r in pairs(self.active) do
        if not self.dismissed[guid] then list[#list + 1] = r end
    end
    table.sort(list, function(a, b)
        if (a.relevant and 1 or 0) ~= (b.relevant and 1 or 0) then return a.relevant end
        return (a.name or "") < (b.name or "")
    end)

    for _, row in ipairs(self.rows) do row:Hide() end
    if #list == 0 then f:Hide() return end

    local y = TITLE_H + 4
    local shown = 0
    for _, r in ipairs(list) do
        if shown >= MAX_ROWS then break end
        shown = shown + 1
        local row = AcquireRow(self, shown)
        row.guid, row.rare = r.guid, r
        if r.atlas then
            row.icon:SetAtlas(r.atlas)          -- the vignette's own skull/chest/glyph icon
        else
            row.icon:SetTexture(RARE_ICON)
            row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        end
        local tag = r.relevant and "|cff00ff00 [needed]|r" or ""
        row.label:SetText((r.name or "Rare") .. tag)
        row.track:SetEnabled(r.x ~= nil and r.y ~= nil)
        -- Only rares get the Target button; treasures/glyphs are Track-only.
        -- Secure-frame show/hide + macro assignment are combat-protected, so guard.
        if not InCombatLockdown() then
            if r.kind == "rare" then
                row.target:SetAttribute("type", "macro")
                row.target:SetAttribute("macrotext", "/target " .. (r.name or ""))
                row.target:Enable()
                row.target:Show()
                row.label:SetPoint("RIGHT", row.target, "LEFT", -4, 0)
            else
                row.target:Hide()
                row.label:SetPoint("RIGHT", row.track, "LEFT", -4, 0)
            end
        end
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -y)
        row:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -y)
        row:Show()
        y = y + ROW_HEIGHT
    end

    f:SetHeight(y + 8)
    f:Show()
end

-- Re-scan vignettes and update the toasts (fires on VIGNETTES_UPDATED / zone change).
function rares:Refresh()
    local s = Private.Settings and Private.Settings:Rares()
    if not s or not s.enabled then
        if self.frame then self.frame:Hide() end
        return
    end
    local mapID = C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
    if not mapID then
        if self.frame then self.frame:Hide() end
        return
    end

    local current = collectRares(mapID)
    -- If we're routing to an objective whose name matches a live rare here, snap the
    -- arrow onto the rare's (more accurate) live vignette position. Done against ALL
    -- detected rares, before the "Meta only" filter below can strip some out.
    if Private.WaypointsUtil and Private.WaypointsUtil.MatchActiveToRare then
        Private.WaypointsUtil:MatchActiveToRare(current)
    end
    for guid, r in pairs(current) do
        local rel = Private.Objectives and Private.Objectives:FindRareMatch(r.name, mapID)
        r.relevant = rel ~= nil
        r.relAch = rel and rel.ach
        if s.onlyRelevant and not r.relevant then current[guid] = nil end
    end

    -- a genuinely new (non-dismissed) rare => alert sound
    local isNew = false
    for guid in pairs(current) do
        if not self.active[guid] and not self.dismissed[guid] then isNew = true end
    end

    self.active = current
    self:Render()
    if isNew and s.sound then pcall(PlaySound, SOUNDKIT and SOUNDKIT.RAID_WARNING or 8959) end
end

function rares:Initialize()
    if self.frame then return end
    local s = Private.Settings:Rares()

    local f = CreateFrame("Frame", "MetaTrackerRareAlerts", UIParent, "BackdropTemplate")
    f:SetSize(WIDTH, 60)
    f:SetBackdrop(FrameBackdrop)
    f:SetBackdropColor(0.09, 0.09, 0.11, 0.97) -- dark, matches the rest of the addon
    f:SetBackdropBorderColor(1, 1, 1, 1)
    f:SetFrameStrata("HIGH")
    f:SetClampedToScreen(true)
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function(self2)
        if not Private.Settings:Rares().locked then self2:StartMoving() end
    end)
    f:SetScript("OnDragStop", function(self2)
        self2:StopMovingOrSizing()
        local rs = Private.Settings:Rares()
        local point, _, relPoint, x, y = self2:GetPoint()
        rs.point, rs.relPoint, rs.x, rs.y = point, relPoint, x, y
    end)
    if s and s.point then
        f:SetPoint(s.point, UIParent, s.relPoint or s.point, s.x or 0, s.y or -200)
    else
        f:SetPoint("TOP", UIParent, "TOP", 0, -200)
    end
    self.frame = f

    local titleBar = f:CreateTexture(nil, "ARTWORK")
    titleBar:SetTexture("Interface\\AchievementFrame\\UI-Achievement-Borders")
    titleBar:SetPoint("TOPLEFT", 8, -6)
    titleBar:SetPoint("TOPRIGHT", -8, -6)
    titleBar:SetHeight(20)
    titleBar:SetVertexColor(1, 1, 1, 0.8)
    titleBar:SetTexCoord(0, 0.9765625, 0.66015625, 0.73828125)

    local title = f:CreateFontString(nil, "OVERLAY")
    title:SetFontObject(const.FONT_OBJECTS.HEADING)
    title:SetPoint("LEFT", titleBar, "LEFT", 6, -1)
    title:SetText("Rares Nearby")
    title:SetTextColor(1, 0.85, 0.32)
    title:SetShadowColor(0, 0, 0, 1)
    title:SetShadowOffset(1, -1)

    -- "Meta only" filter: a real check box (ticked = meta-relevant rares only).
    local filter = CreateFrame("Button", nil, f)
    filter:SetSize(18, 18)
    filter:SetPoint("RIGHT", titleBar, "RIGHT", -4, -1)
    filter:SetNormalTexture("Interface\\Buttons\\UI-CheckBox-Up")
    filter:SetPushedTexture("Interface\\Buttons\\UI-CheckBox-Down")
    filter:SetHighlightTexture("Interface\\Buttons\\UI-CheckBox-Highlight", "ADD")
    filter.check = filter:CreateTexture(nil, "OVERLAY")
    filter.check:SetPoint("CENTER")
    filter.check:SetSize(18, 18)
    filter.check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
    filter.text = filter:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    filter.text:SetPoint("RIGHT", filter, "LEFT", -1, 0)
    filter.text:SetText("Meta only")
    filter:SetScript("OnClick", function()
        local rs = Private.Settings:Rares()
        if rs then rs.onlyRelevant = not rs.onlyRelevant end
        rares:UpdateFilterLabel()
        rares:Refresh()
    end)
    filter:SetScript("OnEnter", function(b)
        GameTooltip:SetOwner(b, "ANCHOR_LEFT")
        GameTooltip:AddLine("Rare filter", 1, 0.82, 0)
        GameTooltip:AddLine("Click to switch between showing ALL nearby rares and only "
            .. "those needed for a tracked meta.", 1, 1, 1, true)
        GameTooltip:Show()
    end)
    filter:SetScript("OnLeave", function() GameTooltip:Hide() end)
    f.filter = filter
    self:UpdateFilterLabel()

    -- own event handling
    local ev = CreateFrame("Frame")
    ev:RegisterEvent("VIGNETTES_UPDATED")
    ev:RegisterEvent("ZONE_CHANGED_NEW_AREA")
    ev:RegisterEvent("PLAYER_ENTERING_WORLD")
    ev:RegisterEvent("PLAYER_REGEN_ENABLED") -- leaving combat: apply any deferred /target macros
    ev:SetScript("OnEvent", function() rares:Refresh() end)
    self.events = ev

    f:Hide()
end
