-- =========================================================
-- FS25 Fertilizer Depot - Storage & Transaction System
-- =========================================================

---@class DepotSystem
DepotSystem = {}
local DepotSystem_mt = Class(DepotSystem)

-- ─── MoneyType seguro ─────────────────────────────────────
-- Bug conhecido no FS25: usar um MoneyType inexistente faz addMoney
-- falhar silenciosamente (o pagamento nunca acontece). Resolvemos com
-- fallback garantido (OTHER sempre existe).
local function safeMoneyType(...)
    local candidates = {...}
    for _, name in ipairs(candidates) do
        if MoneyType ~= nil and MoneyType[name] ~= nil then
            return MoneyType[name]
        end
    end
    if MoneyType and MoneyType.OTHER then return MoneyType.OTHER end
    return nil
end

local MT_SELLER_INCOME, MT_OWNER_EXPENSE, MT_BUYER_EXPENSE, MT_OWNER_INCOME

local function resolveMoneyTypes()
    if MT_SELLER_INCOME ~= nil then return end
    MT_SELLER_INCOME = safeMoneyType("HARVEST_INCOME", "SOLD_PRODUCTS", "OTHER")
    MT_OWNER_EXPENSE = safeMoneyType("PURCHASE_FERTILIZER", "FIELD_BUY", "SHOP_PROPERTY_BUY", "OTHER")
    MT_BUYER_EXPENSE = safeMoneyType("PURCHASE_FERTILIZER", "PURCHASE_SEEDS", "SHOP_PROPERTY_BUY", "OTHER")
    MT_OWNER_INCOME  = safeMoneyType("SOLD_PRODUCTS", "HARVEST_INCOME", "PROPERTY_INCOME", "OTHER")
    DepotLogger.info(">>> MoneyTypes resolvidos: sellerInc=%s ownerExp=%s buyerExp=%s ownerInc=%s",
        tostring(MT_SELLER_INCOME), tostring(MT_OWNER_EXPENSE),
        tostring(MT_BUYER_EXPENSE), tostring(MT_OWNER_INCOME))
end

function DepotSystem.new(pricing)
    local self = setmetatable({}, DepotSystem_mt)
    self._pricing   = pricing
    self._depots    = {}   -- [depotId] = depot state table
    self._nextId    = 1
    return self
end

-- ─── Depot Registration ──────────────────────────────────

function DepotSystem:registerDepot()
    local id = self._nextId
    self._nextId = self._nextId + 1
    self._depots[id] = {
        id           = id,
        storageLevel = {},  -- [fillTypeName] = liters
    }
    return id
end

function DepotSystem:unregisterDepot(depotId)
    self._depots[depotId] = nil
end

function DepotSystem:getDepot(depotId)
    return self._depots[depotId]
end

-- ─── Storage Helpers ─────────────────────────────────────

function DepotSystem:getStorageLevel(depotId, fillTypeName)
    local depot = self._depots[depotId]
    if not depot then return 0 end
    return depot.storageLevel[fillTypeName] or 0
end

function DepotSystem:setStorageLevel(depotId, fillTypeName, liters)
    local depot = self._depots[depotId]
    if not depot then return end
    local cap = (g_DepotManager and g_DepotManager.settings.storageCapacity)
                or DepotConstants.STORAGE_CAPACITY
    local clamped = math.max(0, math.min(cap, liters))
    depot.storageLevel[fillTypeName] = clamped
end

-- Returns table: {[fillTypeName] = {current, capacity}} for all known fill types.
function DepotSystem:getStorageInfo(depotId)
    local depot = self._depots[depotId]
    if not depot then return {} end
    local info = {}
    local fillTypes = g_DepotManager and g_DepotManager.sfBridge:getFillTypeList() or {}
    for _, ft in ipairs(fillTypes) do
        info[ft.name] = {
            current  = depot.storageLevel[ft.name] or 0,
            capacity = DepotConstants.STORAGE_CAPACITY,
        }
    end
    return info
end

-- ─── Vehicle Search ──────────────────────────────────────

local VEHICLE_SEARCH_RADIUS_SQ       = 8  * 8   -- 8m: parked trailers/sprayers at depot building
local VEHICLE_UNLOAD_SEARCH_RADIUS_SQ = 15 * 15  -- 15m: spawned pallets near unload zone (may drift after physics)

local function collectVehiclesRecursive(v, list)
    table.insert(list, v)
    local ok, impls = pcall(function() return v:getAttachedImplements() end)
    if ok and impls then
        for _, impl in ipairs(impls) do
            if impl.object then
                collectVehiclesRecursive(impl.object, list)
            end
        end
    end
end

-- Finds a compatible vehicle+fillUnit near a world position (px, pz).
-- forSell=true  → unit currently HAS the fill type loaded.
-- forSell=false → unit can ACCEPT the fill type and has free capacity.
local function vehicleFillUnitAccepts(veh, fuIdx, fillTypeIndex)
    if veh.getFillUnitSupportsFillType == nil then return false end
    return veh:getFillUnitSupportsFillType(fuIdx, fillTypeIndex)
end

