NavMeshLayer = {}
local NavMeshLayer_mt = Class(NavMeshLayer)

function NavMeshLayer.new(framework)
    local self = setmetatable({}, NavMeshLayer_mt)
    self.framework = framework
    self.meshes = {}
    self.readinessWatch = {} -- [husbandryId] = { startedAt, lastResult, lastChecked }
    return self
end

function NavMeshLayer:watchReadiness(husbandryId, navMeshId, clusterHusbandry, label)
    if husbandryId == nil or husbandryId == 0 then return end
    self.readinessWatch[husbandryId] = {
        navMeshId = navMeshId,
        clusterHusbandry = clusterHusbandry,
        label = label or "?",
        startedAt = (g_currentMission and g_currentMission.time) or 0,
        lastResult = nil,
        lastChecked = 0,
        checks = 0,
    }
    PastureHerdingLog.dev("readinessWatch: started for husbandryId=%s navMesh=%s (%s)",
        tostring(husbandryId), tostring(navMeshId), tostring(label))
end

function NavMeshLayer:pollReadiness()
    if isHusbandryReady == nil then return end
    local now = (g_currentMission and g_currentMission.time) or 0
    for husbandryId, w in pairs(self.readinessWatch) do
        if now - w.startedAt > 30000 then
            PastureHerdingLog.dev("readinessWatch: giving up on husbandryId=%s after 30s (last=%s, checks=%d)",
                tostring(husbandryId), tostring(w.lastResult), w.checks)
            self.readinessWatch[husbandryId] = nil
        elseif now - w.lastChecked >= 500 then
            w.lastChecked = now
            w.checks = w.checks + 1
            local ok, ready = pcall(isHusbandryReady, husbandryId)
            local result = (ok and ready) or false
            if result ~= w.lastResult then
                local area = "?"
                if getNavMeshSurfaceArea ~= nil and w.navMeshId ~= nil then
                    local okA, a = pcall(getNavMeshSurfaceArea, w.navMeshId)
                    if okA then area = tostring(a) end
                end
                PastureHerdingLog.dev("readinessWatch: husbandryId=%s isReady %s -> %s at t+%dms (navMesh=%s area=%s, %s)",
                    tostring(husbandryId), tostring(w.lastResult), tostring(result),
                    now - w.startedAt, tostring(w.navMeshId), area, tostring(w.label))
                w.lastResult = result
                if result == true then
                    -- Re-bind visuals. Calling clusterHusbandry:updateVisuals()
                    -- alone is NOT enough — it reads from nextUpdateClusters
                    -- which is nil after our recreate. The proper entry point
                    -- is placeable.spec_husbandryAnimals:updateVisualAnimals(),
                    -- which does: animals = clusterSystem:getAnimals();
                    -- clusterHusbandry:setClusters(animals);
                    -- clusterHusbandry:updateVisuals(). That populates the
                    -- visual queue before processing.
                    local ch = w.clusterHusbandry
                    local placeable = ch and ch.placeable
                    local spec = placeable and placeable.spec_husbandryAnimals
                    if spec ~= nil and type(spec.updateVisualAnimals) == "function" then
                        if ch ~= nil then ch.visualUpdatePending = true end
                        local okV, errV = pcall(function() placeable:updateVisualAnimals() end)
                        if okV then
                            PastureHerdingLog.dev("readinessWatch: ran spec:updateVisualAnimals on husbandryId=%s after readiness arrived",
                                tostring(husbandryId))
                        else
                            PastureHerdingLog.warning("readinessWatch: updateVisualAnimals raised: %s", tostring(errV))
                        end
                    elseif ch ~= nil and type(ch.updateVisuals) == "function" then
                        -- Fallback for non-RLM contexts
                        ch.visualUpdatePending = true
                        pcall(function() ch:updateVisuals(true) end)
                    end
                    self.readinessWatch[husbandryId] = nil
                end
            end
        end
    end
end

