-- scripts/PlaceableRLSlaughterhouse.lua
-- VERSÃO: FINAL STABLE (Com DirtyFlag Check + Notificação)
-- PATCH: Storage Bootstrap + Checker + addFillLevel wrapper + logs

PlaceableRLSlaughterhouse = {}
local PlaceableRLSlaughterhouse_mt = Class(PlaceableRLSlaughterhouse)

local RL_LOG_LEVEL = 0 -- 0=silencioso, 1=essencial, 2=debug
if RL_LOG_LEVEL > 0 then
    print("[RLSlaughterhouse] script loaded")
end

-- ==========================================
-- 1. NETWORK EVENT
-- ==========================================

RLSlaughterhouseSlaughterEvent = {}
local RLSlaughterhouseSlaughterEvent_mt = Class(RLSlaughterhouseSlaughterEvent, Event)
if InitEventClass ~= nil then
    InitEventClass(RLSlaughterhouseSlaughterEvent, "RLSlaughterhouseSlaughterEvent")
end

function RLSlaughterhouseSlaughterEvent.emptyNew()
    local self = Event.new(RLSlaughterhouseSlaughterEvent_mt)
    return self
end

function RLSlaughterhouseSlaughterEvent.new(placeable, trailer)
    local self = RLSlaughterhouseSlaughterEvent.emptyNew()
    self.placeable = placeable
    self.trailer = trailer
    return self
end

function RLSlaughterhouseSlaughterEvent:readStream(streamId, connection)
    self.placeable = NetworkUtil.readNodeObject(streamId)
    self.trailer = NetworkUtil.readNodeObject(streamId)
    self:run(connection)
end

function RLSlaughterhouseSlaughterEvent:writeStream(streamId, connection)
    NetworkUtil.writeNodeObject(streamId, self.placeable)
    NetworkUtil.writeNodeObject(streamId, self.trailer)
end

function RLSlaughterhouseSlaughterEvent:run(connection)
    if self.placeable ~= nil and self.placeable.rl_executeSlaughter ~= nil then
        self.placeable:rl_executeSlaughter(self.trailer)
    end
end

-- ==========================================
-- 2. FUNÇÕES AUXILIARES
-- ==========================================

local function getFillTypeIndex(name)
    if name == nil or name == "" then return nil end
    return g_fillTypeManager:getFillTypeIndexByName(name)
end

local function getFillTypeTitle(name)
    if name == nil then return "Produto" end
    local fillType = g_fillTypeManager:getFillTypeByName(name)
    if fillType ~= nil then return fillType.title end
    return name
end

local function computeCarcassKgFromCluster(cluster)
    local weight = 0
    if cluster.weight ~= nil then
        weight = tonumber(cluster.weight) or 0
    else
        weight = 500
    end

    if weight <= 0 then return 0 end

    local q = 1.0
    if cluster.genetics ~= nil and cluster.genetics.quality ~= nil then
        q = tonumber(cluster.genetics.quality) or 1.0
    end

    if q < 0.35 then q = 0.35 end
    if q > 1.75 then q = 1.75 end

    local t = (q - 0.35) / (1.75 - 0.35)
    local bonus = 0.10 * t
    local count = cluster.numAnimals or 1

    return (weight * (1 + bonus)) * count
end

local function rlRemoveClusterFromTrailer(trailer, cluster, numAnimals)
    if trailer == nil or cluster == nil then return false end

    local n = numAnimals
    if n == nil then
        if cluster.getNumAnimals ~= nil then n = cluster:getNumAnimals() else n = cluster.numAnimals end
    end
    if n == nil or n <= 0 then n = 1 end

    -- Estratégia 1
    if cluster.changeNumAnimals ~= nil then
        local remaining = cluster:changeNumAnimals(-n)
        if remaining == nil or remaining >= 0 then
            local cs = (trailer.getClusterSystem ~= nil) and trailer:getClusterSystem() or nil
            if cs ~= nil and cs.updateNow ~= nil then cs:updateNow() end
            if trailer.updateAnimals ~= nil then trailer:updateAnimals() end
            return true
        end
    end

    -- Estratégia 2
    local cs = (trailer.getClusterSystem ~= nil) and trailer:getClusterSystem() or nil
    if cs ~= nil then
        if cs.addPendingRemoveCluster ~= nil then
            cs:addPendingRemoveCluster(cluster)
        elseif cs.removeCluster ~= nil then
            cs:removeCluster(cluster)
        elseif cs.removeClusterById ~= nil and cluster.getId ~= nil then
            cs:removeClusterById(cluster:getId())
        else
            return false
        end

        if cs.updateNow ~= nil then cs:updateNow() end
        if trailer.updateAnimals ~= nil then trailer:updateAnimals() end
        return true
    end

    return false
end

local function getRootVehicleFromNode(nodeId)
    local obj = g_currentMission:getNodeObject(nodeId)
    if obj == nil then return nil end
    if obj.getRootVehicle ~= nil then return obj:getRootVehicle() end
    return obj
end

-- ==========================================
-- 2.1 LOG + STORAGE BOOTSTRAP / CHECKER
-- ==========================================

local _rlLogOnce = {}

local function rlLog(fmt, ...)
    local msg = nil
    if select("#", ...) > 0 then
        msg = string.format(fmt, ...)
    else
        msg = tostring(fmt)
    end

    if RL_LOG_LEVEL <= 0 then
        -- Mesmo em modo silencioso, mantenha erros/avisos no log
        if msg ~= nil and (string.find(msg, "ERROR") ~= nil or string.find(msg, "Error") ~= nil or string.find(msg, "WARN") ~= nil or string.find(msg, "WARNING") ~= nil) then
            print(string.format("[RLSlaughterhouse] %s", msg))
        end
        return
    end

    -- Nível 1 (essencial): imprime só marcos importantes + erros
    if RL_LOG_LEVEL == 1 then
        local keep = false
        if msg ~= nil then
            if string.find(msg, "ERROR") ~= nil or string.find(msg, "Error") ~= nil then
                keep = true
            elseif string.find(msg, "WARN") ~= nil or string.find(msg, "WARNING") ~= nil then
                keep = true
            elseif string.find(msg, "onLoad") ~= nil or string.find(msg, "onPostLoad") ~= nil then
                keep = true
            elseif string.find(msg, "bootstrap") ~= nil or string.find(msg, "patch:") ~= nil then
                keep = true
            end
        end
        if not keep then
            return
        end
    end

    print(string.format("[RLSlaughterhouse] %s", msg))
