-- FS25_AD_CP_Zubringer - ADCP_Bridge
--
-- Phase 1 (fertig & testbar): erkennt, wenn ein CP-gesteuerter Ausbringer
-- waehrend der Feldarbeit leer ist, haelt CP an und merkt sich die Stelle.
--
-- Phase 2 (Entwurf, gemeinsam zu testen): sucht einen passenden, beladenen
-- AD-Zubringer, schickt ihn per ADCPSupplyTask zum wartenden Fahrzeug,
-- und laesst CP nach dem Umladen automatisch weiterarbeiten.

ADCP.waitingVehicles = {} -- [vehicle] = {supplierDeparted=false} (CP-Job bleibt aktiv und wird nur gehalten)
ADCP.busyVehicles = {}    -- [vehicle] = true  (Zubringer, die gerade unterwegs sind oder vorwaerts Abstand gewinnen)
ADCP.departingSuppliers = {} -- [supplier] = {target=vehicle}; AD faehrt bereits wieder normal vorwaerts

-- ---------------------------------------------------------------------
-- Hilfsfunktionen: Fuellstand pruefen / umladen
-- ---------------------------------------------------------------------

--- Prueft, ob das Fahrzeug (inkl. angehaengter Geraete) eines der
--- ueberwachten Fuellgueter geladen hat und ob es leer ist.
--- @return isRelevant boolean  true, wenn ueberhaupt ein ueberwachter Fuelltyp vorhanden ist
--- @return isEmpty boolean     true, wenn Summe <= Schwellenwert
function ADCP.getMonitoredFillState(vehicle)
    local total = 0
    local isRelevant = false

    local childVehicles = vehicle:getChildVehicles()
    for _, v in pairs(childVehicles) do
        if v.getFillUnits ~= nil and v.getFillUnitSupportsFillType ~= nil then
            local fillUnits = v:getFillUnits()
            for fillUnitIndex, _ in pairs(fillUnits) do
                -- WICHTIG: wir pruefen, was das FillUnit AUFNEHMEN KANN, nicht
                -- was gerade drin ist. Ein komplett leeres FillUnit meldet
                -- seinen aktuellen Fuelltyp als UNKNOWN (siehe FUELLTYP-DIAGNOSE:
                -- "fillType=UNKNOWN fillLevel=0") - genau dann, wenn wir es
                -- eigentlich erkennen muessen ("leer, muss nachgefuellt werden").
                for _, name in ipairs(ADCP.MONITORED_FILLTYPES) do
                    local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(name)
                    if fillTypeIndex ~= nil and v:getFillUnitSupportsFillType(fillUnitIndex, fillTypeIndex) then
                        isRelevant = true
                        total = total + v:getFillUnitFillLevel(fillUnitIndex)
                        break
                    end
                end
            end
        end
    end

    return isRelevant, (total <= ADCP.EMPTY_THRESHOLD_LITERS)
end

--- Liefert den Fuellstand aller relevanten FillUnits als Anteil 0..1.
function ADCP.getMonitoredFillPercent(vehicle)
    local level, capacity = 0, 0
    local isRelevant = false
    for _, v in pairs(vehicle:getChildVehicles()) do
        if v.getFillUnits ~= nil and v.getFillUnitSupportsFillType ~= nil then
            for fillUnitIndex, _ in pairs(v:getFillUnits()) do
                for _, name in ipairs(ADCP.MONITORED_FILLTYPES) do
                    local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(name)
                    if fillTypeIndex ~= nil and v:getFillUnitSupportsFillType(fillUnitIndex, fillTypeIndex) then
                        isRelevant = true
                        level = level + v:getFillUnitFillLevel(fillUnitIndex)
                        capacity = capacity + v:getFillUnitCapacity(fillUnitIndex)
                        break
                    end
                end
            end
        end
    end
    return isRelevant, capacity > 0 and (level / capacity) or 0
end

--- Prueft den echten Vollzustand ueber die verbleibende Kapazitaet.
--- Ein Rest unter einem Liter entsteht durch FS-Rundung und wird im HUD als 100 % angezeigt.
function ADCP.getMonitoredFullState(vehicle)
    local isRelevant = false
    local totalFreeCapacity = 0
    for _, v in pairs(vehicle:getChildVehicles()) do
        if v.getFillUnits ~= nil and v.getFillUnitSupportsFillType ~= nil then
            for fillUnitIndex, _ in pairs(v:getFillUnits()) do
                for _, name in ipairs(ADCP.MONITORED_FILLTYPES) do
                    local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(name)
                    if fillTypeIndex ~= nil and v:getFillUnitSupportsFillType(fillUnitIndex, fillTypeIndex) then
                        isRelevant = true
                        totalFreeCapacity = totalFreeCapacity + v:getFillUnitFreeCapacity(fillUnitIndex)
                        break
                    end
                end
            end
        end
    end
    return isRelevant, isRelevant and totalFreeCapacity <= 1
end

--- Haelt alle klappbaren Arbeitsgeraete oben und gibt den Einklappbefehl
--- waehrend der gesamten Wartephase erneut aus. CP versucht bei aktivem
--- Feldarbeitsjob sonst teilweise, das Geraet direkt wieder auszuklappen.
local function getFoldableVehicles(vehicle)
    local result = {}
    local seen = {}
    local function add(v)
        if v ~= nil and not seen[v] then
            seen[v] = true
            table.insert(result, v)
        end
    end
    -- Nur angehaengte Arbeitsgeraete klappen. Das Traegerfahrzeug selbst
    -- bleibt unberuehrt, damit z.B. der Saugarm des AT5105 nicht ausklappt.
    if vehicle.getChildVehicles ~= nil then
        for _, child in pairs(vehicle:getChildVehicles()) do
            -- getChildVehicles() kann bei manchen Selbstfahrern auch das
            -- Root-Fahrzeug selbst enthalten. Dieses niemals falten, damit
            -- z.B. der Saugarm des AT5105 unangetastet bleibt.
            if child ~= vehicle and child ~= vehicle:getRootVehicle() then
                add(child)
            end
        end
    end
    return result
end

local function getFoldDirections(v)
    local spec = v.spec_foldable
    -- turnOnFoldDirection ist die Richtung, in der das Gerät in Arbeitsstellung
    -- gebracht wird. Zum Transport wird exakt die Gegenrichtung benötigt.
    local unfoldDirection = spec ~= nil and spec.turnOnFoldDirection or 1
    if unfoldDirection == 0 then
        unfoldDirection = 1
    end
    return -unfoldDirection, unfoldDirection
end

function ADCP.raiseWaitingImplements(vehicle)
    local strategy = vehicle.getCpDriveStrategy ~= nil and vehicle:getCpDriveStrategy() or nil
    if strategy ~= nil and strategy.raiseImplements ~= nil then
        strategy:raiseImplements()
    end
end

function ADCP.foldWaitingImplements(vehicle)
    for _, v in ipairs(getFoldableVehicles(vehicle)) do
        if v.setFoldDirection ~= nil and v.spec_foldable ~= nil then
            local foldDirection = getFoldDirections(v)
            local allowed = true
            if v.getIsFoldAllowed ~= nil then
                local ok, value = pcall(v.getIsFoldAllowed, v, foldDirection, false)
                if ok and value ~= nil then
                    allowed = value
                end
            end
            if allowed then
                v:setFoldDirection(foldDirection, false)
            end

            if not v.adcpFoldDiagPrinted then
                v.adcpFoldDiagPrinted = true
                ADCP.debugPrint(
                    "FALT-DIAGNOSE %s: turnOnFoldDirection=%s foldDirection=%s animTime=%s allowed=%s",
                    tostring(v.getName ~= nil and v:getName() or v),
                    tostring(v.spec_foldable.turnOnFoldDirection),
                    tostring(foldDirection),
                    tostring(v.spec_foldable.foldAnimTime),
                    tostring(allowed)
                )
            end
        end
    end
