--[[
    TFU chaff/forage contract compatibility layer

    Additional Contracts 1.0.0.6 accepts every fruit that exposes a forage
    growth range, but its medium/large GRAIN vehicle groups use maize headers.
    Multifruit maps can also make Cutter:onAIImplementStart register a very
    large crop list, which may exceed FieldCropsQuery's per-type limit.

    This layer keeps contracts crop-specific from generation through start,
    validates the forageHarvester conversion, adapts the active cutter to the
    exact contract fruit for manual work, and narrows its AI query to one fruit.
]]

ChaffMission.TFU_COMPATIBILITY_FIX_VERSION = 4;
ChaffMission.TFU_AI_QUERY_FIX_INSTALLED = false;
ChaffMission.TFU_PENDING_RESTORES = ChaffMission.TFU_PENDING_RESTORES or setmetatable({}, {__mode="k"});

local function copyArray(values)
    local result = {};
    if values ~= nil then
        for i, value in ipairs(values) do
            result[i] = value;
        end;
    end;
    return result;
end;

local function copyMap(values)
    local result = {};
    if values ~= nil then
        for key, value in pairs(values) do
            result[key] = value;
        end;
    end;
    return result;
end;

local function copyFruitRequirements(requirements)
    local result = {};
    if requirements ~= nil then
        for _, requirement in ipairs(requirements) do
            table.insert(result, {
                fruitType = requirement.fruitType,
                minGrowthState = requirement.minGrowthState,
                maxGrowthState = requirement.maxGrowthState,
                customMapId = requirement.customMapId,
                customMapStartChannel = requirement.customMapStartChannel,
                customMapNumChannels = requirement.customMapNumChannels
            });
        end;
    end;
    return result;
end;

local function containsValue(values, value)
    if values == nil then
        return false;
    end;
    for _, entry in ipairs(values) do
        if entry == value then
            return true;
        end;
    end;
    return false;
end;

local function getRootVehicle(vehicle)
    if vehicle == nil then
        return nil;
    end;

    if vehicle.getRootVehicle ~= nil then
        local success, rootVehicle = pcall(vehicle.getRootVehicle, vehicle);
        if success and rootVehicle ~= nil then
            return rootVehicle;
        end;
    end;

    if vehicle.rootVehicle ~= nil then
        return vehicle.rootVehicle;
    end;

    local currentVehicle = vehicle;
    local visited = {};
    while currentVehicle ~= nil and not visited[currentVehicle] do
        visited[currentVehicle] = true;
        local attachableSpec = currentVehicle.spec_attachable;
        local parentVehicle = attachableSpec ~= nil and attachableSpec.attacherVehicle or nil;
        if parentVehicle == nil then
            break;
        end;
        currentVehicle = parentVehicle;
    end;

    return currentVehicle or vehicle;
end;

local function getVehicleLogName(vehicle)
    if vehicle == nil then
        return "unknown";
    end;
    if vehicle.getName ~= nil then
        local success, name = pcall(vehicle.getName, vehicle);
        if success and name ~= nil and name ~= "" then
            return name;
        end;
    end;
    return vehicle.configFileName or "unknown";
end;

local function getFieldLogId(field)
    if field == nil then
        return "unknown";
    end;
    if field.getId ~= nil then
        local success, fieldId = pcall(field.getId, field);
        if success and fieldId ~= nil then
            return tostring(fieldId);
        end;
    end;
    if field.farmland ~= nil and field.farmland.getId ~= nil then
        local success, farmlandId = pcall(field.farmland.getId, field.farmland);
        if success and farmlandId ~= nil then
            return tostring(farmlandId);
        end;
    end;
    return "unknown";
end;

local function getFruitLogName(fruitTypeIndex)
    local fruitDesc = fruitTypeIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitTypeIndex) or nil;
    return fruitDesc ~= nil and tostring(fruitDesc.name or fruitTypeIndex) or tostring(fruitTypeIndex or "unknown");
end;

local function isVehicleAIActive(vehicle)
    local rootVehicle = getRootVehicle(vehicle);
    if rootVehicle == nil then
        return true;
    end;

    if rootVehicle.getIsAIActive ~= nil then
        local success, isActive = pcall(rootVehicle.getIsAIActive, rootVehicle);
        if not success then
            return true;
        end;
        if isActive == true then
            return true;
        end;
    end;

    if rootVehicle.spec_aiJobVehicle ~= nil and rootVehicle.spec_aiJobVehicle.job ~= nil then
        return true;
    end;
    if rootVehicle.spec_aiDrivable ~= nil and rootVehicle.spec_aiDrivable.isRunning == true then
        return true;
    end;
    if rootVehicle.spec_aiFieldWorker ~= nil and rootVehicle.spec_aiFieldWorker.isActive == true then
        return true;
    end;

    return false;