end

local function rlLogOnce(key, fmt, ...)
    if key == nil then
        rlLog(fmt, ...)
        return
    end
    if _rlLogOnce[key] == true then
        return
    end
    _rlLogOnce[key] = true
    rlLog(fmt, ...)
end

local function rlSafeCall(obj, fnName, ...)
    if obj == nil then return false, nil end
    local fn = obj[fnName]
    if fn == nil then return false, nil end
    return pcall(fn, obj, ...)
end

local function rlTableCount(t)
    local n = 0
    if t ~= nil then for _ in pairs(t) do n = n + 1 end end
    return n
end

local function rlFillTypeNameByIndex(idx)
    if g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeNameByIndex ~= nil then
        return g_fillTypeManager:getFillTypeNameByIndex(idx) or tostring(idx)
    end
    return tostring(idx)
end

function PlaceableRLSlaughterhouse:rl_getFactoryStorageCandidates()
    local list = {}

    local function add(name, storage)
        if storage ~= nil then table.insert(list, {name=name, storage=storage}) end
    end

    if self.spec_productionPoint ~= nil then
        if self.spec_productionPoint.productionPoint ~= nil then
            add("spec_productionPoint.productionPoint.storage", self.spec_productionPoint.productionPoint.storage)
            add("spec_productionPoint.productionPoint.unloadingStation.storage", self.spec_productionPoint.productionPoint.unloadingStation and self.spec_productionPoint.productionPoint.unloadingStation.storage)
            add("spec_productionPoint.productionPoint.loadingStation.storage", self.spec_productionPoint.productionPoint.loadingStation and self.spec_productionPoint.productionPoint.loadingStation.storage)
        end
        add("spec_productionPoint.storage", self.spec_productionPoint.storage)
    end

    if self.spec_silo ~= nil then
        add("spec_silo.storage", self.spec_silo.storage)
    end

    if self.storage ~= nil then
        add("self.storage", self.storage)
    end

    return list
end

function PlaceableRLSlaughterhouse:rl_getFactoryStorage()
    local candidates = self:rl_getFactoryStorageCandidates()
    for _, c in ipairs(candidates) do
        if c.name == "spec_productionPoint.productionPoint.storage" and c.storage ~= nil then
            return c.storage, c.name
        end
    end
    if #candidates > 0 then
        return candidates[1].storage, candidates[1].name
    end
    return nil, "none"
end

-- Lê o XML e retorna:
-- capsByIndex: [fillTypeIndex]=capacity
-- capsByName:  [fillTypeName]=capacity
-- unresolvedNames: { "FOO", "BAR" } (quando o fillType ainda não existe no manager)
function PlaceableRLSlaughterhouse:rl_resolveXmlStorageCapacities()
    local capsByIndex = {}
    local capsByName = {}
    local unresolved = {}

    if self.xmlFile == nil or self.xmlFile.iterate == nil then
        return capsByIndex, capsByName, unresolved
    end

    self.xmlFile:iterate("placeable.productionPoint.storage.capacity", function(_, capKey)
        local ftName = self.xmlFile:getValue(capKey .. "#fillType")
        local capVal = self.xmlFile:getValue(capKey .. "#capacity", nil)
        local cap = (capVal ~= nil) and (tonumber(capVal) or 0) or 0

        if ftName ~= nil and ftName ~= "" and cap > 0 then
            capsByName[ftName] = cap
            local idx = getFillTypeIndex(ftName)
            if idx ~= nil then
                capsByIndex[idx] = cap
            else
                table.insert(unresolved, ftName)
            end
        end
    end)

    return capsByIndex, capsByName, unresolved
end

function PlaceableRLSlaughterhouse:rl_getXmlStorageCapacities()
    -- compat: retorna somente por index (para quem já usa)
    local spec = self.spec_rlSlaughterhouse
    if spec ~= nil and spec._rl_xmlCapsByIndex ~= nil then
        return spec._rl_xmlCapsByIndex
    end

    local byIdx, byName, unresolved = self:rl_resolveXmlStorageCapacities()

    if spec ~= nil then
        spec._rl_xmlCapsByIndex = byIdx
        spec._rl_xmlCapsByName = byName
        spec._rl_xmlUnresolved = unresolved
    end

    return byIdx
end

function PlaceableRLSlaughterhouse:rl_applyTotalCapacityFields(storage, totalCap)
    if storage == nil or totalCap == nil then return end

    -- Campos comuns usados por UI/mods
    storage.capacity = totalCap
    storage.totalCapacity = totalCap
    storage.maxCapacity = totalCap

    -- Se existir setter, use
    local ok = select(1, rlSafeCall(storage, "setCapacity", totalCap))
    if ok then
        -- noop
    end
end

function PlaceableRLSlaughterhouse:rl_ensureFillTypeInStorage(storage, fillTypeIndex, desiredCapacity)
    if storage == nil or fillTypeIndex == nil then return false end

    storage.fillTypes = storage.fillTypes or {}
    storage.fillLevels = storage.fillLevels or {}
    storage.capacities = storage.capacities or {}

    storage.fillTypes[fillTypeIndex] = true
    if storage.fillLevels[fillTypeIndex] == nil then storage.fillLevels[fillTypeIndex] = 0 end

    if desiredCapacity ~= nil and desiredCapacity > 0 then
        local cur = storage.capacities[fillTypeIndex]
        if cur == nil or cur < desiredCapacity then
            storage.capacities[fillTypeIndex] = desiredCapacity
        end

        -- tentar setters (quando existirem)
        rlSafeCall(storage, "setFillTypeCapacity", fillTypeIndex, desiredCapacity)
        rlSafeCall(storage, "setCapacity", fillTypeIndex, desiredCapacity)
        rlSafeCall(storage, "registerFillType", fillTypeIndex, desiredCapacity)
    end

    return true
end