end

--- Klappt die Arbeitsgeraete wieder aus. CP bleibt bis zum Abschluss gehalten.
function ADCP.unfoldWaitingImplements(vehicle)
    local allUnfolded = true
    local foundFoldable = false
    for _, v in ipairs(getFoldableVehicles(vehicle)) do
        if v.setFoldDirection ~= nil and v.spec_foldable ~= nil then
            foundFoldable = true
            local _, unfoldDirection = getFoldDirections(v)
            local isUnfolded = false
            if v.getIsUnfolded ~= nil then
                local ok, value = pcall(v.getIsUnfolded, v)
                isUnfolded = ok and value == true
            elseif v.getFoldAnimTime ~= nil then
                local ok, animTime = pcall(v.getFoldAnimTime, v)
                if ok and animTime ~= nil then
                    isUnfolded = unfoldDirection > 0 and animTime >= 0.999 or animTime <= 0.001
                end
            elseif v.spec_foldable.foldAnimTime ~= nil then
                local animTime = v.spec_foldable.foldAnimTime
                isUnfolded = unfoldDirection > 0 and animTime >= 0.999 or animTime <= 0.001
            end
            if not isUnfolded then
                allUnfolded = false
                v:setFoldDirection(unfoldDirection, false)
            end
        end
    end
    return not foundFoldable or allUnfolded
end

--- Schiebt Fuellstand von sourceVehicle in targetVehicle (nur ueberwachte Fuelltypen).
--- @return transferredThisTick number
--- @return sourceEmpty boolean   true, wenn die Quelle jetzt leer ist
--- @return targetFull boolean    true, wenn das Ziel jetzt voll ist (fuer den passenden Typ)
function ADCP.transferFill(dt, sourceVehicle, targetVehicle)
    local maxAmount = ADCP.TRANSFER_RATE_L_PER_S * (dt / 1000)
    local transferred = 0
    local sourceHasMore = false
    local targetHasRoom = false

    local sourceUnits = ADCP.collectFillUnits(sourceVehicle)
    local targetUnits = ADCP.collectFillUnits(targetVehicle)

    for _, src in ipairs(sourceUnits) do
        if ADCP.isMonitoredFillType(src.fillTypeName) and src.fillLevel > 0 then
            -- passende Zieleinheit suchen (gleicher Fuelltyp oder leer/UNKNOWN)
            for _, tgt in ipairs(targetUnits) do
                local remainingBudget = maxAmount - transferred
                if remainingBudget <= 0 then
                    break
                end
                local freeCapacity = tgt.vehicle:getFillUnitFreeCapacity(tgt.fillUnitIndex)
                local canAccept = tgt.vehicle.getFillUnitSupportsFillType ~= nil
                    and tgt.vehicle:getFillUnitSupportsFillType(tgt.fillUnitIndex, src.fillTypeIndex)
                if canAccept and freeCapacity > 0 and src.fillLevel > 0 then
                    local amount = math.min(remainingBudget, freeCapacity, src.fillLevel)
                    if amount > 0 then
                        src.vehicle:addFillUnitFillLevel(src.vehicle:getOwnerFarmId(), src.fillUnitIndex, -amount, src.fillTypeIndex, ToolType.UNDEFINED, nil)
                        tgt.vehicle:addFillUnitFillLevel(tgt.vehicle:getOwnerFarmId(), tgt.fillUnitIndex, amount, src.fillTypeIndex, ToolType.UNDEFINED, nil)
                        transferred = transferred + amount
                        src.fillLevel = src.fillLevel - amount
                    end
                end
            end
        end
        if ADCP.isMonitoredFillType(src.fillTypeName) and src.fillLevel > 0.5 then
            sourceHasMore = true
        end
    end

    -- Zielstatus nach dem Umladen neu ermitteln: ist noch Platz fuer die
    -- ueberwachten Fuelltypen in mindestens einer FillUnit des Ziels?
    -- WICHTIG: ueber getFillUnitSupportsFillType pruefen (was das FillUnit
    -- AUFNEHMEN KANN), nicht ueber den aktuellen Fuelltyp-Namen - ein leeres
    -- FillUnit meldet sich als "UNKNOWN" (String, nicht nil), das haette
    -- targetIsFull sonst faelschlich sofort auf true stehen lassen.
    local targetIsFull = true
    for _, tgt in ipairs(ADCP.collectFillUnits(targetVehicle)) do
        for _, name in ipairs(ADCP.MONITORED_FILLTYPES) do
            local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(name)
            if fillTypeIndex ~= nil and tgt.vehicle.getFillUnitSupportsFillType ~= nil
                and tgt.vehicle:getFillUnitSupportsFillType(tgt.fillUnitIndex, fillTypeIndex) then
                if tgt.vehicle:getFillUnitFreeCapacity(tgt.fillUnitIndex) > 1 then
                    targetIsFull = false
                end
                break
            end
        end
    end

    return transferred, (not sourceHasMore), targetIsFull
end

function ADCP.isMonitoredFillType(name)
    if name == nil then
        return false
    end
    for _, n in ipairs(ADCP.MONITORED_FILLTYPES) do
        if n == name then
            return true
        end
    end
    return false
end

--- Sammelt alle FillUnits eines Fahrzeugverbunds in eine flache Liste.
function ADCP.collectFillUnits(vehicle)
    local list = {}
    for _, v in pairs(vehicle:getChildVehicles()) do
        if v.getFillUnits ~= nil then
            for fillUnitIndex, _ in pairs(v:getFillUnits()) do
                local fillTypeIndex = v:getFillUnitFillType(fillUnitIndex)
                local fillType = g_fillTypeManager:getFillTypeByIndex(fillTypeIndex)
                table.insert(list, {
                    vehicle = v,
                    fillUnitIndex = fillUnitIndex,
                    fillTypeIndex = fillTypeIndex,
                    fillTypeName = fillType and fillType.name or nil,
                    fillLevel = v:getFillUnitFillLevel(fillUnitIndex),
                })
            end
        end
    end
    return list
end


-- ---------------------------------------------------------------------
-- CP stoppen / wieder starten und kurze Ruecksetzfahrt
-- ---------------------------------------------------------------------
function ADCP.stopCourseplayForSupply(vehicle, waitState)
    waitState.savedStrategy = vehicle.getCpDriveStrategy ~= nil and vehicle:getCpDriveStrategy() or nil
    waitState.savedJob = vehicle.getCurrentAIJob ~= nil and vehicle:getCurrentAIJob() or nil
    waitState.cpWasStopped = false

    -- Den CP-Job absichtlich NICHT beenden. Der aktive AI-Kontext wird fuer
    -- die spaetere kontrollierte Rueckwaertsfahrt benoetigt. CP bleibt ueber
    -- hold() sicher stehen und kann danach ohne Neuberechnung fortgesetzt werden.
    local strategy = waitState.savedStrategy
    if strategy ~= nil and strategy.hold ~= nil then
        strategy:hold(600000, false)
        ADCP.debugPrint("%s: CP per hold() angehalten; AI-Kontext bleibt fuer Rueckwaertsfahrt aktiv", tostring(vehicle:getName()))
        return true
    end

    -- Nur als Notfall-Fallback, falls eine CP-Version kein hold() anbietet.
    if vehicle.stopCurrentAIJob ~= nil then
        local message = nil
        if AIMessageSuccessStoppedByUser ~= nil and AIMessageSuccessStoppedByUser.new ~= nil then
            message = AIMessageSuccessStoppedByUser.new()
        end
        local ok, err = pcall(vehicle.stopCurrentAIJob, vehicle, message)
        if ok then
            waitState.cpWasStopped = true
            ADCP.debugPrint("%s: CP-Job mangels hold() komplett gestoppt", tostring(vehicle:getName()))
            return true
        end
        ADCP.debugPrint("WARNUNG: stopCurrentAIJob fehlgeschlagen: %s", tostring(err))
    end
    return false