end;

local function getForageConverter(fruitTypeIndex)
    if g_fruitTypeManager == nil or g_fruitTypeManager.getConverterDataByName == nil then
        return nil;
    end;
    local converterData = g_fruitTypeManager:getConverterDataByName("forageHarvester");
    if converterData == nil then
        converterData = g_fruitTypeManager:getConverterDataByName("FORAGEHARVESTER");
    end;
    return converterData ~= nil and converterData[fruitTypeIndex] or nil;
end;

function ChaffMission.usesMaizeHeader(fruitTypeIndex)
    if fruitTypeIndex == FruitType.MAIZE or fruitTypeIndex == FruitType.SUNFLOWER then
        return true;
    end;
    local fruitDesc = fruitTypeIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitTypeIndex) or nil;
    local fruitName = fruitDesc ~= nil and string.upper(tostring(fruitDesc.name or "")) or "";
    return string.find(fruitName, "MAIZE", 1, true) ~= nil
        or string.find(fruitName, "CORN", 1, true) ~= nil;
end;

-- Returns: valid, reason, code, fruitDesc, fieldState, converter
function ChaffMission.getCompatibleFieldState(field, targetFruitTypeIndex, requireFruitMatch, requireGrowthState)
    if field == nil or field.getFieldState == nil then
        return false, "field is missing", "NO_FIELD";
    end;

    local fieldState = field:getFieldState();
    if fieldState == nil or not fieldState.isValid then
        return false, "field state is invalid", "INVALID_FIELD_STATE", nil, fieldState;
    end;

    local fruitTypeIndex = targetFruitTypeIndex;
    if fruitTypeIndex == nil or fruitTypeIndex == FruitType.UNKNOWN then
        fruitTypeIndex = fieldState.fruitTypeIndex;
    end;
    if fruitTypeIndex == nil or fruitTypeIndex == FruitType.UNKNOWN then
        return false, "field has no crop", "NO_FRUIT", nil, fieldState;
    end;

    if requireFruitMatch and fieldState.fruitTypeIndex ~= fruitTypeIndex then
        return false,
            string.format("field crop '%s' differs from contract crop '%s'", getFruitLogName(fieldState.fruitTypeIndex), getFruitLogName(fruitTypeIndex)),
            "FRUIT_MISMATCH",
            nil,
            fieldState;
    end;

    local fruitDesc = g_fruitTypeManager:getFruitTypeByIndex(fruitTypeIndex);
    if fruitDesc == nil then
        return false, "contract crop is not registered", "FRUIT_NOT_REGISTERED", nil, fieldState;
    end;
    if fruitDesc.getIsCatchCrop ~= nil and fruitDesc:getIsCatchCrop() then
        return false, "catch crops are not supported", "CATCH_CROP", fruitDesc, fieldState;
    end;

    if ChaffMission.ACCEPTS_FRUITTYPE.STATE then
        local fruitName = tostring(fruitDesc.name or ""):upper();
        if ChaffMission.ACCEPTS_FRUITTYPE[fruitName] ~= true then
            return false, "crop is disabled in the chaff contract allow-list", "NOT_ALLOWED", fruitDesc, fieldState;
        end;
    end;

    local minForageState = fruitDesc.minForageGrowthState;
    local maxForageState = fruitDesc.maxForageGrowthState;
    if type(minForageState) ~= "number" or type(maxForageState) ~= "number" or minForageState <= 0 or maxForageState < minForageState then
        return false, "crop has no valid forage growth range", "NO_FORAGE_RANGE", fruitDesc, fieldState;
    end;

    if requireGrowthState then
        local growthState = fieldState.growthState;
        if type(growthState) ~= "number" then
            return false, "field growth state is missing", "NO_GROWTH_STATE", fruitDesc, fieldState;
        end;
        if growthState < minForageState then
            return false,
                string.format("growth state %d is below forage range %d-%d", growthState, minForageState, maxForageState),
                "BELOW_FORAGE_RANGE",
                fruitDesc,
                fieldState;
        end;
        if growthState > maxForageState then
            return false,
                string.format("growth state %d is above forage range %d-%d", growthState, minForageState, maxForageState),
                "ABOVE_FORAGE_RANGE",
                fruitDesc,
                fieldState;
        end;
        if fruitDesc.getIsWithered ~= nil and fruitDesc:getIsWithered(growthState) then
            return false, "crop is withered", "WITHERED", fruitDesc, fieldState;
        end;
    end;

    if fruitDesc.terrainDataPlaneId == nil
        or type(fruitDesc.startStateChannel) ~= "number"
        or type(fruitDesc.numStateChannels) ~= "number"
        or fruitDesc.numStateChannels <= 0 then
        return false, "crop has incomplete density-map registration", "INVALID_DENSITY_MAP", fruitDesc, fieldState;
    end;
    if type(fruitDesc.cutState) ~= "number" or fruitDesc.cutStates == nil or next(fruitDesc.cutStates) == nil then
        return false, "crop has no valid cut state", "NO_CUT_STATE", fruitDesc, fieldState;
    end;

    local converter = getForageConverter(fruitTypeIndex);
    if converter == nil then
        return false, "crop is missing from the forageHarvester converter", "NO_FORAGE_CONVERTER", fruitDesc, fieldState;
    end;
    if converter.fillTypeIndex ~= FillType.CHAFF then
        return false,
            string.format("forageHarvester converter outputs fill type %s instead of CHAFF", tostring(converter.fillTypeIndex)),
            "WRONG_CONVERTER_OUTPUT",
            fruitDesc,
            fieldState,
            converter;
    end;
    if type(converter.conversionFactor) ~= "number" or converter.conversionFactor <= 0 then
        return false,
            "forageHarvester converter has no positive conversion factor",
            "INVALID_CONVERSION_FACTOR",
            fruitDesc,
            fieldState,
            converter;
    end;

    return true, nil, nil, fruitDesc, fieldState, converter;
