-- =========================================================
-- FS25 Fertilizer Depot - Manager Singleton
-- =========================================================

local _depotMgrModName = g_currentModName

local function tr(key, fallback)
    local modEnv = g_modEnvironments and g_modEnvironments[_depotMgrModName]
    local i18n = (modEnv and modEnv.i18n) or g_i18n
    if i18n then
        local ok, text = pcall(function() return i18n:getText(key) end)
        if ok and text and text ~= "" and text ~= ("$l10n_" .. key)
           and not text:find("^Missing '") then
            return text
        end
    end
    return fallback or key
end

-- Scan vehicle + all attached implements for the FIRST non-empty fill unit.
-- Na cooperativa, aceitamos QUALQUER fillType (não filtramos por lista).
-- A validação real (preço, categoria) acontece depois em sellFillType.
local function findFertilizerInVehicle(vehicle, sfBridge)
    -- IGNORED list: fillTypes que sabidamente não fazem sentido (diesel, água, etc.)
    local ignored = DepotConstants.IGNORED_FILLTYPE_NAMES or {}

    DepotLogger.debug(">>> findFertilizerInVehicle iniciando scan em '%s'",
        tostring(vehicle.typeName or "?"))

    local function scan(v)
        local spec = v.spec_fillUnit
        if spec and spec.fillUnits then
            DepotLogger.debug("    veiculo '%s' tem %d fillUnits",
                tostring(v.typeName or "?"), #spec.fillUnits)
            for fuIdx, fu in ipairs(spec.fillUnits) do
                local lvl = fu.fillLevel or 0
                local ftName = nil
                if fu.fillType and fu.fillType > 0 and g_fillTypeManager then
                    ftName = g_fillTypeManager:getFillTypeNameByIndex(fu.fillType)
                end
                DepotLogger.debug("    fu[%d] fillType=%s name=%s level=%.1f",
                    fuIdx, tostring(fu.fillType), tostring(ftName), lvl)
                if lvl > 0 and fu.fillType and fu.fillType > 0 then
                    -- Aceita qualquer fillType com conteúdo, exceto os ignorados explicitamente
                    if ftName and not ignored[ftName] then
                        DepotLogger.info(">>> ACEITANDO %s (%.0f L) do veículo '%s' fu[%d]",
                            ftName, lvl, tostring(v.typeName or "?"), fuIdx)
                        return {
                            vehicle      = v,
                            unitIndex    = fuIdx,
                            fillTypeIndex= fu.fillType,
                            fillTypeName = ftName,
                            amount       = lvl,
                        }
                    elseif ftName and ignored[ftName] then
                        DepotLogger.debug("    %s ignorado (lista negra)", ftName)
                    end
                end
            end
        else
            DepotLogger.debug("    veiculo '%s' SEM spec_fillUnit",
                tostring(v.typeName or "?"))
        end
        if v.getAttachedImplements then
            for _, impl in pairs(v:getAttachedImplements() or {}) do
                if impl and impl.object then
                    local res = scan(impl.object)
                    if res then return res end
                end
            end
        end
        return nil
    end

    return scan(vehicle)
end

---@class DepotManager
DepotManager = {}
local DepotManager_mt = Class(DepotManager)

local PROXIMITY_THRESHOLD     = 5.0    -- metres, depot & silo on-foot radius (keep < gate distance)
local SILO_VEHICLE_PROXIMITY  = 10.0   -- metres, vehicle near silo (larger — harder to park precisely)
local VEHICLE_UNLOAD_PROXIMITY= 15.0   -- metres, vehicle near depot unload marker (aumentado: 8m era muito apertado)
local SILO_FILL_COOLDOWN      = 2000   -- ms between silo fill triggers
local DEPOT_SELL_COOLDOWN     = 5000   -- ms cooldown after sell dialog closes

function DepotManager.new()
    local self = setmetatable({}, DepotManager_mt)
    self.settings     = DepotSettings.new()
    self.coopState    = CoopState.new()
    self.transactionLog = TransactionLog.new()
    self.sfBridge     = SoilFertilizerBridge.new()
    self.pricing      = DepotPricing.new(self.sfBridge)
    self.depotSystem  = DepotSystem.new(self.pricing)
    self.deliverySystem = nil   -- created in initialize() after all singletons exist

    self.depots       = {}
    self.depotNodes   = {}
    self.depotUnloadNodes = {}
    self.depotProductSpawnNodes = {}
    self.silos        = {}
    self.siloNodes    = {}
    self._nextSiloId  = 1

    -- Pickup zone registry (PlaceableDepotPickup instances)
    self.pickupPlaceables = {}
    self.pickupNodes      = {}
    self._nextPickupId    = 1

    self.pendingOrders = {}
    self.activeDialog  = nil

    self._initialized       = false
    self._nearDepotId       = nil
    self._nearSiloId        = nil       -- on-foot silo proximity
    self._nearVehicleSiloId = nil       -- vehicle silo proximity (auto-trigger)
    self._nearUnloadDepotId = nil
    self._nearPickupId      = nil       -- on-foot pickup zone proximity
    self._proximityTimer    = 0
    self._settingsEventId   = nil

    -- Single persistent ACTIVATE_HANDTOOL event (shared for depot + silo + pickup)
    self._interactEventId  = nil
    self._siloFillCooldown = 0
    self._depotSellCooldown= 0

    -- Pending context for YesNo callbacks
    self._pendingSiloFill       = nil
    self._pendingDepotSell      = nil
    self._pendingDeliveryPickup = nil

    self.hud = nil   -- created in initialize()

    return self
end

function DepotManager:initialize()
    if self._initialized then return end
    self._initialized = true
    -- COOPERATIVA: delivery system desabilitado (não faz sentido pedido prévio
    -- pra cooperativa — a venda/compra é sempre direta com trailer encostado).
    self.deliverySystem = nil
    if g_dedicatedServerInfo == nil
    and g_currentMission and g_currentMission:getIsClient() then
        self.hud = DepotHUD.new()
    end
    DepotLogger.info(">>> DepotManager inicializado (delivery DESABILITADO na cooperativa)")
    addConsoleCommand("SoilDebugDepot",   "Toggle FertDepot debug logging",            "cmdDebugDepot",      self)
    addConsoleCommand("FDFillStock",      "Fill all depot storage to max [depotId]",   "cmdFDFillStock",     self)
    addConsoleCommand("FDEmptyStock",     "Empty all depot storage [depotId]",         "cmdFDEmptyStock",    self)
end

function DepotManager:delete()
    if self.activeDialog then
        self.activeDialog:close()
        self.activeDialog = nil
    end
    if self.hud then
        self.hud:delete()
        self.hud = nil
    end
    if self._interactEventId and g_inputBinding then
        g_inputBinding:removeActionEvent(self._interactEventId)
        self._interactEventId = nil
    end
    self.sfBridge:invalidateCache()
    DepotLogger.info("DepotManager deleted")
end

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

function DepotManager:registerDepot(placeableDepot)
    DepotLogger.info(">>> registerDepot chamado")

    -- ====== COOPERATIVA: só permite UMA instância ======
    if self.coopState and not self.coopState:canRegisterNewDepot() then
        DepotLogger.warning(
            "Tentativa de colocar 2ª cooperativa rejeitada — só pode haver uma por mapa")
        placeableDepot._coopRejected = true
        return nil
    end

    local id = self.depotSystem:registerDepot()
    self.depots[id] = placeableDepot
    placeableDepot.depotId = id
    local spec = placeableDepot[PlaceableDepot.SPEC_TABLE_NAME]
    self.depotNodes[id] = (spec and spec.playerTriggerNode) or placeableDepot.rootNode

    -- Captura o farmId do dono (quem colocou o placeable)
    if self.coopState then
        self.coopState:onDepotRegistered()
        local placingFarmId = placeableDepot:getOwnerFarmId()
        if placingFarmId and placingFarmId > 0 then
            self.coopState:setOwner(placingFarmId)
            -- Avisa todos os clientes do novo proprietário
            if CoopOwnerSyncEvent and g_server then
                CoopOwnerSyncEvent.broadcast(self.coopState.ownerFarmId)
            end
        end
    end

    DepotLogger.info(">>> Cooperativa registrada: id=%d ownerFarmId=%d playerTrigger=%s",
        id, (self.coopState and self.coopState.ownerFarmId) or 0,
        tostring(spec and spec.playerTriggerNode))
    return id
end

function DepotManager:unregisterDepot(depotId)
    self.depotSystem:unregisterDepot(depotId)
    self.depots[depotId] = nil
    self.depotNodes[depotId] = nil
    self.depotUnloadNodes[depotId] = nil
    self.depotProductSpawnNodes[depotId] = nil
    if self._nearDepotId == depotId then
        self._nearDepotId = nil
        self:_updateInteractPrompt()
    end
    if self._nearUnloadDepotId == depotId then
        self._nearUnloadDepotId = nil
    end
    -- ====== COOPERATIVA: avisa o coopState ======
    if self.coopState then
        self.coopState:onDepotUnregistered()
    end
    DepotLogger.info("Cooperativa #%d removida", depotId)
end

function DepotManager:registerDepotUnloadNode(depotId, node)
    self.depotUnloadNodes[depotId] = node
    DepotLogger.info(">>> Cooperativa #%d UNLOAD NODE registrado: %s", depotId, tostring(node))
end

function DepotManager:registerDepotProductSpawnNode(depotId, node)
    self.depotProductSpawnNodes[depotId] = node
    DepotLogger.info("Depot #%d product spawn node registered: %s", depotId, tostring(node))
end

-- ─── Pickup Zone Registration ────────────────────────────

function DepotManager:registerPickup(placeablePickup)
    local id = self._nextPickupId
    self._nextPickupId = self._nextPickupId + 1
    self.pickupPlaceables[id] = placeablePickup
    local spec = placeablePickup[PlaceableDepotPickup.SPEC_TABLE_NAME]
    self.pickupNodes[id] = (spec and spec.playerTriggerNode) or placeablePickup.rootNode
    DepotLogger.info("Pickup zone #%d registered", id)
    return id
end

function DepotManager:unregisterPickup(pickupId)
    self.pickupPlaceables[pickupId] = nil
    self.pickupNodes[pickupId] = nil
    if self._nearPickupId == pickupId then
        self._nearPickupId = nil
        self:_updateInteractPrompt()
    end
    DepotLogger.info("Pickup zone #%d unregistered", pickupId)
end

-- ─── Silo Registration ───────────────────────────────────

function DepotManager:registerSilo(placeableSilo, loadStationNode)
    local id = self._nextSiloId
    self._nextSiloId = self._nextSiloId + 1
    self.silos[id] = placeableSilo
    self.siloNodes[id] = loadStationNode or placeableSilo.rootNode
    DepotLogger.info("Silo #%d registered (loadStation node: %s)", id, tostring(self.siloNodes[id]))
    return id
end

function DepotManager:unregisterSilo(siloId)
    self.silos[siloId] = nil
    self.siloNodes[siloId] = nil
    if self._nearSiloId == siloId then
        self._nearSiloId = nil
        self:_updateInteractPrompt()
    end
    if self._nearVehicleSiloId == siloId then
        self._nearVehicleSiloId = nil
    end
    DepotLogger.info("Silo #%d unregistered", siloId)
end

-- ─── Pending Orders ──────────────────────────────────────

function DepotManager:setPendingOrder(farmId, depotId, fillTypeName, fillTypeIndex, maxLiters, displayName)
    self.pendingOrders[farmId] = {
        depotId       = depotId,
        fillTypeName  = fillTypeName,
        fillTypeIndex = fillTypeIndex,
        maxLiters     = maxLiters,
        displayName   = displayName or fillTypeName,
    }
    self:_updateInteractPrompt()
    DepotLogger.info(">>> Pedido registrado: farm=%d %s %.0fL — colete no silo da cooperativa (E)",
        farmId, fillTypeName, maxLiters)
end

function DepotManager:getPendingOrder(farmId)
    return self.pendingOrders[farmId]
end

function DepotManager:clearPendingOrder(farmId)
    self.pendingOrders[farmId] = nil
    self:_updateInteractPrompt()
end

-- ─── Network Helpers ─────────────────────────────────────

-- Resolve o NOME da fazenda a partir do farmId (com fallback "Fazenda N")
function DepotManager:getFarmName(farmId)
    if farmId == nil or farmId == 0 then
        return g_i18n:getText("fd_hist_farm") .. " ?"
    end
    if g_farmManager then
        local farm = g_farmManager:getFarmById(farmId)
        if farm then
            -- farm.name é o nome definido pelo jogador
            if farm.name and farm.name ~= "" then
                return farm.name
            end
        end
    end
    -- Fallback: "Fazenda N"
    return string.format("%s %d", g_i18n:getText("fd_hist_farm"), farmId)
end

function DepotManager:broadcastSync(depotId)
    DepotSyncEvent.broadcast(depotId)
end

-- Chamado (no servidor) quando uma transação é registrada.
-- Dispara o evento de rede que notifica o dono.
function DepotManager:onTransactionRecorded(kind, farmId, fillTypeName, liters, value)
    if not g_server then return end
    local ownerFarmId = self.coopState and self.coopState.ownerFarmId or 0
    if ownerFarmId == 0 then return end

    if CoopTransactionEvent then
        CoopTransactionEvent.broadcast(kind, farmId, fillTypeName, liters, value, ownerFarmId)
    end

    -- Se o servidor É o dono (host jogando como dono), notifica localmente também
    local hostFarmId = self.getLocalFarmId and self:getLocalFarmId() or 0
    if hostFarmId == ownerFarmId and farmId ~= ownerFarmId then
        self:showTransactionNotification(kind, farmId, fillTypeName, liters, value)
    end
end

-- Mostra a notificação lateral pro dono.
function DepotManager:showTransactionNotification(kind, farmId, fillTypeName, liters, value)
    if g_currentMission == nil or g_currentMission.hud == nil then return end
    if g_currentMission.hud.addSideNotification == nil then return end

    -- Nome amigável do produto
    local displayName = fillTypeName
    if g_fillTypeManager then
        local ft = g_fillTypeManager:getFillTypeByName(fillTypeName)
        if ft and ft.title then displayName = ft.title end
    end

    local moneyStr = g_i18n and g_i18n:formatMoney(value, 0, true) or ("$" .. math.floor(value))
    local farmName = self:getFarmName(farmId)
    local color, msg

    if kind == "sell" then
        -- Fazendeiro vendeu À coop (dono pagou)
        color = {1.0, 0.7, 0.3, 1.0}
        msg = string.format(
            g_i18n:getText("fd_notif_sell"),
            farmName, math.floor(liters), displayName, moneyStr)
    else
        -- Fazendeiro comprou DA coop (dono recebeu)
        color = {0.4, 0.9, 0.4, 1.0}
        msg = string.format(
            g_i18n:getText("fd_notif_buy"),
            farmName, math.floor(liters), displayName, moneyStr)
    end

    g_currentMission.hud:addSideNotification(color, msg)
end

-- ─── Dialog Management ───────────────────────────────────

function DepotManager:openDialog(depotId)
    DepotLogger.info("openDialog called for depot #%s", tostring(depotId))
    if self.activeDialog then
        self.activeDialog:close()
        self.activeDialog = nil
    end
    DepotDialog.show(depotId)
    self.activeDialog = DepotDialog.getInstance()
end

function DepotManager:closeDialog()
    if self.activeDialog then
        self.activeDialog:close()
        self.activeDialog = nil
    end
end

-- Helper: resolve o farmId do jogador local de forma robusta
function DepotManager:getLocalFarmId()
    -- Tenta várias fontes (FS25 expõe de jeitos diferentes conforme o contexto)
    if g_localPlayer and g_localPlayer.farmId and g_localPlayer.farmId > 0 then
        return g_localPlayer.farmId
    end
    if g_currentMission and g_currentMission.getFarmId then
        local ok, fid = pcall(function() return g_currentMission:getFarmId() end)
        if ok and fid and fid > 0 then return fid end
    end
    if g_currentMission and g_currentMission.playerSystem and g_localPlayer then
        local p = g_currentMission.playerSystem:getPlayerByUserId(g_localPlayer.userId)
        if p and p.farmId and p.farmId > 0 then return p.farmId end
    end
    if g_farmManager and g_localPlayer and g_localPlayer.userId then
        local farm = g_farmManager:getFarmByUserId(g_localPlayer.userId)
        if farm and farm.farmId and farm.farmId > 0 then return farm.farmId end
    end
    return 0
end

function DepotManager:openSettingsDialog()
    -- COOPERATIVA: configurações são exclusivas do proprietário.
    local coop = self.coopState
    local myFarmId = self:getLocalFarmId()

    DepotLogger.info(">>> openSettingsDialog: myFarmId resolvido = %d, owner = %d",
        myFarmId, (coop and coop.ownerFarmId) or 0)

    if coop and coop:hasOwner() then
        if not coop:isOwner(myFarmId) then
            DepotLogger.info(">>> openSettingsDialog BLOQUEADO: farmId=%d não é o dono (owner=%d)",
                myFarmId, coop.ownerFarmId or 0)
            if g_currentMission and g_currentMission.hud and g_currentMission.hud.addSideNotification then
                g_currentMission.hud:addSideNotification(
                    {1.0, 0.4, 0.4, 1.0},
                    g_i18n:getText("fd_coop_settings_owner_only"))
            end
            return
        end
    else
        DepotLogger.info(">>> openSettingsDialog BLOQUEADO: cooperativa sem proprietário")
        return
    end

    DepotLogger.info(">>> openSettingsDialog: proprietário farmId=%d abrindo configurações", myFarmId)
    DepotSettingsDialog.show()
end

-- ─── Update ──────────────────────────────────────────────

function DepotManager:update(dt)
    if self.hud then self.hud:update(dt) end

    if self._siloFillCooldown > 0 then
        self._siloFillCooldown = self._siloFillCooldown - dt
    end
    if self._depotSellCooldown > 0 then
        self._depotSellCooldown = self._depotSellCooldown - dt
    end

    self._proximityTimer = self._proximityTimer + dt
    if self._proximityTimer < 500 then return end
    self._proximityTimer = 0

    self:_checkDepotProximity()
    self:_checkSiloProximity()
    self:_checkPickupProximity()
    self:_checkDepotVehicles()
end

-- ─── Single Persistent Interact Action ───────────────────

function DepotManager:_getOrRegisterInteractEvent()
    if self._interactEventId then return self._interactEventId end
    if not g_inputBinding then return nil end

    g_inputBinding:beginActionEventsModification(PlayerInputComponent.INPUT_CONTEXT_NAME)
    local ok, evId = g_inputBinding:registerActionEvent(
        InputAction.FD_INTERACT, self,
        DepotManager._onInteractAction, false, true, false, true)
    if ok and evId then
        self._interactEventId = evId
        g_inputBinding:setActionEventTextVisibility(evId, false)
        g_inputBinding:setActionEventActive(evId, false)
        DepotLogger.info("FD_INTERACT registered (id=%s)", tostring(evId))
    end
    g_inputBinding:endActionEventsModification()
    return self._interactEventId
end

function DepotManager:_updateInteractPrompt()
    local evId = self:_getOrRegisterInteractEvent()
    if not evId or not g_inputBinding then return end

    local farmId = self:getLocalFarmId()

    -- Silo (placeable separado, se existir) — coleta de pedido
    if self._nearSiloId then
        local order = self.pendingOrders[farmId]
        if order then
            local label = string.format(
                tr("fd_silo_collect_action", "Coletar %s (%.0fL)"),
                order.displayName, order.maxLiters)
            g_inputBinding:setActionEventText(evId, label)
            g_inputBinding:setActionEventTextVisibility(evId, true)
            g_inputBinding:setActionEventActive(evId, true)
            return
        else
            g_inputBinding:setActionEventText(evId,
                tr("fd_silo_no_order", "Sem pedido pendente. Compre na cooperativa primeiro."))
            g_inputBinding:setActionEventTextVisibility(evId, true)
            g_inputBinding:setActionEventActive(evId, false)
            return
        end
    end

    -- COOPERATIVA: se há pedido pendente, o E coleta AQUI MESMO (sem silo separado).
    -- Se não há pedido, o E abre o menu normalmente.
    if self._nearDepotId then
        local order = self.pendingOrders[farmId]
        if order then
            local label = string.format(
                tr("fd_coop_collect_action", "Coletar %s (%.0fL)"),
                order.displayName, order.maxLiters)
            g_inputBinding:setActionEventText(evId, label)
            g_inputBinding:setActionEventTextVisibility(evId, true)
            g_inputBinding:setActionEventActive(evId, true)
            return
        end
        g_inputBinding:setActionEventText(evId,
            tr("fd_depot_open_action", "Abrir Cooperativa"))
        g_inputBinding:setActionEventTextVisibility(evId, true)
        g_inputBinding:setActionEventActive(evId, true)
        return
    end

    -- Nothing nearby — hide prompt
    g_inputBinding:setActionEventTextVisibility(evId, false)
    g_inputBinding:setActionEventActive(evId, false)
end

function DepotManager:_onInteractAction()
    local farmId = self:getLocalFarmId()

    -- Silo separado (se existir) — coleta de pedido
    if self._nearSiloId then
        if self._siloFillCooldown > 0 then return end
        local order = self.pendingOrders[farmId]
        if not order then return end
        self:_tryShowSiloFillDialog(self._nearSiloId, farmId, order)
        return
    end

    -- COOPERATIVA: se há pedido pendente, COLETA aqui mesmo. Senão abre o menu.
    if self._nearDepotId then
        local order = self.pendingOrders[farmId]
        if order then
            if self._siloFillCooldown > 0 then return end
            DepotLogger.info(">>> Coletando pedido na cooperativa: %s %.0fL farm=%d",
                order.fillTypeName, order.maxLiters, farmId)
            -- Usa a própria cooperativa como ponto de coleta (depot é também o silo)
            self:_tryShowCoopCollectDialog(self._nearDepotId, farmId, order)
            return
        end
        DepotLogger.info("_onInteractAction: abrir cooperativa #%d", self._nearDepotId)
        self:openDialog(self._nearDepotId)
    end
end

-- COOPERATIVA: coleta um pedido pendente diretamente na cooperativa.
-- Regra: se há trailer compatível encostado → granel. Senão, se o produto
-- tem palete → spawna o palete físico. Senão → avisa pra encostar trailer.
function DepotManager:_tryShowCoopCollectDialog(depotId, farmId, order)
    -- 1) Tem trailer/tanque compatível encostado? → granel
    local vehicle, unitIndex = self.depotSystem:findCompatibleVehicle(
        depotId, order.fillTypeIndex, false)

    if vehicle and unitIndex then
        local text = string.format(
            tr("fd_coop_collect_confirm", "Coletar %.0f L de %s no seu veículo?"),
            order.maxLiters, order.displayName or order.fillTypeName)
        self._pendingCoopCollect = {
            mode          = "bulk",
            depotId       = depotId,
            fillTypeName  = order.fillTypeName,
            fillTypeIndex = order.fillTypeIndex,
            maxLiters     = order.maxLiters,
            farmId        = farmId,
        }
        YesNoDialog.show(DepotManager._onCoopCollectConfirm, self, text)
        return
    end

    -- 2) Sem trailer, mas o produto tem palete? → spawna palete físico
    if self.depotSystem:fillTypeHasPallet(order.fillTypeName, order.fillTypeIndex) then
        local qty = math.max(1, math.ceil(order.maxLiters / DepotConstants.PRODUCT_LITRES_PER_UNIT))
        qty = math.min(qty, DepotConstants.MAX_PRODUCT_QUANTITY)
        local text = string.format(
            tr("fd_coop_collect_pallet_confirm",
               "Gerar %d palete(s) de %s ao lado da cooperativa?"),
            qty, order.displayName or order.fillTypeName)
        self._pendingCoopCollect = {
            mode          = "pallet",
            depotId       = depotId,
            fillTypeName  = order.fillTypeName,
            fillTypeIndex = order.fillTypeIndex,
            quantity      = qty,
            farmId        = farmId,
        }
        YesNoDialog.show(DepotManager._onCoopCollectConfirm, self, text)
        return
    end

    -- 3) Nem trailer nem palete → precisa de trailer
    if g_currentMission and g_currentMission.hud and g_currentMission.hud.addSideNotification then
        g_currentMission.hud:addSideNotification({1.0, 0.8, 0.3, 1.0},
            tr("fd_coop_collect_no_vehicle",
               "Encoste um reboque compatível na cooperativa para coletar."))
    end
