-- WarBoard_SupplyRun.lua
-- "Supply Run" daily delivery events — one per faction, same rules:
--   Horde    — haul lumber from Warsong Lumber Camp (Gorthak) to Tweedle at Hellscream's Watch.
--   Alliance — carry ironwood beams from Astranaar (Faldreas) to Apprentice Boulian at Forest Song.
--
-- Flow: talk to the quartermaster → "loot" the shipment (Begin) → carry it ON FOOT (no mounting,
-- flying, or vehicles) to the delivery NPC (Complete). DAILY: once completed OR failed it's locked
-- until the next reset. A run FAILS on death, logging out mid-run, or crossing a loading screen.
-- State persists per-run in WarBoardCharDB with a daily expiry. A player only ever sees / runs
-- their OWN faction's supply run.

local addonName, WB = ...
WB.SupplyRun = WB.SupplyRun or {}
local SR = WB.SupplyRun

-- ── Run definitions (one per faction) ────────────────────────────────────────────
SR.RUNS = {
    horde = {
        id        = "supply_run_ashenvale_horde",
        faction   = "horde",
        zone      = "Ashenvale",   -- warfront zone key: the run earns the active-warfront bonus
        title     = "Supply Run - Ashenvale",
        desc      = "Deliver lumber from Warsong Lumber Camp to Hellscream's Watch.",
        icon      = "minimap-genericevent-hornicon",
        texture   = "Interface\\QuestionFrame\\Answer-WarBoard-Classic-Ashenvale.blp",
        marker    = { mapID = 63, x = 0.9095, y = 0.5814 },   -- Warsong Lumber Camp
        radius    = 2, goal = 1, gold = 300, warCredit = 150,
        startNPC  = 17304,   -- Overseer Gorthak
        endNPC    = 34395,   -- Tweedle
        lootLink  = "|cff00aa00You receive loot:|r |cffffffff|Hitem:0::::::::::|h[Warsong Lumber]|h|r",
        turnInAt  = "Tweedle at Hellscream's Watch",   -- where stolen cargo is also delivered
        turnInMarker = { mapID = 63, x = 0.3804, y = 0.4384 },   -- Tweedle — X-marked while carrying
        steps = {
            idle   = "Speak to Overseer Gorthak to begin a supply run",
            active = "Deliver the lumber to Tweedle at Hellscream's Watch",
        },
    },
    alliance = {
        id        = "supply_run_ashenvale_alliance",
        faction   = "alliance",
        zone      = "Ashenvale",   -- warfront zone key: the run earns the active-warfront bonus
        title     = "Supply Run - Ashenvale",
        desc      = "Deliver ironwood beams from Astranaar to Forest Song.",
        icon      = "minimap-genericevent-hornicon",
        texture   = "Interface\\QuestionFrame\\Answer-WarBoard-Classic-Ashenvale.blp",
        marker    = { mapID = 63, x = 0.3606, y = 0.5064 },   -- Astranaar
        radius    = 2, goal = 1, gold = 300, warCredit = 150,
        startNPC  = 3996,    -- Faldreas Goeth'Shael
        endNPC    = 17409,   -- Apprentice Boulian
        lootLink  = "|cff00aa00You receive loot:|r |cffffffff|Hitem:0::::::::::|h[Ironwood Beams]|h|r",
        turnInAt  = "Apprentice Boulian at Forest Song",   -- where stolen cargo is also delivered
        turnInMarker = { mapID = 63, x = 0.8661, y = 0.4474 },   -- Boulian — X-marked while carrying
        steps = {
            idle   = "Speak to Faldreas Goeth'Shael to begin a supply run",
            active = "Deliver the ironwood beams to Apprentice Boulian at Forest Song",
        },
    },
}

-- The run for THIS player's faction (resolved at login). Everything below operates on it.
SR.DEF = nil
local function ResolveDef()
    local f = (WB.faction or UnitFactionGroup("player") or "Horde"):lower()
    SR.DEF = SR.RUNS[f]
end

-- Resolve DEF on demand. The War Board frame is built during Frame.lua's PLAYER_LOGIN handler,
-- which runs BEFORE this module's PLAYER_LOGIN (Frame loads earlier in Load.xml) — so DEF would
-- still be nil when the objective list is built and the Supply Run card would be skipped. The
-- frame calls this so the card resolves regardless of handler order (faction is available by
-- login time either way).
function SR.EnsureDef()
    if not SR.DEF then ResolveDef() end
    return SR.DEF
end

-- Warfront zone for a supply-run objId (either faction's) — deterministic from the static RUNS
-- table, so the Verifier can recompute a record's legal warfront bonus without trusting the wire.
function SR.ZoneForId(objId)
    if not objId then return nil end
    for _, def in pairs(SR.RUNS) do
        if def.id == objId then return def.zone end
    end
    return nil
end

-- "NPC at Location" the local player's faction delivers to (used by the stolen-cargo objective,
-- which turns in at the same quartermaster). Falls back to a generic phrase before login.
function SR.TurnInLocation()
    local d = SR.EnsureDef and SR.EnsureDef() or SR.DEF
    return (d and d.turnInAt) or "your quartermaster"
