-- WarBoard_Cargo.lua
-- Supply Runner / stolen-cargo INTEL, opt-in tracking, and CARGO THEFT.
--
-- Kept separate from WarBoard_SupplyRun.lua (which owns the LOCAL player's DAILY run):
--   * RUNNER / CARRIER broadcast — while you're carrying (a daily supply run OR stolen cargo),
--     broadcast a lightweight alert so the ENEMY faction sees it; a silent refresh keeps it fresh
--     for late logins. NO precise location is shared by default.
--   * VIEWER tracking — enemies get the alert; clicking TRACK opts THAT viewer into live location,
--     reusing the bounty transport (which streams a target's position to any acknowledger). The
--     map shows a distinct icon (priority: stolen cargo > supply runner > bounty).
--   * THEFT — landing the killing blow on an active enemy carrier steals their cargo: you become a
--     stolen-cargo carrier, subject to the same no-mount rule, and can turn it in with NO cooldown.
--
-- Durability = LOCAL persistence (your carried cargo survives /reload) + live comm alerts.
-- Turn-ins BANK real synced rewards: each delivered shipment writes a "supply_run" ledger record
-- (the same fixed WB.SUPPLY_REWARD a daily run pays), validated by the Verifier and counted toward
-- gold/WC totals, the leaderboard, and the war bar.

local addonName, WB = ...
WB.Cargo = WB.Cargo or {}
local C = WB.Cargo

local REFRESH_INTERVAL = 60   -- silent re-broadcast cadence while carrying
-- A turned-in stolen shipment pays the SAME as completing a Supply Run (WB.SUPPLY_REWARD),
-- per shipment. Single source of truth lives in Core so the run/cargo layers and the Verifier
-- all agree; the local alias just keeps older references working.
C.REWARD = WB.SUPPLY_REWARD or { gold = 300, warCredit = 150 }

local function MyFaction() return WB.faction or UnitFactionGroup("player") or "Horde" end
local function Shard()
    return WB.ShardDetector and WB.ShardDetector.GetShardID and WB.ShardDetector:GetShardID() or nil
end
local function AlertId(guid) return "supply:" .. tostring(guid) end

-- ── Stolen-cargo carrier state (persistent — survives /reload) ────────────────────
-- A carrier can hold MULTIPLE stolen shipments; each is one record.
local function CargoDB()
    WarBoardCharDB = WarBoardCharDB or {}
    WarBoardCharDB.stolenCargo = WarBoardCharDB.stolenCargo or {}   -- array of { id, fromGuid, fromRunner, fromFaction, stolenAt }
    return WarBoardCharDB.stolenCargo
end
function C.CarriedCount() return #CargoDB() end
function C.IsCarrying()   return #CargoDB() > 0 end

-- Am I carrying ANYTHING (daily run or stolen cargo), and what's the highest-priority type?
local function AmICarrying()
    return (WB.SupplyRun and WB.SupplyRun:IsActive()) or C.IsCarrying()
end
local function CurrentCarrierType()
    if C.IsCarrying() then return "stolen_cargo" end                       -- stolen outranks the daily run
    if WB.SupplyRun and WB.SupplyRun:IsActive() then return "supply_run" end
    return nil
end

-- ── Broadcast (RUNNER / CARRIER side) ─────────────────────────────────────────────
local broadcastTicker
local function BuildPayload(carrierType)
    local sub = GetSubZoneText()
    local p = {
        kind          = "supply_runner",
        id            = AlertId(UnitGUID("player")),
        runnerGuid    = UnitGUID("player"),
        runner        = UnitNameUnmodified("player"),
        runnerRealm   = GetNormalizedRealmName() or "",
        runnerFaction = MyFaction(),
        place         = (sub and sub ~= "" and sub) or GetZoneText(),
        zone          = GetZoneText(),
        shardId       = Shard(),
        carrierType   = carrierType or "supply_run",
        cargoCount    = C.CarriedCount(),
        isBounty      = (WB.Bounties and WB.Bounties.GetBountyOnMe and WB.Bounties.GetBountyOnMe()) and true or false,
    }
    if carrierType == "stolen_cargo" then
        -- The victims this cargo was taken from, so a viewer tracking one of them can follow the
        -- cargo to the THIEF instead of losing it at the corpse (tracking continuity).
        local from = {}
        for _, rec in ipairs(CargoDB()) do
            if rec.fromGuid then from[#from + 1] = rec.fromGuid end
        end
        p.stolenFrom = from
    end
    return p
end
local function Broadcast(carrierType)
    if not (WB.Comm and WB.Comm.BroadcastAlert) then return end
    if WB.InInstance and WB.InInstance() then return end
    WB.Comm.BroadcastAlert(BuildPayload(carrierType))
end
local function EndBroadcast()
    if broadcastTicker then broadcastTicker:Cancel(); broadcastTicker = nil end
    if WB.Comm and WB.Comm.BroadcastAlert then WB.Comm.BroadcastAlert(BuildPayload("ended")) end
end

-- ── "Stolen Cargo!" objective (a tracker step ONLY — no War Effort card) ──────────
-- Shown while carrying stolen cargo; the instruction points at the same quartermaster the daily
-- run delivers to. Removed automatically once the cargo is turned in / lost.
local STOLEN_OBJ_ID = "wb_stolen_cargo"
function C.RefreshStolenObjective()
    if not (WB.Tracker and WB.Tracker.AddObjective) then return end
    local n = C.CarriedCount()
    if n > 0 then
        local where = (WB.SupplyRun and WB.SupplyRun.TurnInLocation and WB.SupplyRun.TurnInLocation())
                      or "your quartermaster"
        local label = (n > 1) and ("Deliver %d seized shipments to %s"):format(n, where)
                              or  ("Deliver the seized supplies to %s"):format(where)
        WB.Tracker:AddObjective(STOLEN_OBJ_ID, "Stolen Cargo!", label, 0, 0)
    elseif WB.Tracker.RemoveObjective then
        WB.Tracker:RemoveObjective(STOLEN_OBJ_ID)
    end
end

-- Re-evaluate what I'm carrying and (re)broadcast the matching alert, or end it. Called whenever
-- carrying state changes (run begin/end, cargo stolen/turned-in/lost) and on reload resume.
function C.OnCarryChanged()
    C.RefreshStolenObjective()   -- add/remove the "Stolen Cargo!" tracker step to match the cargo
    local ct = CurrentCarrierType()
    if not ct then EndBroadcast(); return end
    Broadcast(ct)
    if not broadcastTicker then
        broadcastTicker = C_Timer.NewTicker(REFRESH_INTERVAL, function()
            local t = CurrentCarrierType()
            if t then Broadcast(t) else EndBroadcast() end
        end)
    end
end

-- ── VIEWER side: receive a remote carrier alert ──────────────────────────────────
-- guid -> { faction, carrierType, name, realm } of enemy carriers we currently know about
-- (from alerts). Drives both the map role and the "can I steal from this kill?" check.
C.knownCarriers = C.knownCarriers or {}
C.robbedNotified = C.robbedNotified or {}   -- [thiefGuid] = true once we've told the victim who robbed them

function C.HandleRemoteAlert(a)
    if type(a) ~= "table" then return end
    if a.runnerFaction and MyFaction() == a.runnerFaction then return end   -- ally carrier → not our intel
    if a.carrierType == "ended" then
        if a.runnerGuid then
            C.knownCarriers[a.runnerGuid] = nil
            C.robbedNotified[a.runnerGuid] = nil   -- they dropped it → re-notify if they rob me again later
            C.StopTrackingCarrier(a.runnerGuid)   -- drop their now-stale map icon at once (don't wait to time out)
        end
        if WB.Alerts and WB.Alerts.RemoveSupplyRunner then
            WB.Alerts.RemoveSupplyRunner(a.id or AlertId(a.runnerGuid))
        end
        return
    end
    if a.runnerGuid then
        C.knownCarriers[a.runnerGuid] = {
            faction = a.runnerFaction, carrierType = a.carrierType, name = a.runner, realm = a.runnerRealm,
        }
    end
    if WB.Alerts and WB.Alerts.AddSupplyRunner then WB.Alerts.AddSupplyRunner(a, true) end
    -- If this stolen cargo was seized from ME, tell me who has it (once per thief, until they drop it).
    if a.carrierType == "stolen_cargo" and a.runnerGuid and type(a.stolenFrom) == "table" then
        local myGuid = UnitGUID("player")
        for _, victimGuid in ipairs(a.stolenFrom) do
            if myGuid and victimGuid == myGuid then
                if not C.robbedNotified[a.runnerGuid] then
                    C.robbedNotified[a.runnerGuid] = true
                    print((WB.TAG .. " Your supply cargo was seized by %s! Cut them down to take it back.")
                        :format(a.runner or "an enemy"))
                end
                break
            end
        end
    end
    -- Tracking continuity: a stolen-cargo alert names the victims it was taken from. If I was
    -- tracking one of them (or just was — recentlyTracked survives the victim's "ended", whichever
    -- arrives first), move my track to the THIEF so the cargo doesn't vanish at the corpse.
    if a.carrierType == "stolen_cargo" and a.runnerGuid and not C.roles[a.runnerGuid]
       and type(a.stolenFrom) == "table" then
        for _, victimGuid in ipairs(a.stolenFrom) do
            local wasTracking = C.roles[victimGuid]
                or (C.recentlyTracked[victimGuid] and (time() - C.recentlyTracked[victimGuid]) < 90)
            if wasTracking and victimGuid ~= a.runnerGuid then
                C.StopTrackingCarrier(victimGuid)
                C.recentlyTracked[victimGuid] = nil   -- consumed → don't re-transfer on the next refresh
                C.TrackAlert(a)                        -- follow the thief
                break
            end
        end
    end
end

-- ── VIEWER side: opt-in tracking (reuses the bounty location transport) ──────────
C.roles = C.roles or {}   -- guid -> "stolen_cargo" | "supply_runner" (for the map icon priority)
C.recentlyTracked = C.recentlyTracked or {}   -- [guid] = time() I last opted into tracking a carrier
function C.RoleFor(guid) return guid and C.roles[guid] or nil end
function C.ClearRole(guid) if guid then C.roles[guid] = nil end end

-- Stop following a carrier we were tracking (they died / delivered / lost the cargo). Clears the
-- map role AND the live track — which removes their world-map icon immediately instead of leaving
-- it to go stale ~130s later. Left alone only if they're ALSO a live bounty (the bounty keeps them).
function C.StopTrackingCarrier(guid)
    if not guid or not C.roles[guid] then return end
    C.roles[guid] = nil
    local stillBounty = WB.Bounties and WB.Bounties.IsAccepted and WB.Bounties.IsAccepted(guid)
    if not stillBounty and WB.BountyTracker and WB.BountyTracker.StopTracking then
        WB.BountyTracker.StopTracking(guid)
    end
end

-- Are we still hearing live carrier alerts for this guid? knownCarriers is populated from each
-- alert and cleared the moment their run ENDS (the "ended" alert), so a tracker can tell an
-- active carrier apart from a stale map flag whose owner has already failed / delivered / lost
-- the cargo — even while the bounty location transport is still streaming their position.
function C.IsKnownCarrier(guid) return (guid and C.knownCarriers[guid]) and true or false end

function C.TrackAlert(al)
    if type(al) ~= "table" then return end
    local guid = al.runnerGuid
    if not guid or guid == "" then
        print(WB.TAG .. " Can't track this carrier — no target id on the alert.")
        return
    end
    C.roles[guid] = (al.carrierType == "stolen_cargo") and "stolen_cargo" or "supply_runner"
    C.recentlyTracked[guid] = time()
    if WB.BountyTracker and WB.BountyTracker.StartHunting then
        WB.BountyTracker.StartHunting({ targetGuid = guid, targetName = al.runner, targetRealm = al.runnerRealm })
        print((WB.TAG .. " Tracking %s — open the map to follow them."):format(al.runner or "the carrier"))
    end
    if WB.UI and WB.UI.RefreshAlerts then WB.UI.RefreshAlerts() end
end

function C.TrackState(al)
    local guid = type(al) == "table" and al.runnerGuid
    if guid and WB.BountyTracker and WB.BountyTracker.GetTrackState then
        return WB.BountyTracker.GetTrackState(guid)
    end
    return "idle"
end

-- ── THEFT: killing blow on an active enemy carrier steals their cargo ─────────────
-- Lightweight validation: the victim must be a KNOWN active carrier of the OPPOSITE faction.
local function StealFrom(guid, info)
    local db = CargoDB()
    db[#db + 1] = {
        id = "cargo:" .. tostring(guid) .. ":" .. tostring(time()),
        fromGuid = guid, fromRunner = info.name, fromFaction = info.faction, stolenAt = time(),
    }
    C.knownCarriers[guid] = nil   -- they're down; their own client clears their alert
    C.StopTrackingCarrier(guid)   -- I downed them → clear their (now-stale) map icon on my side too
    if WB.SupplyRun and WB.SupplyRun.EnsureCarryWatch then WB.SupplyRun.EnsureCarryWatch() end  -- no mounting now
    C.OnCarryChanged()            -- broadcast my stolen-cargo alert + raise the "Stolen Cargo!" objective
    if WB.Popup and WB.Popup.Show then
        WB.Popup:Show("Stolen Cargo!", "Recovered Supplies", STOLEN_OBJ_ID, 15)
    end
    print((WB.TAG .. " You seized the enemy's cargo from %s! Deliver it to your quartermaster — no daily limit.")
        :format(info.name or "an enemy"))
    if WB.UI and WB.UI.RefreshAlerts then WB.UI.RefreshAlerts() end
end

local function OnKill(attackerGUID, targetGUID)
    if not targetGUID then return end
    if WB.InInstance and WB.InInstance() then return end
    local me, pet = UnitGUID("player"), UnitGUID("pet")
    if attackerGUID ~= me and not (pet and attackerGUID == pet) then return end   -- must be my killing blow
    local info = C.knownCarriers[targetGUID]
    if not info then return end                                                    -- victim wasn't a known carrier
    if info.faction == MyFaction() then return end                                 -- must be opposite faction
    StealFrom(targetGUID, info)
end

-- ── CARRIER lifecycle: lose / turn in stolen cargo ───────────────────────────────
-- Lose ALL stolen cargo (death, logout, loading screen, taxi). If an enemy landed the killing
-- blow, THEY steal it (their own PARTY_KILL handles the gain) — from our side it's simply gone.
function C.LoseCargo(reason)
    if not C.IsCarrying() then return end
    wipe(CargoDB())
    C.OnCarryChanged()   -- ends my alert (unless I still have a daily run going)
    print(WB.TAG .. " You lost the stolen cargo" .. (reason and (" (" .. reason .. ")") or "") .. ".")
    if WB.UI and WB.UI.RefreshAlerts then WB.UI.RefreshAlerts() end
end

-- Turn stolen cargo in at your quartermaster. NO daily cooldown. Each shipment banks the fixed
-- Supply Run reward (WB.SUPPLY_REWARD) as its own synced "supply_run" ledger record — same as
-- completing a daily run — so the payout, leaderboard, and war bar all count it.
function C.TurnIn()
    local n = C.CarriedCount()
    if n == 0 then return false end
    local rw = WB.SUPPLY_REWARD or { gold = 300, warCredit = 150 }
    wipe(CargoDB())
    C.OnCarryChanged()
    if WB.WarEffort and WB.WarEffort.RecordSupplyRun then
        for _ = 1, n do WB.WarEffort.RecordSupplyRun("stolen_cargo", "Recovered Stolen Supplies") end
    end
    if PlaySound then PlaySound(619, "Master") end   -- "questcompleted" jingle, like objective completions
    print((WB.TAG .. " Delivered %d stolen shipment(s)! (+%d gold, +%d war credit each)")
        :format(n, rw.gold, rw.warCredit))
    if WB.UI and WB.UI.RefreshAlerts then WB.UI.RefreshAlerts() end
    return true
end

-- ── Events ────────────────────────────────────────────────────────────────────────
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_LOGIN")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:RegisterEvent("PLAYER_DEAD")
ev:RegisterEvent("PARTY_KILL")
ev:SetScript("OnEvent", function(_, event, a1, a2)
    if event == "PLAYER_LOGIN" then
        if C.IsCarrying() then
            if WB.SupplyRun and WB.SupplyRun.EnsureCarryWatch then WB.SupplyRun.EnsureCarryWatch() end
        end
    elseif event == "PLAYER_ENTERING_WORLD" then
        -- isLogin, isReload = a1, a2. A reload keeps the cargo (and re-announces); a fresh login or
        -- any zone loading screen loses it (matches the daily run's rule).
        if C.IsCarrying() then
            if a2 then C.OnCarryChanged() else C.LoseCargo("left the field") end
        end
    elseif event == "PLAYER_DEAD" then
        if C.IsCarrying() then C.LoseCargo("died") end   -- an enemy killer steals it via their PARTY_KILL
    elseif event == "PARTY_KILL" then
        OnKill(a1, a2)   -- a1 = attackerGUID, a2 = targetGUID
    end
end)