end

function DepotManager:_onCoopCollectConfirm(result)
    local ctx = self._pendingCoopCollect
    self._pendingCoopCollect = nil
    self._siloFillCooldown = 1500
    if not result or not ctx then return end

    if ctx.mode == "pallet" then
        DepotLogger.info(">>> Coleta via PALETE: %s ×%d farm=%d",
            ctx.fillTypeName, ctx.quantity, ctx.farmId)
        DepotProductOrderEvent.sendToServer(
            ctx.depotId, ctx.fillTypeName, ctx.fillTypeIndex, ctx.quantity, ctx.farmId)
    else
        -- Granel: reusa o evento de compra
        DepotLogger.info(">>> Coleta via GRANEL: %s %.0fL farm=%d",
            ctx.fillTypeName, ctx.maxLiters, ctx.farmId)
        DepotPurchaseEvent.sendToServer(
            ctx.depotId, ctx.fillTypeName, ctx.fillTypeIndex, ctx.maxLiters, ctx.farmId)
    end

    self.pendingOrders[ctx.farmId] = nil
    self:_updateInteractPrompt()
end

function DepotManager:_onSiloFillConfirm(result)
    local ctx = self._pendingSiloFill
    self._pendingSiloFill = nil
    if not result or not ctx then return end

    DepotLogger.info("Silo fill confirmed: depot=%d silo=%d %s %.0fL farm=%d",
        ctx.depotId, ctx.siloId, ctx.fillTypeName, ctx.maxLiters, ctx.farmId)

    DepotSiloFillEvent.sendToServer(
        ctx.depotId, ctx.siloId,
        ctx.fillTypeName, ctx.fillTypeIndex, ctx.maxLiters, ctx.farmId)

    self.pendingOrders[ctx.farmId] = nil
    self:_updateInteractPrompt()
