if isClient() then return end

-- Optional config
pcall(function() require "MSW_Config" end)

local Commands = {}

-- ========== Helpers ==========

local function deepCopyTable(original)
    if type(original) ~= 'table' then return original end
    local copy = {}
    for k, v in pairs(original) do
        copy[k] = type(v) == 'table' and deepCopyTable(v) or v
    end
    return copy
end

local function toNumber(v)
    if v == nil then return nil end
    return tonumber(v)
end

local function hasValue(v)
    return v ~= nil
end

local function clamp01(n)
    n = tonumber(n)
    if not n then return nil end
    if n < 0 then n = 0 end
    if n > 1 then n = 1 end
    return n
end

local function clampSlots(n)
    local minS = (rawget(_G, 'MSW_Config') and tonumber(MSW_Config.MinSlots)) or 1
    local maxS = (rawget(_G, 'MSW_Config') and tonumber(MSW_Config.MaxSlots)) or 6
    if not n then return nil end
    if n < minS then n = minS end
    if n > maxS then n = maxS end
    return n
end

local function getSandboxSlotsOverride()
    local sv = SandboxVars and SandboxVars.MSW and SandboxVars.MSW.Slots
    if sv ~= nil then return clampSlots(tonumber(sv)) end
    return nil
end

local function getGlobalSlotsOverride()
    if rawget(_G, 'MSW_Config') and MSW_Config and MSW_Config.Slots ~= nil then
        return clampSlots(tonumber(MSW_Config.Slots))
    end
    return nil
end

local function forEachLoadedVehicle(visitor)
    if type(visitor) ~= "function" then
        return
    end

    local cell = getCell and getCell() or nil
    if not cell or not cell.getVehicles then
        return
    end

    local vehicles = cell:getVehicles()
    if not vehicles then
        return
    end

    if type(vehicles.size) == "function" and type(vehicles.get) == "function" then
        for index = 0, vehicles:size() - 1 do
            visitor(vehicles:get(index), index)
        end
        return
    end

    if type(vehicles.iterator) == "function" then
        local ok, iterator = pcall(function() return vehicles:iterator() end)
        if ok and iterator then
            local index = 0
            while iterator:hasNext() do
                visitor(iterator:next(), index)
                index = index + 1
            end
            return
        end
    end

    local count = nil
    if type(vehicles.size) == "function" then
        local ok, sizeValue = pcall(function() return vehicles:size() end)
        if ok then
            count = sizeValue
        end
    elseif type(vehicles) == "table" then
        count = #vehicles
    end

    if count then
        for index = 0, count - 1 do
            local ok, vehicle = pcall(function() return vehicles[index] end)
            if not ok or vehicle == nil then
                ok, vehicle = pcall(function() return vehicles[index + 1] end)
            end
            if ok and vehicle then
                visitor(vehicle, index)
            end
        end
        return
    end

    if type(vehicles) == "table" then
        for index, vehicle in ipairs(vehicles) do
            visitor(vehicle, index - 1)
        end
    end
end

local function getScriptItem(itemType)
    if type(itemType) ~= "string" or itemType == "" then return nil end
    local manager = (getScriptManager and getScriptManager()) or (ScriptManager and ScriptManager.instance) or nil
    if not manager then return nil end
    return manager:FindItem(itemType) or (manager.getItem and manager:getItem(itemType)) or nil
end

local function getMoveableSpriteName(itemData)
    if type(itemData) ~= "table" then return nil end

    if type(itemData.worldSprite) == "string" and itemData.worldSprite ~= "" then
        return itemData.worldSprite
    end

    if type(itemData.fullType) ~= "string" or itemData.fullType == "" then
        return nil
    end

    local spriteName = itemData.fullType:match("^[^.]+%.(.+)$") or itemData.fullType
    if not spriteName or spriteName == "" then return nil end
    if spriteName:find("%.") then return nil end
    if not spriteName:find("_") then return nil end

    return spriteName
end

local function createMoveableItemFromSprite(spriteName)
    if type(spriteName) ~= "string" or spriteName == "" then return nil end

    local directType = "Moveables." .. spriteName
    if getScriptItem(directType) then
        local ok, item = pcall(function() return instanceItem(directType) end)
        if ok and item then return item end
    end

    if not getScriptItem("Moveables.Moveable") then return nil end

    local ok, item = pcall(function() return instanceItem("Moveables.Moveable") end)
    if not ok or not item then return nil end
    if not (instanceof and instanceof(item, "Moveable") == true) then return nil end

    local readOk, didRead = pcall(function() return item:ReadFromWorldSprite(spriteName) end)
    if not readOk or not didRead then return nil end

    if item.setWorldSprite then
        pcall(function() item:setWorldSprite(spriteName) end)
    end

    return item
end

local function createSerializedItem(itemData)
    if type(itemData) ~= "table" then return nil end

    if type(itemData.fullType) == "string" and itemData.fullType ~= "" and getScriptItem(itemData.fullType) then
        local ok, item = pcall(function() return instanceItem(itemData.fullType) end)
        if ok and item then return item end
    end

    local spriteName = getMoveableSpriteName(itemData)
    if spriteName then
        return createMoveableItemFromSprite(spriteName)
    end

    return nil
end

-- ========== Item (De-)Serialization ==========

local serializeItemToModData
local deserializeItemFromModData
local serializeContainerToModData
local deserializeContainerFromModData