local function findVehicleNearPosition(px, pz, fillTypeIndex, forSell, radiusSq)
    if not g_currentMission then return nil, nil end

    local rSq = radiusSq or VEHICLE_SEARCH_RADIUS_SQ
    local vehicleList = g_currentMission.vehicleSystem and g_currentMission.vehicleSystem.vehicles or {}
    local totalVehicles, inRange, rejectType, rejectFull = 0, 0, 0, 0

    for _, vehicle in ipairs(vehicleList) do
        if vehicle and vehicle.rootNode then
            totalVehicles = totalVehicles + 1
            local vx, _, vz = getWorldTranslation(vehicle.rootNode)
            local dx, dz = vx - px, vz - pz
            local distSq = dx * dx + dz * dz
            if DepotLogger._debug then
                DepotLogger.debug("  vehicle '%s' dist=%.1fm inRange=%s",
                    tostring(vehicle.typeName or "?"), math.sqrt(distSq), tostring(distSq <= rSq))
            end
            if distSq <= rSq then
                inRange = inRange + 1
                local targets = {}
                collectVehiclesRecursive(vehicle, targets)
                for _, veh in ipairs(targets) do
                    local spec = veh.spec_fillUnit
                    if spec and spec.fillUnits then
                        for fuIdx, fillUnit in ipairs(spec.fillUnits) do
                            if forSell then
                                if fillUnit.fillType == fillTypeIndex and (fillUnit.fillLevel or 0) > 0 then
                                    return veh, fuIdx
                                end
                            else
                                local accepts = vehicleFillUnitAccepts(veh, fuIdx, fillTypeIndex)
                                local free = accepts and (veh:getFillUnitFreeCapacity(fuIdx) or 0) or 0
                                if DepotLogger._debug then
                                    DepotLogger.debug("    fu[%d] fillType=%s accepts=%s free=%.0f",
                                        fuIdx, tostring(fillUnit.fillType), tostring(accepts), free)
                                end
                                if accepts and free > 0 then
                                    return veh, fuIdx
                                elseif not accepts then
                                    rejectType = rejectType + 1
                                elseif free <= 0 then
                                    rejectFull = rejectFull + 1
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    DepotLogger.debug("Vehicle search failed: origin=(%.0f,%.0f) ftIdx=%s total=%d inRange=%d rejectType=%d rejectFull=%d",
        px, pz, tostring(fillTypeIndex), totalVehicles, inRange, rejectType, rejectFull)
    return nil, nil
end

-- Single-pass scan: returns {[fillTypeIndex] = {vehicle, unitIndex, fillLevel}} for all
-- fill units that have content AND are within range of any of the depot's search nodes.
-- Used by refreshSellTab to avoid N separate vehicle scans (one per fill type).
function DepotSystem:buildNearbyFillMap(depotId)
    local placeable = g_DepotManager and g_DepotManager.depots[depotId]
    if not placeable or not placeable.rootNode then return {} end
    if not g_currentMission then return {} end

    local positions = {}
    local px, _, pz = getWorldTranslation(placeable.rootNode)
    table.insert(positions, {x = px, z = pz, rSq = VEHICLE_SEARCH_RADIUS_SQ})

    local unloadNode = g_DepotManager.depotUnloadNodes[depotId]
    if unloadNode then
        local ux, _, uz = getWorldTranslation(unloadNode)
        table.insert(positions, {x = ux, z = uz, rSq = VEHICLE_UNLOAD_SEARCH_RADIUS_SQ})
    end

    local spawnNode = g_DepotManager.depotProductSpawnNodes[depotId]
    if spawnNode then
        local sx, _, sz = getWorldTranslation(spawnNode)
        table.insert(positions, {x = sx, z = sz, rSq = VEHICLE_UNLOAD_SEARCH_RADIUS_SQ})
    end

    local result = {}
    local vehicleList = g_currentMission.vehicleSystem and g_currentMission.vehicleSystem.vehicles or {}

    for _, vehicle in ipairs(vehicleList) do
        if vehicle and vehicle.rootNode then
            local vx, _, vz = getWorldTranslation(vehicle.rootNode)
            local inRange = false
            for _, pos in ipairs(positions) do
                local dx, dz = vx - pos.x, vz - pos.z
                if dx * dx + dz * dz <= pos.rSq then
                    inRange = true
                    break
                end
            end
            if inRange then
                local targets = {}
                collectVehiclesRecursive(vehicle, targets)
                for _, veh in ipairs(targets) do
                    local spec = veh.spec_fillUnit
                    if spec and spec.fillUnits then
                        for fuIdx, fillUnit in ipairs(spec.fillUnits) do
                            local ftIdx = fillUnit.fillType
                            if ftIdx and ftIdx > 0 and not result[ftIdx] then
                                local level = fillUnit.fillLevel or 0
                                if level > 0 then
                                    result[ftIdx] = {vehicle = veh, unitIndex = fuIdx, fillLevel = level}
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    return result
end

-- Public wrapper using the depot's root node as search origin.
function DepotSystem:findCompatibleVehicle(depotId, fillTypeIndex, forSell)
    local placeable = g_DepotManager and g_DepotManager.depots[depotId]
    if not placeable or not placeable.rootNode then return nil, nil end

    local px, _, pz = getWorldTranslation(placeable.rootNode)
    local v, u = findVehicleNearPosition(px, pz, fillTypeIndex, forSell)
    if v then return v, u end

    -- Search near unload node (vehicle sell zone)
    local unloadNode = g_DepotManager.depotUnloadNodes[depotId]
    if unloadNode then
        local ux, _, uz = getWorldTranslation(unloadNode)
        local v, u = findVehicleNearPosition(ux, uz, fillTypeIndex, forSell, VEHICLE_UNLOAD_SEARCH_RADIUS_SQ)
        if v then return v, u end
    end

    -- Search near product spawn node (palletTrigger) — bags/tanks are placed here after purchase.
    -- Use the same larger radius since spawned pallets may drift slightly after physics settles.
    local spawnNode = g_DepotManager.depotProductSpawnNodes[depotId]
    if spawnNode then
        local sx, _, sz = getWorldTranslation(spawnNode)
        return findVehicleNearPosition(sx, sz, fillTypeIndex, forSell, VEHICLE_UNLOAD_SEARCH_RADIUS_SQ)
    end

    return nil, nil