end

-- ─── Depot Proximity (on-foot player) ───────────────────

function DepotManager:_checkDepotProximity()
    if not next(self.depotNodes) then return end

    local px, pz
    if g_localPlayer and g_localPlayer.rootNode then
        local ok, x, y, z = pcall(getWorldTranslation, g_localPlayer.rootNode)
        if ok and x then px, pz = x, z end
    end
    if not px then
        local cv = g_currentMission and g_currentMission.controlledVehicle
        if cv and cv.rootNode then
            local ok, x, y, z = pcall(getWorldTranslation, cv.rootNode)
            if ok and x then px, pz = x, z end
        end
    end

    if not px then
        if self._nearDepotId then
            self._nearDepotId = nil
            self:_updateInteractPrompt()
            self:closeDialog()
        end
        return
    end

    local nearId, nearDist = nil, PROXIMITY_THRESHOLD + 1
    for id, node in pairs(self.depotNodes) do
        if node then
            local ok, dx, dy, dz = pcall(getWorldTranslation, node)
            if ok and dx then
                local dist = math.sqrt((px - dx) ^ 2 + (pz - dz) ^ 2)
                if dist < PROXIMITY_THRESHOLD and dist < nearDist then
                    nearId, nearDist = id, dist
                end
            end
        end
    end

    if nearId ~= self._nearDepotId then
        local prev = self._nearDepotId
        self._nearDepotId = nearId
        if not nearId and prev then
            DepotLogger.info("Proximity: left depot #%d", prev)
            self:closeDialog()
        elseif nearId then
            DepotLogger.info("Proximity: entered depot #%d", nearId)
        end
        self:_updateInteractPrompt()
    end