serializeItemToModData = function(item, depth)
    if not item then return nil end
    depth = depth or 0
    if depth > 10 then return nil end

    local itemData = {}
    itemData.fullType = item:getFullType()

    if item.getCondition then itemData.condition = item:getCondition() end
    if item.getCount then itemData.count = item:getCount() end
    if item.getCurrentUses then itemData.currentUses = item:getCurrentUses() end
    if item.getUsesDelta then itemData.usesDelta = item:getUsesDelta() end

    local isDrainable = (type(item.getUsedDelta) == "function" and type(item.setUsedDelta) == "function")
        or (type(item.getCurrentUsesFloat) == "function")
        or (instanceof and instanceof(item, "DrainableComboItem") == true)

    if isDrainable then
        if type(item.getUsedDelta) == "function" then
            local ok, ud = pcall(function() return item:getUsedDelta() end)
            if ok and type(ud) == "number" then itemData.usedDelta = ud end
        end
        if itemData.usedDelta == nil and type(item.getCurrentUsesFloat) == "function" then
            local ok, fill = pcall(function() return item:getCurrentUsesFloat() end)
            if ok and type(fill) == "number" then itemData.fillLevel = fill end
        end
        if type(item.isTaintedWater) == "function" then
            local ok, tainted = pcall(function() return item:isTaintedWater() end)
            if ok then itemData.taintedWater = tainted end
        end
    else
        if type(item.getFluidContainer) == "function" then
            local fluidContainer = nil
            pcall(function() fluidContainer = item:getFluidContainer() end)
            if fluidContainer then
                local amount = nil
                pcall(function() amount = fluidContainer:getAmount() end)
                if amount and type(amount) == "number" and amount > 0 then
                    itemData.hadFluidContainer = true
                end
            end
        end
    end

    if item.getNote then itemData.note = item:getNote() end

    if item.getKeyId then
        local ok, keyId = pcall(function() return item:getKeyId() end)
        if ok and keyId and keyId >= 0 then
            itemData.keyId = keyId
            if item.getName then itemData.keyName = item:getName() end
        end
    end

    -- Only save explicit player/custom names, not runtime-generated display labels.
    if item.getName and item.isCustomName and item:isCustomName() then
        local itemName = item:getName()
        if itemName and itemName ~= "" then itemData.customName = itemName end
    end

    if item.isFavorite then itemData.favorite = item:isFavorite() end
    if item.isActivated then itemData.activated = item:isActivated() end
    if item.getJobType then itemData.jobType = item:getJobType() end
    if item.getJobDelta then itemData.jobDelta = item:getJobDelta() end
    if item.getRemoteControlID then itemData.remoteControlID = item:getRemoteControlID() end
    if item.getRemoteRange then itemData.remoteRange = item:getRemoteRange() end

    -- Save tire pressure (item capacity)
    if item.getItemCapacity then
        local itemCapacity = item:getItemCapacity()
        if itemCapacity and itemCapacity > 0 then
            itemData.itemCapacity = itemCapacity
        end
    end

    if item.getMaxCapacity then
        local ok, maxCapacity = pcall(function() return item:getMaxCapacity() end)
        if ok and maxCapacity and maxCapacity > 0 then
            itemData.maxCapacity = maxCapacity
        end
    end

    if instanceof and instanceof(item, "Radio") and item.getMediaType then
        local ok, mediaType = pcall(function() return item:getMediaType() end)
        if ok and mediaType then itemData.mediaType = mediaType end
    end
    
    -- Save VHS/RecordedMedia data using correct API
    if item.isRecordedMedia and item:isRecordedMedia() then
        if item.getRecordedMediaIndex then
            local ok, index = pcall(function() return item:getRecordedMediaIndex() end)
            if ok and index and index >= 0 then
                itemData.recordedMediaIndex = index
            end
        end
    end
    
    -- Save animal body part data (for carcasses and meat)
    if item.getAnimalType then
        local ok, atype = pcall(function() return item:getAnimalType() end)
        if ok and atype then 
            itemData.animalType = atype
        end
    end
    if item.getAnimalSize then
        local ok, asize = pcall(function() return item:getAnimalSize() end)
        if ok and asize then 
            itemData.animalSize = asize
        end
    end
    if item.getAnimalWeight then
        local ok, aweight = pcall(function() return item:getAnimalWeight() end)
        if ok and aweight then 
            itemData.animalWeight = aweight
        end
    end
    
    -- Save hunger/food values (correct method is getHungChange with 'u')
    if item.getHungChange then
        local ok, hunger = pcall(function() return item:getHungChange() end)
        if ok and hunger then 
            itemData.hungChange = hunger
        end
    end
    if item.getBaseHunger then
        local ok, baseHunger = pcall(function() return item:getBaseHunger() end)
        if ok and baseHunger then 
            itemData.baseHunger = baseHunger
        end
    end

    if item.getAge then itemData.age = item:getAge() end
    if item.getOffAge then itemData.offAge = item:getOffAge() end
    if item.getOffAgeMax then itemData.offAgeMax = item:getOffAgeMax() end
    if item.getLastAged then itemData.lastAged = item:getLastAged() end
    if item.isCooked then itemData.cooked = item:isCooked() end
    if item.isBurnt then itemData.burnt = item:isBurnt() end
    if item.getWetness then itemData.wetness = item:getWetness() end
    if item.getItemWetness then itemData.itemWetness = item:getItemWetness() end
    if item.getWorldSprite then itemData.worldSprite = item:getWorldSprite() end
    if item.getActualWeight then itemData.actualWeight = item:getActualWeight() end

    if instanceof and instanceof(item, "InventoryContainer") and item.getItemContainer then
        local itemContainer = item:getItemContainer()
        if itemContainer then
            itemData.nestedContainer = serializeContainerToModData(itemContainer, depth + 1)
        end
    end

    if item.getModData then
        itemData.itemModData = deepCopyTable(item:getModData())
    end

    return itemData