end;

local originalInit = ChaffMission.init;
function ChaffMission:init(field, fruitTypeIndex, sellingStation)
    local valid, reason, _, fruitDesc, fieldState = ChaffMission.getCompatibleFieldState(field, fruitTypeIndex, true, true);
    if not valid then
        Logging.warning("Additional Contracts | Chaff mission rejected during creation: field=%s fruit='%s' reason=%s",
            getFieldLogId(field), getFruitLogName(fruitTypeIndex), tostring(reason));
        return false;
    end;

    self.tfuTargetGrowthState = fieldState.growthState;
    local success = originalInit(self, field, fruitTypeIndex, sellingStation);
    if success then
        Logging.info("Additional Contracts | Chaff mission created: mission=%s field=%s fruit='%s' growth=%d forageRange=%d-%d variant='%s'",
            tostring(self.uniqueId or self.activeMissionId or "pending"),
            getFieldLogId(field),
            tostring(fruitDesc.name or fruitTypeIndex),
            fieldState.growthState,
            fruitDesc.minForageGrowthState,
            fruitDesc.maxForageGrowthState,
            tostring(self:getVehicleVariant()));
    end;
    return success;
end;

function ChaffMission.isAvailableForField(field, mission)
    if mission == nil then
        local valid = ChaffMission.getCompatibleFieldState(field, nil, true, true);
        return valid;
    end;

    if mission.getIsFinished ~= nil and mission:getIsFinished() then
        return true;
    end;

    local isInProgress = mission.getIsInProgress ~= nil and mission:getIsInProgress();
    local valid = ChaffMission.getCompatibleFieldState(field, mission.fruitTypeIndex, not isInProgress, not isInProgress);
    return valid;
end;

local originalStart = ChaffMission.start;
function ChaffMission:start(spawnVehicles)
    local valid, reason, _, fruitDesc, fieldState = ChaffMission.getCompatibleFieldState(self.field, self.fruitTypeIndex, true, true);
    if not valid then
        Logging.warning("Additional Contracts | Chaff mission start blocked: mission=%s field=%s fruit='%s' reason=%s",
            tostring(self.uniqueId or self.activeMissionId or "unknown"),
            getFieldLogId(self.field),
            getFruitLogName(self.fruitTypeIndex),
            tostring(reason));
        return false;
    end;

    self.tfuTargetGrowthState = fieldState.growthState;
    Logging.info("Additional Contracts | Chaff mission starting: mission=%s field=%s fruit='%s' growth=%d forageRange=%d-%d borrowedVehicles=%s",
        tostring(self.uniqueId or self.activeMissionId or "unknown"),
        getFieldLogId(self.field),
        tostring(fruitDesc.name or self.fruitTypeIndex),
        fieldState.growthState,
        fruitDesc.minForageGrowthState,
        fruitDesc.maxForageGrowthState,
        tostring(spawnVehicles == true));

    local success = originalStart(self, spawnVehicles);
    if not success and self.sellingStation ~= nil then
        self.sellingStation.missions[self] = nil;
    end;
    return success;
end;