end

-- ─── Silo Proximity ──────────────────────────────────────
-- On foot : sets _nearSiloId → shows E-key prompt → player presses E → YesNo.
-- In vehicle: auto-shows YesNo when vehicle pulls up (same pattern as depot sell).

function DepotManager:_checkSiloProximity()
    if not next(self.siloNodes) then return end

    local cv = g_currentMission and g_currentMission.controlledVehicle

    if cv then
        self:_checkSiloProximityVehicle(cv)
    else
        self:_checkSiloProximityOnFoot()
    end
end

function DepotManager:_checkSiloProximityVehicle(cv)
    -- Clear any on-foot silo state when the player gets in a vehicle
    if self._nearSiloId then
        self._nearSiloId = nil
        self:_updateInteractPrompt()
    end

    if not cv.rootNode then return end
    local ok, vx, vy, vz = pcall(getWorldTranslation, cv.rootNode)
    if not ok or not vx then return end

    local nearSiloId = nil
    for id, node in pairs(self.siloNodes) do
        if node then
            local nok, sx, sy, sz = pcall(getWorldTranslation, node)
            if nok and sx then
                local dist = math.sqrt((vx - sx) ^ 2 + (vz - sz) ^ 2)
                if dist <= SILO_VEHICLE_PROXIMITY then
                    nearSiloId = id
                    break
                end
            end
        end
    end

    if nearSiloId ~= self._nearVehicleSiloId then
        local prev = self._nearVehicleSiloId
        self._nearVehicleSiloId = nearSiloId
        if nearSiloId and not prev then
            DepotLogger.info("Silo proximity: vehicle entered silo #%d", nearSiloId)
            local farmId = g_localPlayer and g_localPlayer.farmId or 0
            local order = self.pendingOrders[farmId]
            if order and self._siloFillCooldown <= 0 then
                self:_tryShowSiloFillDialog(nearSiloId, farmId, order)
            end
        elseif not nearSiloId and prev then
            DepotLogger.info("Silo proximity: vehicle left silo #%d", prev)
        end
    end
