-- WarBoard_Race.lua
-- "Breaking Badlands" — a client-side, no-mounts foot race for BOTH factions. Speak to a Demolitions
-- Analyst II (npc 50512) at the Badlands start line → a 3s countdown → GO → reach the finish line on
-- foot within the time limit. Entirely local: no tracking, no sync, no server. The card + pin + reward
-- live on the War Effort objective of the same id; this file owns the race mechanic.

local addonName, WB = ...
WB.Race = WB.Race or {}
local R = WB.Race

-- Finishing banks the Breaking Badlands objective (150g / 75wc, repeatable at the 10% repeat rule,
-- plus any active-warfront bonus) and records it like any completion. Set false to test silently.
R.REWARD = true

R.DEF = {
    id        = "breaking_badlands_race",                 -- matches the War Effort objective id
    title     = "Breaking Badlands",
    startNPC  = 50512,                                    -- Demolitions Analyst II (this location only)
    start     = { mapID = 15, x = 0.8273, y = 0.3351 },   -- start line (NPC gate + pre-race guidance)
    finish    = { mapID = 15, x = 0.6649, y = 0.3644 },   -- finish line
    radius    = 1,                                        -- start point: 1% map-percent
    finishRadius = 0.5,                                   -- finish line: tighter, 0.5% map-percent
    countdown = 3,                                        -- C_PartyInfo.DoCountdown seconds before GO
    limit     = 45,                                       -- seconds to reach the finish
}

R.state = R.state or "idle"   -- idle | countdown | active | complete | failed
local deadline, startedAt, shownSecs
local START_STEP = "Speak to one of the Demolitions Analyst II to start the race."

-- ── Point checks (npc 50512 is shared across the Badlands → only respond at the start line) ──
function R.AtStart()
    local s = R.DEF.start
    return (WB.Proximity and WB.Proximity.IsWithin
        and WB.Proximity:IsWithin(s.mapID, s.x, s.y, R.DEF.radius)) or false
end
local function AtFinish()
    local f = R.DEF.finish
    return WB.Proximity and WB.Proximity.IsWithin
        and WB.Proximity:IsWithin(f.mapID, f.x, f.y, R.DEF.finishRadius)
end

-- ── Objective-tracker step (side frame) ──────────────────────────────────────────────
local function TrackStep(label)
    if WB.Tracker and WB.Tracker.AddObjective then
        WB.Tracker:AddObjective(R.DEF.id, R.DEF.title, label, 0, 0)
    end
end
local function TrackTimer(label)
    if WB.Tracker and WB.Tracker.SetProgress then WB.Tracker:SetProgress(R.DEF.id, nil, nil, label) end
end
local function TrackClear()
    if WB.Tracker and WB.Tracker.RemoveObjective then WB.Tracker:RemoveObjective(R.DEF.id) end
end

-- ── Race tick: finish + time limit + no-mount enforcement (runs only while live) ─────
local raceTick = CreateFrame("Frame")
raceTick:Hide()
raceTick.acc = 0
local lastMountWarn = 0
raceTick:SetScript("OnUpdate", function(self, dt)
    self.acc = self.acc + (dt or 0)
    if self.acc < 0.1 then return end
    self.acc = 0
    if R.state ~= "active" then self:Hide(); return end
    local now = GetTime()

    if now >= (deadline or 0) then R:Fail("time"); return end   -- out of time

    -- Same restriction as Supply Runners, different message. Taxi can't be dismounted → a fail.
    if UnitOnTaxi and UnitOnTaxi("player") then R:Fail("took a flight path"); return end
    local inVehicle = UnitInVehicle and UnitInVehicle("player")
    if IsMounted() or IsFlying() or inVehicle then
        if now - lastMountWarn > 2 then
            print(WB.TAG .. " |cffff6060No mounts allowed during this race!|r")
            lastMountWarn = now
        end
        if IsFlying() and CancelShapeshiftForm then pcall(CancelShapeshiftForm) end   -- Travel/Flight Form
        if inVehicle and VehicleExit then pcall(VehicleExit) end
        if Dismount then pcall(Dismount) end
    end

    if AtFinish() then R:Complete(); return end   -- crossed the finish line

    -- Live countdown timer — whole seconds remaining.
    local remain = math.max(0, math.ceil((deadline or now) - now))
    if remain ~= shownSecs then
        shownSecs = remain
        TrackTimer(("Reach the finish! Time left: %ds"):format(remain))
    end
end)

