-- Move all animals from one husbandry placeable to another.
-- Wraps RLM's RLAnimalMoveService.moveAnimals so the grazing scheduler can
-- transfer cows barn->pasture and pasture->barn with one call.
--
-- moveType string "SOURCE" tells RLM the source is initiating the move (we
-- read animals from source's clusterSystem before dispatching).

AnimalMover = {}
local AnimalMover_mt = Class(AnimalMover)

function AnimalMover.new()
    local self = setmetatable({}, AnimalMover_mt)
    return self
end

local function isUsable(placeable)
    if placeable == nil or placeable.isDeleted == true then return false end
    if placeable.rootNode == nil or placeable.rootNode == 0 then return false end
    return placeable.spec_husbandryAnimals ~= nil
end

local function listAnimalsAt(placeable)
    local spec = placeable.spec_husbandryAnimals
    if spec == nil or spec.clusterSystem == nil then return {} end
    local cs = spec.clusterSystem
    local out = {}
    if type(cs.getAnimals) == "function" then
        local ok, list = pcall(function() return cs:getAnimals() end)
        if ok and type(list) == "table" then
            for _, a in pairs(list) do out[#out + 1] = a end
            return out
        end
    end
    if cs.animals ~= nil then
        for _, a in pairs(cs.animals) do out[#out + 1] = a end
    end
    return out
end

-- ROOT-CAUSE FIX (Option A) for the post-move health dip.
--
-- RLRM disabled the vanilla on-animal-change food refresh (it no-ops
-- onHusbandryAnimalsUpdate on every food-type spec) and only recomputes each
-- husbandry's food demand (spec_husbandryFood.litersPerHour) at the TAIL of
-- PlaceableHusbandry.onHourChanged -- AFTER the base body's updateFeeding has
-- already consumed it. So on the first hour tick after a herd arrives at a pen,
-- litersPerHour is stale (0 for a previously-empty pasture): updateFeeding ->
-- consumeFood(type, 0, ...) -> foodFactor 0 -> RLRM's steep updateHealth docks
-- ~35 health. It recovers the next hour once RLRM's tail refresh finally runs.
--
-- refreshFoodDemand primes litersPerHour to reflect the CURRENT clusters,
-- exactly the way RL_PlaceableHusbandry:onHourChanged does (per-animal
-- updateInput/updateOutput, then placeable:updateInputAndOutput). Running it
-- right after a move closes the seam so the next hour tick reads real demand.
-- Does NOT touch health and consumes NO food (unlike the old updateFeeding
-- nudge, which prematurely ate an hour of food and never fixed litersPerHour).
-- Server-only; every call pcall-guarded.
-- Fetch the real cluster OBJECTS (each an RLM animal record with getInput/
-- updateInput), the same way RL_PlaceableHusbandry:onHourChanged does. Prefer
-- getClusters() over the cs.animals fallback, which can be an id->count map
-- rather than cluster objects (see PastureHerding.lua note on that map).
local function clustersOf(placeable)
    local spec = placeable and placeable.spec_husbandryAnimals
    if spec == nil then return {} end
    if type(spec.getClusters) == "function" then
        local ok, list = pcall(function() return spec:getClusters() end)
        if ok and type(list) == "table" then return list end
    end
    local cs = spec.clusterSystem
    if cs ~= nil and type(cs.getClusters) == "function" then
        local ok, list = pcall(function() return cs:getClusters() end)
        if ok and type(list) == "table" then return list end
    end
    return {}
end

local function refreshFoodDemand(placeable, label)
    if placeable == nil or placeable.spec_husbandryAnimals == nil then return end
    if placeable.isServer ~= true then return end
    if type(placeable.updateInputAndOutput) ~= "function" then return end
    local animals = clustersOf(placeable)
    local temp = 20
    local env = g_currentMission and g_currentMission.environment
    local tu = env and env.weather and env.weather.temperatureUpdater
    if tu ~= nil and type(tu.currentMin) == "number" then temp = tu.currentMin end
    for _, a in pairs(animals) do
        if type(a) == "table" then
            if type(a.updateInput) == "function" then pcall(function() a:updateInput() end) end
            if type(a.updateOutput) == "function" then pcall(function() a:updateOutput(temp) end) end
        end
    end
    pcall(function() placeable:updateInputAndOutput(animals) end)
    PastureHerdingLog.dev("AnimalMover: refreshFoodDemand(%s) clusters=%d", tostring(label), #animals)
end

-- Prime food/water demand + poke visual recompute flags after a move.
local function nudgeFeeding(placeable, label)
    if placeable == nil then return end
    refreshFoodDemand(placeable, label)
    local spec = placeable.spec_husbandryAnimals
    if spec ~= nil then
        if spec.clusterSystem ~= nil then spec.clusterSystem.needsUpdate = true end
        if spec.clusterHusbandry ~= nil then spec.clusterHusbandry.visualUpdatePending = true end
    end
end

-- Deferred refresh queue: the move is async — animals materialize at the
-- destination a moment (sometimes several frames) after dispatch, so a single
-- refresh at dispatch time would loop the still-empty dest and leave
-- litersPerHour at 0. We also enqueue source + dest for a short window and
-- re-run refreshFoodDemand a few times as the herd settles, guaranteeing
-- demand is correct well before the next hourly updateFeeding tick.
local AnimalMover_pendingRefresh = {}
local AnimalMover_REFRESH_WINDOW_MS = 6000
local AnimalMover_REFRESH_EVERY_MS  = 750

local function enqueueRefresh(placeable, label)
    if placeable == nil then return end
    local now = (g_currentMission and g_currentMission.time) or 0
    for _, e in ipairs(AnimalMover_pendingRefresh) do
        if e.placeable == placeable then
            e.untilMs = now + AnimalMover_REFRESH_WINDOW_MS  -- extend the window
            return
        end
    end
    AnimalMover_pendingRefresh[#AnimalMover_pendingRefresh + 1] = {
        placeable = placeable, label = label,
        untilMs = now + AnimalMover_REFRESH_WINDOW_MS, nextMs = 0,
    }
end

-- Called every frame from PastureHerding:update. Throttled per-entry.
function AnimalMover.tickPendingRefresh()
    if #AnimalMover_pendingRefresh == 0 then return end
    local now = (g_currentMission and g_currentMission.time) or 0
    for i = #AnimalMover_pendingRefresh, 1, -1 do
        local e = AnimalMover_pendingRefresh[i]
        if now > e.untilMs then
            table.remove(AnimalMover_pendingRefresh, i)
        elseif now >= e.nextMs then
            e.nextMs = now + AnimalMover_REFRESH_EVERY_MS
            refreshFoodDemand(e.placeable, e.label)
        end
    end
end

local function nudgeBoth(source, dest)
    nudgeFeeding(source, "source")
    nudgeFeeding(dest, "dest")
    enqueueRefresh(source, "source")
    enqueueRefresh(dest, "dest")
end

-- (Removed: the carryHealth / captureHealth / tickPendingHealth pending-hold
-- machinery and carryProductivity. The refreshFoodDemand fix above keeps
-- health flat across moves (verified: health-hold lifts were 0), so these
-- were dead weight. Grazing-pasture productivity is handled by the grass
-- floor; the barn recovers via vanilla/RLRM.)

local function resolveMoveService()
    if rawget(_G, "RLAnimalMoveService") ~= nil then return _G.RLAnimalMoveService end
    if RLAnimalMoveService ~= nil then return RLAnimalMoveService end
    return nil
end

local function resolveMoveEvent()
    if rawget(_G, "AnimalMoveEvent") ~= nil then return _G.AnimalMoveEvent end
    if AnimalMoveEvent ~= nil then return AnimalMoveEvent end
    return nil
end

-- Move a SINGLE animal source -> destination. Used by the per-walker grazing
-- transition flow so each cow's data move pairs 1:1 with its walker's spawn.
function AnimalMover:moveOne(sourcePlaceable, destinationPlaceable, animal, callback)
    if not isUsable(sourcePlaceable) then return false, "source unusable" end
    if not isUsable(destinationPlaceable) then return false, "dest unusable" end
    if animal == nil then return false, "no animal" end

    local svc = resolveMoveService()
    if svc ~= nil and type(svc.moveAnimals) == "function" then
        local okCall, err = pcall(function()
            svc.moveAnimals(sourcePlaceable, destinationPlaceable, { animal }, "SOURCE", callback, nil)
        end)
        if okCall then
            nudgeBoth(sourcePlaceable, destinationPlaceable)
            return true, "dispatched 1 animal via RLAnimalMoveService"
        end
        PastureHerdingLog.warning("AnimalMover.moveOne: service raised: %s; falling back to direct", tostring(err))
    end

    local evtClass = resolveMoveEvent()
    if evtClass == nil or type(evtClass.new) ~= "function" then
        return false, "AnimalMoveEvent missing"
    end
    if g_client == nil or g_client.getServerConnection == nil then
        return false, "no client/server connection"
    end
    local okSend, err = pcall(function()
        local evt = evtClass.new(sourcePlaceable, destinationPlaceable, { animal }, "SOURCE")
        g_client:getServerConnection():sendEvent(evt)
    end)
    if not okSend then return false, "send raised: " .. tostring(err) end
    nudgeBoth(sourcePlaceable, destinationPlaceable)
    return true, "dispatched 1 animal via AnimalMoveEvent"
end

-- Returns: ok (bool), msg (string). If ok, the move has been DISPATCHED;
-- the actual transfer happens asynchronously on the server.
function AnimalMover:moveAll(sourcePlaceable, destinationPlaceable, callback)
    if not isUsable(sourcePlaceable) then
        return false, "source has no spec_husbandryAnimals"
    end
    if not isUsable(destinationPlaceable) then
        return false, "destination has no spec_husbandryAnimals"
    end
    if sourcePlaceable == destinationPlaceable then
        return false, "source and destination are the same placeable"
    end

    local animals = listAnimalsAt(sourcePlaceable)
    if #animals == 0 then
        return false, "source has 0 animals"
    end

    PastureHerdingLog.dev("AnimalMover: dispatching move of %d animals from %s to %s",
        #animals,
        tostring(sourcePlaceable.getName and sourcePlaceable:getName() or sourcePlaceable),
        tostring(destinationPlaceable.getName and destinationPlaceable:getName() or destinationPlaceable))

    -- Preferred: RLM's service wrapper (does notifications + callback wiring)
    local svc = resolveMoveService()
    if svc ~= nil and type(svc.moveAnimals) == "function" then
        local okCall, err = pcall(function()
            svc.moveAnimals(sourcePlaceable, destinationPlaceable, animals, "SOURCE", callback, nil)
        end)
        if okCall then
            nudgeBoth(sourcePlaceable, destinationPlaceable)
            return true, string.format("dispatched %d animals via RLAnimalMoveService", #animals)
        end
        PastureHerdingLog.warning("AnimalMover: RLAnimalMoveService.moveAnimals raised: %s; falling back to direct AnimalMoveEvent",
            tostring(err))
    end

    -- Fallback: construct AnimalMoveEvent directly. This is the underlying
    -- mechanism RLAnimalMoveService uses; works without the service wrapper
    -- if the global is sandboxed away from our scope.
    local evtClass = resolveMoveEvent()
    if evtClass == nil or type(evtClass.new) ~= "function" then
        return false, "AnimalMoveEvent class not available (engine global missing)"
    end
    if g_client == nil or g_client.getServerConnection == nil then
        return false, "g_client/server connection missing — singleplayer host required"
    end

    local okSend, err = pcall(function()
        local evt = evtClass.new(sourcePlaceable, destinationPlaceable, animals, "SOURCE")
        g_client:getServerConnection():sendEvent(evt)
    end)
    if not okSend then
        return false, string.format("AnimalMoveEvent send raised: %s", tostring(err))
    end
    nudgeBoth(sourcePlaceable, destinationPlaceable)
    return true, string.format("dispatched %d animals via AnimalMoveEvent (direct)", #animals)
end