-- Use the placeable table itself as the key. NetworkUtil.getObjectId(placeable)
-- is unstable: it returns nil during early load and gets assigned later, which
-- caused entries to be tracked under one key by substituteContour and a different
-- key by the sweep — making the sweep see a stale fabricatedMeshId=nil and
-- repeatedly destroy+recreate the mesh.
local function placeableKey(placeable)
    return placeable
end

local function keyToLog(key)
    if type(key) == "table" then return tostring(key) end
    return tostring(key)
end

local function husbandryHasAnimals(spec)
    if spec == nil then return false end
    local cs = spec.clusterSystem
    if cs == nil then return false end
    if cs.animals ~= nil then
        for _ in pairs(cs.animals) do return true end
    end
    if cs.clusters ~= nil then
        for _ in pairs(cs.clusters) do return true end
    end
    return false
end

local function countAnimals(spec)
    local n = 0
    if spec == nil then return 0 end
    local cs = spec.clusterSystem
    if cs == nil then return 0 end
    if cs.animals ~= nil then
        for _ in pairs(cs.animals) do n = n + 1 end
    end
    return n
end

function NavMeshLayer:registerHusbandry(placeable)
    if placeable == nil then return nil end
    local spec = placeable.spec_husbandryAnimals
    if spec == nil then return nil end

    local key = placeableKey(placeable)
    if self.meshes[key] ~= nil then
        return self.meshes[key]
    end

    local meshId = spec.navigationMesh
        or (spec.clusterHusbandry and spec.clusterHusbandry.navigationNode)
    local rootNode = spec.navigationMeshRootNode

    if meshId == nil or meshId == 0 then
        -- Benign: this husbandry simply isn't registered with the (experimental)
        -- engine-steering layer. The herd walk uses Herding Lite spawns, not
        -- engine steering, so this is not a problem — keep it out of normal logs.
        PastureHerdingLog.dev("NavMeshLayer: placeable %s has no engine nav mesh; not steerable (Herding Lite path unaffected)",
            tostring(key))
        return nil
    end

    local animalType = (spec.clusterHusbandry and spec.clusterHusbandry.animalTypeName) or "?"

    local cx, cy, cz
    if meshId ~= nil and meshId ~= 0 and getWorldTranslation ~= nil then
        cx, cy, cz = getWorldTranslation(meshId)
    end

    local entry = {
        placeable = placeable,
        meshId = meshId,
        rootNode = rootNode,
        animalTypeName = animalType,
        centerX = cx, centerY = cy, centerZ = cz,
    }
    self.meshes[key] = entry

    PastureHerdingLog.dev("NavMeshLayer: registered husbandry %s (animalType=%s meshId=%s rootNode=%s center=(%.2f,%.2f,%.2f))",
        tostring(key), animalType, tostring(meshId), tostring(rootNode),
        cx or 0, cy or 0, cz or 0)
    return entry
end

function NavMeshLayer:getCenter(placeable)
    local entry = self.meshes[placeableKey(placeable)]
    if entry == nil then return nil end
    return entry.centerX, entry.centerY, entry.centerZ
end

function NavMeshLayer:getMeshForHusbandry(placeable)
    return self.meshes[placeableKey(placeable)]
end

function NavMeshLayer:unregister(placeable)
    local key = placeableKey(placeable)
    local entry = self.meshes[key]
    if entry == nil then return end
    -- Free fabricated engine resources before dropping the entry. onDelete
    -- calls unregister BEFORE sweepDeadEntries can ever see the entry, and
    -- these nodes are root-level scene objects (not parented under the
    -- placeable), so the engine won't reap them — they'd leak. Do NOT touch
    -- originalMeshId (the engine owns that).
    if entry.fabricatedMeshId ~= nil then
        ContourBuilder.delete(entry.fabricatedMeshId)
    end
    if entry.fabricatedPositioning ~= nil then
        ContourBuilder.deletePositioning(entry.fabricatedPositioning)
    end
    self.meshes[key] = nil
    PastureHerdingLog.dev("NavMeshLayer: unregistered husbandry %s", tostring(key))