end

--- Startet den CP-Feldarbeits-Job DIREKT serverseitig, ohne den
--- client-exklusiven Weg ueber vehicle:startCpAtLastWp()/startCpAtFirstWp().
--- Diese beiden Methoden bauen intern zwar das Job-Objekt korrekt auf, enden
--- aber mit "g_client:getServerConnection():sendEvent(...)" (siehe
--- FS25_Courseplay/scripts/specializations/CpAIFieldWorker.lua) - das ist
--- fuer einen Klick im Menü gedacht (Client schickt Anfrage an den Server).
--- Auf einem echten Dedicated Server ohne lokalen Spieler schlaegt entweder
--- g_client oder die serverseitige Berechtigungspruefung
--- (g_currentMission:getHasPlayerPermission(..., connection, ...) mit
--- connection=nil-Kontext) fehl - OHNE Lua-Fehler zu werfen, weshalb unser
--- pcall das faelschlich als Erfolg wertet.
---
--- Der eigentliche serverseitige Start-Befehl dahinter ist einfach
--- g_currentMission.aiSystem:startJob(job, farmId) (siehe
--- AIJobStartRequestEvent:run() in der GameSource). Den rufen wir hier
--- direkt auf, mit einem frisch aus CP's eigenem Spec-Objekt geholten
--- Job. Der Zugriff auf CP's Spezialisierung erfolgt bewusst ueber den
--- direkten Feldnamen (vehicle["spec_FS25_Courseplay.cpAIFieldWorker"])
--- statt ueber das globale CpAIFieldWorker-Table, das aus unserem eigenen
--- Mod-Skript heraus nicht zuverlaessig sichtbar ist (dasselbe Problem wie
--- bei AutoDrive/ADGraphManager).
function ADCP.startCpJobDirectlyServerSide(vehicle, useLastWp)
    if g_currentMission == nil or g_currentMission.aiSystem == nil or g_currentMission.aiSystem.startJob == nil then
        return false, "g_currentMission.aiSystem.startJob nicht verfuegbar"
    end

    local spec = vehicle["spec_FS25_Courseplay.cpAIFieldWorker"]
    if spec == nil then
        -- Fallback, falls der CP-Modordner/-Name abweicht (z.B. andere
        -- Version/Variante): nach einem passenden Spec-Feld suchen statt
        -- nur den hartkodierten Namen zu versuchen.
        for k, v in pairs(vehicle) do
            if type(k) == "string" and k:sub(1, 5) == "spec_" and k:sub(-15) == ".cpAIFieldWorker" then
                spec = v
                break
            end
        end
    end
    if spec == nil then
        return false, "spec_*.cpAIFieldWorker nicht gefunden (evtl. anderer CP-Modname)"
    end

    local job = useLastWp and spec.cpJobStartAtLastWp or spec.cpJobStartAtFirstWp
    if job == nil then
        return false, "CP-Job-Objekt (cpJobStartAt" .. (useLastWp and "LastWp" or "FirstWp") .. ") nicht gefunden"
    end

    if vehicle.updateAIFieldWorkerImplementData ~= nil then
        vehicle:updateAIFieldWorkerImplementData()
    end
    if vehicle.hasCpCourse ~= nil and not vehicle:hasCpCourse() then
        return false, "kein CP-Kurs auf dem Fahrzeug vorhanden"
    end
    if vehicle.getCanStartCpFieldWork ~= nil and not vehicle:getCanStartCpFieldWork() then
        return false, "CP-Feldarbeit kann aktuell nicht gestartet werden (getCanStartCpFieldWork=false)"
    end

    local farmId = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() or 1

    local ok, errOrSuccess = pcall(function()
        job:applyCurrentState(vehicle, g_currentMission, farmId, true, false)
        if job.setValues ~= nil then
            job:setValues()
        end
        local startable = job.validate == nil or job:validate(false)
        if not startable then
            return false
        end
        g_currentMission.aiSystem:startJob(job, farmId)
        return true
    end)

    if not ok then
        return false, tostring(errOrSuccess)
    end
    if errOrSuccess ~= true then
        return false, "job:validate() lieferte false (Job nicht startbar)"
    end
    return true
end

function ADCP.restartCourseplayAfterSupply(vehicle, waitState)
    if not waitState.cpWasStopped then
        local strategy = vehicle.getCpDriveStrategy ~= nil and vehicle:getCpDriveStrategy() or waitState.savedStrategy
        if strategy ~= nil and strategy.unhold ~= nil then
            strategy:unhold()
            ADCP.debugPrint("%s: CP-Fallback per unhold() fortgesetzt", tostring(vehicle:getName()))
            return true
        end
        return false
    end

    -- Bevorzugt: direkter serverseitiger Start (funktioniert auch ohne
    -- lokalen Client/Spieler, siehe ADCP.startCpJobDirectlyServerSide).
    local ok, err = ADCP.startCpJobDirectlyServerSide(vehicle, true)
    if ok then
        ADCP.debugPrint("%s: CP direkt serverseitig neu gestartet (letzter Wegpunkt)", tostring(vehicle:getName()))
        return true
    end
    ADCP.debugPrint("WARNUNG: direkter CP-Start (letzter WP) fehlgeschlagen: %s - versuche ersten Wegpunkt", tostring(err))

    ok, err = ADCP.startCpJobDirectlyServerSide(vehicle, false)
    if ok then
        ADCP.debugPrint("%s: CP direkt serverseitig neu gestartet (erster Wegpunkt)", tostring(vehicle:getName()))
        return true
    end
    ADCP.debugPrint("WARNUNG: direkter CP-Start (erster WP) fehlgeschlagen: %s - versuche client-abhaengige Kandidaten", tostring(err))

    -- CP-Versionen stellen unterschiedliche Startfunktionen bereit. Nur
    -- vorhandene Methoden werden aufgerufen; jeder Versuch ist abgesichert.
    -- WICHTIG: Diese Kandidaten enden alle im client-exklusiven
    -- g_client:getServerConnection():sendEvent(...) und funktionieren daher
    -- nur, wenn ein lokaler Spieler/Client vorhanden ist (z.B. Singleplayer).
    -- Auf einem echten Dedicated Server sind sie nur ein Fallback fuer den
    -- Fall, dass der direkte Weg oben aus irgendeinem Grund nicht passt.
    local candidates = {
        {"startCpAtLastWp", true},
        {"startCpAtFirstWp", true},
        {"startCpDriver", false},
        {"cpStartStopDriver", false},
    }
    for _, candidate in ipairs(candidates) do
        local name = candidate[1]
        local fn = vehicle[name]
        if fn ~= nil then
            local ok, result
            if candidate[2] then
                ok, result = pcall(fn, vehicle, true)
            else
                ok, result = pcall(fn, vehicle)
            end
            if ok then
                ADCP.debugPrint("%s: CP neu gestartet ueber %s()", tostring(vehicle:getName()), name)
                return true
            end
            ADCP.debugPrint("WARNUNG: CP-Neustart ueber %s() fehlgeschlagen: %s", name, tostring(result))
        end
    end

    if waitState.savedJob ~= nil and g_currentMission ~= nil and g_currentMission.aiSystem ~= nil
        and g_currentMission.aiSystem.startJob ~= nil then
        local ok, err = pcall(g_currentMission.aiSystem.startJob, g_currentMission.aiSystem, waitState.savedJob)
        if ok then
            ADCP.debugPrint("%s: gespeicherter CP-Job ueber aiSystem:startJob() neu gestartet", tostring(vehicle:getName()))
            return true
        end
        ADCP.debugPrint("WARNUNG: aiSystem:startJob() fehlgeschlagen: %s", tostring(err))
    end

    ADCP.debugPrint("FEHLER: Keine kompatible CP-Neustartfunktion fuer %s gefunden", tostring(vehicle:getName()))
    return false