end

-- ─── Buy Transaction (from Depot dialog, vehicle must be near DEPOT) ─────
-- Returns: success (bool), message key (string), actualLiters (number)
function DepotSystem:buyFillType(depotId, fillTypeName, fillTypeIndex, requestedLiters, farmId)
    if not g_server then return false, "fd_error_server", 0 end

    DepotLogger.info(">>> buyFillType chamado: depot=%d fillType=%s farmId=%d liters=%.0f",
        depotId, tostring(fillTypeName), farmId, requestedLiters or 0)

    local depot = self._depots[depotId]
    if not depot then return false, "fd_error_depot", 0 end

    -- Resolve fillTypeIndex from name
    if g_fillTypeManager then
        local resolvedIdx = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)
        if resolvedIdx and resolvedIdx > 0 then
            fillTypeIndex = resolvedIdx
        end
    end

    -- ====== COOPERATIVA: só pode comprar o que está no estoque ======
    local available = depot.storageLevel[fillTypeName] or 0
    DepotLogger.info(">>> estoque disponível de %s: %.0f L", fillTypeName, available)
    if available <= 0 then return false, "fd_coop_no_stock", 0 end

    local liters = math.max(DepotConstants.MIN_PURCHASE_LITERS,
                   math.min(DepotConstants.MAX_PURCHASE_LITERS, requestedLiters))
    liters = math.min(liters, available)

    local vehicle, unitIndex = self:findCompatibleVehicle(depotId, fillTypeIndex, false)
    if not vehicle then return false, "fd_depot_no_trailer", 0 end

    local freeCapacity = vehicle:getFillUnitFreeCapacity(unitIndex)
    if freeCapacity <= 0 then return false, "fd_depot_tank_full", 0 end
    liters = math.min(liters, freeCapacity)
    if liters <= 0 then return false, "fd_depot_tank_full", 0 end

    -- Calcula custo considerando se é o dono ou outro fazendeiro
    local cost = self._pricing:calculateBuyCost(fillTypeName, liters, farmId)

    local buyerFarm = g_farmManager and g_farmManager:getFarmById(farmId)
    if not buyerFarm then return false, "fd_error_farm", 0 end
    if buyerFarm:getBalance() < cost then return false, "fd_depot_no_money", 0 end

    -- ULTRA DEBUG do coopState na compra
    local coop = g_DepotManager and g_DepotManager.coopState
    if coop then
        DepotLogger.info(">>> coopState COMPRA: hasOwner=%s ownerFarmId=%d buyerFarmId=%d isOwner=%s cost=%.2f",
            tostring(coop:hasOwner()), coop.ownerFarmId or 0, farmId,
            tostring(coop:isOwner(farmId)), cost)
    end

    -- Tira do estoque
    depot.storageLevel[fillTypeName] = available - liters

    -- ====== Roteamento de dinheiro ======
    resolveMoneyTypes()
    -- Comprador sempre paga o `cost` calculado (com markup se não-dono)
    DepotLogger.info(">>> addMoney(-%.2f, buyerFarmId=%d)", cost, farmId)
    g_currentMission:addMoney(-cost, farmId, MT_BUYER_EXPENSE, true, true)

    -- Se o comprador NÃO é o dono, o dono RECEBE o dinheiro
    if coop and coop:hasOwner() and not coop:isOwner(farmId) then
        local ownerFarmId = coop.ownerFarmId
        DepotLogger.info(">>> addMoney(+%.2f, ownerFarmId=%d) — DONO RECEBE", cost, ownerFarmId)
        g_currentMission:addMoney(cost, ownerFarmId, MT_OWNER_INCOME, true, true)
        DepotLogger.info(">>> COMPRA OK: buyer=%d pagou %.2f / owner=%d recebeu %.2f / %.0f L de %s",
            farmId, cost, ownerFarmId, cost, liters, fillTypeName)
    else
        DepotLogger.info(">>> COMPRA do próprio dono (sem repasse): %.0f L de %s por %.2f",
            liters, fillTypeName, cost)
    end

    local actualFilled = vehicle:addFillUnitFillLevel(farmId, unitIndex, liters, fillTypeIndex,
                                                      ToolType.UNDEFINED, nil)
    DepotLogger.info(">>> veículo preenchido: '%s' fu=%d requested=%.0f filled=%.0f",
        tostring(vehicle.typeName or "?"), unitIndex, liters, actualFilled or 0)

    -- ====== EXCEDENTE: o que foi PEDIDO mas não coube no veículo ======
    -- Se o jogador pediu mais do que a carreta comporta, o restante fica
    -- reservado como pedido pendente pra ele buscar com outra carreta.
    local requestedTotal = math.min(requestedLiters or liters, available)
    local leftover = requestedTotal - (actualFilled or liters)
    if leftover > 1 and g_DepotManager and g_DepotManager.setPendingOrder then
        -- Já cobramos só o que coube; o excedente continua no estoque.
        -- Reserva ele como pedido pendente pra este farm.
        local displayName = fillTypeName
        if g_fillTypeManager then
            local ft = g_fillTypeManager:getFillTypeByName(fillTypeName)
            if ft and ft.title then displayName = ft.title end
        end
        g_DepotManager:setPendingOrder(
            farmId, depotId, fillTypeName, fillTypeIndex, leftover, displayName)
        DepotLogger.info(">>> EXCEDENTE: %.0f L de %s reservados como pendente pra farm %d",
            leftover, fillTypeName, farmId)

        -- Avisa o jogador que sobrou (na tela)
        if g_currentMission and g_currentMission.hud and g_currentMission.hud.addSideNotification then
            local msg = string.format(
                g_i18n:getText("fd_leftover_reserved"),
                math.floor(leftover), displayName)
            g_currentMission.hud:addSideNotification({1.0, 0.85, 0.3, 1.0}, msg)
        end
    end

    -- Registra a transação (fazendeiro COMPROU da coop)
    if g_DepotManager and g_DepotManager.transactionLog then
        g_DepotManager.transactionLog:record("buy", farmId, fillTypeName, actualFilled or liters, cost)
        if g_DepotManager.onTransactionRecorded then
            g_DepotManager:onTransactionRecorded("buy", farmId, fillTypeName, liters, cost)
        end
    end

    return true, "fd_depot_buy_success", actualFilled or liters