end

deserializeItemFromModData = function(itemData, depth)
    if not itemData or not itemData.fullType then return nil end
    depth = depth or 0
    if depth > 10 then return nil end

    local item = createSerializedItem(itemData)
    if not item then return nil end

    local isDrainable = (type(item.setUsedDelta) == "function" or type(item.setCurrentUsesFloat) == "function")
        or (instanceof and instanceof(item, "DrainableComboItem") == true)

    if hasValue(itemData.maxCapacity) and item.setMaxCapacity then
        pcall(function() item:setMaxCapacity(itemData.maxCapacity) end)
    end

    if itemData.condition ~= nil and item.setCondition then item:setCondition(itemData.condition) end

    if not isDrainable and hasValue(itemData.count) and item.setCount then item:setCount(itemData.count) end
    if not isDrainable and hasValue(itemData.currentUses) and item.setCurrentUses then item:setCurrentUses(itemData.currentUses) end

    if itemData.usesDelta ~= nil and item.setUsesDelta then
        local ud = toNumber(itemData.usesDelta)
        if ud ~= nil then item:setUsesDelta(ud) end
    end

    if isDrainable then
        local ud = clamp01(itemData.usedDelta)
        local ff = clamp01(itemData.fillLevel)
        if ud ~= nil and item.setUsedDelta then
            item:setUsedDelta(ud)
        elseif ff ~= nil and item.setCurrentUsesFloat then
            pcall(function() item:setCurrentUsesFloat(ff) end)
        end
        if itemData.taintedWater ~= nil and type(item.setTaintedWater) == "function" then
            pcall(function() item:setTaintedWater(itemData.taintedWater) end)
        end
    else
        if itemData.hadFluidContainer then
            -- Engine limitation
        end
    end

    if hasValue(itemData.keyId) and item.setKeyId then
        pcall(function()
            item:setKeyId(itemData.keyId)
            if itemData.keyName and item.setName then
                item:setName(itemData.keyName)
            end
        end)
    end

    if itemData.customName and item.setName then
        item:setName(itemData.customName)
        if item.setCustomName then
            item:setCustomName(true)
        end
    end
    if itemData.favorite ~= nil and item.setFavorite then item:setFavorite(itemData.favorite) end
    if itemData.activated ~= nil and item.setActivated then item:setActivated(itemData.activated) end
    if itemData.jobType and item.setJobType then item:setJobType(itemData.jobType) end
    if hasValue(itemData.jobDelta) and item.setJobDelta then item:setJobDelta(itemData.jobDelta) end
    if hasValue(itemData.remoteControlID) and item.setRemoteControlID then item:setRemoteControlID(itemData.remoteControlID) end
    if hasValue(itemData.remoteRange) and item.setRemoteRange then item:setRemoteRange(itemData.remoteRange) end

    -- Restore tire pressure (item capacity)
    if hasValue(itemData.itemCapacity) and item.setItemCapacity then
        item:setItemCapacity(itemData.itemCapacity)
    end

    -- Skip recorded media index; restore only mediaType
    if itemData.mediaType and instanceof and instanceof(item, "Radio") and item.setMediaType then
        pcall(function() item:setMediaType(itemData.mediaType) end)
    end
    
    -- Restore VHS/RecordedMedia using correct API
    if hasValue(itemData.recordedMediaIndex) and item.setRecordedMediaIndexInteger then
        pcall(function()
            item:setRecordedMediaIndexInteger(itemData.recordedMediaIndex)
        end)
    end
    
    -- Restore animal body part data (preserves meat type, quality, etc.)
    if itemData.animalType and item.setAnimalType then
        pcall(function() 
            item:setAnimalType(itemData.animalType)
        end)
    end
    if hasValue(itemData.animalSize) and item.setAnimalSize then
        pcall(function() 
            item:setAnimalSize(itemData.animalSize)
        end)
    end
    if hasValue(itemData.animalWeight) and item.setAnimalWeight then
        pcall(function() 
            item:setAnimalWeight(itemData.animalWeight)
        end)
    end
    
    -- Use explicit nil checks here so valid 0/false state survives round-tripping.
    if hasValue(itemData.hungChange) and item.setHungChange then
        pcall(function() 
            item:setHungChange(itemData.hungChange)
        end)
    end
    if hasValue(itemData.baseHunger) and item.setBaseHunger then
        pcall(function() 
            item:setBaseHunger(itemData.baseHunger)
        end)
    end

    if itemData.note and item.setNote then item:setNote(itemData.note) end
    if hasValue(itemData.age) and item.setAge then item:setAge(itemData.age) end
    if hasValue(itemData.offAge) and item.setOffAge then item:setOffAge(itemData.offAge) end
    if hasValue(itemData.offAgeMax) and item.setOffAgeMax then item:setOffAgeMax(itemData.offAgeMax) end
    if hasValue(itemData.lastAged) and item.setLastAged then item:setLastAged(itemData.lastAged) end
    if itemData.cooked ~= nil and item.setCooked then item:setCooked(itemData.cooked) end
    if itemData.burnt ~= nil and item.setBurnt then item:setBurnt(itemData.burnt) end
    if hasValue(itemData.wetness) and item.setWetness then item:setWetness(itemData.wetness) end
    if hasValue(itemData.itemWetness) and item.setItemWetness then item:setItemWetness(itemData.itemWetness) end
    if itemData.worldSprite and item.setWorldSprite then item:setWorldSprite(itemData.worldSprite) end
    if hasValue(itemData.actualWeight) and item.setActualWeight then item:setActualWeight(itemData.actualWeight) end

    if itemData.nestedContainer and instanceof and instanceof(item, "InventoryContainer") and item.getItemContainer then
        local itemContainer = item:getItemContainer()
        if itemContainer then
            deserializeContainerFromModData(itemData.nestedContainer, itemContainer, (depth or 0) + 1)
        end
    end

    if itemData.itemModData then
        local modData = item:getModData()
        for k, v in pairs(itemData.itemModData) do
            modData[k] = v
        end
    end

    return item