-- Fire the client-side "3, 2, 1" big-number countdown (the one BG starts use). Purely local —
-- drives TimerTracker's own OnEvent the way START_PLAYER_COUNTDOWN would, so nothing goes to a
-- party. Best-effort: pcall'd, and does nothing on a client without TimerTracker.
local function ClientCountdown(seconds)
    if not (TimerTracker and TimerTracker.GetScript) then return end
    local onEvent = TimerTracker:GetScript("OnEvent")
    if not onEvent then return end
    pcall(onEvent, TimerTracker, "START_PLAYER_COUNTDOWN", UnitGUID("player"), seconds, seconds, false, UnitName("player"))
end

-- ── Lifecycle ────────────────────────────────────────────────────────────────────────
local function StopRace()
    raceTick:Hide()
    if WB.MapPins and WB.MapPins.HideRaceFinish then WB.MapPins:HideRaceFinish() end   -- clear the X
    if WB.FakeDebuff then WB.FakeDebuff:Hide("race") end   -- drop the race debuff on finish/fail/death
    deadline, startedAt, shownSecs = nil, nil, nil
end

-- GO! — fires after the countdown, unless the run was cancelled (died / left) during it.
local function StartRunning()
    if R.state ~= "countdown" then return end
    R.state    = "active"
    startedAt  = GetTime()
    deadline   = startedAt + R.DEF.limit
    shownSecs  = nil
    lastMountWarn = 0
    if RaidNotice_AddMessage and RaidWarningFrame and ChatTypeInfo then
        RaidNotice_AddMessage(RaidWarningFrame, "GO!", ChatTypeInfo["RAID_WARNING"])
    end
    print(WB.TAG .. " |cff44ff44Breaking Badlands|r — GO! Reach Fuselight-by-the-Sea on foot before time runs out.")
    TrackStep(("Reach the finish! Time left: %ds"):format(R.DEF.limit))
    -- Race debuff: timed to the race limit (45s). Cleared early by StopRace on finish/fail/death.
    -- Not shown in instanced content (a race can't run there anyway — zoning in fails it).
    if WB.FakeDebuff and not (WB.InInstance and WB.InInstance()) then
        WB.FakeDebuff:Show("race", {
            name        = "Breaking Badlands",
            description = "Racing on foot to the finish line before time runs out. Mounting is disabled while this effect is active.",
            icon        = 1100022,
            duration    = R.DEF.limit,   -- 45s
        })
    end
    R.BroadcastStart()   -- warn the enemy faction a race is on (they can try to cut it short)
    raceTick.acc = 0
    raceTick:Show()
end

function R:Begin()
    if not R.AtStart() then
        print(WB.TAG .. " You must be at the start line to begin the race.")
        return
    end
    if self.state == "countdown" or self.state == "active" then return end
    self.state = "countdown"
    -- Get them on foot the instant they accept (pure ground-speed trial), so the countdown runs
    -- unmounted; the race tick keeps them dismounted for the duration.
    if IsMounted() and Dismount then pcall(Dismount) end
    if IsFlying() and CancelShapeshiftForm then pcall(CancelShapeshiftForm) end   -- Travel/Flight Form
    if UnitInVehicle and UnitInVehicle("player") and VehicleExit then pcall(VehicleExit) end
    -- Drop the "X marks the spot" finish marker on the map the moment they accept (visible through
    -- the countdown so they can line up), cleared again on finish/fail via StopRace.
    if WB.MapPins and WB.MapPins.ShowRaceFinish then
        WB.MapPins:ShowRaceFinish(R.DEF.finish.mapID, R.DEF.finish.x, R.DEF.finish.y)
    end
    TrackStep("Race starting\226\128\166")   -- "Race starting…"
    ClientCountdown(R.DEF.countdown)          -- client-side 3-2-1 (no party involved)
    print((WB.TAG .. " Breaking Badlands starts in %d\226\128\166"):format(R.DEF.countdown))
    if C_Timer then C_Timer.After(R.DEF.countdown, StartRunning) end
end

function R:Complete()
    if self.state ~= "active" then return end
    self.state = "complete"
    local secs = startedAt and (GetTime() - startedAt) or 0
    StopRace()
    TrackClear()
    print((WB.TAG .. " |cff44ff44Breaking Badlands|r — finished in |cffffd100%.1fs|r!"):format(secs))
    -- Reward + completion announcements are gated OFF while testing (R.REWARD). Play the
    -- "questcompleted" jingle either way — when rewards ARE on, GrantCompletion plays it itself, so
    -- guard against a double.
    if R.REWARD and WB.WarEffort and WB.WarEffort.GrantObjectiveCompletion then
        WB.WarEffort.GrantObjectiveCompletion(self.DEF.id)   -- banks reward + plays 619
    elseif PlaySound then
        PlaySound(619, "Master")   -- "questcompleted" jingle (matches other objective completions)
    end
end

function R:Fail(reason)
    if self.state ~= "active" and self.state ~= "countdown" then return end
    self.state = "failed"
    StopRace()
    TrackClear()
    if PlaySound then PlaySound(847, "Master") end   -- "igQuestFailed" sting
    local why = (reason == "time") and "out of time" or (reason or "failed")
    print((WB.TAG .. " Breaking Badlands — run failed (%s)."):format(why))
end

-- ── Enemy-race alert ─────────────────────────────────────────────────────────────────
-- Broadcast when a race starts so the OTHER faction learns of it (dying fails the run, so a race is
-- an opening to cut them down). Sprayed like a supply-runner alert; the receiver filters to enemies.
function R.BroadcastStart()
    if not (WB.Comm and WB.Comm.BroadcastAlert) then return end
    if WB.InInstance and WB.InInstance() then return end
    local sub = GetSubZoneText()
    WB.Comm.BroadcastAlert({
        kind          = "race",
        id            = "race:" .. tostring(UnitGUID("player")) .. ":" .. tostring(time()),
        runnerGuid    = UnitGUID("player"),
        runner        = UnitNameUnmodified("player"),
        runnerRealm   = GetNormalizedRealmName() or "",
        runnerFaction = WB.faction or UnitFactionGroup("player") or "Horde",
        place         = (sub and sub ~= "" and sub) or GetZoneText(),
        zone          = GetZoneText(),
        shardId       = WB.ShardDetector and WB.ShardDetector.GetShardID and WB.ShardDetector:GetShardID() or nil,
        t             = time(),
    })
end

-- Receiver: an ENEMY (opposite faction) started a race → chat ping + an Alerts-feed entry. Same-
-- faction races are ignored (we don't alert on our own side; our own broadcast is dropped as self).
function R.HandleRemoteAlert(a)
    if type(a) ~= "table" then return end
    local myFaction = WB.faction or UnitFactionGroup("player")
    if not a.runnerFaction or a.runnerFaction == myFaction then return end
    local runner = (WB.SanitizeDisplay and WB.SanitizeDisplay(a.runner, 32)) or a.runner or "An enemy"
    local zone   = (a.zone and a.zone ~= "") and a.zone or "the Badlands"
    print(("|cffff3030WarBoard Alert:|r %s has started a race in %s! Cut them down before they finish.")
        :format(runner, zone))
    if WB.Alerts and WB.Alerts.AddEvent then
        WB.Alerts.AddEvent({
            id       = "racealert:" .. tostring(a.runnerGuid or runner) .. ":" .. tostring(a.t or time()),
            category = (WB.Alerts.CATEGORY and WB.Alerts.CATEGORY.WORLD) or "world",
            kind     = "event",
            title    = "Enemy Race Underway",
            body     = ("%s has started a race in %s."):format(runner, zone),
        })
    end
end

-- ── Pre-race guidance: at the start line, show the objective in the tracker + the warpopup ──
local function RegisterProximity()
    if not (WB.Proximity and WB.Proximity.Register) then return end
    local s = R.DEF.start
    WB.Proximity:Register(R.DEF.id .. ":start", s.mapID, s.x, s.y, R.DEF.radius, { raceStart = true })
    WB.Proximity:OnEnter(function(pt)
        if not (pt and pt.data and pt.data.raceStart) then return end
        if R.state == "idle" or R.state == "complete" or R.state == "failed" then
            TrackStep(START_STEP)
            if WB.Popup and WB.Popup.Show then WB.Popup:Show(R.DEF.title, "Foot Race", R.DEF.id, 30) end
        end
    end)
    WB.Proximity:OnLeave(function(pt)
        if not (pt and pt.data and pt.data.raceStart) then return end
        if R.state ~= "active" and R.state ~= "countdown" then TrackClear() end   -- a live race keeps its timer
    end)
    if WB.Proximity._UpdateTicker then WB.Proximity._UpdateTicker() end
end

-- ── Events ───────────────────────────────────────────────────────────────────────────
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_LOGIN")
ev:RegisterEvent("PLAYER_DEAD")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:SetScript("OnEvent", function(_, event, isLogin, isReload)
    if event == "PLAYER_LOGIN" then
        RegisterProximity()
    elseif event == "PLAYER_DEAD" then
        if R.state == "active" or R.state == "countdown" then R:Fail("died") end
    elseif event == "PLAYER_ENTERING_WORLD" then
        if (R.state == "active" or R.state == "countdown") and not (isLogin or isReload) then
            R:Fail("left the course")
        end
    end
end)