end

-- ─── Silo Fill (pre-order collected at Silo, vehicle must be near SILO) ──
-- siloNode: world node to use as vehicle search origin (silo's root node).
-- Returns: success (bool), message key (string), actualLiters (number)
function DepotSystem:buyFromSilo(depotId, siloNode, fillTypeName, fillTypeIndex, requestedLiters, farmId)
    if not g_server then return false, "fd_error_server", 0 end

    local depot = self._depots[depotId]
    if not depot then return false, "fd_error_depot", 0 end

    local liters = math.max(DepotConstants.MIN_PURCHASE_LITERS,
                   math.min(DepotConstants.MAX_PURCHASE_LITERS, requestedLiters))

    -- Resolve fillTypeIndex from name in case the index drifted (e.g. SoilFertilizer remapping)
    if g_fillTypeManager then
        local resolvedIdx = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)
        if resolvedIdx and resolvedIdx > 0 then
            fillTypeIndex = resolvedIdx
        end
    end

    local vehicle, unitIndex = nil, nil

    -- 1st: search near the silo (player walked up on foot)
    if siloNode then
        local px, _, pz = getWorldTranslation(siloNode)
        vehicle, unitIndex = findVehicleNearPosition(px, pz, fillTypeIndex, false)
    end

    -- 2nd fallback: search near the depot node itself (sprayer parked at depot)
    if not vehicle then
        local depotPlaceable = g_DepotManager and g_DepotManager.depots[depotId]
        local depotNode = depotPlaceable and depotPlaceable.rootNode
        if depotNode then
            local px, _, pz = getWorldTranslation(depotNode)
            vehicle, unitIndex = findVehicleNearPosition(px, pz, fillTypeIndex, false)
        end
    end

    -- 3rd fallback: search near every registered unload node for this depot
    if not vehicle then
        local unloadNode = g_DepotManager and g_DepotManager.depotUnloadNodes[depotId]
        if unloadNode then
            local px, _, pz = getWorldTranslation(unloadNode)
            vehicle, unitIndex = findVehicleNearPosition(px, pz, fillTypeIndex, false)
        end
    end

    if not vehicle then return false, "fd_depot_no_trailer", 0 end

    local freeCapacity = vehicle:getFillUnitFreeCapacity(unitIndex)
    if freeCapacity <= 0 then return false, "fd_depot_tank_full", 0 end
    liters = math.min(liters, freeCapacity)

    -- Custo considerando se é o dono ou outro fazendeiro
    local cost = self._pricing:calculateBuyCost(fillTypeName, liters, farmId)

    local farm = g_farmManager and g_farmManager:getFarmById(farmId)
    if not farm then return false, "fd_error_farm", 0 end
    if farm:getBalance() < cost then return false, "fd_depot_no_money", 0 end

    local fromStorage = math.min(liters, depot.storageLevel[fillTypeName] or 0)
    if fromStorage > 0 then
        depot.storageLevel[fillTypeName] = (depot.storageLevel[fillTypeName] or 0) - fromStorage
    end

    -- ====== Roteamento de dinheiro (igual buyFillType) ======
    resolveMoneyTypes()
    local coop = g_DepotManager and g_DepotManager.coopState
    DepotLogger.info(">>> buyFromSilo: farm=%d paga %.2f (isOwner=%s)",
        farmId, cost, tostring(coop and coop:isOwner(farmId)))
    g_currentMission:addMoney(-cost, farmId, MT_BUYER_EXPENSE, true, true)

    -- Se o comprador NÃO é o dono, o dono RECEBE
    if coop and coop:hasOwner() and not coop:isOwner(farmId) then
        local ownerFarmId = coop.ownerFarmId
        g_currentMission:addMoney(cost, ownerFarmId, MT_OWNER_INCOME, true, true)
        DepotLogger.info(">>> buyFromSilo: owner=%d RECEBEU %.2f", ownerFarmId, cost)
    end

    local actualFilled = vehicle:addFillUnitFillLevel(farmId, unitIndex, liters, fillTypeIndex,
                                                      ToolType.UNDEFINED, nil)
    DepotLogger.info("Silo fill result: vehicle='%s' fu=%d requested=%.0f filled=%.0f type=%s",
        tostring(vehicle.typeName or "?"), unitIndex, liters, actualFilled or 0, fillTypeName)

    -- ====== Atualiza o pedido pendente com o que ainda falta ======
    -- Se a carreta encheu menos que o pendente, o restante continua reservado.
    if g_DepotManager and g_DepotManager.pendingOrders then
        local order = g_DepotManager.pendingOrders[farmId]
        if order and order.fillTypeName == fillTypeName then
            local remaining = (order.maxLiters or 0) - (actualFilled or liters)
            if remaining > 1 then
                order.maxLiters = remaining
                DepotLogger.info(">>> Pendente atualizado: ainda faltam %.0f L de %s pra farm %d",
                    remaining, fillTypeName, farmId)
                if g_currentMission and g_currentMission.hud and g_currentMission.hud.addSideNotification then
                    local dn = order.displayName or fillTypeName
                    local msg = string.format(g_i18n:getText("fd_leftover_reserved"),
                        math.floor(remaining), dn)
                    g_currentMission.hud:addSideNotification({1.0, 0.85, 0.3, 1.0}, msg)
                end
            else
                -- Coletou tudo: limpa o pendente
                g_DepotManager.pendingOrders[farmId] = nil
                DepotLogger.info(">>> Pendente completo pra farm %d — removido", farmId)
            end
            if g_DepotManager._updateInteractPrompt then
                g_DepotManager:_updateInteractPrompt()
            end
        end
    end

    return true, "fd_depot_buy_success", actualFilled or liters