function PlaceableRLSlaughterhouse:rl_dumpStorageForXmlFillTypes(storage, capsByIndex, label)
    if storage == nil then
        rlLog("%s: storage=nil", tostring(label))
        return
    end

    rlLog("%s: rootNode=%s | hasFillLevels=%s | hasCaps=%s", tostring(label), tostring(storage.rootNode or "n/a"), tostring(storage.fillLevels ~= nil), tostring(storage.capacities ~= nil))

    local keys = {}
    for ft,_ in pairs(capsByIndex) do table.insert(keys, ft) end
    table.sort(keys)

    for _, ft in ipairs(keys) do
        local xmlCap = capsByIndex[ft] or 0
        local lvl = 0
        if storage.getFillLevel ~= nil then
            lvl = storage:getFillLevel(ft) or 0
        elseif storage.fillLevels ~= nil then
            lvl = storage.fillLevels[ft] or 0
        end

        local cap = nil
        if storage.getCapacity ~= nil then
            cap = storage:getCapacity(ft)
        end
        if cap == nil and storage.capacities ~= nil then
            cap = storage.capacities[ft]
        end
        cap = tonumber(cap) or tonumber(xmlCap) or 0

        local free = nil
        if storage.getFreeCapacity ~= nil then
            free = storage:getFreeCapacity(ft)
        end
        if free == nil then
            free = cap - lvl
        end
        free = tonumber(free) or -1

        rlLog("  - %s: lvl=%.3f cap=%.3f(xml=%.3f) free=%.3f", rlFillTypeNameByIndex(ft), tonumber(lvl) or 0, cap, tonumber(xmlCap) or 0, free)
    end
end


function PlaceableRLSlaughterhouse:rl_forceFactoryStorageFromXml(storage, capsByIndex, capsByName)
    if storage == nil then return 0,0 end

    local applied, total = 0, 0
    local totalCap = 0

    if capsByIndex == nil then
        capsByIndex = self:rl_getXmlStorageCapacities()
    end

    -- soma e aplica por index
    for ft, cap in pairs(capsByIndex) do
        total = total + 1
        totalCap = totalCap + cap
        if self:rl_ensureFillTypeInStorage(storage, ft, cap) then
            applied = applied + 1
        end
    end

    -- aplica total
    if totalCap > 0 then
        self:rl_applyTotalCapacityFields(storage, totalCap)
    end

    -- dirty flags do storage
    if storage.raiseDirtyFlags ~= nil then
        if storage.dirtyFlag ~= nil then storage:raiseDirtyFlags(storage.dirtyFlag) end
        if storage.storageDirtyFlag ~= nil then storage:raiseDirtyFlags(storage.storageDirtyFlag) end
    end

    return applied, totalCap
end

-- (RLSlaughterhouse) rl_linkProductionStationsToStorage removed (no longer used; linking can crash SellingStation.update)


function PlaceableRLSlaughterhouse:rl_patchProductionPointCaches(pp, capsByIndex)
    if pp == nil or capsByIndex == nil then return end

    -- caches comuns (dependem do build do jogo, mas são inofensivos se não usados)
    pp.storageCapacities = pp.storageCapacities or {}
    pp.fillTypeCapacities = pp.fillTypeCapacities or {}
    pp.storageCapacitiesByFillType = pp.storageCapacitiesByFillType or {}

    pp.storageFillTypes = pp.storageFillTypes or {}
    pp.acceptedFillTypes = pp.acceptedFillTypes or {}

    for ft, cap in pairs(capsByIndex) do
        pp.storageCapacities[ft] = cap
        pp.fillTypeCapacities[ft] = cap
        pp.storageCapacitiesByFillType[ft] = cap
        pp.storageFillTypes[ft] = true
        pp.acceptedFillTypes[ft] = true
    end
end

function PlaceableRLSlaughterhouse:rl_patchProductionPointMethods(pp, storage, capsByIndex)
    if pp == nil or storage == nil then return end
    if pp._rl_storageMethodPatch == true then return end
    pp._rl_storageMethodPatch = true

    local function capFallback(fillTypeIndex, cap)
        if cap == nil or cap <= 0 then
            if storage.getCapacity ~= nil then
                cap = storage:getCapacity(fillTypeIndex)
            elseif storage.capacities ~= nil then
                cap = storage.capacities[fillTypeIndex]
            end
        end
        return tonumber(cap) or 0
    end

    local function lvlFallback(fillTypeIndex, lvl)
        if lvl == nil or lvl <= 0 then
            if storage.getFillLevel ~= nil then
                lvl = storage:getFillLevel(fillTypeIndex)
            elseif storage.fillLevels ~= nil then
                lvl = storage.fillLevels[fillTypeIndex]
            end
        end
        return tonumber(lvl) or 0
    end

    local function freeFallback(fillTypeIndex, free)
        if free == nil or free <= 0 then
            if storage.getFreeCapacity ~= nil then
                free = storage:getFreeCapacity(fillTypeIndex)
            else
                local cap = capFallback(fillTypeIndex, nil)
                local lvl = lvlFallback(fillTypeIndex, nil)
                free = cap - lvl
            end
        end
        return tonumber(free) or 0
    end

    -- Wrapper de capacidade (corrige o 0/0 no menu quando o vanilla usa caches incompletos)
    if pp.getCapacity ~= nil and pp._rl_orig_getCapacity == nil then
        local orig = pp.getCapacity
        pp._rl_orig_getCapacity = orig
        pp.getCapacity = function(selfPP, fillTypeIndex)
            local ok, cap = pcall(orig, selfPP, fillTypeIndex)
            if not ok then cap = nil end
            return capFallback(fillTypeIndex, cap)
        end
        rlLog("patch: productionPoint.getCapacity wrapper enabled")
    end

    if pp.getFillLevel ~= nil and pp._rl_orig_getFillLevel == nil then
        local orig = pp.getFillLevel
        pp._rl_orig_getFillLevel = orig
        pp.getFillLevel = function(selfPP, fillTypeIndex)
            local ok, lvl = pcall(orig, selfPP, fillTypeIndex)
            if not ok then lvl = nil end
            return lvlFallback(fillTypeIndex, lvl)
        end
        rlLog("patch: productionPoint.getFillLevel wrapper enabled")
    end

    if pp.getFreeCapacity ~= nil and pp._rl_orig_getFreeCapacity == nil then
        local orig = pp.getFreeCapacity
        pp._rl_orig_getFreeCapacity = orig
        pp.getFreeCapacity = function(selfPP, fillTypeIndex)
            local ok, free = pcall(orig, selfPP, fillTypeIndex)
            if not ok then free = nil end
            return freeFallback(fillTypeIndex, free)
        end
        rlLog("patch: productionPoint.getFreeCapacity wrapper enabled")
    end

    -- Alguns builds usam nomes alternativos; aplicamos também se existirem.
    if pp.getStorageCapacity ~= nil and pp._rl_orig_getStorageCapacity == nil then
        local orig = pp.getStorageCapacity
        pp._rl_orig_getStorageCapacity = orig
        pp.getStorageCapacity = function(selfPP, fillTypeIndex)
            local ok, cap = pcall(orig, selfPP, fillTypeIndex)
            if not ok then cap = nil end
            return capFallback(fillTypeIndex, cap)
        end
        rlLog("patch: productionPoint.getStorageCapacity wrapper enabled")
    end

    if pp.getStorageFillLevel ~= nil and pp._rl_orig_getStorageFillLevel == nil then
        local orig = pp.getStorageFillLevel
        pp._rl_orig_getStorageFillLevel = orig
        pp.getStorageFillLevel = function(selfPP, fillTypeIndex)
            local ok, lvl = pcall(orig, selfPP, fillTypeIndex)
            if not ok then lvl = nil end
            return lvlFallback(fillTypeIndex, lvl)
        end
        rlLog("patch: productionPoint.getStorageFillLevel wrapper enabled")
    end

    -- Opcional: reforçar acceptedFillTypes do próprio productionPoint (não dos stations)
    if capsByIndex ~= nil then
        pp.acceptedFillTypes = pp.acceptedFillTypes or {}
        pp.storageFillTypes = pp.storageFillTypes or {}
        for ft,_ in pairs(capsByIndex) do
            pp.acceptedFillTypes[ft] = true
            pp.storageFillTypes[ft] = true
        end
    end