end

function DepotManager:_checkSiloProximityOnFoot()
    -- Clear vehicle silo state when player exits vehicle
    if self._nearVehicleSiloId then
        self._nearVehicleSiloId = nil
    end

    local px, pz
    if g_localPlayer and g_localPlayer.rootNode then
        local ok, x, y, z = pcall(getWorldTranslation, g_localPlayer.rootNode)
        if ok and x then px, pz = x, z end
    end

    if not px then
        if self._nearSiloId then
            self._nearSiloId = nil
            self:_updateInteractPrompt()
        end
        return
    end

    local nearSiloId = nil
    for id, node in pairs(self.siloNodes) do
        if node then
            local ok, sx, sy, sz = pcall(getWorldTranslation, node)
            if ok and sx then
                local dist = math.sqrt((px - sx) ^ 2 + (pz - sz) ^ 2)
                if dist <= PROXIMITY_THRESHOLD then
                    nearSiloId = id
                    break
                end
            end
        end
    end

    if nearSiloId ~= self._nearSiloId then
        local prev = self._nearSiloId
        self._nearSiloId = nearSiloId
        if nearSiloId then
            DepotLogger.info("Silo proximity: entered silo #%d (on foot)", nearSiloId)
        elseif prev then
            DepotLogger.info("Silo proximity: left silo #%d (on foot)", prev)
        end
        self:_updateInteractPrompt()
    elseif nearSiloId then
        self:_updateInteractPrompt()
    end