end

-- ─── Dono vende UM item do estoque ao mercado ────────────
-- Vende apenas o fillType especificado (todo o estoque dele) ao preço de
-- mercado. Permite ao dono liquidar seletivamente, mantendo os itens que quer.
function DepotSystem:sellStockItem(depotId, fillTypeName, ownerFarmId)
    if not g_server then return false, "fd_error_server", 0, 0 end

    local depot = self._depots[depotId]
    if not depot then return false, "fd_error_depot", 0, 0 end

    local liters = depot.storageLevel[fillTypeName] or 0
    if liters <= 0 then return false, "fd_coop_nothing_to_sell", 0, 0 end

    resolveMoneyTypes()
    local marketPrice = self._pricing:getMarketPrice(fillTypeName)
    local revenue = marketPrice * liters

    depot.storageLevel[fillTypeName] = 0
    g_currentMission:addMoney(revenue, ownerFarmId, MT_SELLER_INCOME, true, true)

    DepotLogger.info(">>> sellStockItem: %s %.0fL x %.4f = %.2f pro dono %d",
        fillTypeName, liters, marketPrice, revenue, ownerFarmId)

    return true, "fd_coop_item_sold", liters, revenue
end

-- ─── Dono liquida TODO o estoque ao mercado (um clique) ──
-- Vende todo o estoque acumulado na cooperativa ao preço de mercado vanilla.
-- O dinheiro vai pro dono. É como o dono "escoar" o inventário e realizar
-- o lucro do spread (comprou a -10%, vende ao mercado cheio).
function DepotSystem:sellAllStock(depotId, ownerFarmId)
    if not g_server then return false, "fd_error_server", 0, 0 end

    local depot = self._depots[depotId]
    if not depot then return false, "fd_error_depot", 0, 0 end

    resolveMoneyTypes()

    local totalRevenue = 0
    local totalLiters  = 0
    local itemsSold    = 0

    for fillTypeName, liters in pairs(depot.storageLevel) do
        if liters and liters > 0 then
            local marketPrice = self._pricing:getMarketPrice(fillTypeName)
            local revenue = marketPrice * liters
            totalRevenue = totalRevenue + revenue
            totalLiters  = totalLiters + liters
            itemsSold    = itemsSold + 1
            depot.storageLevel[fillTypeName] = 0
            DepotLogger.info(">>> sellAllStock: %s %.0fL x %.4f = %.2f",
                fillTypeName, liters, marketPrice, revenue)
        end
    end

    if itemsSold == 0 then
        return false, "fd_coop_nothing_to_sell", 0, 0
    end

    g_currentMission:addMoney(totalRevenue, ownerFarmId, MT_SELLER_INCOME, true, true)
    DepotLogger.info(">>> sellAllStock CONCLUIDO: %d produtos, %.0f L, $%.2f pro dono %d",
        itemsSold, totalLiters, totalRevenue, ownerFarmId)

    return true, "fd_coop_sold_all", totalLiters, totalRevenue
end

-- ─── Helper: fillType tem palete físico? ─────────────────
-- Usado pra decidir coleta: trailer=granel, sem trailer + tem palete=spawn palete
function DepotSystem:fillTypeHasPallet(fillTypeName, fillTypeIndex)
    if g_fillTypeManager and fillTypeName then
        local idx = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)
        if idx and idx > 0 then fillTypeIndex = idx end
    end
    local ft = g_fillTypeManager and g_fillTypeManager:getFillTypeByIndex(fillTypeIndex)
    return ft ~= nil and ft.palletFilename ~= nil and ft.palletFilename ~= ""
end