end


function PlaceableRLSlaughterhouse:rl_bootstrapStorages(context)
    local spec = self.spec_rlSlaughterhouse
    local pp = (self.spec_productionPoint ~= nil) and self.spec_productionPoint.productionPoint or nil
    local storage = nil
    local from = nil
    storage, from = self:rl_getFactoryStorage()

    local capsByIndex, capsByName, unresolved = self:rl_resolveXmlStorageCapacities()

    if rlTableCount(capsByName) == 0 then
        rlLog("bootstrap(%s): xml capacities=0 (nada a aplicar)", tostring(context))
        if spec ~= nil then
            spec._rl_xmlUnresolved = unresolved
        end
        return
    end

    if storage == nil then
        rlLog("bootstrap(%s): storage=nil (from=%s)", tostring(context), tostring(from))
        return
    end

    local applied, totalCap = self:rl_forceFactoryStorageFromXml(storage, capsByIndex, capsByName)
    local totalXml = rlTableCount(capsByName)
    local resolved = rlTableCount(capsByIndex)
    local prettyFrom = tostring(from)
    if prettyFrom == "spec_productionPoint.productionPoint.storage" then prettyFrom = "pp.storage" end
    rlLog("bootstrap(%s): %s | applied=%d/%d | totalCap=%.0f | from=%s", tostring(context), prettyFrom, applied, totalXml, totalCap, tostring(from))

    -- link stations + caches
    if pp ~= nil then
        -- (RLSlaughterhouse) linking stations to pp.storage disabled: can break station invariants and crash SellingStation.update
        self:rl_patchProductionPointCaches(pp, capsByIndex)
        self:rl_patchProductionPointMethods(pp, storage, capsByIndex)
    end

        rlLog("bootstrap(%s): productionPoint patches installed (methods+caches)", tostring(context))


    -- dirty flag do production point (refresh UI)
    if self.spec_productionPoint ~= nil and self.spec_productionPoint.dirtyFlag ~= nil then
        self:raiseDirtyFlags(self.spec_productionPoint.dirtyFlag)
        rlLog("bootstrap(%s): productionPoint dirtyFlag raised", tostring(context))
    end

    if spec ~= nil then
        spec._rl_xmlUnresolved = unresolved
        spec._rl_bootstrapped = true
    end
end

function PlaceableRLSlaughterhouse:rl_runStorageChecker(context)
    local storage, from = self:rl_getFactoryStorage()
    local capsByIndex = self:rl_getXmlStorageCapacities()

    rlLog("StorageChecker [%s] | placeable=%s", tostring(context), tostring(self.configFileName or (self.getName ~= nil and self:getName()) or "n/a"))

    local candidates = self:rl_getFactoryStorageCandidates()
    for _, c in ipairs(candidates) do
        rlLog("Candidate: %s | storage=%s | rootNode=%s", tostring(c.name), tostring(c.storage), tostring(c.storage.rootNode or "n/a"))
    end

    rlLog("SELECTED: %s", tostring(from))
    self:rl_dumpStorageForXmlFillTypes(storage, capsByIndex, "STORAGE SNAPSHOT")
end

-- Wrapper de "addFillLevel" (tenta usar método oficial se existir, senão usa setFillLevel)
function PlaceableRLSlaughterhouse:rl_storageAddFillLevel(storage, fillTypeIndex, delta)
    if storage == nil or fillTypeIndex == nil or delta == 0 then return false end

    local before = (storage.getFillLevel ~= nil) and storage:getFillLevel(fillTypeIndex) or 0

    -- Tenta addFillLevel com assinaturas diferentes
    local ok = false
    if storage.addFillLevel ~= nil then
        ok = select(1, rlSafeCall(storage, "addFillLevel", delta, fillTypeIndex, nil))
        if not ok then ok = select(1, rlSafeCall(storage, "addFillLevel", fillTypeIndex, delta, nil)) end
        if not ok then ok = select(1, rlSafeCall(storage, "addFillLevel", delta, fillTypeIndex)) end
    end

    if not ok and storage.setFillLevel ~= nil then
        local target = before + delta
        ok = select(1, rlSafeCall(storage, "setFillLevel", target, fillTypeIndex, nil))
        if not ok then ok = select(1, rlSafeCall(storage, "setFillLevel", target, fillTypeIndex)) end
    end

    -- valida mudança (evita "ok" falso)
    local after = (storage.getFillLevel ~= nil) and storage:getFillLevel(fillTypeIndex) or before
    if ok and math.abs(after - before) < 0.0001 then
        -- fallback: força setFillLevel
        if storage.setFillLevel ~= nil then
            local target = before + delta
            ok = select(1, rlSafeCall(storage, "setFillLevel", target, fillTypeIndex, nil))
        end
    end

    return ok
end

-- ==========================================
-- 3. REGISTRO GIANTS
-- ==========================================