function ChaffMission:onSavegameLoaded()
    if self.field == nil then
        Logging.error("Field is not set for chaff mission");
        g_missionManager:markMissionForDeletion(self);
        return;
    end;

    local isFinished = self:getIsFinished();
    if not isFinished then
        local wasStarted = self.getWasStarted ~= nil and self:getWasStarted();
        local valid, reason, code = ChaffMission.getCompatibleFieldState(self.field, self.fruitTypeIndex, not wasStarted, not wasStarted);
        if not valid then
            self.isCancelByGrowthState = code == "ABOVE_FORAGE_RANGE";
            Logging.warning("Additional Contracts | Chaff mission removed while loading: mission=%s field=%s fruit='%s' reason=%s",
                tostring(self.uniqueId or self.activeMissionId or "unknown"),
                getFieldLogId(self.field),
                getFruitLogName(self.fruitTypeIndex),
                tostring(reason));
            g_missionManager:markMissionForDeletion(self);
            return;
        end;

        if wasStarted then
            local fieldState = self.field:getFieldState();
            if fieldState ~= nil and fieldState.fruitTypeIndex ~= self.fruitTypeIndex then
                Logging.info("Additional Contracts | Chaff mission load keeps started delivery phase: mission=%s field=%s targetFruit='%s' currentFieldFruit='%s'",
                    tostring(self.uniqueId or self.activeMissionId or "unknown"),
                    getFieldLogId(self.field),
                    getFruitLogName(self.fruitTypeIndex),
                    getFruitLogName(fieldState.fruitTypeIndex));
            end;
        end;

        local sellingStation = g_currentMission.placeableSystem:getPlaceableByUniqueId(self.sellingStationPlaceableUniqueId);
        if sellingStation == nil then
            Logging.error("Selling station placeable with uniqueId '%s' not available for chaff mission", self.sellingStationPlaceableUniqueId);
            g_missionManager:markMissionForDeletion(self);
            return;
        end;
        local unloadingStation = g_currentMission.storageSystem:getPlaceableUnloadingStation(sellingStation, self.unloadingStationIndex);
        if unloadingStation == nil then
            Logging.error("Unable to retrieve unloadingStation %d for placeable %s for chaff mission", self.unloadingStationIndex, sellingStation.configFileName);
            g_missionManager:markMissionForDeletion(self);
            return;
        end;
        self:setSellingStation(unloadingStation);
        if wasStarted then
            unloadingStation.missions[self] = self;
        end;
    end;

    ChaffMission:superClass().onSavegameLoaded(self);
end;

function ChaffMission:validate(event)
    if not ChaffMission:superClass().validate(self, event) then
        return false;
    end;

    if not self:getIsFinished() then
        local isInProgress = self.getIsInProgress ~= nil and self:getIsInProgress();
        local valid, reason, code = ChaffMission.getCompatibleFieldState(self.field, self.fruitTypeIndex, not isInProgress, not isInProgress);
        if not valid then
            self.isCancelByGrowthState = code == "ABOVE_FORAGE_RANGE";
            if not self.tfuValidationWarningLogged then
                self.tfuValidationWarningLogged = true;
                Logging.warning("Additional Contracts | Chaff mission validation failed: mission=%s field=%s fruit='%s' reason=%s",
                    tostring(self.uniqueId or self.activeMissionId or "unknown"),
                    getFieldLogId(self.field),
                    getFruitLogName(self.fruitTypeIndex),
                    tostring(reason));
            end;
            return false;
        end;
        if self.sellingStation ~= nil and not self.sellingStation.isRegistered then
            return false;
        end;
    end;

    return true;
end;

function ChaffMission:getVehicleVariant()
    local isMaizeHeaderCrop = ChaffMission.usesMaizeHeader(self.fruitTypeIndex);
    if self.field:getAreaHa() >= ChaffMission.data.ownTable.FIELD_SIZE_BIGLARGE then
        local networkSuffix = g_currentMission.missionDynamicInfo.isMultiplayer and "MP" or "SP";
        local cropSuffix = isMaizeHeaderCrop and "MAIZE" or "GRAIN";
        return string.format("BIGLARGE_%s_%s", cropSuffix, networkSuffix);
    end;
    return isMaizeHeaderCrop and "MAIZE" or "GRAIN";
end;