-- ─── Product Order (bigBag / liquidTank spawn) ───────────
-- Deducts storage, charges farm, spawns one or more physical pallet objects near the depot.
-- Returns: success (bool), message key (string)
function DepotSystem:orderProduct(depotId, fillTypeName, fillTypeIndex, quantity, spawnX, spawnZ, farmId)
    if not g_server then return false, "fd_error_server" end

    local depot = self._depots[depotId]
    if not depot then return false, "fd_error_depot" end

    -- Resolve index from name (guard against SF drift)
    if g_fillTypeManager then
        local resolvedIdx = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)
        if resolvedIdx and resolvedIdx > 0 then
            fillTypeIndex = resolvedIdx
        end
    end

    local fillType = g_fillTypeManager and g_fillTypeManager:getFillTypeByIndex(fillTypeIndex)
    if not fillType or not fillType.palletFilename or fillType.palletFilename == "" then
        return false, "fd_products_no_object"
    end

    quantity = math.max(1, math.min(DepotConstants.MAX_PRODUCT_QUANTITY, quantity or 1))
    local litresNeeded = quantity * DepotConstants.PRODUCT_LITRES_PER_UNIT

    local stored = depot.storageLevel[fillTypeName] or 0
    if stored < litresNeeded then return false, "fd_products_no_stock" end

    local cost = self._pricing:calculateBuyCost(fillTypeName, litresNeeded)
    local farm = g_farmManager and g_farmManager:getFarmById(farmId)
    if not farm then return false, "fd_error_farm" end
    if farm:getBalance() < cost then return false, "fd_depot_no_money" end

    depot.storageLevel[fillTypeName] = stored - litresNeeded

    -- ====== Roteamento de dinheiro (igual buyFillType) ======
    resolveMoneyTypes()
    local coop = g_DepotManager and g_DepotManager.coopState
    g_currentMission:addMoney(-cost, farmId, MT_BUYER_EXPENSE, true, true)
    if coop and coop:hasOwner() and not coop:isOwner(farmId) then
        g_currentMission:addMoney(cost, coop.ownerFarmId, MT_OWNER_INCOME, true, true)
        DepotLogger.info(">>> orderProduct: owner=%d recebeu %.2f de %d", coop.ownerFarmId, cost, farmId)
    end

    -- Spawn objects side by side (1.5m apart along world X)
    local palletFile = fillType.palletFilename
    for i = 1, quantity do
        local offsetX = (i - 1) * 1.5
        local data = VehicleLoadingData.new()
        data:setFilename(palletFile)
        data:setPosition(spawnX + offsetX, nil, spawnZ)  -- nil Y = terrain height auto-resolve
        data:setPropertyState(VehiclePropertyState.OWNED)
        data:setOwnerFarmId(farmId)
        data:load(function(_, vehicles, state, _)
            if state ~= VehicleLoadingState.OK then
                DepotLogger.warning("Product spawn failed for %s unit %d/%d",
                    fillTypeName, i, quantity)
                return
            end
            -- Fill the spawned vehicle to its full capacity.
            -- BigBags pre-fill via XML so addFillUnitFillLevel just tops them off (no-op if already full).
            -- Liquid IBC pallets spawn empty and need an explicit fill.
            -- We pre-deducted PRODUCT_LITRES_PER_UNIT from depot; deduct any extra here so stock
            -- accounting matches the actual fill amount.
            for _, veh in ipairs(vehicles or {}) do
                local spec = veh.spec_fillUnit
                if spec and spec.fillUnits then
                    for fuIdx = 1, #spec.fillUnits do
                        if veh:getFillUnitSupportsFillType(fuIdx, fillTypeIndex) then
                            local cap = veh:getFillUnitCapacity(fuIdx) or 0
                            if cap > 0 then
                                -- Deduct additional stock if the container holds more than the pre-deducted amount
                                local extra = math.max(0, cap - DepotConstants.PRODUCT_LITRES_PER_UNIT)
                                if extra > 0 then
                                    local remaining = math.max(0, depot.storageLevel[fillTypeName] or 0)
                                    local taken = math.min(extra, remaining)
                                    depot.storageLevel[fillTypeName] = remaining - taken
                                    cap = DepotConstants.PRODUCT_LITRES_PER_UNIT + taken
                                end
                                veh:addFillUnitFillLevel(farmId, fuIdx, cap,
                                    fillTypeIndex, ToolType.UNDEFINED, nil)
                                DepotLogger.info("Filled spawned %s: %.0fL %s",
                                    tostring(veh.typeName or "?"), cap, fillTypeName)
                            end
                            break
                        end
                    end
                end
            end
        end)
    end

    DepotLogger.info("Product order: %s ×%d (%.0fL) cost=$%.2f farm=%d",
        fillTypeName, quantity, litresNeeded, cost, farmId)

    return true, "fd_products_ordered"
end