end

--- Haelt das Fahrzeug waehrend des Ausklappens still. WICHTIG: hier KEIN
--- roher AIVehicleUtil-Aufruf mehr - der CP-Job ist zu diesem Zeitpunkt
--- ueber stopCurrentAIJob() bereits komplett gestoppt (siehe
--- ADCP.stopCourseplayForSupply), es gibt also keinen aktiven AI-Bootstrap
--- mehr, der self.motor populiert - das fuehrte zu "attempt to index nil
--- with 'setSpeedLimit'". Das Fahrzeug steht nach stopCurrentAIJob() ohnehin
--- bereits still (kein Fahrer/keine AI treibt es mehr an); wir schalten nur
--- vorsichtshalber den Tempomat aus, falls noch aktiv.
local function stopModControlledVehicle(vehicle)
    if vehicle.setSteeringInput ~= nil then
        pcall(vehicle.setSteeringInput, vehicle, 0, false)
    end
    if WheelsUtil ~= nil and WheelsUtil.updateWheelsPhysics ~= nil and vehicle.spec_motorized ~= nil then
        pcall(WheelsUtil.updateWheelsPhysics, vehicle, 16, vehicle.lastSpeedReal * vehicle.movingDirection, 0, true, true)
    end
    if vehicle.setCruiseControlState ~= nil and Drivable ~= nil then
        pcall(vehicle.setCruiseControlState, vehicle, Drivable.CRUISECONTROL_STATE_OFF)
    end
end


local function stopCourseplayJobForReverse(vehicle, waitState)
    if waitState.cpWasStopped then
        return true
    end

    if vehicle.getCurrentAIJob ~= nil then
        waitState.savedJob = vehicle:getCurrentAIJob() or waitState.savedJob
    end

    if vehicle.stopCurrentAIJob ~= nil then
        local message = nil
        if AIMessageSuccessStoppedByUser ~= nil and AIMessageSuccessStoppedByUser.new ~= nil then
            message = AIMessageSuccessStoppedByUser.new()
        end
        local ok, err = pcall(vehicle.stopCurrentAIJob, vehicle, message)
        if ok then
            waitState.cpWasStopped = true
            ADCP.debugPrint("%s: CP-Job vor Mod-Rueckwaertsfahrt vollstaendig gestoppt", tostring(vehicle:getName()))
            return true
        end
        ADCP.debugPrint("FEHLER: CP-Job konnte vor Rueckwaertsfahrt nicht gestoppt werden: %s", tostring(err))
    end
    return false
end

local function driveVehicleBackward(vehicle, waitState, dt)
    local node = vehicle.components[1].node
    local x, _, z = getWorldTranslation(node)
    if waitState.reverseStartX == nil then
        waitState.reverseStartX = x
        waitState.reverseStartZ = z
        ADCP.debugPrint("%s: Mod-gesteuerte %.1f m Rueckwaertsfahrt startet; CP bleibt gehalten", tostring(vehicle:getName()), ADCP.WORKER_REVERSE_DISTANCE_M)
    end

    local travelled = MathUtil.vector2Length(x - waitState.reverseStartX, z - waitState.reverseStartZ)
    if travelled >= ADCP.WORKER_REVERSE_DISTANCE_M then
        stopModControlledVehicle(vehicle)
        ADCP.debugPrint("%s: Mod-Rueckwaertsfahrt beendet (%.2f m) -> erst jetzt Ausklappen/CP-Freigabe", tostring(vehicle:getName()), travelled)
        return true
    end

    -- Der CP-Job ist beendet. Deshalb steuert unser Mod direkt die FS25-
    -- Radphysik an, statt nicht vorhandene Spieler-Eingabemethoden zu nutzen.
    if vehicle.setSteeringInput ~= nil then
        pcall(vehicle.setSteeringInput, vehicle, 0, false)
    end

    local controlled = false
    if WheelsUtil ~= nil and WheelsUtil.updateWheelsPhysics ~= nil and vehicle.spec_motorized ~= nil then
        local motor = vehicle.getMotor ~= nil and vehicle:getMotor() or nil
        if motor ~= nil and motor.setSpeedLimit ~= nil then
            pcall(motor.setSpeedLimit, motor, ADCP.WORKER_REVERSE_SPEED_KMH)
        end
        local currentSpeed = (vehicle.lastSpeedReal or 0) * (vehicle.movingDirection or 1)
        controlled = pcall(WheelsUtil.updateWheelsPhysics, vehicle, dt, currentSpeed, -1, false, true)
    end

    if not controlled and not waitState.reverseErrorLogged then
        waitState.reverseErrorLogged = true
        ADCP.debugPrint("FEHLER: %s konnte nicht ueber WheelsUtil rueckwaerts gesteuert werden", tostring(vehicle:getName()))
    end
    return false
end

local function holdVehicleStationary(vehicle, dt)
    if vehicle.setCruiseControlState ~= nil and Drivable ~= nil then
        vehicle:setCruiseControlState(Drivable.CRUISECONTROL_STATE_OFF)
    end
end

-- ---------------------------------------------------------------------
-- Phase 1: Ueberwachung waehrend CP-Feldarbeit
-- ---------------------------------------------------------------------

local checkTimer = 0
local seenVehicles = {}

-- Courseplay meldet bei leerem Arbeitsgeraet normalerweise AIMessageErrorOutOfFill.
-- Diese Bruecke faengt genau diese Meldung fuer unsere ueberwachten Fuelltypen
-- ab. Dadurch ist die Courseplay-Einstellung "Geraet auffuellen" fuer den
-- Ablauf dieser Mod bedeutungslos: CP darf den Auftrag nicht wegen Leere
-- beenden, waehrend unsere eigene Zubringerlogik uebernimmt.
local cpOutOfFillHookInstalled = false
local function installCourseplayOutOfFillHook()
    if cpOutOfFillHookInstalled or CpAIJobFieldWork == nil or CpAIJobFieldWork.isFinishingAllowed == nil then
        return
    end

    local originalIsFinishingAllowed = CpAIJobFieldWork.isFinishingAllowed
    CpAIJobFieldWork.isFinishingAllowed = function(job, message)
        local isOutOfFill = AIMessageErrorOutOfFill ~= nil
            and message ~= nil
            and message.isa ~= nil
            and message:isa(AIMessageErrorOutOfFill)

        if isOutOfFill and job.getVehicle ~= nil then
            local vehicle = job:getVehicle()
            local isRelevant = vehicle ~= nil and ADCP.getMonitoredFillState(vehicle) or false
            if isRelevant then
                if ADCP.waitingVehicles[vehicle] == nil then
                    ADCP.debugPrint("Courseplay-Leermeldung abgefangen; CP-Einstellung wird ignoriert")
                    ADCP.onAusbringerEmpty(vehicle)
                end

                -- CP intern ebenfalls im Feldarbeits-Task warten lassen, falls
                -- die Methode in der installierten CP-Version vorhanden ist.
                if job.currentTaskIndex ~= nil and job.fieldWorkTask ~= nil
                    and job.currentTaskIndex == job.fieldWorkTask.taskIndex
                    and job.fieldWorkTask.setWaitingForRefillingActive ~= nil then
                    job.fieldWorkTask:setWaitingForRefillingActive()
                end
                return false
            end
        end

        return originalIsFinishingAllowed(job, message)
    end

    cpOutOfFillHookInstalled = true
    ADCP.debugPrint("Courseplay-Leermeldungs-Hook installiert")