end

-- Shared: builds context and shows YesNo confirmation for silo fill.
-- Called from both the on-foot E-key path and the vehicle auto-trigger path.
function DepotManager:_tryShowSiloFillDialog(siloId, farmId, order)
    local depotId = order.depotId
    if not depotId or not self.depots[depotId] then
        depotId = next(self.depots)
    end
    if not depotId then
        DepotLogger.warning("Silo fill: no depot registered")
        return
    end

    self._pendingSiloFill = {
        depotId      = depotId,
        siloId       = siloId,
        fillTypeName = order.fillTypeName,
        fillTypeIndex= order.fillTypeIndex,
        maxLiters    = order.maxLiters,
        displayName  = order.displayName,
        farmId       = farmId,
    }

    self._siloFillCooldown = SILO_FILL_COOLDOWN

    local text = string.format(
        tr("fd_silo_fill_confirm", "Collect %.0fL of %s?\n\nYour vehicle will be filled."),
        order.maxLiters, order.displayName)
    YesNoDialog.show(DepotManager._onSiloFillConfirm, self, text)
end

-- ─── Pickup Zone Proximity (on-foot) ────────────────────

function DepotManager:_checkPickupProximity()
    if not next(self.pickupNodes) then return end
    -- Only relevant when on foot
    if g_currentMission and g_currentMission.controlledVehicle then
        if self._nearPickupId then
            self._nearPickupId = nil
            self:_updateInteractPrompt()
        end
        return
    end

    local px, pz
    if g_localPlayer and g_localPlayer.rootNode then
        local ok, x, y, z = pcall(getWorldTranslation, g_localPlayer.rootNode)
        if ok and x then px, pz = x, z end
    end

    if not px then
        if self._nearPickupId then
            self._nearPickupId = nil
            self:_updateInteractPrompt()
        end
        return
    end

    local nearId = nil
    for id, node in pairs(self.pickupNodes) do
        if node then
            local ok, nx, ny, nz = pcall(getWorldTranslation, node)
            if ok and nx then
                local dist = math.sqrt((px - nx)^2 + (pz - nz)^2)
                if dist <= DepotConstants.DELIVERY.PROXIMITY_PICKUP then
                    nearId = id
                    break
                end
            end
        end
    end

    if nearId ~= self._nearPickupId then
        local prev = self._nearPickupId
        self._nearPickupId = nearId
        if nearId then
            DepotLogger.info("Pickup proximity: entered zone #%d", nearId)
        elseif prev then
            DepotLogger.info("Pickup proximity: left zone #%d", prev)
        end
        self:_updateInteractPrompt()
    elseif nearId then
        self:_updateInteractPrompt()
    end