function ChaffMission:getDetails()
    local details = ChaffMission:superClass().getDetails(self);
    local stationName = nil;
    if self.pendingSellingStationId ~= nil then
        self:tryToResolveSellingStation();
    end;
    if self.sellingStation ~= nil then
        stationName = self.sellingStation:getName();
    end;
    if stationName ~= nil then
        table.insert(details, {
            ["title"] = g_i18n:getText("contract_details_harvesting_sellingStation"),
            ["value"] = stationName
        });
    end;

    local fruitFillTypeIndex = g_fruitTypeManager:getFillTypeIndexByFruitTypeIndex(self.fruitTypeIndex);
    local fruitTitle = fruitFillTypeIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fruitFillTypeIndex) or nil;
    if fruitTitle == nil or fruitTitle == "" then
        fruitTitle = getFruitLogName(self.fruitTypeIndex);
    end;
    table.insert(details, {
        ["title"] = g_i18n:getText("contract_details_harvesting_crop"),
        ["value"] = fruitTitle
    });

    return details;
end;

function ChaffMission:getMaxCutLiters(fieldAreaHa)
    local fruitTypeIndex = self.fruitTypeIndex;
    local fieldState = self.field:getFieldState();
    local multiplier = fieldState:getHarvestScaleMultiplier();
    local areaHa = (fieldAreaHa or self.field:getAreaHa()) * multiplier;
    local areaPixel = MathUtil.haToSqm(areaHa) / g_currentMission:getFruitPixelsToSqm();
    local converter = getForageConverter(fruitTypeIndex);
    local conversionFactor = converter ~= nil and converter.conversionFactor or nil;
    if type(conversionFactor) ~= "number" or conversionFactor <= 0 then
        conversionFactor = ChaffMission.data.ownTable.CONVERT_FACTOR;
    end;
    return conversionFactor * g_fruitTypeManager:getFruitTypeAreaLiters(fruitTypeIndex, areaPixel, false);
end;

local function restoreCutterState(vehicle, original)
    if vehicle == nil or vehicle.isDeleted == true then
        return true;
    end;
    if original == nil or vehicle.spec_cutter == nil then
        return true;
    end;
    if isVehicleAIActive(vehicle) then
        return false;
    end;

    local spec = vehicle.spec_cutter;
    spec.fruitTypeIndices = copyArray(original.fruitTypeIndices);
    spec.fruitTypeConverters = copyMap(original.fruitTypeConverters);
    spec.outputFillTypes = copyArray(original.outputFillTypes);
    spec.allowsForageGrowthState = original.allowsForageGrowthState;
    spec.currentInputFruitTypeAI = original.currentInputFruitTypeAI;
    if spec.workAreaParameters ~= nil then
        spec.workAreaParameters.fruitTypeIndicesToUse = spec.fruitTypeIndices;
        spec.workAreaParameters.lastFruitType = nil;
    end;
    if vehicle.setCutterCutHeight ~= nil and original.currentCutHeight ~= nil then
        vehicle:setCutterCutHeight(original.currentCutHeight);
    end;

    if vehicle.spec_aiImplement ~= nil and original.aiFruitRequirements ~= nil then
        vehicle.spec_aiImplement.requiredFruitTypes = copyFruitRequirements(original.aiFruitRequirements);
        vehicle.spec_aiImplement.fieldCropyQuery = nil;
        vehicle.spec_aiImplement.fieldCropyQueryValid = false;
    end;

    if vehicle.spec_testAreas ~= nil and original.testArea ~= nil then
        vehicle.spec_testAreas.fruitTypeIndex = original.testArea.fruitTypeIndex;
        vehicle.spec_testAreas.fillTypeIndex = original.testArea.fillTypeIndex;
        vehicle.spec_testAreas.allowsForageGrowthState = original.testArea.allowsForageGrowthState;
    end;

    vehicle.tfuChaffCompatibilityOwner = nil;
    vehicle.tfuChaffCompatibilityMarker = nil;
    vehicle.tfuChaffCompatibilityOriginal = nil;
    return true;
end;