end

-- Courseplay entscheidet bei leerem Arbeitsgeraet anhand von refillOnTheField,
-- ob der Auftrag beendet wird. Dieser kleine PRE-Update-Hook laeuft vor
-- Courseplays Fahrzeug-Update und setzt fuer relevante Feldarbeitsfahrzeuge
-- intern voruebergehend "Warten". So erreicht die Leermeldung niemals den
-- Zustand "Deaktiviert". Der vom Spieler gewaehlte Wert wird nach Ende der
-- Feldarbeit wiederhergestellt.
local function onVehiclePreUpdate(vehicle, dt)
    if vehicle == nil or not vehicle.isServer
        or vehicle.getIsCpFieldWorkActive == nil
        or vehicle.getCpSettings == nil then
        return
    end

    local fieldWorkActive = vehicle:getIsCpFieldWorkActive()
    local settings = vehicle:getCpSettings()
    local refillSetting = settings ~= nil and settings.refillOnTheField or nil
    if refillSetting == nil or refillSetting.getValue == nil or refillSetting.setValue == nil then
        return
    end

    if fieldWorkActive then
        local isRelevant = ADCP.getMonitoredFillState(vehicle)
        if isRelevant then
            if vehicle.adcpOriginalRefillOnFieldValue == nil then
                vehicle.adcpOriginalRefillOnFieldValue = refillSetting:getValue()
            end

            local waitingValue = 1
            if CpVehicleSettings ~= nil and CpVehicleSettings.REFILL_ON_FIELD_WAITING ~= nil then
                waitingValue = CpVehicleSettings.REFILL_ON_FIELD_WAITING
            end

            if refillSetting:getValue() ~= waitingValue then
                -- noEventSend=true: nur der serverseitige Laufzeitwert fuer die
                -- CP-Entscheidung wird angepasst; keine dauerhafte Benutzereinstellung.
                refillSetting:setValue(waitingValue, true)
                ADCP.debugPrint("CP-Nachfuellschutz aktiv: Geraet auffuellen intern auf Warten gesetzt")
            end
        end
    elseif vehicle.adcpOriginalRefillOnFieldValue ~= nil and ADCP.waitingVehicles[vehicle] == nil then
        refillSetting:setValue(vehicle.adcpOriginalRefillOnFieldValue, true)
        vehicle.adcpOriginalRefillOnFieldValue = nil
        ADCP.debugPrint("CP-Nachfuellschutz beendet: urspruengliche Einstellung wiederhergestellt")
    end
end

local function onVehicleUpdate(vehicle, dt)
    installCourseplayOutOfFillHook()
    if vehicle == nil or not vehicle.isServer then
        return
    end
    if vehicle.getIsCpFieldWorkActive == nil then
        return -- kein CP-Fahrzeug
    end

    if not seenVehicles[vehicle] then
        seenVehicles[vehicle] = true
        ADCP.debugPrint("DIAGNOSE: CP-faehiges Fahrzeug erkannt: %s", tostring(vehicle:getName()))
    end

    vehicle.adcpDiagTimer = (vehicle.adcpDiagTimer or 0) + dt
    local shouldLogDiag = vehicle.adcpDiagTimer >= 5000
    if shouldLogDiag then
        vehicle.adcpDiagTimer = 0
    end

    local fieldWorkActive = vehicle:getIsCpFieldWorkActive()
    local isRelevant, isEmpty = ADCP.getMonitoredFillState(vehicle)


    if shouldLogDiag then
        ADCP.debugPrint(
            "DIAGNOSE: %s fieldWorkActive=%s isRelevant=%s isEmpty=%s waiting=%s",
            tostring(vehicle:getName()), tostring(fieldWorkActive), tostring(isRelevant), tostring(isEmpty), tostring(ADCP.waitingVehicles[vehicle] ~= nil)
        )
    end

    -- 1) Laeuft CP gerade und arbeitet das Fahrzeug?
    if fieldWorkActive and not ADCP.waitingVehicles[vehicle] then
        vehicle.adcpCheckTimer = (vehicle.adcpCheckTimer or 0) + dt
        if vehicle.adcpCheckTimer >= ADCP.CHECK_INTERVAL_MS then
            vehicle.adcpCheckTimer = 0
            if isRelevant and isEmpty then
                ADCP.onAusbringerEmpty(vehicle)
            end
        end
    end

    -- 2) Zustandsmaschine: CP aus -> einklappen -> befuellen -> Abstand ->
    -- direkt ausklappen -> CP ohne Umweg wieder fortsetzen.
    local waitState = ADCP.waitingVehicles[vehicle]
    if waitState then
        local strategy = vehicle.getCpDriveStrategy ~= nil and vehicle:getCpDriveStrategy() or waitState.savedStrategy
        if not waitState.cpWasStopped and strategy ~= nil and strategy.hold ~= nil then
            -- Bis zur Rueckwaertsfahrt bleibt CP gehalten. Beim Start der
            -- Rueckwaertsfahrt wird der CP-Job komplett beendet, damit er
            -- unsere direkten Fahrbefehle nicht mehr ueberschreiben kann.
            strategy:hold(ADCP.HOLD_REFRESH_MS, false)
        end
        local fillRelevant, isFull = ADCP.getMonitoredFullState(vehicle)

        if waitState.state == "WAITING_SUPPLY" then
            ADCP.foldWaitingImplements(vehicle)
            if fillRelevant and isFull and waitState.supplierDeparted then
                if stopCourseplayJobForReverse(vehicle, waitState) then
                    waitState.state = "REVERSING"
                    ADCP.debugPrint("%s: Zubringer weg, Tank voll, CP gestoppt -> Mod-Rueckwaertsfahrt", tostring(vehicle:getName()))
                end
            end
        elseif waitState.state == "REVERSING" then
            if driveVehicleBackward(vehicle, waitState, dt) then
                waitState.state = "UNFOLDING"
            end
        elseif waitState.state == "UNFOLDING" then
            holdVehicleStationary(vehicle, dt)
            if ADCP.unfoldWaitingImplements(vehicle) then
                -- Kein eigener RESTARTING_CP-Wartezustand mehr: Sobald das
                -- Arbeitsgeraet fertig ausgeklappt ist, wird CP im selben Tick
                -- fortgesetzt. Das entfernt die zusaetzliche Startpause.
                if ADCP.restartCourseplayAfterSupply(vehicle, waitState) then
                    ADCP.waitingVehicles[vehicle] = nil
                    ADCP.debugPrint("%s: Geraet ausgeklappt -> CP arbeitet sofort weiter", tostring(vehicle:getName()))
                end
            end
        end
    end
end