function PlaceableRLSlaughterhouse.prerequisitesPresent(specializations) return true end

function PlaceableRLSlaughterhouse.registerFunctions(placeableType)
    -- Core
    SpecializationUtil.registerFunction(placeableType, "rl_onAnimalTrigger", PlaceableRLSlaughterhouse.rl_onAnimalTrigger)
    SpecializationUtil.registerFunction(placeableType, "rl_actionInteract", PlaceableRLSlaughterhouse.rl_actionInteract)
    SpecializationUtil.registerFunction(placeableType, "rl_updateActionText", PlaceableRLSlaughterhouse.rl_updateActionText)
    SpecializationUtil.registerFunction(placeableType, "rl_tryResolveTrailer", PlaceableRLSlaughterhouse.rl_tryResolveTrailer)
    SpecializationUtil.registerFunction(placeableType, "rl_collectEligibleAnimals", PlaceableRLSlaughterhouse.rl_collectEligibleAnimals)
    SpecializationUtil.registerFunction(placeableType, "rl_addToStorage", PlaceableRLSlaughterhouse.rl_addToStorage)
    SpecializationUtil.registerFunction(placeableType, "rl_executeSlaughter", PlaceableRLSlaughterhouse.rl_executeSlaughter)
    SpecializationUtil.registerFunction(placeableType, "rl_onDialogCallback", PlaceableRLSlaughterhouse.rl_onDialogCallback)
    SpecializationUtil.registerFunction(placeableType, "rl_updateActivatableRegistration", PlaceableRLSlaughterhouse.rl_updateActivatableRegistration)
    SpecializationUtil.registerFunction(placeableType, "rl_requestSlaughter", PlaceableRLSlaughterhouse.rl_requestSlaughter)
    SpecializationUtil.registerFunction(placeableType, "rl_showYesNoDialog", PlaceableRLSlaughterhouse.rl_showYesNoDialog)

    -- Helpers (Storage Bootstrap / Checker)
    SpecializationUtil.registerFunction(placeableType, "rl_getFactoryStorageCandidates", PlaceableRLSlaughterhouse.rl_getFactoryStorageCandidates)
    SpecializationUtil.registerFunction(placeableType, "rl_getFactoryStorage", PlaceableRLSlaughterhouse.rl_getFactoryStorage)
    SpecializationUtil.registerFunction(placeableType, "rl_resolveXmlStorageCapacities", PlaceableRLSlaughterhouse.rl_resolveXmlStorageCapacities)
    SpecializationUtil.registerFunction(placeableType, "rl_getXmlStorageCapacities", PlaceableRLSlaughterhouse.rl_getXmlStorageCapacities)
    SpecializationUtil.registerFunction(placeableType, "rl_applyTotalCapacityFields", PlaceableRLSlaughterhouse.rl_applyTotalCapacityFields)
    SpecializationUtil.registerFunction(placeableType, "rl_ensureFillTypeInStorage", PlaceableRLSlaughterhouse.rl_ensureFillTypeInStorage)
    SpecializationUtil.registerFunction(placeableType, "rl_dumpStorageForXmlFillTypes", PlaceableRLSlaughterhouse.rl_dumpStorageForXmlFillTypes)
    SpecializationUtil.registerFunction(placeableType, "rl_forceFactoryStorageFromXml", PlaceableRLSlaughterhouse.rl_forceFactoryStorageFromXml)
    SpecializationUtil.registerFunction(placeableType, "rl_patchProductionPointCaches", PlaceableRLSlaughterhouse.rl_patchProductionPointCaches)
    SpecializationUtil.registerFunction(placeableType, "rl_patchProductionPointMethods", PlaceableRLSlaughterhouse.rl_patchProductionPointMethods)
    SpecializationUtil.registerFunction(placeableType, "rl_bootstrapStorages", PlaceableRLSlaughterhouse.rl_bootstrapStorages)
    SpecializationUtil.registerFunction(placeableType, "rl_runStorageChecker", PlaceableRLSlaughterhouse.rl_runStorageChecker)
    SpecializationUtil.registerFunction(placeableType, "rl_storageAddFillLevel", PlaceableRLSlaughterhouse.rl_storageAddFillLevel)
end


function PlaceableRLSlaughterhouse.registerEventListeners(placeableType)
    SpecializationUtil.registerEventListener(placeableType, "onLoad", PlaceableRLSlaughterhouse)
    SpecializationUtil.registerEventListener(placeableType, "onPostLoad", PlaceableRLSlaughterhouse)
    SpecializationUtil.registerEventListener(placeableType, "onUpdate", PlaceableRLSlaughterhouse)
    SpecializationUtil.registerEventListener(placeableType, "onDelete", PlaceableRLSlaughterhouse)
    SpecializationUtil.registerEventListener(placeableType, "onRegisterActionEvents", PlaceableRLSlaughterhouse)
end

function PlaceableRLSlaughterhouse.registerXMLPaths(schema, basePath)
    schema:setXMLSpecializationType("RLSlaughterhouse")
    schema:register(XMLValueType.NODE_INDEX, basePath .. ".rlSlaughterhouse#animalTriggerNode", "Trigger node")
    schema:register(XMLValueType.INT, basePath .. ".rlSlaughterhouse#minAgeCow", "Min age", 12)
    schema:register(XMLValueType.INT, basePath .. ".rlSlaughterhouse#minAgePig", "Min age", 6)
    schema:register(XMLValueType.INT, basePath .. ".rlSlaughterhouse#minAgeSheep", "Min age", 8)
    schema:register(XMLValueType.INT, basePath .. ".rlSlaughterhouse#minAgeChicken", "Min age", 6)
    schema:register(XMLValueType.STRING, basePath .. ".rlSlaughterhouse#carcassFillTypeCow", "FillType", "COW_CARCASS")
    schema:register(XMLValueType.STRING, basePath .. ".rlSlaughterhouse#carcassFillTypePig", "FillType", "PIG_CARCASS")
    schema:register(XMLValueType.STRING, basePath .. ".rlSlaughterhouse#carcassFillTypeSheep", "FillType", "SHEEP_CARCASS")
    schema:register(XMLValueType.STRING, basePath .. ".rlSlaughterhouse#carcassFillTypeChicken", "FillType", "CHICKEN_CARCASS")
    schema:register(XMLValueType.STRING, basePath .. ".rlSlaughterhouse#actionText", "Text", "Abater Animais")
    schema:setXMLSpecializationType()