function ChaffMission:patchCutterCompatibility(vehicle)
    if not self.isServer or vehicle == nil or vehicle.spec_cutter == nil then
        return false;
    end;

    local valid, reason, _, fruitDesc, _, converter = ChaffMission.getCompatibleFieldState(self.field, self.fruitTypeIndex, false, false);
    if not valid then
        if not self.tfuCutterPatchInvalidMissionLogged then
            self.tfuCutterPatchInvalidMissionLogged = true;
            Logging.warning("Additional Contracts | Chaff cutter patch skipped: mission=%s field=%s fruit='%s' reason=%s",
                tostring(self.uniqueId or self.activeMissionId or "unknown"),
                getFieldLogId(self.field),
                getFruitLogName(self.fruitTypeIndex),
                tostring(reason));
        end;
        return false;
    end;

    local marker = string.format("%s:%s:%s", tostring(self.uniqueId or 0), tostring(self.activeMissionId or 0), tostring(self.fruitTypeIndex));
    if vehicle.tfuChaffCompatibilityOwner ~= nil and vehicle.tfuChaffCompatibilityOwner ~= self then
        local oldOriginal = vehicle.tfuChaffCompatibilityOriginal;
        if not restoreCutterState(vehicle, oldOriginal) then
            return false;
        end;
    end;

    if vehicle.tfuChaffCompatibilityOriginal == nil then
        local aiRequirements = vehicle.getAIFruitRequirements ~= nil and vehicle:getAIFruitRequirements()
            or (vehicle.spec_aiImplement ~= nil and vehicle.spec_aiImplement.requiredFruitTypes or nil);
        local testArea = nil;
        if vehicle.spec_testAreas ~= nil then
            testArea = {
                fruitTypeIndex = vehicle.spec_testAreas.fruitTypeIndex,
                fillTypeIndex = vehicle.spec_testAreas.fillTypeIndex,
                allowsForageGrowthState = vehicle.spec_testAreas.allowsForageGrowthState
            };
        end;
        vehicle.tfuChaffCompatibilityOriginal = {
            fruitTypeIndices = copyArray(vehicle.spec_cutter.fruitTypeIndices),
            fruitTypeConverters = copyMap(vehicle.spec_cutter.fruitTypeConverters),
            outputFillTypes = copyArray(vehicle.spec_cutter.outputFillTypes),
            allowsForageGrowthState = vehicle.spec_cutter.allowsForageGrowthState,
            currentInputFruitTypeAI = vehicle.spec_cutter.currentInputFruitTypeAI,
            currentCutHeight = vehicle.spec_cutter.currentCutHeight,
            aiFruitRequirements = copyFruitRequirements(aiRequirements),
            testArea = testArea
        };
    end;

    local spec = vehicle.spec_cutter;
    spec.fruitTypeIndices = spec.fruitTypeIndices or {};
    spec.fruitTypeConverters = spec.fruitTypeConverters or {};
    spec.outputFillTypes = spec.outputFillTypes or {};
    spec.allowsForageGrowthState = true;
    spec.fruitTypeConverters[self.fruitTypeIndex] = converter;
    if not containsValue(spec.fruitTypeIndices, self.fruitTypeIndex) then
        table.insert(spec.fruitTypeIndices, self.fruitTypeIndex);
    end;
    if not containsValue(spec.outputFillTypes, converter.fillTypeIndex) then
        table.insert(spec.outputFillTypes, converter.fillTypeIndex);
    end;
    spec.currentInputFruitTypeAI = self.fruitTypeIndex;
    if spec.workAreaParameters ~= nil then
        spec.workAreaParameters.fruitTypeIndicesToUse = spec.fruitTypeIndices;
        spec.workAreaParameters.lastFruitType = nil;
    end;
    if vehicle.setCutterCutHeight ~= nil and g_fruitTypeManager.getCutHeightByFruitTypeIndex ~= nil then
        local cutHeight = g_fruitTypeManager:getCutHeightByFruitTypeIndex(self.fruitTypeIndex, true);
        if cutHeight ~= nil then
            vehicle:setCutterCutHeight(cutHeight);
        end;
    end;

    if vehicle.setTestAreaRequirements ~= nil then
        vehicle:setTestAreaRequirements(self.fruitTypeIndex, nil, true);
    end;
    if vehicle.setAIFruitRequirements ~= nil then
        -- Cutter:onAIImplementStart rebuilds the broad list every time AI starts,
        -- therefore this is intentionally re-applied even when the marker matches.
        vehicle:setAIFruitRequirements(self.fruitTypeIndex, fruitDesc.minForageGrowthState, fruitDesc.maxForageGrowthState);
    elseif vehicle.spec_aiImplement ~= nil then
        vehicle.spec_aiImplement.requiredFruitTypes = {{
            fruitType = self.fruitTypeIndex,
            minGrowthState = fruitDesc.minForageGrowthState,
            maxGrowthState = fruitDesc.maxForageGrowthState
        }};
        vehicle.spec_aiImplement.fieldCropyQuery = nil;
        vehicle.spec_aiImplement.fieldCropyQueryValid = false;
    end;

    local firstPatch = vehicle.tfuChaffCompatibilityMarker ~= marker;
    vehicle.tfuChaffCompatibilityOwner = self;
    vehicle.tfuChaffCompatibilityMarker = marker;
    self.tfuPatchedCutters = self.tfuPatchedCutters or {};
    self.tfuPatchedCutters[vehicle] = true;

    if firstPatch then
        Logging.info("Additional Contracts | Chaff cutter compatibility: mission=%s field=%s vehicle='%s' fruit='%s' states=%d-%d output=CHAFF",
            tostring(self.uniqueId or self.activeMissionId or "unknown"),
            getFieldLogId(self.field),
            getVehicleLogName(vehicle),
            tostring(fruitDesc.name or self.fruitTypeIndex),
            fruitDesc.minForageGrowthState,
            fruitDesc.maxForageGrowthState);
    end;

    return true;