end

-- ── Persistent, daily state ─────────────────────────────────────────────────────
-- WarBoardCharDB.supplyRun[id] = { status, expiresAt }. Everything but "idle" is scoped to the
-- current daily window (expiresAt = next daily reset) and rolls back to "idle" once it passes.
SR.state = SR.state or "idle"   -- "idle" | "active" | "complete" | "failed"

local function DB()
    WarBoardCharDB = WarBoardCharDB or {}
    WarBoardCharDB.supplyRun = WarBoardCharDB.supplyRun or {}
    return WarBoardCharDB.supplyRun
end

local function DailyExpiry()
    local secs = (GetQuestResetTime and GetQuestResetTime()) or 0
    if not secs or secs <= 0 then secs = 86400 end
    return time() + secs
end

local function Save()
    if not SR.DEF then return end
    local e = DB()
    e[SR.DEF.id] = e[SR.DEF.id] or {}
    e[SR.DEF.id].status    = SR.state
    e[SR.DEF.id].expiresAt = (SR.state ~= "idle") and DailyExpiry() or nil
end

function SR:LoadState()
    if not self.DEF then self.state = "idle"; return end
    local e = DB()[self.DEF.id]
    if not e then self.state = "idle"; return end
    if e.status and e.status ~= "idle" and e.expiresAt and time() >= e.expiresAt then
        e.status, e.expiresAt = "idle", nil   -- daily rollover → available again
    end
    self.state = e.status or "idle"
end

-- ── Queries ──────────────────────────────────────────────────────────────────────
function SR:IsActive()  return self.state == "active" end
function SR:HasLumber() return self.state == "active" end            -- carrying the shipment
function SR:CanStart()  return self.state == "idle" end
function SR:StepText()  return self.DEF and (self.DEF.steps[self.state] or self.DEF.steps.idle) or "" end
function SR:Progress()  return self.state == "complete" and (self.DEF and self.DEF.goal or 1) or 0 end

-- ── UI helpers ────────────────────────────────────────────────────────────────────
local function ShowTrackerStep()
    if SR.DEF and WB.Tracker and WB.Tracker.AddObjective then
        WB.Tracker:AddObjective(SR.DEF.id, SR.DEF.title, SR:StepText(), 0, 0)
    end
end
local function HideTracker()
    if SR.DEF and WB.Tracker and WB.Tracker.RemoveObjective then WB.Tracker:RemoveObjective(SR.DEF.id) end
end
local function RefreshCard()
    if WB.UI and WB.UI.RefreshSupplyRunCard then WB.UI.RefreshSupplyRunCard() end
end

-- ── "Supply_Runner": no mounts / flying / vehicles while carrying the shipment ───
-- The load is too heavy. A light ticker dismounts, drops a druid out of Travel/Flight Form, and
-- ejects the player from any vehicle the instant we detect one. Dismount / form-cancel / vehicle
-- exit from an addon are best-effort (they work out of combat); if the client blocks them we've at
-- least warned the player — matching the /run IsMounted()/Dismount() + /cancelaura macros.
local runnerWatch
local function StartRunnerWatch()
    if not runnerWatch then
        runnerWatch = CreateFrame("Frame")
        runnerWatch.t = 0
        runnerWatch:SetScript("OnUpdate", function(self, dt)
            self.t = self.t + (dt or 0)
            if self.t < 0.35 then return end
            self.t = 0
            -- Runs while carrying EITHER a daily run or stolen cargo (same no-mount rule).
            if not (SR:IsActive() or (WB.Cargo and WB.Cargo.IsCarrying())) then self:Hide(); return end
            if UnitOnTaxi and UnitOnTaxi("player") then
                if SR:IsActive() then SR:Fail("taxi") end
                if WB.Cargo and WB.Cargo.IsCarrying() then WB.Cargo.LoseCargo("taxi") end
                return
            end
            local inVehicle = UnitInVehicle and UnitInVehicle("player")
            if IsMounted() or IsFlying() or inVehicle then
                local now = GetTime()
                if not self.lastWarn or (now - self.lastWarn) > 2 then
                    print(WB.TAG .. " Too heavy to mount!")
                    self.lastWarn = 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
        end)
    end
    runnerWatch.t, runnerWatch.lastWarn = 0, nil
    runnerWatch:Show()
end
local function StopRunnerWatch()
    if runnerWatch then runnerWatch:Hide() end
end

-- Exposed so the cargo layer can arm the same no-mount watch when stolen cargo is picked up.
function SR.EnsureCarryWatch() StartRunnerWatch() end