end

-- ==========================================
-- 4. ON LOAD / POSTLOAD / UPDATE
-- ==========================================

function PlaceableRLSlaughterhouse:onLoad(savegame)
    local spec = nil
    local targetSpecName = "rlSlaughterhouse"

    if self["spec_" .. targetSpecName] ~= nil then
        spec = self["spec_" .. targetSpecName]
    else
        for k, v in pairs(self) do
            if string.find(tostring(k), "spec_") and string.find(tostring(k), "%." .. targetSpecName) then
                spec = v; break
            end
        end
    end

    if spec == nil then return end
    self.spec_rlSlaughterhouse = spec
    local key = "placeable.rlSlaughterhouse"

    spec.animalTriggerNode = self.xmlFile:getValue(key .. "#animalTriggerNode", nil, self.components, self.i3dMappings)
    spec.minAgeCow = self.xmlFile:getInt(key .. "#minAgeCow", 12)
    spec.minAgePig = self.xmlFile:getInt(key .. "#minAgePig", 6)
    spec.minAgeSheep = self.xmlFile:getInt(key .. "#minAgeSheep", 8)
    spec.minAgeChicken = self.xmlFile:getInt(key .. "#minAgeChicken", 6)
    spec.carcassCow = self.xmlFile:getString(key .. "#carcassFillTypeCow", "COW_CARCASS")
    spec.carcassPig = self.xmlFile:getString(key .. "#carcassFillTypePig", "PIG_CARCASS")
    spec.carcassSheep = self.xmlFile:getString(key .. "#carcassFillTypeSheep", "SHEEP_CARCASS")
    spec.carcassChicken = self.xmlFile:getString(key .. "#carcassFillTypeChicken", "CHICKEN_CARCASS")
    spec.actionText = self.xmlFile:getString(key .. "#actionText", "Abater Animais")

    spec.currentTrailer = nil 
    spec.playerInTrigger = false
    spec.pendingDialog = false
    spec.trailerInTrigger = false
    spec.slaughterActivatable = nil
    spec.slaughterActivatableAdded = false

    

    spec._rlDockMsgTrailer = nil
-- flags internos
    spec._rl_bootstrapped = false
    spec._rl_bootstrapTries = 0
    spec._rl_xmlUnresolved = {}
    spec._rl_updateTick = 0

    if self.isClient then
        spec.slaughterActivatable = RLSlaughterhouseActivatable.new(self, spec.animalTriggerNode)
        if spec.animalTriggerNode ~= nil then
            addTrigger(spec.animalTriggerNode, "rl_onAnimalTrigger", self)
        end
    end

    rlLog("onLoad ok | checker scheduled (1500ms)")
end

function PlaceableRLSlaughterhouse:onPostLoad(savegame)
    rlLog("onPostLoad -> bootstrap storages (xml capacities)")
    local ok, err = pcall(function()
        self:rl_bootstrapStorages("onPostLoad")
    end)
    if not ok then
        rlLog("bootstrap ERROR: %s", tostring(err))
    end
end