--- Diagnose fuer die Zubringer-Seite: separat gehookt, damit auch reine
--- AD-Fahrzeuge (ohne CP-Spec) erfasst werden.
local function onSupplierDiagnoseUpdate(vehicle, dt)
    if vehicle == nil or not vehicle.isServer or vehicle.ad == nil or vehicle.ad.stateModule == nil then
        return
    end

    -- Typ-Diagnose: einmal pro Fahrzeug den vehicleType UND alle FillUnits
    -- (mit ihrem tatsaechlichen Fuelltyp-Namen) jeder angehaengten Einheit
    -- ausgeben - damit wir sehen, ob ADCP.MONITORED_FILLTYPES passt.
    if not vehicle.adcpSpecDiagDone then
        vehicle.adcpSpecDiagDone = true
        for _, v in pairs(vehicle:getChildVehicles()) do
            ADCP.debugPrint("TYP-DIAGNOSE: %s -> typeName=%s", tostring(v:getName()), tostring(v.typeName))
            if v.getFillUnits ~= nil then
                for fillUnitIndex, _ in pairs(v:getFillUnits()) do
                    local fillTypeIndex = v:getFillUnitFillType(fillUnitIndex)
                    local fillType = g_fillTypeManager:getFillTypeByIndex(fillTypeIndex)
                    ADCP.debugPrint(
                        "FUELLTYP-DIAGNOSE: %s FillUnit#%s -> fillType=%s fillLevel=%s capacity=%s",
                        tostring(v:getName()), tostring(fillUnitIndex), tostring(fillType and fillType.name or "NIL"),
                        tostring(v:getFillUnitFillLevel(fillUnitIndex)), tostring(v:getFillUnitCapacity(fillUnitIndex))
                    )
                end
            end
        end
    end

    -- Muss JEDEN Tick laufen (nicht nur bei der 5s-Diagnose), sonst waere
    -- der Stillstands-Timer ungenau.
    ADCP.updateStillTimer(vehicle, dt)

    local isRelevant, isEmpty = ADCP.getMonitoredFillState(vehicle)
    local mode = vehicle.ad.stateModule:getMode()
    local isSupplyType = ADCP.isSupplyVehicleType(vehicle)

    -- Periodische Diagnose nur fuer Fahrzeuge, die entweder etwas
    -- Ueberwachtes geladen haben, als Zubringer-Typ erkannt wurden, oder im
    -- Abholen&Anliefern-Modus sind - sonst wuerde hier jeder x-beliebige
    -- AD-Traktor mitgeloggt.
    if not (isRelevant or isSupplyType or mode == ADCP.AD_MODE_PICKUPANDDELIVER) then
        return
    end

    vehicle.adcpSupplierDiagTimer = (vehicle.adcpSupplierDiagTimer or 0) + dt
    if vehicle.adcpSupplierDiagTimer < 5000 then
        return
    end
    vehicle.adcpSupplierDiagTimer = 0

    local isActive = vehicle.ad.stateModule:isActive()
    local speed = vehicle.lastSpeedReal or -1
    local available = ADCP.isAvailableForCall(vehicle)

    ADCP.debugPrint(
        "DIAGNOSE Zubringer: %s isActive=%s mode=%s speed=%.5f isRelevant=%s isEmpty=%s isSupplyType=%s available=%s busy=%s",
        tostring(vehicle.ad.stateModule:getName()), tostring(isActive), tostring(mode), speed,
        tostring(isRelevant), tostring(isEmpty), tostring(isSupplyType), tostring(available), tostring(ADCP.busyVehicles[vehicle] == true)
    )
end

--- Wird aufgerufen, sobald ein CP-Ausbringer waehrend der Feldarbeit leer ist.
function ADCP.onAusbringerEmpty(vehicle)
    ADCP.debugPrint("Ausbringer %s ist leer -> CP stoppen, Geraet einklappen und Zubringer rufen", tostring(vehicle:getName()))
    local waitState = {
        supplierDeparted = false,
        foldStartedAt = g_currentMission.time,
        foldCommandAt = g_currentMission.time + (ADCP.FOLD_START_DELAY_MS or 0),
        foldCommandSent = false,
        supplierDispatched = false,
        state = "WAITING_SUPPLY"
    }
    ADCP.waitingVehicles[vehicle] = waitState
    ADCP.stopCourseplayForSupply(vehicle, waitState)
    ADCP.raiseWaitingImplements(vehicle)
    ADCP.debugPrint("%s ist leer und wartet auf Nachschub.", vehicle:getName())
    ADCP.debugPrint("%s: CP gehalten, Geraet angehoben; Einklappen folgt weich nach %.1f s", tostring(vehicle:getName()), (ADCP.FOLD_START_DELAY_MS or 0) / 1000)
end
-- ---------------------------------------------------------------------
-- Phase 2: Zubringer finden und losschicken
-- ---------------------------------------------------------------------