-- ─── Sell Transaction ────────────────────────────────────
-- Returns: success (bool), message key (string), liters sold (number), revenue (number)
function DepotSystem:sellFillType(depotId, fillTypeName, fillTypeIndex, requestedLiters, farmId)
    if not g_server then return false, "fd_error_server", 0, 0 end

    DepotLogger.info(">>> sellFillType chamado: depot=%d fillType=%s farmId=%d liters=%.0f",
        depotId, tostring(fillTypeName), farmId, requestedLiters or 0)

    local depot = self._depots[depotId]
    if not depot then return false, "fd_error_depot", 0, 0 end

    -- Re-resolve fill type index from name
    if fillTypeName and g_fillTypeManager then
        local resolvedIdx = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)
        if resolvedIdx and resolvedIdx > 0 then fillTypeIndex = resolvedIdx end
    end

    local vehicle, unitIndex = self:findCompatibleVehicle(depotId, fillTypeIndex, true)
    if not vehicle then return false, "fd_depot_no_trailer", 0, 0 end

    local available = vehicle:getFillUnitFillLevel(unitIndex)
    if available <= 0 then return false, "fd_depot_tank_empty", 0, 0 end

    local liters = math.min(requestedLiters or available, available)

    local cap = (g_DepotManager and g_DepotManager.settings.storageCapacity)
                or DepotConstants.STORAGE_CAPACITY
    local currentStored = depot.storageLevel[fillTypeName] or 0
    local space = cap - currentStored
    liters = math.min(liters, math.max(0, space))
    if liters <= 0 then return false, "fd_depot_storage_full", 0, 0 end

    -- ====== COOPERATIVA: roteamento de dinheiro ======
    local coop = g_DepotManager and g_DepotManager.coopState
    local revenue = self._pricing:calculateSellRevenue(fillTypeName, liters, farmId)

    -- ULTRA DEBUG do estado do coopState
    if coop then
        DepotLogger.info(">>> coopState: hasOwner=%s ownerFarmId=%d sellerFarmId=%d isOwner=%s revenue=%.2f",
            tostring(coop:hasOwner()),
            coop.ownerFarmId or 0,
            farmId,
            tostring(coop:isOwner(farmId)),
            revenue)
    else
        DepotLogger.warning(">>> coopState NIL no momento da venda!")
    end

    -- Se o vendedor NÃO é o owner E há owner → roteia dinheiro
    if coop and coop:hasOwner() and not coop:isOwner(farmId) then
        local ownerFarmId = coop.ownerFarmId
        local ownerFarm = g_farmManager and g_farmManager:getFarmById(ownerFarmId)
        if not ownerFarm then
            DepotLogger.warning(">>> ownerFarm NIL para id=%d!", ownerFarmId)
            return false, "fd_coop_no_owner", 0, 0
        end
        local ownerBalance = ownerFarm:getBalance()
        DepotLogger.info(">>> RAMO: outro fazendeiro vendendo. owner=%d balance=%.2f revenue=%.2f",
            ownerFarmId, ownerBalance, revenue)

        -- ====== VENDA PARCIAL: se o dono não tem o valor todo, vende só
        -- o quanto o dinheiro dele cobre. O restante FICA no trailer do
        -- vendedor (ninguém perde produto sem receber). ======
        local partial = false
        if ownerBalance < revenue then
            if ownerBalance <= 0 then
                -- Dono sem nenhum caixa: não dá pra comprar nada
                DepotLogger.info(">>> BLOQUEADO: cooperativa sem caixa (saldo=%.2f)", ownerBalance)
                return false, "fd_coop_owner_no_funds", 0, 0
            end
            -- Preço por litro pra recalcular quanto cabe no orçamento do dono
            local pricePerLiter = revenue / liters
            if pricePerLiter <= 0 then
                return false, "fd_coop_owner_no_funds", 0, 0
            end
            -- Litros que o saldo do dono consegue pagar
            local affordableLiters = math.floor(ownerBalance / pricePerLiter)
            if affordableLiters <= 0 then
                DepotLogger.info(">>> BLOQUEADO: saldo do dono não paga nem 1L")
                return false, "fd_coop_owner_no_funds", 0, 0
            end
            liters  = affordableLiters
            revenue = pricePerLiter * liters
            partial = true
            DepotLogger.info(">>> VENDA PARCIAL: dono paga %.0f L de %s (saldo cobre $%.2f)",
                liters, fillTypeName, revenue)
        end

        -- Drena do veículo APENAS o que foi vendido (parcial ou total)
        vehicle:addFillUnitFillLevel(farmId, unitIndex, -liters, fillTypeIndex,
                                     ToolType.UNDEFINED, nil)
        -- Estoque
        depot.storageLevel[fillTypeName] = currentStored + liters
        -- Dono PAGA
        resolveMoneyTypes()
        DepotLogger.info(">>> addMoney(-%.2f, ownerFarmId=%d)", revenue, ownerFarmId)
        g_currentMission:addMoney(-revenue, ownerFarmId, MT_OWNER_EXPENSE, true, true)
        -- Vendedor RECEBE
        DepotLogger.info(">>> addMoney(+%.2f, sellerFarmId=%d)", revenue, farmId)
        g_currentMission:addMoney(revenue, farmId, MT_SELLER_INCOME, true, true)
        DepotLogger.info(">>> TRANSAÇÃO COOPERATIVA OK: seller=%d +%.2f / owner=%d -%.2f / %.0f L de %s",
            farmId, revenue, ownerFarmId, revenue, liters, fillTypeName)

        -- Registra a transação (fazendeiro VENDEU à coop)
        if g_DepotManager and g_DepotManager.transactionLog then
            g_DepotManager.transactionLog:record("sell", farmId, fillTypeName, liters, revenue)
            if g_DepotManager.onTransactionRecorded then
                g_DepotManager:onTransactionRecorded("sell", farmId, fillTypeName, liters, revenue)
            end
        end

        -- Se foi venda PARCIAL, avisa o vendedor que sobrou produto no trailer
        if partial then
            if g_currentMission and g_currentMission.hud and g_currentMission.hud.addSideNotification then
                local dn = fillTypeName
                if g_fillTypeManager then
                    local ft = g_fillTypeManager:getFillTypeByName(fillTypeName)
                    if ft and ft.title then dn = ft.title end
                end
                local msg = string.format(
                    g_i18n:getText("fd_coop_partial_sell"),
                    math.floor(liters), dn)
                g_currentMission.hud:addSideNotification({1.0, 0.85, 0.3, 1.0}, msg)
            end
            return true, "fd_coop_partial_sell_ok", liters, revenue
        end

        return true, "fd_depot_sell_success", liters, revenue
    end

    -- Caso 1: vendedor é o dono OU não há dono ainda — depósito direto
    DepotLogger.info(">>> RAMO: depósito direto (vendedor é o dono OU sem owner). farmId=%d", farmId)
    vehicle:addFillUnitFillLevel(farmId, unitIndex, -liters, fillTypeIndex,
                                 ToolType.UNDEFINED, nil)
    depot.storageLevel[fillTypeName] = currentStored + liters
    DepotLogger.info(">>> DEPÓSITO OK: %.0f L de %s pra estoque", liters, fillTypeName)
    return true, "fd_coop_deposit_success", liters, 0