function PlaceableRLSlaughterhouse:onUpdate(dt)
    -- heartbeat + retry de bootstrap quando fillTypes ainda não existem no client
    local spec = self.spec_rlSlaughterhouse
    if spec == nil then return end

    spec._rl_updateTick = (spec._rl_updateTick or 0) + dt
    if spec._rl_updateTick < 1000 then return end
    spec._rl_updateTick = 0

    rlLog("onUpdate alive (dt=%d)", dt or 0)

    local unresolved = spec._rl_xmlUnresolved or {}
    if #unresolved > 0 and spec._rl_bootstrapTries < 10 then
        spec._rl_bootstrapTries = spec._rl_bootstrapTries + 1
        rlLog("retry bootstrap (unresolved=%d try=%d)", #unresolved, spec._rl_bootstrapTries)
        self:rl_bootstrapStorages("onUpdate-retry")
    end
end

function PlaceableRLSlaughterhouse:onDelete()
    local spec = self.spec_rlSlaughterhouse
    if self.isClient and spec ~= nil then
        if spec.animalTriggerNode ~= nil then removeTrigger(spec.animalTriggerNode) end
        if spec.slaughterActivatableAdded and spec.slaughterActivatable ~= nil then
            g_currentMission.activatableObjectsSystem:removeActivatable(spec.slaughterActivatable)
        end
    end
end

-- ==========================================
-- 5. LÓGICA DO ACTIVATABLE
-- ==========================================
function PlaceableRLSlaughterhouse:rl_updateActivatableRegistration()
    local spec = self.spec_rlSlaughterhouse
    if not self.isClient or spec == nil or spec.slaughterActivatable == nil then return end

    local shouldBeAdded = (spec.playerInTrigger == true)
    if not shouldBeAdded and spec.trailerInTrigger == true and g_localPlayer ~= nil and spec.currentTrailer ~= nil then
        local currentVehicle = g_localPlayer:getCurrentVehicle()
        if currentVehicle ~= nil then
            local root = currentVehicle
            if root.getRootVehicle ~= nil then root = root:getRootVehicle() end
            if spec.currentTrailer.rootVehicle == root then shouldBeAdded = true end
        end
    end

    if shouldBeAdded and not spec.slaughterActivatableAdded then
        g_currentMission.activatableObjectsSystem:addActivatable(spec.slaughterActivatable)
        spec.slaughterActivatableAdded = true
    elseif (not shouldBeAdded) and spec.slaughterActivatableAdded then
        g_currentMission.activatableObjectsSystem:removeActivatable(spec.slaughterActivatable)
        spec.slaughterActivatableAdded = false
    end
    if spec.slaughterActivatableAdded then spec.slaughterActivatable:updateActionEvent() end
end

function PlaceableRLSlaughterhouse:onRegisterActionEvents(isActiveForInput, isActiveForInputIgnoreSelection) end
function PlaceableRLSlaughterhouse:rl_updateActionText() end

function PlaceableRLSlaughterhouse:rl_tryResolveTrailer(vehicle)
    if vehicle == nil then return nil end
    local function isLivestock(obj)
        if obj == nil then return false end
        if obj.spec_livestockTrailer ~= nil then return true end
        if obj.getClusters ~= nil then return true end
        return false
    end
    local function scanAttachments(obj, visited)
        if obj == nil or visited[obj] then return nil end
        visited[obj] = true
        if isLivestock(obj) then return obj end
        if obj.getAttachedImplements ~= nil then
            local implements = obj:getAttachedImplements()
            if implements ~= nil then
                for _, implement in pairs(implements) do
                    local found = scanAttachments(implement.object, visited)
                    if found then return found end
                end
            end
        end
        return nil
    end
    local visited = {}
    return scanAttachments(vehicle, visited)
end

function PlaceableRLSlaughterhouse:rl_collectEligibleAnimals(trailer, estimateOnly)
    if trailer == nil then return 0 end
    local clusters = trailer:getClusters()
    if clusters == nil then return 0 end
    local count = 0
    local spec = self.spec_rlSlaughterhouse

    for _, c in pairs(clusters) do
        if c ~= nil then
            local age = tonumber(c.age) or 0
            local t = c.animalTypeIndex
            if t == nil and c.subTypeIndex ~= nil then
                t = g_currentMission.animalSystem:getTypeIndexBySubTypeIndex(c.subTypeIndex)
            end
            local minAge = nil
            if t == AnimalType.COW then minAge = spec.minAgeCow
            elseif t == AnimalType.PIG then minAge = spec.minAgePig
            elseif t == AnimalType.SHEEP then minAge = spec.minAgeSheep
            elseif t == AnimalType.CHICKEN then minAge = spec.minAgeChicken end

            if minAge ~= nil and age >= minAge then
                count = count + (c.numAnimals or 1)
            end
        end
    end
    return count
end

function PlaceableRLSlaughterhouse:rl_onAnimalTrigger(triggerId, otherActorId, onEnter, onLeave, onStay, otherShapeId)
    local object = g_currentMission:getNodeObject(otherActorId)
    if object == nil then return end
    local spec = self.spec_rlSlaughterhouse
    local isPlayer = object:isa(Player) or (g_currentMission.player ~= nil and otherActorId == g_currentMission.player.rootNode)
    local vehicle = nil
    if not isPlayer then
        vehicle = getRootVehicleFromNode(otherActorId)
        vehicle = self:rl_tryResolveTrailer(vehicle)
    end
    if onEnter then
    if vehicle ~= nil then
        spec.currentTrailer = vehicle
        spec.trailerInTrigger = true

        -- Evita spam: notifica apenas 1x por trailer enquanto ele permanecer no trigger
        if spec._rlDockMsgTrailer ~= vehicle then
            local count = self:rl_collectEligibleAnimals(vehicle, true)
            if count > 0 then
                local vName = vehicle:getName() or "Veículo"
                g_currentMission:addIngameNotification(
                    FSBaseMission.INGAME_NOTIFICATION_INFO,
                    string.format("%s na doca (%d animais). Aperte 'R' para abater.", vName, count)
                )
                spec._rlDockMsgTrailer = vehicle
            end
        end
    end
        if isPlayer then spec.playerInTrigger = true end
    elseif onLeave then
        if vehicle ~= nil and spec.currentTrailer == vehicle then
            spec.currentTrailer = nil
            spec.trailerInTrigger = false
        end
        if vehicle ~= nil and spec._rlDockMsgTrailer == vehicle then
            spec._rlDockMsgTrailer = nil
        end
        if isPlayer then spec.playerInTrigger = false end
    end
    self:rl_updateActivatableRegistration()
end

function PlaceableRLSlaughterhouse:rl_actionInteract(actionName, inputValue, callbackState, isAnalog)
    local spec = self.spec_rlSlaughterhouse
    if spec.currentTrailer == nil then return end
    if spec.pendingDialog then return end
    local count = self:rl_collectEligibleAnimals(spec.currentTrailer, true)
    if count <= 0 then return end
    spec.pendingDialog = true
    local txt = string.format("Abater %d animais do veículo estacionado?", count)
    if not self:rl_showYesNoDialog(txt, "CONFIRMAR ABATE") then
        self.spec_rlSlaughterhouse.pendingDialog = false
        self:rl_requestSlaughter(spec.currentTrailer)
    end
end

function PlaceableRLSlaughterhouse:rl_showYesNoDialog(text, title)
    if g_gui == nil then return false end
    if YesNoDialog ~= nil and YesNoDialog.show ~= nil then
        pcall(function() YesNoDialog.show(self.rl_onDialogCallback, self, text, title or "") end)
        return true
    end
    if g_gui.showYesNoDialog ~= nil then
        g_gui:showYesNoDialog({ text = text, title = title, callback = self.rl_onDialogCallback, target = self })
        return true
    end
    return false
end

function PlaceableRLSlaughterhouse:rl_onDialogCallback(a, b)
    local yes = (type(a) == "boolean" and a) or b
    self.spec_rlSlaughterhouse.pendingDialog = false
    if yes == true then
        self:rl_requestSlaughter(self.spec_rlSlaughterhouse.currentTrailer)
    end
    if self.isClient then self:rl_updateActivatableRegistration() end
end

-- ==========================================
-- 8. EXECUÇÃO
-- ==========================================

function PlaceableRLSlaughterhouse:rl_requestSlaughter(trailer)
    if trailer == nil then return end
    if self.isServer then
        self:rl_executeSlaughter(trailer)
        return
    end
    if g_client ~= nil and g_client:getServerConnection() ~= nil then
        g_client:getServerConnection():sendEvent(RLSlaughterhouseSlaughterEvent.new(self, trailer))
    end
end

function PlaceableRLSlaughterhouse:rl_executeSlaughter(trailer)
    if not self.isServer then return end
    if trailer == nil then return end

    local clusters = trailer:getClusters()
    if clusters == nil then return end

    local totalKgAdded = 0
    local totalAnimals = 0
    local lastFillTypeName = ""
    local spec = self.spec_rlSlaughterhouse

    for i = #clusters, 1, -1 do
        local c = clusters[i]
        if c ~= nil then
            local age = tonumber(c.age) or 0
            local t = c.animalTypeIndex
            if t == nil and c.subTypeIndex ~= nil then
                t = g_currentMission.animalSystem:getTypeIndexBySubTypeIndex(c.subTypeIndex)
            end

            local minAge, fillName = nil, nil
            if t == AnimalType.COW then minAge = spec.minAgeCow; fillName = spec.carcassCow
            elseif t == AnimalType.PIG then minAge = spec.minAgePig; fillName = spec.carcassPig
            elseif t == AnimalType.SHEEP then minAge = spec.minAgeSheep; fillName = spec.carcassSheep
            elseif t == AnimalType.CHICKEN then minAge = spec.minAgeChicken; fillName = spec.carcassChicken end

            if minAge ~= nil and fillName ~= nil and age >= minAge then
                local carcassKg = computeCarcassKgFromCluster(c)
                local added = self:rl_addToStorage(fillName, carcassKg)

                if added > 0.001 then 
                    local numAnimals = (c.getNumAnimals ~= nil and c:getNumAnimals()) or (c.numAnimals) or 1
                    local removed = rlRemoveClusterFromTrailer(trailer, c, numAnimals)

                    if removed then
                        totalKgAdded = totalKgAdded + added
                        totalAnimals = totalAnimals + numAnimals
                        lastFillTypeName = fillName
                    else
                        -- Rollback
                        self:rl_addToStorage(fillName, -added)
                    end
                end
            end
        end
    end

    if totalAnimals > 0 then
        local productName = getFillTypeTitle(lastFillTypeName)
        local msg = string.format("Você abateu %d animais que geraram %.1f Kg de %s.", totalAnimals, totalKgAdded, productName)

        g_currentMission:addIngameNotification(FSBaseMission.INGAME_NOTIFICATION_INFO, msg)

        if self.spec_productionPoint ~= nil and self.spec_productionPoint.dirtyFlag ~= nil then
             self:raiseDirtyFlags(self.spec_productionPoint.dirtyFlag)
        end
    else
        g_currentMission:showBlinkingWarning("Erro ao processar abate!", 4000)
    end
end

function PlaceableRLSlaughterhouse:rl_addToStorage(fillTypeName, amount)
    local fillTypeIndex = getFillTypeIndex(fillTypeName)
    if fillTypeIndex == nil then return 0 end

    local storage, pickedFrom = self:rl_getFactoryStorage()
    if storage == nil then return 0 end

    -- lazy checker 1x (visibilidade)
    local spec = self.spec_rlSlaughterhouse
    if spec ~= nil and spec._rl_loggedOnce ~= true then
        spec._rl_loggedOnce = true
        rlLog("StorageChecker [lazy-addToStorage] | placeable=%s", tostring(self.configFileName or "n/a"))
        rlLog("Candidate: %s | storage=%s | rootNode=%s", tostring(pickedFrom), tostring(storage), tostring(storage.rootNode or "n/a"))
        self:rl_runStorageChecker("lazy-addToStorage")
    end

    -- garantir capacity do fillType (para não ter free=0 por falta de registro)
    local capsByIndex = self:rl_getXmlStorageCapacities()
    local desiredCap = capsByIndex[fillTypeIndex] or 50000
    self:rl_ensureFillTypeInStorage(storage, fillTypeIndex, desiredCap)

    local current = storage:getFillLevel(fillTypeIndex)
    if amount >= 0 then
        local free = storage:getFreeCapacity(fillTypeIndex)
        if free <= 0 then return 0 end
        local delta = math.min(amount, free)
        local ok = self:rl_storageAddFillLevel(storage, fillTypeIndex, delta)
        if not ok then return 0 end
        return delta
    end

    -- Rollback
    local delta = -math.min(-amount, current)
    if delta ~= 0 then
        local ok = self:rl_storageAddFillLevel(storage, fillTypeIndex, delta)
        if not ok then return 0 end
    end
    return delta
end

-- ==========================================
-- 9. ACTIVATABLE
-- ==========================================

RLSlaughterhouseActivatable = {}
local RLSlaughterhouseActivatable_mt = Class(RLSlaughterhouseActivatable)

function RLSlaughterhouseActivatable.new(placeable, triggerNode)
    local self = setmetatable({}, RLSlaughterhouseActivatable_mt)
    self.placeable = placeable
    self.triggerNode = triggerNode
    self.activateText = "Abater Animais"
    self.actionEventId = nil
    return self
end

function RLSlaughterhouseActivatable:registerCustomInput(inputContext)
    local _, actionEventId = g_inputBinding:registerActionEvent(InputAction.ACTIVATE_OBJECT, self, self.onInputActivate, false, true, false, true)
    g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_VERY_HIGH)
    self.actionEventId = actionEventId
    self:updateActionEvent()