end

function NavMeshLayer:count()
    local n = 0
    for _ in pairs(self.meshes) do n = n + 1 end
    return n
end

local function isPlaceableAlive(placeable)
    if placeable == nil then return false end
    if placeable.isDeleted == true then return false end
    if placeable.rootNode == nil or placeable.rootNode == 0 then return false end
    if entityExists ~= nil and not entityExists(placeable.rootNode) then return false end
    return true
end

function NavMeshLayer:sweepDeadEntries()
    local removed = 0
    for key, entry in pairs(self.meshes) do
        if not isPlaceableAlive(entry.placeable) then
            if entry.fabricatedMeshId ~= nil then
                ContourBuilder.delete(entry.fabricatedMeshId)
                PastureHerdingLog.dev("sweepDeadEntries: deleted orphaned fabricated mesh %s for vanished placeable %s",
                    tostring(entry.fabricatedMeshId), tostring(key))
            end
            if entry.fabricatedPositioning ~= nil then
                ContourBuilder.deletePositioning(entry.fabricatedPositioning)
            end
            self.meshes[key] = nil
            removed = removed + 1
        end
    end
    return removed
end

function NavMeshLayer:sweepEmptyPensForAutoMigrate()
    local framework = self.framework
    if framework == nil or not framework.autoSubstituteOnPlacement then return 0 end
    local migrated = 0
    for key, entry in pairs(self.meshes) do
        if isPlaceableAlive(entry.placeable)
           and entry.fabricatedMeshId == nil
           and not entry.autoMigrateAttempted then
            local spec = entry.placeable.spec_husbandryAnimals
            if spec ~= nil and not husbandryHasAnimals(spec) then
                local contour = ContourReader.readOutdoorContourPolygon(entry.placeable)
                if contour ~= nil then
                    PastureHerdingLog.dev("autoMigrate: pen %s is empty + has outdoorContourPolygon (%d pts); substituting now",
                        tostring(key), #contour / 3)
                    if self:substituteContour(entry.placeable, contour) then
                        migrated = migrated + 1
                    else
                        -- Substitution failed — mark attempted so we don't
                        -- re-read the contour and re-attempt every 2s forever
                        -- with no back-off (deleteHusbandry may already have
                        -- run, leaving the pen husbandry-less).
                        entry.autoMigrateAttempted = true
                    end
                else
                    -- Mark so we don't keep re-checking pens that will never have a polygon (e.g. vanilla).
                    entry.autoMigrateAttempted = true
                end
            end
        end
    end
    return migrated
end

local function isActiveGrazingPasture(framework, placeable)
    if framework == nil or framework.grazingRegistry == nil then return false end
    local barnIdx, pastureIdx = framework.grazingRegistry:findPastureRef(placeable)
    if barnIdx == nil then return false end
    local barn = framework.grazingRegistry:getBarn(barnIdx)
    return barn ~= nil and barn.activeIndex == pastureIdx
end

local function isRegisteredGrazingPasture(framework, placeable)
    if framework == nil or framework.grazingRegistry == nil then return false end
    local barnIdx = framework.grazingRegistry:findPastureRef(placeable)
    return barnIdx ~= nil
end

function NavMeshLayer:drawDebugContours()
    if drawDebugLine == nil then return end
    local framework = self.framework
    for _, entry in pairs(self.meshes) do
        if isPlaceableAlive(entry.placeable) then
            local c = entry.lastContour
            -- substituteContour is refused while the pen holds animals (so on a
            -- save loaded WITH cows at pasture, lastContour is never set). Fall
            -- back to the pen's live outdoor contour so the outline still draws.
            if (c == nil or #c < 9 or (#c % 3) ~= 0) and ContourReader ~= nil
               and type(ContourReader.readOutdoorContourPolygon) == "function" then
                local ok, poly = pcall(function()
                    return ContourReader.readOutdoorContourPolygon(entry.placeable)
                end)
                if ok and type(poly) == "table" then c = poly end
            end
            if c ~= nil and #c >= 9 and (#c % 3) == 0 then
                local n = #c / 3
                local r, g, b
                if isActiveGrazingPasture(framework, entry.placeable) then
                    r, g, b = 0.1, 1.0, 0.1  -- bright green: today's grazing
                elseif isRegisteredGrazingPasture(framework, entry.placeable) then
                    r, g, b = 0.2, 0.4, 1.0  -- blue: resting pasture (in rotation)
                else
                    r, g, b = 0.6, 0.6, 0.6  -- grey: substituted but not in a grazing rotation
                end
                local yLift = 0.2
                for i = 0, n - 1 do
                    local i0 = i * 3
                    local i1 = ((i + 1) % n) * 3
                    local x1, y1, z1 = c[i0 + 1], c[i0 + 2] + yLift, c[i0 + 3]
                    local x2, y2, z2 = c[i1 + 1], c[i1 + 2] + yLift, c[i1 + 3]
                    drawDebugLine(x1, y1, z1, r, g, b, x2, y2, z2, r, g, b)
                end
            end
        end
    end

    -- Also outline the NORMAL in-game husbandry pastures that are NOT under our
    -- custom nav mesh (other/vanilla pens) so they can be compared / seen for
    -- registration. Orange = not managed by the mod. Uses the vanilla outdoor
    -- contour (fenced pens only; indoor barns return nil and are skipped).
    local sys = g_currentMission and g_currentMission.placeableSystem
    if sys ~= nil and type(sys.placeables) == "table"
       and ContourReader ~= nil and type(ContourReader.readOutdoorContourPolygon) == "function" then
        for _, p in pairs(sys.placeables) do
            if type(p) == "table" and p.spec_husbandryAnimals ~= nil
               and isPlaceableAlive(p) and self:getMeshForHusbandry(p) == nil then
                local ok, poly = pcall(function() return ContourReader.readOutdoorContourPolygon(p) end)
                if ok and type(poly) == "table" and #poly >= 9 and (#poly % 3) == 0 then
                    local n = #poly / 3
                    local r, g, b = 1.0, 0.55, 0.1  -- orange: not mod-managed
                    local yLift = 0.2
                    for i = 0, n - 1 do
                        local i0 = i * 3
                        local i1 = ((i + 1) % n) * 3
                        drawDebugLine(poly[i0 + 1], poly[i0 + 2] + yLift, poly[i0 + 3], r, g, b,
                                      poly[i1 + 1], poly[i1 + 2] + yLift, poly[i1 + 3], r, g, b)
                    end
                end
            end
        end
    end
end

function NavMeshLayer:substituteContour(placeable, contour3D, options)
    options = options or {}
    local spec = placeable and placeable.spec_husbandryAnimals
    if spec == nil then
        PastureHerdingLog.error("substituteContour: placeable has no spec_husbandryAnimals")
        return false
    end
    local clusterHusbandry = spec.clusterHusbandry
    if clusterHusbandry == nil then
        PastureHerdingLog.error("substituteContour: no clusterHusbandry on placeable")
        return false
    end
    local preCount = countAnimals(spec)
    if preCount > 0 and not options.allowAnimals then
        PastureHerdingLog.error("substituteContour: refusing — pen has %d animals (pass options.allowAnimals=true to override)", preCount)
        return false
    end
    if preCount > 0 then
        PastureHerdingLog.warning("substituteContour: pen has %d animals; proceeding with destroy+recreate (animal preservation depends on RLM redistribution)", preCount)
    end

    local xmlFilename = clusterHusbandry.xmlFilename
    local raycastDistance = clusterHusbandry.raycastDistance or 10
    local collisionMask = clusterHusbandry.collisionMask
    if xmlFilename == nil or collisionMask == nil then
        PastureHerdingLog.error("substituteContour: clusterHusbandry missing xmlFilename or collisionMask")
        return false
    end

    PastureHerdingLog.dev("substituteContour: fabricating navMesh from %d-point contour for placeable %s",
        contour3D and (#contour3D / 3) or 0, tostring(NetworkUtil.getObjectId(placeable) or placeable))

    -- Build the nav mesh straight from the contour. We used to try
    -- ContourBuilder.fabricateFromShapes first, but a runtime plane shape has no
    -- CPU-accessible mesh, so the engine's buildNavMesh ALWAYS rejected it
    -- ("missing CPU mesh flag" / "no triangles collected for navigation mesh")
    -- and we fell back to this contour path anyway — emitting two engine
    -- warnings per pasture for zero benefit. The contour builder produces a
    -- working nav mesh, so go straight to it.
    local newMeshId, newMeshName, newPositioning = ContourBuilder.fabricate(contour3D, collisionMask)
    if newMeshId == nil then return false end

    local oldMeshId = clusterHusbandry.navigationNode

    -- Cache pen center now while the OLD nav node still exists and (if it's
    -- the engine's mesh) is at the real pen world position. Subsequent
    -- substitutes preserve this cache so reshape can read the original center
    -- even after our fabricated mesh (worldPos 0,0,0) replaces it.
    local key = placeableKey(placeable)
    local existingEntry = self.meshes[key]
    local cachedCx, cachedCy, cachedCz
    if existingEntry ~= nil and existingEntry.centerX ~= nil
       and not (existingEntry.centerX == 0 and existingEntry.centerY == 0 and existingEntry.centerZ == 0) then
        cachedCx, cachedCy, cachedCz = existingEntry.centerX, existingEntry.centerY, existingEntry.centerZ
    elseif oldMeshId ~= nil and oldMeshId ~= 0 and getWorldTranslation ~= nil then
        cachedCx, cachedCy, cachedCz = getWorldTranslation(oldMeshId)
    end

    PastureHerdingLog.dev("substituteContour: calling clusterHusbandry:deleteHusbandry()")
    local okDel, errDel = pcall(function() clusterHusbandry:deleteHusbandry() end)
    if not okDel then
        PastureHerdingLog.error("substituteContour: deleteHusbandry failed: %s", tostring(errDel))
        ContourBuilder.delete(newMeshId)
        return false
    end

    PastureHerdingLog.dev("substituteContour: calling clusterHusbandry:create(xml=%s, newMeshId=%s, raycast=%s, collisionMask=%s)",
        tostring(xmlFilename), tostring(newMeshId), tostring(raycastDistance), tostring(collisionMask))
    local okCreate, errCreate = pcall(function()
        clusterHusbandry:create(xmlFilename, newMeshId, raycastDistance, collisionMask)
    end)
    if not okCreate then
        PastureHerdingLog.error("substituteContour: create with new mesh raised: %s", tostring(errCreate))
        ContourBuilder.delete(newMeshId)
        return false
    end

    if clusterHusbandry.husbandryId == nil then
        PastureHerdingLog.error("substituteContour: create returned but clusterHusbandry.husbandryId is nil — engine rejected our mesh")
        ContourBuilder.delete(newMeshId)
        return false
    end

    local postCount = countAnimals(spec)
    PastureHerdingLog.dev("substituteContour: SUCCESS — new husbandryId=%s navMesh=%s ('%s') animals before=%d after=%d",
        tostring(clusterHusbandry.husbandryId), tostring(newMeshId), tostring(newMeshName), preCount, postCount)

    self:watchReadiness(clusterHusbandry.husbandryId, newMeshId, clusterHusbandry,
        string.format("preCount=%d", preCount))

    -- Force RLM to re-spawn animal visuals: deleteHusbandry destroyed each
    -- animal's VisualAnimal instance, and the freshly-created husbandry has
    -- no visuals attached yet. Setting visualUpdatePending tells RLM to
    -- re-bind on its next tick; calling updateVisuals directly does it now.
    clusterHusbandry.visualUpdatePending = true
    if type(clusterHusbandry.updateVisuals) == "function" then
        local okV, errV = pcall(function() clusterHusbandry:updateVisuals(true) end)
        if not okV then
            PastureHerdingLog.warning("substituteContour: updateVisuals(true) raised: %s", tostring(errV))
        else
            PastureHerdingLog.dev("substituteContour: ran updateVisuals(true) to rebind visuals")
        end
    end

    -- Re-fire onHusbandryAnimalsCreated so the placeable's dependent specs
    -- (water, food, meadow, ...) refresh their cached husbandryId. Each spec
    -- listens for this event, stores the new id, and re-registers its feeding
    -- places via addFeedingPlace. Without this, an hourly tick later will call
    -- updateFeedingPlace with the deleted husbandry id and the engine raises
    -- "Unknown entity id" — exactly the bug we hit when a player emptied a
    -- pen via the RLM move screen.
    if SpecializationUtil ~= nil and SpecializationUtil.raiseEvent ~= nil then
        local okE, errE = pcall(function()
            SpecializationUtil.raiseEvent(placeable, "onHusbandryAnimalsCreated", clusterHusbandry.husbandryId)
        end)
        if not okE then
            PastureHerdingLog.warning("substituteContour: raising onHusbandryAnimalsCreated failed: %s", tostring(errE))
        else
            PastureHerdingLog.dev("substituteContour: re-raised onHusbandryAnimalsCreated(%s) on dependent specs",
                tostring(clusterHusbandry.husbandryId))
        end
    end
    if spec.clusterSystem ~= nil then
        spec.clusterSystem.needsUpdate = true
    end
    if preCount > 0 and postCount ~= preCount then
        PastureHerdingLog.warning("substituteContour: animal count changed (%d -> %d); RLM redistribution may not have completed; reload save if animals vanish",
            preCount, postCount)
    end

    local entry = self.meshes[key]
    if entry == nil then
        entry = { placeable = placeable, animalTypeName = clusterHusbandry.animalTypeName }
        self.meshes[key] = entry
    end

    -- Remember the ORIGINAL engine-built mesh the very first time we
    -- substitute, so phRestoreLast can swap back to it. Subsequent substitutes
    -- must NOT overwrite it (oldMeshId on the 2nd call is our previous fabricated).
    if entry.originalMeshId == nil and oldMeshId ~= nil and oldMeshId ~= 0
       and (entry.fabricatedMeshId == nil or oldMeshId ~= entry.fabricatedMeshId) then
        entry.originalMeshId = oldMeshId
        PastureHerdingLog.dev("substituteContour: captured originalMeshId=%s for placeable %s",
            tostring(oldMeshId), tostring(key))
    end

    local previousFabricated = entry.fabricatedMeshId
    local previousPositioning = entry.fabricatedPositioning
    entry.meshId = newMeshId
    entry.fabricatedMeshId = newMeshId
    entry.fabricatedMeshName = newMeshName
    entry.fabricatedPositioning = newPositioning
    entry.lastContour = contour3D
    if cachedCx ~= nil then
        entry.centerX, entry.centerY, entry.centerZ = cachedCx, cachedCy, cachedCz
    end

    if previousFabricated ~= nil and previousFabricated ~= 0 and previousFabricated ~= newMeshId then
        PastureHerdingLog.dev("substituteContour: deleting previous fabricated mesh %s", tostring(previousFabricated))
        ContourBuilder.delete(previousFabricated)
    elseif oldMeshId ~= nil and oldMeshId ~= 0 and oldMeshId ~= newMeshId then
        PastureHerdingLog.dev("substituteContour: NOT deleting engine-owned old mesh %s (engine owns its lifecycle)", tostring(oldMeshId))
    end
    if previousPositioning ~= nil then
        ContourBuilder.deletePositioning(previousPositioning)
    end

    return true
end

function NavMeshLayer:reshape(placeable, contour3D)
    return self:substituteContour(placeable, contour3D)
end

function NavMeshLayer:restoreOriginal(placeable)
    local spec = placeable and placeable.spec_husbandryAnimals
    if spec == nil then
        PastureHerdingLog.error("restoreOriginal: no spec_husbandryAnimals")
        return false
    end
    local clusterHusbandry = spec.clusterHusbandry
    if clusterHusbandry == nil then
        PastureHerdingLog.error("restoreOriginal: no clusterHusbandry")
        return false
    end
    if husbandryHasAnimals(spec) then
        PastureHerdingLog.error("restoreOriginal: refusing — pen has animals; remove them first")
        return false
    end

    local key = placeableKey(placeable)
    local entry = self.meshes[key]
    if entry == nil or entry.originalMeshId == nil or entry.originalMeshId == 0 then
        PastureHerdingLog.error("restoreOriginal: no originalMeshId recorded for placeable %s (was it ever substituted?)",
            tostring(key))
        return false
    end
    if entry.fabricatedMeshId == nil then
        PastureHerdingLog.dev("restoreOriginal: placeable %s is already on the original mesh; nothing to do", tostring(key))
        return true
    end

    local xmlFilename = clusterHusbandry.xmlFilename
    local raycastDistance = clusterHusbandry.raycastDistance or 10
    local collisionMask = clusterHusbandry.collisionMask
    local fabricatedToDelete = entry.fabricatedMeshId
    local originalMeshId = entry.originalMeshId

    PastureHerdingLog.dev("restoreOriginal: swapping back to engine mesh %s for placeable %s (will delete our fabricated %s)",
        tostring(originalMeshId), tostring(key), tostring(fabricatedToDelete))

    local okDel, errDel = pcall(function() clusterHusbandry:deleteHusbandry() end)
    if not okDel then
        PastureHerdingLog.error("restoreOriginal: deleteHusbandry failed: %s", tostring(errDel))
        return false
    end

    local okCreate, errCreate = pcall(function()
        clusterHusbandry:create(xmlFilename, originalMeshId, raycastDistance, collisionMask)
    end)
    if not okCreate then
        PastureHerdingLog.error("restoreOriginal: create with original mesh raised: %s", tostring(errCreate))
        return false
    end
    if clusterHusbandry.husbandryId == nil then
        PastureHerdingLog.error("restoreOriginal: husbandryId nil after create — engine rejected original mesh (may have been deleted)")
        return false
    end

    ContourBuilder.delete(fabricatedToDelete)
    ContourBuilder.deletePositioning(entry.fabricatedPositioning)
    entry.meshId = originalMeshId
    entry.fabricatedMeshId = nil
    entry.fabricatedMeshName = nil
    entry.fabricatedPositioning = nil
    entry.lastContour = nil
    -- Mark so the every-2s auto-migrate sweep does NOT immediately re-fabricate
    -- this pen (it re-substitutes empty pens where fabricatedMeshId==nil and
    -- not autoMigrateAttempted). The player explicitly asked for the engine
    -- mesh back; honour it until the pen is re-registered/re-placed.
    entry.autoMigrateAttempted = true
    -- The create(originalMeshId) above re-fired the husbandry create hook,
    -- which queues a pending auto-substitute because the engine mesh name lacks
    -- our "PastureHerding_navMesh_" prefix. Drop it so we don't undo the restore
    -- on the next tick.
    if HusbandryHook ~= nil and type(HusbandryHook.pendingAutoSubstitute) == "table" then
        HusbandryHook.pendingAutoSubstitute[placeable] = nil
    end

    PastureHerdingLog.dev("restoreOriginal: SUCCESS — husbandryId=%s now on engine mesh %s",
        tostring(clusterHusbandry.husbandryId), tostring(originalMeshId))
    return true
end