end

serializeContainerToModData = function(container, depth)
    if not container then return nil end
    depth = depth or 0
    if depth > 10 then return nil end
    local containerData = { items = {} }
    -- Always save capacity (also for vehicle part containers, e.g. ki5 custom trunks)
    pcall(function() containerData.capacity = container:getCapacity() end)
    local items = container:getItems()
    for i = 0, items:size() - 1 do
        local item = items:get(i)
        local serializedItem = serializeItemToModData(item, depth + 1)
        if serializedItem then table.insert(containerData.items, serializedItem) end
    end
    return containerData
end

deserializeContainerFromModData = function(containerData, container, depth, vehicle, partId, scriptCap)
    if not containerData or not container then return end
    depth = depth or 0
    if depth > 10 then return end
    
    -- Restore serialized capacity for all containers.
    -- For depth==0 (vehicle part containers): this is called AFTER init() AND setCondition(),
    -- so it corrects any capacity recalculation that setCondition() may have triggered.
    -- For items with no MaxCapacity (e.g. damnCraft.Trunk), setCondition() causes PZ's engine
    -- to recalculate capacity to its fallback default (32). Restoring the serialized value
    -- here fixes that without persisting anything to modData, so the reload-after-repair
    -- issue cannot recur.
    if hasValue(containerData.capacity) then
        pcall(function() container:setCapacity(containerData.capacity) end)
    end
    
    container:removeAllItems()
    if containerData.items then
        for _, itemData in ipairs(containerData.items) do
            local item = deserializeItemFromModData(itemData, depth + 1)
            if item then
                container:AddItem(item)
            else
                --print(("[MSW] Warning: Could not deserialize item '" .. tostring(itemData and itemData.fullType) .. "' - item skipped (mod not loaded?)")
            end
        end
    end
end

-- ========== Vehicle (De-)Serialization ==========

local function serializeVehicleToSlotData(veh)
    local wdata = {
        scriptName = veh:getScript():getName(),
        skin = veh:getSkinIndex(),
        h = veh:getColorHue(),
        s = veh:getColorSaturation(),
        v = veh:getColorValue(),
        rust = veh:getRust(),
        keyId = veh:getKeyId(),
        parts = {}
    }
    
    -- Save engine quality/power/loudness (wichtig für Motor-Zustand)
    if veh.getEngineQuality then
        pcall(function()
            wdata.engineQuality = veh:getEngineQuality()
        end)
    end
    if veh.getEngineLoudness then
        pcall(function()
            wdata.engineLoudness = veh:getEngineLoudness()
        end)
    end
    if veh.getEnginePower then
        pcall(function()
            wdata.enginePower = veh:getEnginePower()
        end)
    end
    
    -- Save headlights state
    if veh.getHeadlightsOn then
        wdata.headlightsOn = veh:getHeadlightsOn()
    end
    if veh.getStoplightsOn then
        wdata.stoplightsOn = veh:getStoplightsOn()
    end
    
    -- Save vehicle modData (für Mod-spezifische Daten)
    if veh.getModData then
        wdata.vehicleModData = deepCopyTable(veh:getModData())
    end
    
    -- Save key in ignition (if present)
    if veh.getCurrentKey then
        local currentKey = veh:getCurrentKey()
        if currentKey then
            wdata.currentKeyData = serializeItemToModData(currentKey, 0)
        end
    end
    
    -- Use vehicle's getPartCount() to include ALL parts (also dynamically added by mods)
    for i=1, veh:getPartCount() do
        local part = veh:getPartByIndex(i-1)
        local pid = part:getId()
        wdata.parts[pid] = {
            Condition = part:getCondition(),
            modData = deepCopyTable(part:getModData())
        }
        
        -- Save condition lower chance (verschlechtert sich schneller/langsamer)
        if part.getConditionLowerChance then
            local clc = part:getConditionLowerChance()
            if clc and clc > 0 then
                wdata.parts[pid].conditionLowerChance = clc
            end
        end
        
        -- Save window state (if it's a window)
        if part.getWindow then
            local ok, window = pcall(function() return part:getWindow() end)
            if ok and window then
                local ws = {}
                pcall(function() if window.getHealth then ws.health = window:getHealth() end end)
                pcall(function() if window.isSmashed then ws.isSmashed = window:isSmashed() end end)
                pcall(function() if window.isGlassRemoved then ws.isGlassRemoved = window:isGlassRemoved() end end)
                pcall(function() if window.isLocked then ws.locked = window:isLocked() end end)
                if hasValue(ws.health) or hasValue(ws.isSmashed) or hasValue(ws.isGlassRemoved) or hasValue(ws.locked) then
                    wdata.parts[pid].windowState = ws
                end
            end
        end
        
        -- Save door state (if it's a door)
        if part.getDoor then
            local ok, door = pcall(function() return part:getDoor() end)
            if ok and door then
                local ds = {}
                pcall(function() if door.isLocked then ds.locked = door:isLocked() end end)
                pcall(function() if door.isOpen then ds.open = door:isOpen() end end)
                if ds.locked ~= nil or ds.open ~= nil then
                    wdata.parts[pid].doorState = ds
                end
            end
        end
        
        -- Save part category and table (for ATA parts)
        if part.getCategory then
            local category = part:getCategory()
            if category and category ~= "" then
                wdata.parts[pid].category = category
            end
        end
        if part.getTable then
            local ok, tbl = pcall(function() return part:getTable(part:getId()) end)
            if ok and tbl then
                wdata.parts[pid].table = tbl
            end
        end
        
        local inv = part:getInventoryItem()
        if inv then
            wdata.parts[pid].InventoryItemData = serializeItemToModData(inv, 0)
            wdata.parts[pid].InventoryItem = inv:getFullType()
            wdata.parts[pid].hasInventoryItem = true
        else
            wdata.parts[pid].InventoryItem = false
            wdata.parts[pid].hasInventoryItem = false
        end
        
        -- Save container content amount (for drainable parts like fuel tanks)
        if part.getContainerContentAmount then
            local contentAmount = part:getContainerContentAmount()
            if contentAmount ~= nil then
                wdata.parts[pid].containerContentAmount = contentAmount
            end
        end
        
        -- Check if part has container
        local container = part:getItemContainer()
        if container then
            wdata.parts[pid].serializedContainer = serializeContainerToModData(container, 0)
            wdata.parts[pid].hasContainer = true
        else
            wdata.parts[pid].hasContainer = false
        end
    end
    return wdata
end

local function restoreSlotDataToVehicle(wdata, veh, playerObj)
    if not wdata or not wdata.parts then return end

    local function applyRestoredPartCondition(vehPart, condition)
        if not vehPart or condition == nil then return end

        if vehPart.setRandomCondition then
            pcall(function() vehPart:setRandomCondition(nil) end)
        end

        vehPart:setCondition(condition)

        local inventoryItem = vehPart:getInventoryItem()
        if inventoryItem and inventoryItem.setCondition then
            inventoryItem:setCondition(condition)
        end

        if vehPart.doInventoryItemStats then
            pcall(function()
                vehPart:doInventoryItemStats(inventoryItem, vehPart:getMechanicSkillInstaller())
            end)
        end
    end

    local function applyRestoredContainerContent(vehPart, amount)
        if not vehPart or amount == nil or not vehPart.setContainerContentAmount then return end

        pcall(function()
            vehPart:setContainerContentAmount(amount)
        end)
    end

    if wdata.skin ~= nil then veh:setSkinIndex(wdata.skin) veh:updateSkin() end
    if wdata.h ~= nil and wdata.s ~= nil and wdata.v ~= nil then
        veh:setColorHSV(wdata.h, wdata.s, wdata.v)
        veh:transmitColorHSV()
    end
    if wdata.rust ~= nil then veh:setRust(wdata.rust) end
    if wdata.keyId ~= nil then veh:setKeyId(wdata.keyId) end
    
    -- Restore vehicle modData (wichtig für Mod-Fahrzeuge!)
    if wdata.vehicleModData then
        local vehModData = veh:getModData()
        for k, v in pairs(wdata.vehicleModData) do
            vehModData[k] = v
        end
        veh:transmitModData()
    end
    
    -- Restore engine properties (Quality, Loudness, Power)
    -- WICHTIG: Muss VOR den Parts gemacht werden, damit Motor-Part korrekt initialisiert wird
    if hasValue(wdata.engineQuality) and hasValue(wdata.engineLoudness) and hasValue(wdata.enginePower) then
        pcall(function()
            if veh.setEngineFeature then
                veh:setEngineFeature(wdata.engineQuality, wdata.engineLoudness, wdata.enginePower)
                --print(("[MSW] Engine restored: Quality=" .. wdata.engineQuality .. ", Loudness=" .. wdata.engineLoudness .. ", Power=" .. wdata.enginePower)
            end
        end)
    end
    
    -- Restore headlights state
    if wdata.headlightsOn ~= nil and veh.setHeadlightsOn then
        veh:setHeadlightsOn(wdata.headlightsOn)
    end
    if wdata.stoplightsOn ~= nil and veh.setStoplightsOn then
        veh:setStoplightsOn(wdata.stoplightsOn)
    end
    
    -- Restore key in ignition (if it was there)
    if wdata.currentKeyData and veh.setCurrentKey then
        pcall(function()
            local key = deserializeItemFromModData(wdata.currentKeyData, 0)
            if key then
                veh:setCurrentKey(key)
            end
        end)
    end

    -- Track parts that couldn't be restored (missing from vehicle script)
    local missingParts = {}
    local installedParts = {}
    -- Capacities to re-apply in the delayed handler, after engine/mod recalculations settle
    local delayedCapacities = {}
    local delayedConditions = {}
    local delayedContainerContent = {}
    
    for pid, pData in pairs(wdata.parts) do
        local vehPart = veh:getPartById(pid)
        if not vehPart then
            -- Part doesn't exist on this vehicle (mod-specific part not in script)
            table.insert(missingParts, pid)
            --print(("[MSW] Warning: Part '" .. pid .. "' not found on restored vehicle - skipping")
        else
            -- Check if part was not installed before restoration
            local wasNotInstalled = pData.hasInventoryItem and not vehPart:getInventoryItem()
            
            -- Restore the part item for all parts, including container-backed ones.
            -- Some KI5/ATA roof racks exist only as installed part items, so skipping
            -- setInventoryItem makes them disappear after unloading. Container capacity is
            -- restored again later via delayedCapacities after mod recalculations settle.
            local installSuccess = false
            
            if pData.InventoryItemData then
                local ok, item = pcall(function() return deserializeItemFromModData(pData.InventoryItemData, 0) end)
                if ok and item then
                    local setOk = pcall(function() vehPart:setInventoryItem(item) end)
                    if setOk then
                        installSuccess = true
                    end
                end
            elseif pData.InventoryItem and pData.InventoryItem ~= false then
                -- No detailed data but part should be installed.
                local ok, item = pcall(function()
                    return createSerializedItem({
                        fullType = pData.InventoryItem,
                    })
                end)
                if ok and item then
                    local setOk = pcall(function() vehPart:setInventoryItem(item) end)
                    if setOk then
                        installSuccess = true
                    else
                        --print(("[MSW] Failed to set item for part: " .. pid)
                    end
                else
                    --print(("[MSW] Failed to create item: " .. tostring(pData.InventoryItem) .. " for part: " .. pid)
                end
            elseif pData.InventoryItem == false then
                pcall(function() vehPart:setInventoryItem(nil) end)
            end
            
            -- Track successful installations of previously missing parts
            if installSuccess and wasNotInstalled then
                table.insert(installedParts, pid)
            end
            
            -- Restore window state
            pcall(function()
                if pData.windowState and type(pData.windowState) == "table" and vehPart.getWindow then
                    local ok, window = pcall(function() return vehPart:getWindow() end)
                    if ok and window then
                        if pData.windowState.health and window.setHealth then
                            pcall(function() window:setHealth(pData.windowState.health) end)
                        end
                        if pData.windowState.isSmashed and window.smashWindow then
                            pcall(function() window:smashWindow() end)
                        end
                        if pData.windowState.isGlassRemoved and window.removeGlass then
                            pcall(function() window:removeGlass() end)
                        end
                        if pData.windowState.locked ~= nil and window.setLocked then
                            pcall(function() window:setLocked(pData.windowState.locked) end)
                        end
                    end
                end
            end)
            
            -- Restore door state
            pcall(function()
                if pData.doorState and type(pData.doorState) == "table" and vehPart.getDoor then
                    local ok, door = pcall(function() return vehPart:getDoor() end)
                    if ok and door then
                        if pData.doorState.locked ~= nil and door.setLocked then
                            pcall(function() door:setLocked(pData.doorState.locked) end)
                        end
                        if pData.doorState.open ~= nil and door.setOpen then
                            pcall(function() door:setOpen(pData.doorState.open) end)
                        end
                    end
                end
            end)
            
            if pData.modData then
                for a, b in pairs(pData.modData) do
                    if a ~= "serializedContainer" then
                        vehPart:getModData()[a] = b
                    end
                end
            end
            
            pcall(function() veh:transmitPartModData(vehPart) end)
            
            pcall(function()
                if vehPart:getLuaFunction("init") then
                    VehicleUtils.callLua(vehPart:getLuaFunction("init"), veh, vehPart, playerObj)
                end
            end)
            
            -- Restore condition AFTER init (init may reset it) and keep the installed item in sync.
            if pData.Condition ~= nil then
                applyRestoredPartCondition(vehPart, pData.Condition)
                delayedConditions[pid] = pData.Condition
            end
            
            -- Restore condition lower chance
            if hasValue(pData.conditionLowerChance) and vehPart.setConditionLowerChance then
                vehPart:setConditionLowerChance(pData.conditionLowerChance)
            end

            -- Restore container content amount AFTER init because spawn/init code may randomize it.
            if pData.containerContentAmount ~= nil then
                applyRestoredContainerContent(vehPart, pData.containerContentAmount)
                delayedContainerContent[pid] = pData.containerContentAmount
            end
            
            -- Transmit condition and the synced part item after restoration.
            pcall(function() veh:updatePartStats() end)
            pcall(function() veh:updateBulletStats() end)
            pcall(function() veh:transmitPartCondition(vehPart) end)
            pcall(function() veh:transmitPartItem(vehPart) end)
            pcall(function() veh:transmitPartModData(vehPart) end)
            
            -- Restore container contents and capacity.
            -- setCapacity is called immediately so items fit, but engine/mod hooks
            -- (e.g. DAMN's hooks on transmitPartItem) may recalculate it afterwards.
            -- We record it in delayedCapacities so the cleanup handler re-applies it
            -- ~1 second later, after all recalculations have settled.
            pcall(function()
                local container = vehPart:getItemContainer()
                if container and pData.serializedContainer then
                    deserializeContainerFromModData(pData.serializedContainer, container, 0, nil, nil, nil)
                    -- Queue a delayed re-apply for every part that has a serialized capacity.
                    -- Engine/mod hooks fired after our restore (e.g. DAMN recalc from item
                    -- MaxCapacity) may override the value we just set, so we stamp it again
                    -- ~1 second later once all recalculations have settled.
                    if hasValue(pData.serializedContainer.capacity) then
                        delayedCapacities[pid] = pData.serializedContainer.capacity
                    end
                end
            end)
        end
    end
    
    -- Log summary for immediate operations
    if #installedParts > 0 then
        --print(("[MSW] Successfully installed " .. #installedParts .. " missing parts: " .. table.concat(installedParts, ", "))
    end
    if #missingParts > 0 then
        --print(("[MSW] Vehicle restored with " .. #missingParts .. " parts not in vehicle script: " .. table.concat(missingParts, ", "))
    end

    pcall(function() veh:updatePartStats() end)
    pcall(function() veh:updateBulletStats() end)
    
    -- Delayed cleanup: Remove parts auto-generated by init() after 1 second
    -- (e.g., spare tires added by mod vehicle init functions)
    local cleanupTicks = 0
    local cleanupHandler
    cleanupHandler = function()
        cleanupTicks = cleanupTicks + 1
        if cleanupTicks >= 70 then  -- ~1 second at normal speed (60 ticks/sec)
            Events.OnTick.Remove(cleanupHandler)
            
            local removedParts = {}
            for i=1, veh:getPartCount() do
                local vehPart = veh:getPartByIndex(i-1)
                if vehPart then
                    local pid = vehPart:getId()
                    local pData = wdata.parts and wdata.parts[pid]
                    
                    -- If part has InventoryItem now but didn't have one when serialized, remove it
                    if pData and pData.hasInventoryItem == false and vehPart:getInventoryItem() then
                        pcall(function()
                            vehPart:setInventoryItem(nil)
                            table.insert(removedParts, pid)
                        end)
                    end

                    if delayedConditions[pid] ~= nil then
                        pcall(function()
                            applyRestoredPartCondition(vehPart, delayedConditions[pid])
                            veh:updatePartStats()
                            veh:updateBulletStats()
                            veh:transmitPartCondition(vehPart)
                            veh:transmitPartItem(vehPart)
                        end)
                    end

                    if delayedContainerContent[pid] ~= nil then
                        pcall(function()
                            applyRestoredContainerContent(vehPart, delayedContainerContent[pid])
                            veh:transmitPartModData(vehPart)
                        end)
                    end
                    
                    -- Re-apply container capacities: DAMN and similar mods recalculate
                    -- capacity on the CLIENT when they receive transmitPartItem with the
                    -- damage condition (using item.MaxCapacity × condition multiplier).
                    -- Strategy:
                    --   1. setCapacity to our serialized value
                    --   2. transmitPartItem  → client receives correct capacity AND any
                    --      mod hooks that fire synchronously on the server reset us to 32
                    --   3. setCapacity again immediately → server ends up at correct value
                    --      after all synchronous hooks, and the outgoing packet already
                    --      captured our first setCapacity call (Lua runs before packet flush)
                    if hasValue(delayedCapacities[pid]) then
                        pcall(function()
                            local container = vehPart:getItemContainer()
                            if container then
                                container:setCapacity(delayedCapacities[pid])
                                veh:transmitPartItem(vehPart)
                                -- Re-set after transmit in case a server-side hook reset it
                                container:setCapacity(delayedCapacities[pid])
                            end
                        end)
                    end
                end
            end
            
            if #removedParts > 0 then
                --print(("[MSW] Removed " .. #removedParts .. " auto-generated parts: " .. table.concat(removedParts, ", "))
            end
        end
    end
    Events.OnTick.Add(cleanupHandler)
    
end

-- ========== Config and Commands ==========

local function getMultiWreckerPart(trailer)
    return trailer and trailer:getPartById("ATAMultiSlotWrecker") or nil
end

local function getOrInitMSW(part)
    local md = part:getModData()
    md.msw = md.msw or {}
    md.msw.slots = md.msw.slots or {}
    return md.msw
end

local function getConfig(part)
    local t = part and part:getTable("ATAMultiSlotWrecker") or nil
    local sandboxSlots = getSandboxSlotsOverride()
    local globalSlots = getGlobalSlotsOverride()
    local slots = sandboxSlots or globalSlots or (t and tonumber(t.slots)) or 3
    return {
        slots = clampSlots(slots) or 3,
        spawnDist = (t and tonumber(t.spawnDist)) or 4,
        spawnSize = (t and tonumber(t.spawnSize)) or 3
    }
end

function Commands.loadVehicle(playerObj, args)
    if not args.trailer or not args.vehicle then return end
    if args.trailer == args.vehicle then
        --print(("[MSW] Aborted: trailer and vehicle IDs are identical")
        return
    end
    local trailer = getVehicleById(args.trailer)
    local vehicle = getVehicleById(args.vehicle)
    if not trailer or not vehicle then return end

    local part = getMultiWreckerPart(trailer)
    if not part then
        --print(("[MSW] Kein ATAMultiSlotWrecker-Part am Trailer gefunden")
        return
    end

    if trailer == vehicle or (trailer.getId and vehicle.getId and trailer:getId() == vehicle:getId()) then
        --print(("[MSW] Aborted: attempted to load trailer into itself")
        return
    end

    local cfg = getConfig(part)
    local msw = getOrInitMSW(part)
    local slotIndex = args.slot or 1
    if slotIndex < 1 or slotIndex > cfg.slots then
        --print(("[MSW] Ungültiger Slot: " .. tostring(slotIndex))
        return
    end
    if msw.slots[slotIndex] then
        --print(("[MSW] Slot " .. tostring(slotIndex) .. " belegt")
        return
    end

    local data = serializeVehicleToSlotData(vehicle)
    msw.slots[slotIndex] = data
    trailer:transmitPartModData(part)

    vehicle:permanentlyRemove()
    --print(("[MSW] Fahrzeug in Slot " .. tostring(slotIndex) .. " geladen")
end

function Commands.launchVehicle(playerObj, args)
    if not args.trailer or not args.slot then return end
    local trailer = getVehicleById(args.trailer)
    if not trailer then return end
    local part = getMultiWreckerPart(trailer)
    if not part then
        --print(("[MSW] Kein ATAMultiSlotWrecker-Part am Trailer gefunden")
        return
    end
    local msw = getOrInitMSW(part)
    local slotIndex = tonumber(args.slot)
    if not slotIndex or not msw.slots[slotIndex] then
        --print(("[MSW] Slot leer oder ungültig: " .. tostring(slotIndex))
        return
    end

    local wdata = msw.slots[slotIndex]
    local scriptName = wdata.scriptName
    if not scriptName then
        --print(("[MSW] Kein scriptName in Slot-Daten")
        return
    end

    local square = getSquare(args.x, args.y, 0)
    if not square then
        --print(("[MSW] Ziel-Square nicht gefunden")
        return
    end

    local veh = addVehicleDebug(scriptName, IsoDirections.N, trailer:getSkinIndex(), square)
    if not veh then
        --print(("[MSW] Spawn fehlgeschlagen")
        return
    end

    veh:setAngles(trailer:getAngleX(), trailer:getAngleY(), trailer:getAngleZ())
    
    -- Force part stats update to trigger any mod hooks that add dynamic parts
    veh:updatePartStats()
    veh:updateTotalMass()
    
    -- Restore vehicle data with error handling
    local restoreOk, restoreErr = pcall(function()
        restoreSlotDataToVehicle(wdata, veh, playerObj)
    end)
    
    if not restoreOk then
        --print(("[MSW] Error restoring vehicle data: " .. tostring(restoreErr))
        -- Vehicle spawned but restoration failed - still proceed
    end

    msw.slots[slotIndex] = nil
    trailer:transmitPartModData(part)

    -- Cleanup duplicate keys near spawn
    pcall(function()
        if not veh then return end
        local xx = veh:getX()
        local yy = veh:getY()
        local keyId = veh:getKeyId()
        if not keyId or keyId < 0 then return end
        
        for z=0, 3 do
            for i=xx - 15, xx + 15 do
                for j=yy - 15, yy + 15 do
                    local tmpSq = getCell():getGridSquare(i, j, z)
                    if tmpSq then
                        local objects = tmpSq:getObjects()
                        if objects then
                            for k=0, objects:size()-1 do
                                local obj = objects:get(k)
                                if obj then
                                    pcall(function()
                                        if obj.getContainer and obj:getContainer() then
                                            local items = obj:getContainer():getItems()
                                            if items then
                                                for ii=items:size()-1, 0, -1 do
                                                    local it = items:get(ii)
                                                    if it and it.getKeyId and it:getKeyId() == keyId then
                                                        items:remove(it)
                                                    end
                                                end
                                            end
                                        end
                                    end)
                                    pcall(function()
                                        if instanceof and instanceof(obj, "IsoWorldInventoryObject") then
                                            local item = obj:getItem()
                                            if item then
                                                if item.getContainer and item:getContainer() then
                                                    local items = item:getContainer():getItems()
                                                    if items then
                                                        for ii=items:size()-1, 0, -1 do
                                                            local it = items:get(ii)
                                                            if it and it.getKeyId and it:getKeyId() == keyId then
                                                                items:remove(it)
                                                            end
                                                        end
                                                    end
                                                end
                                                if item.getKeyId and item:getKeyId() == keyId then
                                                    tmpSq:removeWorldObject(obj)
                                                end
                                            end
                                        end
                                    end)
                                end
                            end
                        end
                    end
                end
            end
        end
    end)

    --print(("[MSW] Fahrzeug aus Slot " .. tostring(slotIndex) .. " abgeladen")
end

local function OnClientCommand(module, command, playerObj, args)
    if module == 'msw' and Commands[command] then
        Commands[command](playerObj, args or {})
    end
end

-- ========== Post-Load Capacity Restore ==========
-- PZ calls init() on vehicle parts during savegame load, which resets container
-- capacities to the script defaults. Legacy saves may still have a persisted
-- custom capacity key from older W900 versions, so clean it up once after load.
local mswCapacityRestored = false
local function MSW_RestoreContainerCapacitiesOnLoad()
    if mswCapacityRestored then return end
    mswCapacityRestored = true

    -- msw_customContainerCapacity is a legacy key written by older versions of this mod.
    -- Current restore code fixes the item MaxCapacity source instead, so later
    -- condition-based capacity updates can shrink naturally. We only remove stale
    -- keys here so they cannot keep forcing old values.
    local count = 0
    local foundVehicles = false
    forEachLoadedVehicle(function(veh)
        foundVehicles = true
        if veh then
            for j = 1, veh:getPartCount() do
                local part = veh:getPartByIndex(j - 1)
                if part then
                    local md = part:getModData()
                    if md and md.msw_customContainerCapacity then
                        local container = part:getItemContainer()
                        if container then
                            pcall(function()
                                if md.msw_customContainerCapacity > container:getCapacity() then
                                    container:setCapacity(md.msw_customContainerCapacity)
                                    count = count + 1
                                end
                            end)
                        end
                        md.msw_customContainerCapacity = nil
                        pcall(function() veh:transmitPartModData(part) end)
                    end
                end
            end
        end
    end)

    if not foundVehicles then
        mswCapacityRestored = false
        return
    end

    if count > 0 then
        --print(("[MSW] Restored custom container capacities for " .. count .. " vehicle parts after game load (legacy cleanup)")
    end
end
Events.EveryOneMinute.Add(MSW_RestoreContainerCapacitiesOnLoad)

Events.OnClientCommand.Add(OnClientCommand)