end

function DepotManager:_tryShowDeliveryPickupDialog()
    local farmId = g_localPlayer and g_localPlayer.farmId or 0
    if not self.deliverySystem then return end

    -- Find a PENDING delivery for this farm across all depots
    local depotId, delivery = nil, nil
    for dId, rec in pairs(self.deliverySystem.deliveries) do
        if rec.farmId == farmId and rec.status == DeliverySystem.STATUS.PENDING then
            depotId  = dId
            delivery = rec
            break
        end
    end
    if not delivery then return end

    -- Client-side money pre-check — gives immediate feedback instead of silent server rejection
    local farm = g_farmManager and g_farmManager:getFarmById(farmId)
    if farm and farm:getBalance() < delivery.deliveryCost then
        if g_currentMission and g_currentMission.hud then
            g_currentMission.hud:showBlinkingWarning(
                tr("fd_depot_no_money", "Not enough money."), 3000)
        end
        return
    end

    self._pendingDeliveryPickup = { depotId = depotId, farmId = farmId }

    local costStr = g_i18n and g_i18n:formatMoney(delivery.deliveryCost, 0, true)
                   or string.format("$%.2f", delivery.deliveryCost)
    local text = string.format(
        tr("fd_delivery_pickup_confirm",
           "Collect delivery for %s?\n\nGoods will be stocked when you return to your depot."),
        costStr)
    YesNoDialog.show(DepotManager._onDeliveryPickupConfirm, self, text)
end

function DepotManager:_onDeliveryPickupConfirm(result)
    local ctx = self._pendingDeliveryPickup
    self._pendingDeliveryPickup = nil
    if not result or not ctx then return end
    DepotLogger.info("Delivery pickup confirmed: depot=%d farm=%d", ctx.depotId, ctx.farmId)
    DepotDeliveryPickupEvent.sendToServer(ctx.depotId, ctx.farmId)
end

-- ─── Vehicle at Depot Unload Trigger ─────────────────────

function DepotManager:_checkDepotVehicles()
    if not next(self.depotUnloadNodes) then
        if not next(self.depots) then
            return
        end
        -- Log uma vez quando não há nodes registrados
        if not self._loggedNoUnloadNodes then
            self._loggedNoUnloadNodes = true
            DepotLogger.warning(">>> SEM unload nodes registrados! depotUnloadNodes vazio")
        end
        return
    end

    local cv = g_currentMission and g_currentMission.controlledVehicle
    if not cv or not cv.rootNode then
        if self._nearUnloadDepotId then
            self._nearUnloadDepotId = nil
        end
        return
    end

    local ok, vx, vy, vz = pcall(getWorldTranslation, cv.rootNode)
    if not ok or not vx then return end

    local nearId = nil
    local closestDist = 9999
    for id, node in pairs(self.depotUnloadNodes) do
        if node then
            local nok, nx, ny, nz = pcall(getWorldTranslation, node)
            if nok and nx then
                local dist = math.sqrt((vx - nx) ^ 2 + (vz - nz) ^ 2)
                if dist < closestDist then closestDist = dist end
                if dist <= VEHICLE_UNLOAD_PROXIMITY then
                    nearId = id
                    break
                end
            end
        end
    end

    -- Log periódico de proximidade (a cada 5s pra não floodar)
    self._lastProxLog = (self._lastProxLog or 0) - 500
    if self._lastProxLog <= 0 and closestDist < 30 then
        self._lastProxLog = 5000
        DepotLogger.debug(">>> veículo a %.1fm do unload mais próximo (limite=%dm)",
            closestDist, VEHICLE_UNLOAD_PROXIMITY)
    end

    if nearId ~= self._nearUnloadDepotId then
        local prev = self._nearUnloadDepotId
        self._nearUnloadDepotId = nearId
        if nearId and not prev then
            DepotLogger.info(">>> Veículo ENTROU na zona de descarga (depot #%d, dist=%.1fm)",
                nearId, closestDist)
            if self._depotSellCooldown <= 0 then
                self:_tryShowDepotSellDialog(nearId, cv)
            else
                DepotLogger.info(">>> mas cooldown ainda ativo (%dms)", self._depotSellCooldown)
            end
        elseif not nearId and prev then
            DepotLogger.info(">>> Veículo SAIU da zona de descarga (depot #%d)", prev)
        end
    end
end