end

function RLSlaughterhouseActivatable:removeCustomInput(inputContext)
    g_inputBinding:removeActionEventsByTarget(self)
    self.actionEventId = nil
end

function RLSlaughterhouseActivatable:onInputActivate()
    self:run()
end

function RLSlaughterhouseActivatable:updateActionEvent()
    if self.actionEventId == nil or self.placeable == nil then return end
    local spec = self.placeable.spec_rlSlaughterhouse
    if spec == nil then return end

    self.activateText = spec.actionText or "Abater Animais"
    local active = self:getIsActivatable()

    g_inputBinding:setActionEventText(self.actionEventId, self.activateText)
    g_inputBinding:setActionEventTextVisibility(self.actionEventId, active)
    g_inputBinding:setActionEventActive(self.actionEventId, active)
end

function RLSlaughterhouseActivatable:getIsActivatable()
    if self.placeable == nil then return false end
    local spec = self.placeable.spec_rlSlaughterhouse
    if spec == nil then return false end
    if g_gui.currentGui ~= nil then return false end
    if spec.currentTrailer == nil then return false end

    if self.placeable:rl_collectEligibleAnimals(spec.currentTrailer, true) <= 0 then
        return false
    end

    if spec.playerInTrigger == true then return true end

    if spec.trailerInTrigger == true and g_localPlayer ~= nil then
        local currentVehicle = g_localPlayer:getCurrentVehicle()
        if currentVehicle ~= nil then
            local root = currentVehicle
            if root.getRootVehicle ~= nil then root = root:getRootVehicle() end
            if spec.currentTrailer.rootVehicle == root then return true end
        end
    end
    return false
end

function RLSlaughterhouseActivatable:getDistance(x, y, z)
    if self.triggerNode ~= nil then
        local tx, ty, tz = getWorldTranslation(self.triggerNode)
        return MathUtil.vector3Length(x - tx, y - ty, z - tz)
    end
    return math.huge
end

function RLSlaughterhouseActivatable:run()
    if self.placeable ~= nil then
        self.placeable:rl_actionInteract()
    end
end