end;

function ChaffMission:patchCutterCompatibilityForVehicleTree(vehicle)
    local rootVehicle = getRootVehicle(vehicle);
    if rootVehicle == nil then
        return false;
    end;

    local patched = self:patchCutterCompatibility(rootVehicle);
    if rootVehicle.getChildVehicles ~= nil then
        local childVehicles = rootVehicle:getChildVehicles();
        if childVehicles ~= nil then
            for _, childVehicle in ipairs(childVehicles) do
                if childVehicle ~= rootVehicle and self:patchCutterCompatibility(childVehicle) then
                    patched = true;
                end;
            end;
        end;
    end;
    if vehicle ~= rootVehicle and self:patchCutterCompatibility(vehicle) then
        patched = true;
    end;
    return patched;
end;

function ChaffMission:restoreAllCutterCompatibility()
    if self.tfuPatchedCutters == nil then
        return false;
    end;

    local hasPending = false;
    for vehicle, _ in pairs(self.tfuPatchedCutters) do
        if vehicle ~= nil and vehicle.tfuChaffCompatibilityOwner == self then
            local success, restored = pcall(restoreCutterState, vehicle, vehicle.tfuChaffCompatibilityOriginal);
            if not success then
                hasPending = true;
                if not self.tfuRestoreErrorLogged then
                    self.tfuRestoreErrorLogged = true;
                    Logging.error("Additional Contracts | Chaff cutter restore failed: %s", tostring(restored));
                end;
            elseif restored then
                self.tfuPatchedCutters[vehicle] = nil;
                Logging.info("Additional Contracts | Restored chaff cutter compatibility for vehicle='%s'", getVehicleLogName(vehicle));
            else
                hasPending = true;
            end;
        else
            self.tfuPatchedCutters[vehicle] = nil;
        end;
    end;
    return hasPending;
end;

function ChaffMission.queuePendingRestore(vehicle)
    if vehicle ~= nil and vehicle.tfuChaffCompatibilityOriginal ~= nil then
        ChaffMission.TFU_PENDING_RESTORES[vehicle] = vehicle.tfuChaffCompatibilityOriginal;
    end;
end;

function ChaffMission.processPendingRestores()
    for vehicle, original in pairs(ChaffMission.TFU_PENDING_RESTORES) do
        local success, restored = pcall(restoreCutterState, vehicle, original);
        if not success then
            Logging.error("Additional Contracts | Deferred chaff cutter restore failed for vehicle='%s': %s", getVehicleLogName(vehicle), tostring(restored));
            ChaffMission.TFU_PENDING_RESTORES[vehicle] = nil;
        elseif restored then
            ChaffMission.TFU_PENDING_RESTORES[vehicle] = nil;
            Logging.info("Additional Contracts | Deferred chaff cutter state restored for vehicle='%s'", getVehicleLogName(vehicle));
        end;
    end;
end;

function ChaffMission:onSpawnedVehicle(vehicles, vehicleLoadState, loadingInfo)
    ChaffMission:superClass().onSpawnedVehicle(self, vehicles, vehicleLoadState, loadingInfo);
    if self.isServer and vehicleLoadState == VehicleLoadingState.OK and vehicles ~= nil then
        for _, vehicle in ipairs(vehicles) do
            self:patchCutterCompatibilityForVehicleTree(vehicle);
        end;
    end;
end;

local originalUpdate = ChaffMission.update;
function ChaffMission:update(dt)
    originalUpdate(self, dt);
    if not self.isServer then
        return;
    end;

    if self.getIsRunning ~= nil and self:getIsRunning() and self.vehicles ~= nil then
        for _, vehicle in ipairs(self.vehicles) do
            self:patchCutterCompatibilityForVehicleTree(vehicle);
        end;
    end;
    if self.tfuRestorePending then
        self.tfuRestorePending = self:restoreAllCutterCompatibility();
    end;
end;

local originalFinish = ChaffMission.finish;
function ChaffMission:finish(finishState)
    originalFinish(self, finishState);
    self.tfuRestorePending = self:restoreAllCutterCompatibility();
end;