function DepotManager:_tryShowDepotSellDialog(depotId, vehicle)
    local found = findFertilizerInVehicle(vehicle, self.sfBridge)
    if not found then
        DepotLogger.info("Vehicle at depot #%d unload: no fillType encontrado no veículo", depotId)
        return
    end

    local farmId = g_localPlayer and g_localPlayer.farmId or 0

    -- Calcula revenue considerando spread (se a farm é o dono, valor zero = depósito)
    local revenue = self.pricing:calculateSellRevenue(found.fillTypeName, found.amount, farmId)
    local revenueStr = g_i18n and g_i18n:formatMoney(revenue, 0, true)
                       or string.format("$%.0f", revenue)

    -- Resolve nome traduzido do fillType (se disponível)
    local ftTitle = found.fillTypeName
    if g_fillTypeManager then
        local ft = g_fillTypeManager:getFillTypeByIndex(found.fillTypeIndex)
        if ft and ft.title then ftTitle = ft.title end
    end

    self._pendingDepotSell = {
        depotId      = depotId,
        fillTypeName = found.fillTypeName,
        fillTypeIndex= found.fillTypeIndex,
        amount       = found.amount,
        farmId       = farmId,
    }

    local text
    local coop = self.coopState
    if coop and coop:hasOwner() and coop:isOwner(farmId) then
        -- Dono está depositando ao próprio estoque
        text = string.format(
            "Depositar %.0f L de %s na cooperativa?",
            found.amount, ftTitle)
    elseif coop and coop:hasOwner() then
        -- Outro fazendeiro vendendo à coop
        text = string.format(
            "Vender %.0f L de %s à cooperativa por %s?\n(Preço com -%.0f%% de desconto)",
            found.amount, ftTitle, revenueStr,
            (coop:getSellSpread() * 100))
    else
        text = string.format(
            "Vender %.0f L de %s por %s?",
            found.amount, ftTitle, revenueStr)
    end

    DepotLogger.info("Showing depot sell dialog: %.0fL %s for farm %d revenue=%.2f",
        found.amount, found.fillTypeName, farmId, revenue)
    YesNoDialog.show(DepotManager._onDepotSellConfirm, self, text)
end

function DepotManager:_onDepotSellConfirm(result)
    local ctx = self._pendingDepotSell
    self._pendingDepotSell = nil
    self._depotSellCooldown = DEPOT_SELL_COOLDOWN

    if not result or not ctx then
        DepotLogger.info(">>> _onDepotSellConfirm CANCELADO (result=%s ctx=%s)",
            tostring(result), tostring(ctx ~= nil))
        return
    end

    DepotLogger.info(">>> _onDepotSellConfirm CONFIRMADO: %.0fL %s pra farm=%d → enviando ao server",
        ctx.amount, ctx.fillTypeName, ctx.farmId)

    DepotSellEvent.sendToServer(
        ctx.depotId, ctx.fillTypeName, ctx.fillTypeIndex, ctx.amount, ctx.farmId)
end

-- ─── HUD ─────────────────────────────────────────────────

function DepotManager:drawHUD()
    if self.hud then self.hud:draw() end
end

-- ─── Console Commands ────────────────────────────────────

function DepotManager:cmdDebugDepot()
    DepotLogger._debug = not DepotLogger._debug
    return DepotConstants.LOG_PREFIX .. " Debug: " .. tostring(DepotLogger._debug)
end

local function _iterateDepots(manager, targetId, fn)
    local count = 0
    for id, placeable in pairs(manager.depots) do
        if targetId == nil or id == targetId then
            fn(id, placeable)
            count = count + 1
        end
    end
    return count
end

function DepotManager:cmdFDFillStock(depotIdArg)
    if not g_server then
        return DepotConstants.LOG_PREFIX .. " FDFillStock: server only"
    end
    local targetId = depotIdArg and tonumber(depotIdArg) or nil
    local fillTypes = self.sfBridge:getFillTypeList()
    local cap = self.settings.storageCapacity or DepotConstants.STORAGE_CAPACITY
    local depotCount = _iterateDepots(self, targetId, function(id, _)
        for _, ft in ipairs(fillTypes) do
            self.depotSystem:setStorageLevel(id, ft.name, cap)
        end
        self:broadcastSync(id)
        DepotLogger.info("FDFillStock: depot #%d filled (%d types, %.0fL each)", id, #fillTypes, cap)
    end)
    return string.format("%s FDFillStock: filled %d depot(s) to %.0fL each type",
        DepotConstants.LOG_PREFIX, depotCount, cap)
end

function DepotManager:cmdFDEmptyStock(depotIdArg)
    if not g_server then
        return DepotConstants.LOG_PREFIX .. " FDEmptyStock: server only"
    end
    local targetId = depotIdArg and tonumber(depotIdArg) or nil
    local fillTypes = self.sfBridge:getFillTypeList()
    local depotCount = _iterateDepots(self, targetId, function(id, _)
        for _, ft in ipairs(fillTypes) do
            self.depotSystem:setStorageLevel(id, ft.name, 0)
        end
        self:broadcastSync(id)
        DepotLogger.info("FDEmptyStock: depot #%d emptied", id)
    end)
    return string.format("%s FDEmptyStock: emptied %d depot(s)",
        DepotConstants.LOG_PREFIX, depotCount)
end

function DepotManager:cmdFDCancelDelivery(depotIdArg)
    if not g_server then
        return DepotConstants.LOG_PREFIX .. " FDCancelDelivery: server only"
    end
    if not self.deliverySystem then
        return DepotConstants.LOG_PREFIX .. " FDCancelDelivery: deliverySystem not initialized"
    end
    local targetId = depotIdArg and tonumber(depotIdArg) or nil
    if not targetId then
        return DepotConstants.LOG_PREFIX .. " FDCancelDelivery: usage: FDCancelDelivery <depotId>"
    end
    local rec = self.deliverySystem:getDelivery(targetId)
    if not rec then
        return string.format("%s FDCancelDelivery: no active delivery for depot #%d",
            DepotConstants.LOG_PREFIX, targetId)
    end
    local farmId = rec.farmId
    local ok, penalty = self.deliverySystem:cancelDelivery(targetId, farmId)
    if ok then
        DepotDeliverySyncEvent.broadcast(targetId)
        return string.format("%s FDCancelDelivery: depot #%d cancelled (penalty=$%.2f)",
            DepotConstants.LOG_PREFIX, targetId, penalty or 0)
    end
    return DepotConstants.LOG_PREFIX .. " FDCancelDelivery: failed"
end
