-- WarBoard_Acquire.lua
-- Acquisition objectives: collect a resource by LOOTING it inside a set of subzones.
--
-- Distinct from the kill / proximity paths. An objective opts in (in WarEffort_Objectives.lua) with:
--   acquire     = true
--   areaIDs     = { <AreaTable IDs> }   -- the subzones the objective is active in
--   lootItemID  = <itemID>              -- the item whose looting counts one unit of progress
--   goal / gold / warCredit / title / desc / progressLabel   (+ optional unlockSpell / lockedTip)
--
-- Two layers, kept separate:
--   1. PRESENCE (subzone) → popup + objective-tracker entry. Presence is by SUBZONE, not a radius:
--      GetMinimapZoneText() is compared against C_Map.GetAreaInfo(areaID), so it matches on any
--      locale (the AreaTable id resolves to the client's own localized name). Entering a valid
--      subzone (while UNLOCKED and not already complete) fires the center-screen "War Objective"
--      banner once and shows the tracker line; leaving clears it.
--   2. PROGRESS (loot) → one unit per CHAT_MSG_LOOT that yields lootItemID inside a valid subzone.
--      CHAT_MSG_LOOT IS the loot interaction (it fires only for real loot, once per item), so we
--      count on it DIRECTLY, gated by the same subzone check the /run macro uses — no reliance on
--      a LOOT window opening (auto-loot gathering often skips it). A short per-objective dedup
--      swallows an accidental duplicate, and the looter GUID must be us.
--
-- The objective's map pin (WarBoard_MapPins) is purely cosmetic here — presence/earning is by
-- subzone, so the pin carries no radius and drives nothing.

local addonName, WB = ...
WB.Acquire = WB.Acquire or {}
local A = WB.Acquire

-- Acquisition objectives that apply to this player (built on login, faction-filtered).
local objs = {}

-- Anti-double-count: ignore a repeat credit for the same objective within this window. Guards an
-- accidental duplicate CHAT_MSG_LOOT for ONE physical loot; real separate gathers are always
-- farther apart (each needs a cast/channel), so this never blocks a genuine second pickup.
local DEDUP_WINDOW = 0.4
local lastCount    = {}   -- objId -> GetTime()

-- ── Gates ──────────────────────────────────────────────────────────────────────────────

-- AreaTable id → localized subzone name, cached forever (static client DB data). Keeps the
-- loot/rescan paths free of repeated C-calls + pcall overhead.
local areaName = setmetatable({}, { __index = function(t, id)
    local ok, name = pcall(C_Map.GetAreaInfo, id)
    name = (ok and name) or false   -- cache a miss as false so it's never re-queried
    rawset(t, id, name)
    return name
end })

-- Is the player standing in one of this objective's subzones? Locale-safe: the live minimap
-- subzone text vs each AreaTable id's localized name (C_Map.GetAreaInfo). This IS the credit gate
-- (matches the /run GetMinimapZoneText() == C_Map.GetAreaInfo(387/388) check).
local function InArea(obj)
    if type(obj.areaIDs) ~= "table" then return false end
    local cur = GetMinimapZoneText()
    if not cur or cur == "" then return false end
    for _, id in ipairs(obj.areaIDs) do
        if areaName[id] == cur then return true end
    end
    return false
end

-- Can this objective be worked right now? Just the unlock gate — per the objective's rule, being
-- in the valid subzone is what determines credit. War Mode is intentionally NOT required (say the
-- word to re-add it). NOT gated on completion, so the repeatable cycle keeps counting like kills.
local function CanEarn(obj)
    if WB.WarEffort and WB.WarEffort.IsLocked and WB.WarEffort.IsLocked(obj) then return false end
    return true
end

-- Should the entry banner fire on this entry? Earnable AND not already completed today (so a
-- finished objective's tracker still shows for the repeat cycle, but the popup stops nagging).
local function ShouldAnnounce(obj)
    if not CanEarn(obj) then return false end
    if WB.WarEffort and WB.WarEffort.IsComplete and WB.WarEffort.IsComplete(obj.id) then return false end
    return true
end

-- ── Presence → popup + tracker ───────────────────────────────────────────────────────────
local function ShowTracker(obj)
    if not (WB.Tracker and WB.Tracker.AddObjective) then return end
    local cur  = (WB.WarEffort and WB.WarEffort.GetProgress and WB.WarEffort.GetProgress(obj.id)) or 0
    local name = (WB.WarEffort and WB.WarEffort.DisplayTitle and WB.WarEffort.DisplayTitle(obj)) or obj.title
    WB.Tracker:AddObjective(obj.id, name, obj.progressLabel or "collected", cur, obj.goal or 0)
end

local function HideTracker(obj)
    if WB.Tracker and WB.Tracker.RemoveObjective then WB.Tracker:RemoveObjective(obj.id) end
end

-- Re-evaluate presence for every acquisition objective. Fires on zone/subzone change, on unlock
-- (SPELLS_CHANGED), and on War Mode flip (PLAYER_FLAGS_CHANGED). `obj.inside` tracks the last state
-- so the popup fires once per genuine entry and the tracker entry is added/removed only on a real
-- transition.
local function ReScan()
    for _, obj in ipairs(objs) do
        local here = InArea(obj) and CanEarn(obj)
        if here and not obj.inside then
            obj.inside = true
            if ShouldAnnounce(obj) and WB.Popup and WB.Popup.Show then
                WB.Popup:Show(obj.title, "War Objective", obj.id)
            end
            ShowTracker(obj)
        elseif not here and obj.inside then
            obj.inside = false
            HideTracker(obj)
        end
    end
end
A.ReScan = ReScan

-- ── Loot detection → progress ────────────────────────────────────────────────────────────
-- CHAT_MSG_LOOT is itself the "actual loot interaction" — it fires only for real loot (corpse /
-- node / container), once per item received. So we count directly on it, gated by the SAME check
-- the /run macro uses (InArea) plus the unlock gate: no reliance on a LOOT window opening, which
-- auto-loot gathering often skips (that was the bug — progress never counted).
--
-- Guards: the looter GUID (arg12) must be US when provided (a groupmate's loot line can't credit
-- us), and a short per-objective dedup window swallows an accidental duplicate of the SAME loot.
-- The item is matched as "item:<id>[:|]" so it can't prefix-collide with a longer id (245586 vs
-- 2455860) while still matching the normal "item:245586:<params>" link.
-- Quantity looted: a stack shows as "...|h[Name]|h|rx3." — pull the trailing count; a single item
-- has no "xN", so default 1. Anchored to the end so it can't grab a number from inside the link.
local function LootQuantity(msg)
    return tonumber(msg:match("x(%d+)%.?%s*$")) or 1
end

-- Warn the DEFENDING faction that we're gathering here (mirrors the NPC-objective assault alert
-- in WarEffort_Tracker's AddProgress). Rides the assault machinery — keyed by objective + shard,
-- so the cooldown carries across BOTH oases (same objId), and all the normal throttles apply
-- (60s re-broadcast per key; 10 min chat warning per key on the receiving side). `place` is the
-- live subzone (whichever oasis the loot happened in); gather = true selects the lumber wording.
local function RaiseGatherAlert(obj)
    if not (WB.Alerts and WB.Alerts.RecordAssault) then return end
    local myFaction = WB.faction or UnitFactionGroup("player") or "Horde"
    local myName    = UnitNameUnmodified("player")
    local myRealm   = GetNormalizedRealmName()
    WB.Alerts.RecordAssault({
        objId           = obj.id,
        gather          = true,
        place           = GetMinimapZoneText(),   -- the oasis we're collecting in right now
        zone            = obj.alertZone or (WB.WarEffort.ZoneFromTexture and WB.WarEffort.ZoneFromTexture(obj.texture)) or nil,
        attacker        = (myRealm and myRealm ~= "") and (myName .. "-" .. myRealm) or myName,
        attackerFaction = myFaction,
        defenderFaction = (myFaction == "Alliance") and "Horde" or "Alliance",
        shardId         = WB.ShardDetector and WB.ShardDetector.GetShardID and WB.ShardDetector:GetShardID() or nil,
    })
end

local function OnLootMessage(msg, guid)
    if type(msg) ~= "string" then return end
    if type(guid) == "string" and guid ~= "" and guid ~= UnitGUID("player") then return end
    for _, obj in ipairs(objs) do
        -- _lootPat precomputed in BuildObjs — the loot path allocates nothing until a real match.
        if obj._lootPat and msg:find(obj._lootPat) and InArea(obj) and CanEarn(obj) then
            local now = GetTime()
            if lastCount[obj.id] and (now - lastCount[obj.id]) < DEDUP_WINDOW then return end
            lastCount[obj.id] = now
            if WB.WarEffort and WB.WarEffort.NoteAcquire then
                WB.WarEffort.NoteAcquire(obj.id, LootQuantity(msg))   -- credit the full stack (xN)
                if obj.inside then ShowTracker(obj) end   -- refresh the tracker line if it's showing
            end
            RaiseGatherAlert(obj)   -- warn the enemy faction (throttled inside Alerts)
            return
        end
    end
end

-- ── Objective registry (faction-filtered, built once) ────────────────────────────────────
local function BuildObjs()
    wipe(objs)
    if not (WB.WarEffort and WB.WarEffort.Objectives) then return end
    local myFaction = (WB.faction or UnitFactionGroup("player") or "Horde"):lower()
    for _, obj in ipairs(WB.WarEffort.Objectives) do
        if obj.acquire and type(obj.areaIDs) == "table" and obj.lootItemID and not obj.retired then
            if not obj.faction or obj.faction:lower() == myFaction then
                obj.inside   = false
                obj._lootPat = "item:" .. obj.lootItemID .. "[:|]"   -- precomputed for the loot hot path
                objs[#objs + 1] = obj
            end
        end
    end
end

-- ── Events ───────────────────────────────────────────────────────────────────────────────
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("ZONE_CHANGED")            -- subzone change (minimap zone text)
f:RegisterEvent("ZONE_CHANGED_INDOORS")
f:RegisterEvent("ZONE_CHANGED_NEW_AREA")
f:RegisterEvent("PLAYER_FLAGS_CHANGED")    -- War Mode / PvP flag toggle
f:RegisterEvent("SPELLS_CHANGED")          -- learning Find Lumber unlocks it live
f:RegisterEvent("CHAT_MSG_LOOT")
f:SetScript("OnEvent", function(_, event, ...)
    if event == "PLAYER_LOGIN" then
        BuildObjs()
        ReScan()
    elseif event == "CHAT_MSG_LOOT" then
        local msg = ...                       -- arg1 = loot text; arg12 = looter GUID
        OnLootMessage(msg, select(12, ...))
    else
        -- All the presence signals (zone/subzone/war-mode/unlock/world-enter) re-evaluate.
        ReScan()
    end
end)