local originalDelete = ChaffMission.delete;
function ChaffMission:delete()
    local hasPending = self:restoreAllCutterCompatibility();
    if hasPending and self.tfuPatchedCutters ~= nil then
        for vehicle, _ in pairs(self.tfuPatchedCutters) do
            if vehicle ~= nil and vehicle.tfuChaffCompatibilityOwner == self then
                ChaffMission.queuePendingRestore(vehicle);
                vehicle.tfuChaffCompatibilityOwner = nil;
                vehicle.tfuChaffCompatibilityMarker = nil;
                vehicle.tfuChaffCompatibilityOriginal = nil;
            end;
        end;
    end;
    self.tfuPatchedCutters = {};
    originalDelete(self);
end;

function ChaffMission:getIsWorkAllowed(farmId, x, z, workAreaType, vehicle)
    local isAllowed = ChaffMission:superClass().getIsWorkAllowed(self, farmId, x, z, workAreaType, vehicle);
    if isAllowed and vehicle ~= nil and (workAreaType == nil or workAreaType == WorkAreaType.CUTTER) then
        self:patchCutterCompatibilityForVehicleTree(vehicle);
    end;
    return isAllowed;
end;

function ChaffMission.onBeforeGetAIAreaOfVehicle(vehicle, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)
    if g_missionManager == nil or vehicle == nil or startWorldX == nil or startWorldZ == nil then
        return;
    end;

    local samplePositions = {
        {(startWorldX + (widthWorldX or startWorldX)) * 0.5, (startWorldZ + (widthWorldZ or startWorldZ)) * 0.5},
        {(startWorldX + (heightWorldX or startWorldX)) * 0.5, (startWorldZ + (heightWorldZ or startWorldZ)) * 0.5},
        {(startWorldX + (widthWorldX or startWorldX) + (heightWorldX or startWorldX)) / 3, (startWorldZ + (widthWorldZ or startWorldZ) + (heightWorldZ or startWorldZ)) / 3},
        {startWorldX, startWorldZ}
    };

    for _, position in ipairs(samplePositions) do
        local mission = g_missionManager:getMissionAtWorldPosition(position[1], position[2]);
        if mission ~= nil
            and mission.isServer
            and mission.getMissionTypeName ~= nil
            and mission:getMissionTypeName() == ChaffMission.NAME
            and mission.getIsRunning ~= nil
            and mission:getIsRunning() then
            local success, errorMessage = pcall(mission.patchCutterCompatibilityForVehicleTree, mission, vehicle);
            if not success and not ChaffMission.TFU_AI_QUERY_FIX_ERROR_LOGGED then
                ChaffMission.TFU_AI_QUERY_FIX_ERROR_LOGGED = true;
                Logging.error("Additional Contracts | Chaff AI compatibility guard failed: %s", tostring(errorMessage));
            end;
            return;
        end;
    end;
end;

function ChaffMission.getAIAreaOfVehicle(vehicle, superFunc, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)
    ChaffMission.onBeforeGetAIAreaOfVehicle(vehicle, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ);

    local area, totalArea = superFunc(vehicle, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ);
    if type(area) ~= "number" or type(totalArea) ~= "number" then
        if not ChaffMission.TFU_AI_AREA_RETURN_GUARD_LOGGED then
            ChaffMission.TFU_AI_AREA_RETURN_GUARD_LOGGED = true;
            Logging.warning("Additional Contracts | Chaff AI area guard received invalid return values area=%s totalArea=%s; using 0,0", tostring(area), tostring(totalArea));
        end;
        return 0, 0;
    end;

    return area, totalArea;
end;

function ChaffMission.installAIQueryFix()
    if ChaffMission.TFU_AI_QUERY_FIX_INSTALLED then
        return;
    end;
    if AIVehicleUtil == nil or AIVehicleUtil.getAIAreaOfVehicle == nil or Utils == nil or Utils.overwrittenFunction == nil then
        Logging.warning("Additional Contracts | Chaff AI compatibility fix could not be installed");
        return;
    end;

    AIVehicleUtil.getAIAreaOfVehicle = Utils.overwrittenFunction(AIVehicleUtil.getAIAreaOfVehicle, ChaffMission.getAIAreaOfVehicle);
    ChaffMission.TFU_AI_QUERY_FIX_INSTALLED = true;
    Logging.info("Additional Contracts | Installed chaff contract crop/growth/header compatibility fix v%s (return-safe)", tostring(ChaffMission.TFU_COMPATIBILITY_FIX_VERSION));
end;

local originalLoadInit = ChaffMission.loadInit;
function ChaffMission:loadInit()
    originalLoadInit(self);
    ChaffMission.installAIQueryFix();
end;