--- Ein AD-Fahrzeug gilt als "wartender Zubringer, der gerufen werden kann", wenn:
--- - es im Modus "Abholen und Anliefern" faehrt und aktiv ist (isActive()==true)
--- - es am Feldpunkt (zweite Marke) steht und dort nicht mehr faehrt
--- WICHTIG: Ohne echte Entlade-Station am Feldpunkt bleibt AD dort in
--- UnloadAtDestinationTask haengen ("Wait at unload point until unloaded
--- somehow", siehe UnloadAtDestinationTask.lua) - diese Task wird NIE von
--- selbst fertig, taskModule:getActiveTask() ist also NIE nil. Deshalb kann
--- "verfuegbar" hier nicht ueber activeTask==nil erkannt werden (das war der
--- Bug, warum bisher nie ein Zubringer gerufen wurde), sondern nur ueber
--- Position (nahe zweiter Marke) + Stillstand.
--- - er einen der ueberwachten Fuelltypen geladen hat
--- - er (oder eine seiner angehaengten Einheiten) vom vehicleType her ein
---   Guelle-/Gaerrest-/Wasserfass ist (vehicle.typeName, siehe modDesc.xml
---   <vehicle type="...">, NICHT die Spezialisierung - deren genauer Name
---   ist je nach Mod unterschiedlich und daher nicht zuverlaessig ratbar)

--- Prueft, ob vehicle oder eine seiner angehaengten Einheiten einem der in
--- ADCP.SUPPLY_VEHICLE_TYPES gelisteten vehicleTypes entspricht.
function ADCP.isSupplyVehicleType(vehicle)
    for _, v in pairs(vehicle:getChildVehicles()) do
        local typeName = v.typeName
        if typeName ~= nil then
            for _, allowed in ipairs(ADCP.SUPPLY_VEHICLE_TYPES) do
                if typeName:lower() == allowed:lower() then
                    return true
                end
            end
        end
    end
    return false
end

--- Liefert alle anderen (geparkten oder gerade nicht aktiven) Zubringer-
--- Fahrzeuge ausser dem uebergebenen. Wird von ADCPSupplyTask genutzt, um
--- sie bei der Kollisionsvermeidung waehrend der Anfahrt auszuschliessen -
--- sonst haelt AD's eigene Kollisionspruefung den fahrenden Zubringer an,
--- sobald ein zweiter, geparkter Zubringer in der Naehe/im Weg steht (das
--- war der Grund fuer "Fahrzeug haengt fest -> Abbruch" bei mehreren
--- Zubringern).
function ADCP.getOtherSupplyVehicles(excludeVehicle)
    local others = {}
    local allVehicles = (g_currentMission and g_currentMission.vehicleSystem and g_currentMission.vehicleSystem.vehicles) or {}
    for _, v in pairs(allVehicles) do
        if v ~= excludeVehicle and v.ad ~= nil and ADCP.isSupplyVehicleType(v) then
            table.insert(others, v)
        end
    end
    return others
end

--- Liefert true, wenn das Fahrzeug im Abholen&Anliefern-Modus aktiv ist und
--- seit mindestens ADCP.STILLSTAND_REQUIRED_MS durchgehend steht - also im
--- typischen "haengt in UnloadAtDestinationTask fest"-Wartezustand.
--- Bewusst OHNE Abstandspruefung zur zweiten Marke: ADGraphManager war aus
--- unserem Mod-Skript heraus zur Laufzeit nicht zuverlaessig sichtbar
--- (siehe Log: "ADGraphManager-Global ist an dieser Stelle nil" - dasselbe
--- Problem wie zuvor bei AutoDrive.MODE_PICKUPANDDELIVER). Anhaltender
--- Stillstand im richtigen Modus ist ein Signal, das keine fremden
--- AD-Globals braucht.
function ADCP.isAvailableForCall(vehicle)
    if not vehicle.ad.stateModule:isActive() then
        vehicle.adcpStillTimer = nil
        return false
    end
    if vehicle.ad.stateModule:getMode() ~= ADCP.AD_MODE_PICKUPANDDELIVER then
        vehicle.adcpStillTimer = nil
        return false
    end
    if (vehicle.lastSpeedReal or 0) > 0.0005 then
        vehicle.adcpStillTimer = nil
        return false -- faehrt noch
    end

    return (vehicle.adcpStillTimer or 0) >= ADCP.STILLSTAND_REQUIRED_MS
end

--- Wird jeden Tick aus onSupplierDiagnoseUpdate gefuettert, damit
--- isAvailableForCall oben nur lesen muss (dt ist dort nicht verfuegbar).
function ADCP.updateStillTimer(vehicle, dt)
    if vehicle.ad == nil or vehicle.ad.stateModule == nil then
        return
    end
    if not vehicle.ad.stateModule:isActive()
        or vehicle.ad.stateModule:getMode() ~= ADCP.AD_MODE_PICKUPANDDELIVER
        or (vehicle.lastSpeedReal or 0) > 0.0005 then
        vehicle.adcpStillTimer = 0
        return
    end
    vehicle.adcpStillTimer = (vehicle.adcpStillTimer or 0) + dt
end

function ADCP.findAvailableSupplier(nearVehicle)
    local best = nil
    local bestDistance = math.huge
    local nx, _, nz = getWorldTranslation(nearVehicle.components[1].node)

    local allVehicles = (g_currentMission and g_currentMission.vehicleSystem and g_currentMission.vehicleSystem.vehicles) or {}
    local now = (g_currentMission and g_currentMission.time) or 0
    for _, v in pairs(allVehicles) do
        local inCooldown = v.adcpFailedUntil ~= nil and v.adcpFailedUntil > now
        if v.ad ~= nil and v.ad.stateModule ~= nil and v.ad.taskModule ~= nil and not ADCP.busyVehicles[v] and not inCooldown then
            if ADCP.isAvailableForCall(v) and ADCP.isSupplyVehicleType(v) then
                local isRelevant, isEmpty = ADCP.getMonitoredFillState(v)
                if isRelevant and not isEmpty then
                    local vx, _, vz = getWorldTranslation(v.components[1].node)
                    local dist = MathUtil.vector2Length(vx - nx, vz - nz)
                    if dist < bestDistance then
                        bestDistance = dist
                        best = v
                    end
                end
            end
        end
    end

    return best
end

function ADCP.tryDispatchSupplier(waitingVehicle)
    if not ADCP.waitingVehicles[waitingVehicle] then
        return -- inzwischen schon wieder befuellt/erledigt
    end

    local supplier = ADCP.findAvailableSupplier(waitingVehicle)
    if supplier == nil then
        ADCP.debugPrint("Kein wartender, beladener Zubringer verfuegbar - versuche es in 10s erneut")
        return
    end

    ADCP.debugPrint("Zubringer %s wird zu %s geschickt (Ruf)", supplier.ad.stateModule:getName(), waitingVehicle:getName())
    ADCP.busyVehicles[supplier] = true
    supplier.adcpSupplyTarget = waitingVehicle

    -- Nur die feststeckende UnloadAtDestinationTask gezielt abbrechen
    -- (kein stopAutoDrive/Shutdown - AD bleibt durchgehend aktiv). Danach
    -- ist taskModule.activeTask nil und unsere Task wird sofort abgeholt.
    local tm = supplier.ad.taskModule
    local modeBefore = supplier.ad.stateModule:getCurrentMode()
    ADCP.diagPrint("DISPATCH before vehicle=%s task=%s mode=%s modeState=%s activeTask=%s",
        tostring(supplier:getName()),
        tostring(tm.activeTask and tm.activeTask.__index or tm.activeTask),
        tostring(modeBefore),
        tostring(modeBefore and modeBefore.state),
        tostring(modeBefore and modeBefore.activeTask))
    tm:abortCurrentTask()
    ADCP.diagPrint("DISPATCH afterAbort activeTask=%s queue=%s",
        tostring(tm.activeTask), tostring(tm.tasks and #tm.tasks or "nil"))
    tm:addTask(ADCPSupplyTask:new(supplier, waitingVehicle))
    ADCP.diagPrint("DISPATCH afterAdd activeTask=%s queue=%s",
        tostring(tm.activeTask), tostring(tm.tasks and #tm.tasks or "nil"))
end

-- ---------------------------------------------------------------------
-- Globaler Update-Hook + einfacher Retry-Scheduler
-- ---------------------------------------------------------------------

-- ---------------------------------------------------------------------
-- Globaler Update-Hook + einfacher Retry-Scheduler
-- ---------------------------------------------------------------------
--
-- WICHTIG: Der Hook muss an Vehicle:update(dt) haengen, NICHT an
-- "Vehicle.onUpdate" - das war der Grund, warum in allen bisherigen Tests
-- ueberhaupt nichts passiert ist: Vehicle.onUpdate existiert in FS25 gar
-- nicht als Klassenmethode (nur als Spezialisierungs-Event, das intern von
-- Vehicle:update() ausgeloest wird), unser Hook lief also komplett ins
-- Leere. Vehicle:update(dt) ist die echte, verifizierte Methode.

Vehicle.update = Utils.prependedFunction(Vehicle.update, onVehiclePreUpdate)
Vehicle.update = Utils.appendedFunction(Vehicle.update, onVehicleUpdate)
Vehicle.update = Utils.appendedFunction(Vehicle.update, onSupplierDiagnoseUpdate)

-- Retry-Scheduler ueber g_currentMission.time (verifiziert vorhanden,
-- statt einer zweiten ungeprueften Klasse wie FSBaseMission zu hooken).
local lastRetryTime = 0
local function onRetryCheck()
    if g_currentMission == nil or g_currentMission.time == nil then
        return
    end
    if g_currentMission.time - lastRetryTime < 10000 then
        return
    end
    lastRetryTime = g_currentMission.time

    for vehicle, _ in pairs(ADCP.waitingVehicles) do
        local alreadyAssigned = false
        for supplier, _ in pairs(ADCP.busyVehicles) do
            if supplier.adcpSupplyTarget == vehicle then
                alreadyAssigned = true
            end
        end
        if not alreadyAssigned then
            local isRelevant, isFull = ADCP.getMonitoredFullState(vehicle)
            local waitState = ADCP.waitingVehicles[vehicle]
            if waitState ~= nil and not waitState.foldCommandSent and g_currentMission.time >= (waitState.foldCommandAt or 0) then
                ADCP.foldWaitingImplements(vehicle)
                waitState.foldCommandSent = true
                ADCP.debugPrint("%s: Beruhigungsphase beendet -> Einklappen gestartet", tostring(vehicle:getName()))
            elseif waitState ~= nil and waitState.foldCommandSent then
                ADCP.foldWaitingImplements(vehicle)
            end
            local foldStartedAt = waitState and waitState.foldStartedAt or g_currentMission.time
            local foldingGraceDone = (g_currentMission.time - foldStartedAt) >= ADCP.FOLDING_GRACE_MS
            if not (isRelevant and isFull) and foldingGraceDone then
                ADCP.tryDispatchSupplier(vehicle)
            end
        end
    end

    -- Zubringer, deren Task fertig ist, wieder freigeben.
    -- AD selbst kuemmert sich danach (ganz normal, ohne unser Zutun) darum,
    -- wieder zur Beladestelle zu fahren - der Zubringer bleibt die ganze
    -- Zeit aktiv, wir haben nichts abgebrochen.
    for supplier, _ in pairs(ADCP.busyVehicles) do
        if supplier.adcpTaskResult ~= nil then
            local result = supplier.adcpTaskResult
            supplier.adcpTaskResult = nil
            local target = supplier.adcpSupplyTarget

            if result ~= "ok" then
                ADCP.busyVehicles[supplier] = nil
                supplier.adcpSupplyTarget = nil
                -- Cooldown setzen: ohne das wuerde findAvailableSupplier beim
                -- naechsten Versuch (10s spaeter) wieder denselben (meist den
                -- naechstgelegenen) Zubringer waehlen, obwohl der gerade erst
                -- gescheitert ist - ein zweiter, verfuegbarer Zubringer kaeme
                -- so nie zum Zug.
                supplier.adcpFailedUntil = (g_currentMission and g_currentMission.time or 0) + ADCP.SUPPLIER_FAIL_COOLDOWN_MS
                ADCP.debugPrint("Andocken bei %s fehlgeschlagen - %s pausiert %d s, naechster Versuch in 10s", target and target:getName() or "?", tostring(supplier:getName()), ADCP.SUPPLIER_FAIL_COOLDOWN_MS / 1000)
            elseif target ~= nil and ADCP.waitingVehicles[target] then
                -- Die eigene Task ist beendet. PickupAndDeliverMode uebernimmt
                -- jetzt wieder und plant die normale Route zur Beladestelle.
                -- Der Zubringer bleibt bis zum echten Sicherheitsabstand gesperrt.
                ADCP.departingSuppliers[supplier] = {target = target}
                ADCP.debugPrint("Umladen und gerade Freifahrt fertig; warte nur noch auf %.1f m Sicherheitsabstand", ADCP.SUPPLIER_DEPARTURE_DISTANCE_M)
            else
                ADCP.busyVehicles[supplier] = nil
                supplier.adcpSupplyTarget = nil
            end
        end
    end

    -- Erst nach real erreichtem Abstand darf der Ausbringer fortfahren.
    -- AutoDrive faehrt zu diesem Zeitpunkt bereits seine normale Vorwaertsroute.
    for supplier, departure in pairs(ADCP.departingSuppliers) do
        local target = departure.target
        local valid = supplier ~= nil and supplier.components ~= nil and target ~= nil and target.components ~= nil
        if not valid then
            ADCP.departingSuppliers[supplier] = nil
            ADCP.busyVehicles[supplier] = nil
            if supplier ~= nil then supplier.adcpSupplyTarget = nil end
        else
            local sx, _, sz = getWorldTranslation(supplier.components[1].node)
            local tx, _, tz = getWorldTranslation(target.components[1].node)
            local distance = MathUtil.vector2Length(sx - tx, sz - tz)
            if distance >= ADCP.SUPPLIER_DEPARTURE_DISTANCE_M then
                ADCP.departingSuppliers[supplier] = nil
                ADCP.busyVehicles[supplier] = nil
                supplier.adcpSupplyTarget = nil
                if ADCP.waitingVehicles[target] ~= nil then
                    ADCP.waitingVehicles[target].supplierDeparted = true
                    local isRelevant, isFull = ADCP.getMonitoredFullState(target)
                    if isRelevant and isFull then
                        ADCP.debugPrint("Zubringer %.1f m entfernt und Ziel voll -> Ausbringer darf fortfahren", distance)
                    else
                        ADCP.debugPrint("Zubringer %.1f m entfernt, Ziel noch nicht voll -> weiterer Zubringer moeglich", distance)
                    end
                end
            end
        end
    end
end

-- onRetryCheck() ist intern durch g_currentMission.time gedrosselt (alle
-- 10s), kann also gefahrlos bei jedem Vehicle:update()-Aufruf mit
-- aufgerufen werden - kein separater, ungeprüfter Mission-Hook noetig.
Vehicle.update = Utils.appendedFunction(Vehicle.update, onRetryCheck)

--- Versucht, AD's "Bunker Silo / Ablade Manager - Reichweite" (UMRange)
--- automatisch auf 0/Aus zu setzen. Diese AD-eigene Einstellung pausiert
--- Fahrzeuge im Abholen&Anliefern-Modus, die sich dieselbe Zielmarke teilen
--- (siehe FS25_AutoDrive/scripts/Manager/UnloadManager.lua) - das haelt
--- unseren zweiten Zubringer faelschlich zurueck. Standardmaessig ist diese
--- Einstellung "Aus" (Index 1); wurde sie manuell erhoeht, versuchen wir
--- hier, sie zurueckzusetzen. Da "AutoDrive" als globales Table aus unserem
--- Mod-Skript heraus bisher NIE zuverlaessig sichtbar war (siehe alle
--- fruehren "AutoDrive-Global ist an dieser Stelle nil"-Faelle), ist dieser
--- Versuch mit klarer Erfolgs-/Fehlermeldung abgesichert - schlaegt er fehl,
--- muss die Einstellung manuell im AD-Menü ("Bunker Silo / Ablade Manager -
--- Reichweite" -> "Aus") gesetzt werden.
local function tryDisableUnloadManagerRange()
    local ok, result = pcall(function()
        if AutoDrive == nil or AutoDrive.setSettingState == nil then
            return "AutoDrive-Global nicht sichtbar"
        end
        AutoDrive.setSettingState("UMRange", 1) -- Index 1 = Wert 0 = "Aus"
        return "ok"
    end)
    if ok and result == "ok" then
        ADCP.debugPrint("UMRange (Bunker Silo/Ablade Manager Reichweite) automatisch auf 'Aus' gesetzt")
    else
        ADCP.debugPrint(
            "UMRange konnte NICHT automatisch gesetzt werden (%s) - bitte manuell im AD-Menue "
            .. "'Bunker Silo / Ablade Manager - Reichweite' auf 'Aus' stellen, sonst haelt AD "
            .. "Zubringer mit gleichem Zielpunkt faelschlich zurueck.",
            tostring(ok and result or result)
        )
    end
end

-- Erst nach kurzer Verzoegerung versuchen (Mod-Ladephase abwarten), danach
-- reicht ein einzelner Versuch, da UMRange eine globale, nicht
-- fahrzeugspezifische Einstellung ist.
local umRangeCheckDone = false
local function onUMRangeInitCheck(vehicle, dt)
    if umRangeCheckDone then
        return
    end
    umRangeCheckDone = true
    tryDisableUnloadManagerRange()
end
Vehicle.update = Utils.appendedFunction(Vehicle.update, onUMRangeInitCheck)

ADCP.debugPrint("Bridge geladen")
