HusbandryHook = {}
local HusbandryHook_mt = Class(HusbandryHook)

function HusbandryHook.new(framework)
    local self = setmetatable({}, HusbandryHook_mt)
    self.framework = framework
    self.installed = false
    return self
end

local function dumpFields(t, label)
    if t == nil then return label .. "=<nil>" end
    local keys = {}
    for k, v in pairs(t) do
        local vt = type(v)
        if vt == "number" or vt == "string" or vt == "boolean" then
            keys[#keys + 1] = string.format("%s(%s)=%s", tostring(k), vt, tostring(v))
        else
            keys[#keys + 1] = string.format("%s(%s)", tostring(k), vt)
        end
    end
    return label .. "={" .. table.concat(keys, ", ") .. "}"
end

local function pickNode(...)
    for i = 1, select("#", ...) do
        local v = select(i, ...)
        if v ~= nil and v ~= 0 then return v end
    end
    return nil
end

local function getNavigationNode(placeable)
    local spec = placeable and placeable.spec_husbandryAnimals
    if spec == nil then return nil end
    return pickNode(
        spec.navigationMeshRootNode,
        spec.clusterHusbandry and spec.clusterHusbandry.navigationNode or nil,
        spec.navigationMesh,
        spec.navigationNode
    )
end

local function logSpecShape(placeable)
    local spec = placeable and placeable.spec_husbandryAnimals
    if spec == nil then
        PastureHerdingLog.warning("logSpecShape: no spec_husbandryAnimals")
        return
    end
    PastureHerdingLog.dev(dumpFields(spec, "spec_husbandryAnimals"))
    if spec.clusterHusbandry ~= nil then
        PastureHerdingLog.dev(dumpFields(spec.clusterHusbandry, "clusterHusbandry"))
    end
    if spec.clusterSystem ~= nil then
        PastureHerdingLog.dev(dumpFields(spec.clusterSystem, "clusterSystem"))
    end
end

local function describeNavigationNode(node)
    if node == nil or node == 0 then return "<nil/0>" end
    local parts = { string.format("id=%d", node) }
    if getName ~= nil then
        local ok, name = pcall(getName, node)
        if ok and name ~= nil then parts[#parts + 1] = "name=" .. tostring(name) end
    end
    if getNumOfChildren ~= nil then
        local ok, n = pcall(getNumOfChildren, node)
        if ok then parts[#parts + 1] = string.format("children=%d", n or -1) end
    end
    if getWorldTranslation ~= nil then
        local ok, x, y, z = pcall(getWorldTranslation, node)
        if ok then parts[#parts + 1] = string.format("worldPos=(%.1f,%.1f,%.1f)", x, y, z) end
    end
    -- (Skipped getShapeBuildNavMeshMask probe: the engine throws a script-error
    -- when called on object/transform-group nodes, which all husbandry nav
    -- nodes are. The pcall catches it but FS25 still logs the stack trace.)
    return table.concat(parts, " ")
end

local createCall = 0

HusbandryHook.lastClusterHusbandry = nil
HusbandryHook.lastPlaceable = nil
HusbandryHook.pendingAutoSubstitute = {}

local function loggedCreateWrapper(self, superFunc, xmlFilename, navigationNode, raycastDistance, collisionMask)
    createCall = createCall + 1
    local n = createCall
    local animalType = (self and self.animalTypeName) or "?"
    PastureHerdingLog.dev("[create#%d] AnimalClusterHusbandry:create IN: animalType=%s navNode={%s} xml=%s raycast=%s collisionMask=%s",
        n, tostring(animalType), describeNavigationNode(navigationNode), tostring(xmlFilename),
        tostring(raycastDistance), tostring(collisionMask))
    local result = superFunc(self, xmlFilename, navigationNode, raycastDistance, collisionMask)
    PastureHerdingLog.dev("[create#%d] AnimalClusterHusbandry:create OUT: returned=%s self.husbandryId=%s",
        n, tostring(result), tostring(self and self.husbandryId))
    HusbandryHook.lastClusterHusbandry = self
    HusbandryHook.lastPlaceable = self and self.placeable

    -- Register with NavMeshLayer here too. The placeable-level
    -- onPostFinalizePlacement hook doesn't fire for fenceless-style
    -- placeables, so without this they'd be invisible to the sweep that
    -- auto-migrates empty pens.
    local fw = g_pastureHerding
    if fw ~= nil and fw.navMeshLayer ~= nil and self ~= nil and self.placeable ~= nil then
        local nodeName = navigationNode and getName ~= nil and getName(navigationNode) or ""
        if not nodeName:find("PastureHerding_navMesh_", 1, true) then
            if fw.navMeshLayer:getMeshForHusbandry(self.placeable) == nil then
                fw.navMeshLayer:registerHusbandry(self.placeable)
            end
        end
    end

    -- Queue auto-substitute for the next tick. Do NOT substitute synchronously
    -- here: at this point the engine is mid-way through the placeable finalize
    -- chain (PlaceableHusbandryFence -> createHusbandry -> this -> still to
    -- come: onHusbandryAnimalsCreated which PlaceableHusbandryFood/Water/etc.
    -- listen to). deleteHusbandry now would invalidate sub-entity ids those
    -- downstream specs are about to use, causing "Unknown entity id" errors.
    local framework = g_pastureHerding
    if framework ~= nil and framework.autoSubstituteOnPlacement and self ~= nil and self.placeable ~= nil then
        local nodeName = navigationNode and getName ~= nil and getName(navigationNode) or ""
        if not nodeName:find("PastureHerding_navMesh_", 1, true) then
            HusbandryHook.pendingAutoSubstitute[self.placeable] = { createCallNum = n }
            PastureHerdingLog.dev("[autoSubst] create#%d queued for next-tick substitute", n)
        end
    end
    return result
end

local function countAnimalsInSpec(spec)
    local n = 0
    if spec == nil or spec.clusterSystem == nil then return 0 end
    for _ in pairs(spec.clusterSystem.animals or {}) do n = n + 1 end
    return n
end

function HusbandryHook.processPendingAutoSubstitute()
    local framework = g_pastureHerding
    if framework == nil or framework.navMeshLayer == nil then return end
    for placeable, meta in pairs(HusbandryHook.pendingAutoSubstitute) do
        HusbandryHook.pendingAutoSubstitute[placeable] = nil
        if placeable.isDeleted == true or placeable.rootNode == nil or placeable.rootNode == 0 then
            PastureHerdingLog.dev("[autoSubst] dropping queued substitute for vanished placeable (create#%s)",
                tostring(meta.createCallNum))
        else
            local existing = framework.navMeshLayer:getMeshForHusbandry(placeable)
            local nAnimals = countAnimalsInSpec(placeable.spec_husbandryAnimals)
            if existing ~= nil and existing.fabricatedMeshId ~= nil then
                PastureHerdingLog.dev("[autoSubst] already substituted; skipping (create#%s)",
                    tostring(meta.createCallNum))
            elseif nAnimals > 0 then
                PastureHerdingLog.dev("[autoSubst] skipping create#%s: pen has %d animals (auto-substitute is empty-pens-only; populated pens stay on engine mesh)",
                    tostring(meta.createCallNum), nAnimals)
            else
                local contour, err = ContourReader.readOutdoorContourPolygon(placeable)
                if contour ~= nil then
                    PastureHerdingLog.dev("[autoSubst] applying substitute (%d pts) on empty pen from create#%s",
                        #contour / 3, tostring(meta.createCallNum))
                    framework.navMeshLayer:substituteContour(placeable, contour)
                else
                    PastureHerdingLog.dev("[autoSubst] skipped: %s", tostring(err))
                end
            end
        end
    end
end

local clusterHookState = { earlyInstalled = false, lateInstalled = false }

local function installClusterCreateHook(phase)
    if AnimalClusterHusbandry == nil or AnimalClusterHusbandry.create == nil then
        PastureHerdingLog.warning("[%s] AnimalClusterHusbandry.create not available", phase)
        return false
    end
    AnimalClusterHusbandry.create = Utils.overwrittenFunction(AnimalClusterHusbandry.create, loggedCreateWrapper)
    PastureHerdingLog.dev("[%s] hooked AnimalClusterHusbandry.create (current fn=%s)",
        phase, tostring(AnimalClusterHusbandry.create))
    return true
end

local function installCreateHusbandryHook()
    if not clusterHookState.earlyInstalled then
        clusterHookState.earlyInstalled = installClusterCreateHook("early-install")
    end
end

function HusbandryHook.installLateClusterHook()
    if not clusterHookState.lateInstalled then
        clusterHookState.lateInstalled = installClusterCreateHook("late-install")
    end
end

-- Hook RLM's per-husbandry setClusters so we can FILTER OUT animals that
-- are currently in transit (being represented by a walker). The husbandry's
-- updateVisuals iterates nextUpdateClusters which setClusters populates; by
-- excluding in-transit animals here we prevent any duplicate visual at
-- either source or destination during the walk.
local setClustersHookInstalled = false
function HusbandryHook.installSetClustersHook()
    if setClustersHookInstalled then return end
    -- setClusters is defined on the VANILLA AnimalClusterHusbandry class;
    -- RLM uses it (calls spec.clusterHusbandry:setClusters(animals)) but
    -- doesn't override it. Hook the vanilla class so we filter at the
    -- canonical entry point.
    if AnimalClusterHusbandry == nil then
        PastureHerdingLog.warning("HusbandryHook: AnimalClusterHusbandry not available; transit suppression won't filter visuals")
        return
    end
    local target = AnimalClusterHusbandry
    if type(target.setClusters) ~= "function" then
        PastureHerdingLog.warning("HusbandryHook: AnimalClusterHusbandry.setClusters missing; transit suppression disabled")
        return
    end

    target.setClusters = Utils.overwrittenFunction(target.setClusters,
        function(self, superFunc, animals)
            local fw = g_pastureHerding
            local tracker = fw and fw.transitTracker
            if tracker == nil or animals == nil then
                return superFunc(self, animals)
            end
            local filtered = {}
            local hidden = 0
            for _, a in pairs(animals) do
                if tracker:isInTransit(a) then
                    hidden = hidden + 1
                else
                    filtered[#filtered + 1] = a
                end
            end
            if hidden > 0 then
                PastureHerdingLog.dev("setClusters: hid %d in-transit animals (had %d, passed %d)",
                    hidden, #animals, #filtered)
            end
            return superFunc(self, filtered)
        end)
    setClustersHookInstalled = true
    PastureHerdingLog.dev("HusbandryHook: installed RLM setClusters transit-suppression hook")
end

local function summarizeContour(c)
    if c == nil then return "<nil>" end
    if type(c) ~= "table" then return string.format("<%s:%s>", type(c), tostring(c)) end
    local n = #c
    if n == 0 then
        local count = 0
        for _ in pairs(c) do count = count + 1 end
        return string.format("table(non-array, keys=%d)", count)
    end
    local elt = c[1]
    if type(elt) == "number" then
        local pairsN = math.floor(n / 2)
        return string.format("flatNumbers(len=%d ~%d pairs first=%.2f,%.2f last=%.2f,%.2f)",
            n, pairsN, c[1] or 0, c[2] or 0, c[n-1] or 0, c[n] or 0)
    elseif type(elt) == "table" then
        local first = c[1]
        local last = c[n]
        local keys = {}
        for k in pairs(first) do keys[#keys + 1] = tostring(k) end
        return string.format("tableOfTables(n=%d keys={%s} first={%s} last={%s})",
            n, table.concat(keys, ","),
            tostring(first and (first.x or first[1])),
            tostring(last and (last.x or last[1])))
    end
    return string.format("table(n=%d eltType=%s)", n, type(elt))
end

local contourCall = 0

local function loggedCreateNavMeshFromContour(self, superFunc, ...)
    contourCall = contourCall + 1
    local n = contourCall
    local nargs = select("#", ...)
    local parts = {}
    for i = 1, nargs do
        local v = select(i, ...)
        local t = type(v)
        if t == "table" and (i == 1 or v[1] ~= nil) then
            parts[#parts + 1] = string.format("arg%d=%s", i, summarizeContour(v))
        else
            parts[#parts + 1] = string.format("arg%d(%s)=%s", i, t, tostring(v))
        end
    end
    PastureHerdingLog.dev("[contour#%d] createNavigationMeshFromContour IN: nargs=%d %s",
        n, nargs, table.concat(parts, " | "))
    local r1, r2, r3 = superFunc(self, ...)
    PastureHerdingLog.dev("[contour#%d] createNavigationMeshFromContour OUT: r1=%s r2=%s r3=%s",
        n, tostring(r1), tostring(r2), tostring(r3))
    return r1, r2, r3
end

local contourHookState = { lateInstalled = false }

function HusbandryHook.installContourHook()
    if contourHookState.lateInstalled then return end
    if PlaceableHusbandryAnimals == nil or PlaceableHusbandryAnimals.createNavigationMeshFromContour == nil then
        PastureHerdingLog.warning("HusbandryHook: PlaceableHusbandryAnimals.createNavigationMeshFromContour not present")
        return
    end
    PlaceableHusbandryAnimals.createNavigationMeshFromContour = Utils.overwrittenFunction(
        PlaceableHusbandryAnimals.createNavigationMeshFromContour,
        loggedCreateNavMeshFromContour
    )
    contourHookState.lateInstalled = true
    PastureHerdingLog.dev("HusbandryHook: hooked createNavigationMeshFromContour")
end

function HusbandryHook:install()
    if self.installed then return end

    installCreateHusbandryHook()

    if PlaceableHusbandryAnimals == nil then
        PastureHerdingLog.warning("HusbandryHook: PlaceableHusbandryAnimals not available; skipping install")
        return
    end

    local framework = self.framework

    local function handlePlacementFinalize(eventName, placeable)
        if framework.navMeshLayer:getMeshForHusbandry(placeable) == nil then
            framework.navMeshLayer:registerHusbandry(placeable)
        end

        if eventName ~= "onPostFinalizePlacement" then return end

        -- Phase 1c: opt-in destroy+recreate test (5m square)
        if framework.shrinkNextPen == true then
            framework.shrinkNextPen = false -- one-shot
            local spec = placeable.spec_husbandryAnimals
            local navNode = spec and (spec.navigationMeshRootNode or spec.navigationMesh)
            local cx, cy, cz = 0, 0, 0
            if navNode ~= nil and navNode ~= 0 and getWorldTranslation ~= nil then
                cx, cy, cz = getWorldTranslation(navNode)
            end
            local r = framework.shrinkRadius or 5.0
            local contour = ContourBuilder.buildSquareAround3D(cx, cy, cz, r)
            PastureHerdingLog.dev("Phase1c: substituting contour at center (%.2f,%.2f,%.2f) radius=%.2f",
                cx, cy, cz, r)
            framework.navMeshLayer:substituteContour(placeable, contour)
            return
        end

        -- Auto-substitute now happens in the AnimalClusterHusbandry.create
        -- hook (loggedCreateWrapper) — it fires reliably for every placeable
        -- type, including fenceless-style ones that bypass onPostFinalizePlacement.
    end

    if PlaceableHusbandryAnimals.onFinalizePlacement ~= nil then
        PlaceableHusbandryAnimals.onFinalizePlacement = Utils.appendedFunction(
            PlaceableHusbandryAnimals.onFinalizePlacement,
            function(placeable) handlePlacementFinalize("onFinalizePlacement", placeable) end
        )
    end

    PlaceableHusbandryAnimals.onPostFinalizePlacement = Utils.appendedFunction(
        PlaceableHusbandryAnimals.onPostFinalizePlacement,
        function(placeable) handlePlacementFinalize("onPostFinalizePlacement", placeable) end
    )

    PlaceableHusbandryAnimals.onDelete = Utils.appendedFunction(
        PlaceableHusbandryAnimals.onDelete,
        function(placeable)
            framework.navMeshLayer:unregister(placeable)
            -- Also drop it from the grazing registry. Without this a SOLD barn
            -- or pasture stays in the scheduler's per-frame loops, which then
            -- call getTranslation on the now-dead node and crash every frame
            -- ("Unknown entity id ... in getTranslation"). unregister() removes
            -- the entry and reindexes the rest.
            if framework.grazingRegistry ~= nil
               and type(framework.grazingRegistry.unregister) == "function" then
                local ok, err = pcall(function()
                    framework.grazingRegistry:unregister(placeable)
                end)
                if not ok then
                    PastureHerdingLog.warning(
                        "HusbandryHook.onDelete: grazingRegistry:unregister raised: %s", tostring(err))
                end
            end
        end
    )

    -- NOTE: onExternalNavigationMeshLoaded is the engine's async-i3d-load
    -- callback (signature: self, node, failedReason, args) for the husbandry's
    -- own nav-mesh i3d file. It is NOT a "tell me which mesh to use" hook.
    -- Mesh handover to the husbandry is deferred until we find the correct
    -- engine entry point (likely overwriting createAnimalHusbandry to pass
    -- our nav mesh in).

    if framework.hasRLM and RealisticLivestock_AnimalClusterHusbandry ~= nil then
        RealisticLivestock_AnimalClusterHusbandry.getAnimalPosition = Utils.appendedFunction(
            RealisticLivestock_AnimalClusterHusbandry.getAnimalPosition,
            function() end
        )
        RealisticLivestock_AnimalClusterHusbandry.updateVisuals = Utils.appendedFunction(
            RealisticLivestock_AnimalClusterHusbandry.updateVisuals,
            function() end
        )
        PastureHerdingLog.dev("HusbandryHook: reserved RLM seams (getAnimalPosition, updateVisuals)")
    end

    -- GRAZING OUTPUT BOOST (server-only): beef gain weight and dairy give more
    -- milk the more they graze GOOD grass, scaled by the herd's grazing condition
    -- (0..1). These wrap RLM's per-animal Animal:updateWeight / Animal:updateOutput
    -- — the same overwrite pattern RLM uses on itself. Per-animal variation comes
    -- free from RLM genetics (metabolism / productivity). Weight/milk are
    -- server-owned and RLM syncs them, so we only run on the server.
    if framework.hasRLM and Animal ~= nil
       and type(Animal.updateWeight) == "function"
       and type(Animal.updateOutput) == "function" then

        -- animal -> husbandry (clusterSystem.owner) -> our barn -> condition.
        -- Returns 0 for anything not in a registered barn, so the boost is a
        -- strict no-op for every other animal on the map.
        local function grazingConditionOf(animal)
            if animal == nil then return 0 end
            local cs = animal.clusterSystem
            local owner = cs ~= nil and cs.owner or nil
            if owner == nil then return 0 end
            local reg = framework.grazingRegistry
            if reg == nil or type(reg.getBarnIndexByPlaceable) ~= "function" then return 0 end
            local barnIdx = reg:getBarnIndexByPlaceable(owner)
            if barnIdx == nil then return 0 end
            return reg:getGrazingCondition(barnIdx) or 0
        end

        local function isServer()
            return g_currentMission ~= nil and g_currentMission.getIsServer ~= nil
                   and g_currentMission:getIsServer()
        end

        -- BEEF WEIGHT: while grazing, briefly lift targetWeight so RLM grows the
        -- animal toward a higher ceiling at its own metabolism-varied,
        -- month-normalised rate, then restore it immediately (nothing persists /
        -- compounds). Housing → no lift → RLM's own decrease trims the excess
        -- back over a few in-game days. Weight drives sale price.
        Animal.updateWeight = Utils.overwrittenFunction(Animal.updateWeight,
            function(self, superFunc, foodFactor)
                if not isServer() then return superFunc(self, foodFactor) end
                local cond = grazingConditionOf(self)
                if cond > 0 and type(self.targetWeight) == "number" then
                    local saved = self.targetWeight
                    self.targetWeight = saved * (1 + (framework.grazeWeightBonus or 0.10) * cond)
                    -- ALWAYS restore, even if RLM's updateWeight throws — otherwise the
                    -- lifted target sticks and next hour compounds off the inflated value.
                    local ok, err = pcall(superFunc, self, foodFactor)
                    self.targetWeight = saved
                    if not ok then error(err) end
                else
                    superFunc(self, foodFactor)
                end
            end)

        -- DAIRY MILK: after RLM computes per-hour output, scale milk by the
        -- condition. Beef produce no milk so this is a no-op for them; per-cow
        -- variation comes from RLM genetics.productivity inside updateOutput.
        Animal.updateOutput = Utils.overwrittenFunction(Animal.updateOutput,
            function(self, superFunc, temp)
                superFunc(self, temp)
                if not isServer() then return end
                local out = self.output
                if out == nil then return end
                local milk = out["milk"]
                if type(milk) == "number" and milk > 0 then
                    local cond = grazingConditionOf(self)
                    if cond > 0 then
                        out["milk"] = milk * (1 + (framework.grazeMilkBonus or 0.15) * cond)
                    end
                end
            end)

        PastureHerdingLog.dev("HusbandryHook: installed grazing output-boost hooks (Animal.updateWeight/updateOutput)")
    end

    self.installed = true
    PastureHerdingLog.dev("HusbandryHook: installed")
end