-- ── Lifecycle: Begin / Resume / Fail / Complete ─────────────────────────────────
-- The quartermaster's accept option calls this — loot the shipment and start the run.
function SR:Begin()
    if not self.DEF then return end
    self:LoadState()
    if not self:CanStart() then
        print(WB.TAG .. " You've already run supplies today — come back after the daily reset.")
        return
    end
    if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage(self.DEF.lootLink) end   -- "You receive loot: [...]"
    self.state = "active"
    Save()
    StartRunnerWatch()
    ShowTrackerStep()   -- the delivery instruction lives in the side tracker only
    if WB.Cargo and WB.Cargo.OnCarryChanged then WB.Cargo.OnCarryChanged() end  -- enemies get the alert
    RefreshCard()
end

-- Re-arm an already-active run (a /reload mid-run).
function SR:Resume()
    if not self:IsActive() then return end
    StartRunnerWatch()
    ShowTrackerStep()
    if WB.Cargo and WB.Cargo.OnCarryChanged then WB.Cargo.OnCarryChanged() end   -- re-announce after reload
end

-- Fail the run: locks it out until the daily reset.
function SR:Fail(reason)
    if self.state ~= "active" then return end
    self.state = "failed"
    Save()
    StopRunnerWatch()
    HideTracker()
    if WB.Cargo and WB.Cargo.OnCarryChanged then WB.Cargo.OnCarryChanged() end
    if PlaySound then PlaySound(847, "Master") end   -- "igQuestFailed" sting
    print(WB.TAG .. " Supply Run failed — the shipment is lost. Try again after the daily reset.")
    RefreshCard()
end

-- Complete the run (delivery turn-in). Banks the fixed Supply Run reward as a synced
-- "supply_run" ledger record (feeds gold/WC totals, the leaderboard, and the war bar).
function SR:Complete()
    if not self:IsActive() then return end
    self.state = "complete"
    Save()
    StopRunnerWatch()
    HideTracker()
    if WB.Cargo and WB.Cargo.OnCarryChanged then WB.Cargo.OnCarryChanged() end
    local rw = WB.SUPPLY_REWARD or { gold = 300, warCredit = 150 }
    local g, w
    if WB.WarEffort and WB.WarEffort.RecordSupplyRun then
        g, w = WB.WarEffort.RecordSupplyRun(self.DEF.id, self.DEF.title)   -- includes any warfront bonus
    end
    if PlaySound then PlaySound(619, "Master") end   -- "questcompleted" jingle, like objective completions
    print((WB.TAG .. " Supply Run complete — the shipment was delivered! (+%d gold, +%d war credit)")
        :format(g or rw.gold, w or rw.warCredit))
    RefreshCard()
end

-- ── Proximity → tracker step near the pickup (idle only) ─────────────────────────
local function OnEnter(pt)
    if not (pt and pt.data and pt.data.supplyRun) then return end
    if SR.state == "idle" then ShowTrackerStep() end
end
local function OnLeave(pt)
    if not (pt and pt.data and pt.data.supplyRun) then return end
    if SR.state == "idle" then HideTracker() end   -- an active run keeps its tracker; it follows you
end

local function Register()
    if SR._registered then return end
    if not (SR.DEF and WB.Proximity and WB.Proximity.Register) then return end
    SR._registered = true
    local m = SR.DEF.marker
    WB.Proximity:Register(SR.DEF.id, m.mapID, m.x, m.y, SR.DEF.radius, { supplyRun = true })
    WB.Proximity:OnEnter(OnEnter)
    WB.Proximity:OnLeave(OnLeave)
    if WB.Proximity._UpdateTicker then WB.Proximity._UpdateTicker() end
end

-- ── Events ────────────────────────────────────────────────────────────────────────
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_LOGIN")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:RegisterEvent("PLAYER_DEAD")
ev:SetScript("OnEvent", function(_, event, isLogin, isReload)
    if event == "PLAYER_LOGIN" then
        ResolveDef()
        SR:LoadState()
        Register()
        -- The War Board frame builds its cards BEFORE this handler runs (Frame loads earlier), so
        -- the Supply Run card was drawn from the default "idle" state — sync it to the loaded one
        -- (e.g. show the completed banner after a reload on a finished daily).
        RefreshCard()
    elseif event == "PLAYER_ENTERING_WORLD" then
        if not SR.DEF then ResolveDef() end
        SR:LoadState()
        if SR:IsActive() then
            -- A loading screen mid-run breaks the delivery — UNLESS it's a /reload, in which case
            -- we resume. A fresh login while still "active" means they logged out mid-run → fail.
            if isReload then SR:Resume() else SR:Fail("loading screen / logout") end
        end
    elseif event == "PLAYER_DEAD" then
        if SR:IsActive() then SR:Fail("died") end
    end
end)