end

-- ─── Save / Load ─────────────────────────────────────────

function DepotSystem:sellBaleFillType(depotId, fillTypeName, fillTypeIndex, availableLiters, farmId)
    if not g_server then return false, "fd_error_server", 0, 0 end

    local depot = self._depots[depotId]
    if not depot then return false, "fd_error_depot", 0, 0 end
    if not fillTypeName or not fillTypeIndex or fillTypeIndex <= 0 then
        return false, "fd_error_filltype", 0, 0
    end

    local liters = math.max(0, availableLiters or 0)
    if liters <= 0 then return false, "fd_depot_tank_empty", 0, 0 end

    local cap = (g_DepotManager and g_DepotManager.settings.storageCapacity)
                or DepotConstants.STORAGE_CAPACITY
    local currentStored = depot.storageLevel[fillTypeName] or 0
    local space = cap - currentStored
    liters = math.min(liters, math.max(0, space))
    if liters <= 0 then return false, "fd_depot_storage_full", 0, 0 end

    local coop = g_DepotManager and g_DepotManager.coopState
    local revenue = self._pricing:calculateSellRevenue(fillTypeName, liters, farmId)

    if coop and coop:hasOwner() and not coop:isOwner(farmId) then
        local ownerFarmId = coop.ownerFarmId
        local ownerFarm = g_farmManager and g_farmManager:getFarmById(ownerFarmId)
        if not ownerFarm then
            DepotLogger.warning("Bale sale failed: owner farm nil for id=%d", ownerFarmId)
            return false, "fd_coop_no_owner", 0, 0
        end

        local ownerBalance = ownerFarm:getBalance()
        if ownerBalance < revenue then
            if ownerBalance <= 0 then
                return false, "fd_coop_owner_no_funds", 0, 0
            end

            local pricePerLiter = revenue / liters
            if pricePerLiter <= 0 then
                return false, "fd_coop_owner_no_funds", 0, 0
            end

            liters = math.floor(ownerBalance / pricePerLiter)
            if liters <= 0 then
                return false, "fd_coop_owner_no_funds", 0, 0
            end
            revenue = pricePerLiter * liters
        end

        depot.storageLevel[fillTypeName] = currentStored + liters

        resolveMoneyTypes()
        g_currentMission:addMoney(-revenue, ownerFarmId, MT_OWNER_EXPENSE, true, true)
        g_currentMission:addMoney(revenue, farmId, MT_SELLER_INCOME, true, true)

        if g_DepotManager and g_DepotManager.transactionLog then
            g_DepotManager.transactionLog:record("sell", farmId, fillTypeName, liters, revenue)
            if g_DepotManager.onTransactionRecorded then
                g_DepotManager:onTransactionRecorded("sell", farmId, fillTypeName, liters, revenue)
            end
        end

        DepotLogger.info("Bale sale accepted: %.0fL %s seller=%d owner=%d revenue=%.2f",
            liters, fillTypeName, farmId, ownerFarmId, revenue)
        return true, "fd_depot_sell_success", liters, revenue
    end

    depot.storageLevel[fillTypeName] = currentStored + liters
    DepotLogger.info("Bale deposit accepted: %.0fL %s farm=%d", liters, fillTypeName, farmId)
    return true, "fd_coop_deposit_success", liters, 0
end

function DepotSystem:saveToXML(xmlFile, depotId, basePath)
    local depot = self._depots[depotId]
    if not depot then return end
    DepotLogger.info("DepotSystem: Saving depot #%d to path '%s'", depotId, basePath)
    local i = 0
    for name, liters in pairs(depot.storageLevel) do
        if liters and liters > 0 then
            local path = basePath .. ".fill(" .. i .. ")"
            xmlFile:setString(path .. "#type", name)
            xmlFile:setFloat(path  .. "#liters", liters)
            DepotLogger.info("DepotSystem: Saved key='%s', name='%s', value=%.0fL", path, name, liters)
            i = i + 1
        end
    end
end

function DepotSystem:loadFromXML(xmlFile, depotId, basePath)
    local depot = self._depots[depotId]
    if not depot then return end
    DepotLogger.info("DepotSystem: Loading depot #%d from path '%s'", depotId, basePath)
    local i = 0
    while true do
        local path = basePath .. ".fill(" .. i .. ")"
        local name = xmlFile:getString(path .. "#type")
        if not name then 
            DepotLogger.info("DepotSystem: No more fill types found at index %d", i)
            break 
        end
        local liters = xmlFile:getFloat(path .. "#liters", 0)
        local cap = (g_DepotManager and g_DepotManager.settings.storageCapacity) or DepotConstants.STORAGE_CAPACITY
        depot.storageLevel[name] = math.max(0, math.min(cap, liters))
        DepotLogger.info("DepotSystem: Loaded key='%s', name='%s', value=%.0fL", path, name, liters)
        i = i + 1
    end
end
