--[[
    TFU5 server-authoritative chaff progress, synchronization and delivery safety.

    The original Additional Contracts chaff mission counted a post-cut foliage
    state. On multifruit maps that state is not guaranteed to remain after a
    forage harvester passes, so valid work could be heavily undercounted. This
    layer measures the exact contract crop that remains in its forage growth
    range and derives completed area from a persistent start baseline.

    The layer also synchronizes the authoritative progress values to late-join
    clients, repairs zero-percent delivery selection, and ensures new contracts use only neutral/NPC-owned mission destinations.
]]

ChaffMission.TFU_PROGRESS_FIX_VERSION = 6;
ChaffMission.TFU_PROGRESS_DATA_VERSION = 1;
ChaffMission.TFU_PROGRESS_SCAN_INTERVAL_MS = 2500;
ChaffMission.TFU_PROGRESS_LOG_INTERVAL_MS = 30000;
ChaffMission.TFU_PROGRESS_SYNC_INTERVAL_MS = 5000;
ChaffMission.TFU_PROGRESS_FINISH_VALUE = 0.995;

local function tfuClamp(value, minimum, maximum)
    if math.clamp ~= nil then
        return math.clamp(value, minimum, maximum);
    end;
    return math.max(minimum, math.min(maximum, value));
end;

local function tfuGetTimeMs()
    if type(g_time) == "number" then
        return g_time;
    end;
    if g_currentMission ~= nil and type(g_currentMission.time) == "number" then
        return g_currentMission.time;
    end;
    return 0;
end;

local function tfuConnectionIsServer(connection)
    if connection == nil then return nil;end;
    if connection.getIsServer ~= nil then
        local success, value = pcall(connection.getIsServer, connection);
        if success then return value == true;end;
    end;
    return connection.isServer == true;
end;

local function tfuGetMissionId(self)
    return tostring(self.uniqueId or self.activeMissionId or "unknown");
end;

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

local function tfuGetFruitDesc(self)
    if g_fruitTypeManager == nil or self.fruitTypeIndex == nil then
        return nil;
    end;
    return g_fruitTypeManager:getFruitTypeByIndex(self.fruitTypeIndex);
end;

local function tfuGetFruitName(self)
    local fruitDesc = tfuGetFruitDesc(self);
    return fruitDesc ~= nil and tostring(fruitDesc.name or self.fruitTypeIndex) or tostring(self.fruitTypeIndex or "unknown");
end;

local function tfuGetForageConverter(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;

local function tfuGetHarvestScaleMultiplier(self)
    if type(self.tfuHarvestScaleMultiplier) == "number" and self.tfuHarvestScaleMultiplier > 0 then
        return self.tfuHarvestScaleMultiplier;
    end;

    local multiplier = 1;
    if self.field ~= nil and self.field.getFieldState ~= nil then
        local fieldState = self.field:getFieldState();
        if fieldState ~= nil and fieldState.getHarvestScaleMultiplier ~= nil then
            local success, value = pcall(fieldState.getHarvestScaleMultiplier, fieldState);
            if success and type(value) == "number" and value > 0 then
                multiplier = value;
            end;
        end;
    end;

    self.tfuHarvestScaleMultiplier = multiplier;
    return multiplier;
end;

local function tfuGetOwnerFarmId(unloadingStation)
    if unloadingStation == nil then
        return nil;
    end;
    local placeable = unloadingStation.owningPlaceable;
    if placeable == nil then
        return nil;
    end;
    if placeable.getOwnerFarmId ~= nil then
        local success, farmId = pcall(placeable.getOwnerFarmId, placeable);
        if success then
            return farmId;
        end;
    end;
    return placeable.ownerFarmId;
end;

local function tfuIsStationNeutral(unloadingStation)
    if g_additionalContractMapData ~= nil and g_additionalContractMapData.isMissionSafeSellingStation ~= nil then
        return g_additionalContractMapData:isMissionSafeSellingStation(unloadingStation, FillType.CHAFF, false, {requireBulk=true});
    end;
    local ownerFarmId = tonumber(tfuGetOwnerFarmId(unloadingStation));
    -- Fail closed when ownership cannot be proven. Public map/NPC stations
    -- report farm 0 (or lower); positive IDs are player farms.
    return ownerFarmId ~= nil and ownerFarmId <= 0;
end;

local function tfuFindSafeSellingStation(fillTypeIndex)
    if g_additionalContractMapData ~= nil
        and g_additionalContractMapData.getMissionSafeSellingStationWithHighestPrice ~= nil then
        return g_additionalContractMapData:getMissionSafeSellingStationWithHighestPrice(fillTypeIndex, false, {requireBulk=true});
    end;

    if g_currentMission == nil or g_currentMission.storageSystem == nil then
        return nil, 0;
    end;

    local selectedStation = nil;
    local highestPrice = -math.huge;
    for _, unloadingStation in pairs(g_currentMission.storageSystem:getUnloadingStations()) do
        local acceptedFillTypes = unloadingStation.acceptedFillTypes;
        local acceptsFillType = acceptedFillTypes ~= nil and acceptedFillTypes[fillTypeIndex] ~= nil and acceptedFillTypes[fillTypeIndex] ~= false;
        if unloadingStation.owningPlaceable ~= nil
            and unloadingStation.isSellingPoint
            and unloadingStation.allowMissions
            and acceptsFillType
            and tfuIsStationNeutral(unloadingStation) then
            local price = 0;
            if unloadingStation.getEffectiveFillTypePrice ~= nil then
                local success, value = pcall(unloadingStation.getEffectiveFillTypePrice, unloadingStation, fillTypeIndex);
                if success and type(value) == "number" then
                    price = value;
                end;
            end;
            if selectedStation == nil or price > highestPrice then
                selectedStation = unloadingStation;
                highestPrice = price;
            end;
        end;
    end;

    if selectedStation == nil then
        return nil, 0;
    end;
    return selectedStation, math.max(highestPrice, 0);
end;

local function tfuRemoveSellingStationHotspot(self)
    if self.sellingStationMapHotspot == nil then
        return;
    end;
    if self.mapHotspots ~= nil and table.removeElement ~= nil then
        table.removeElement(self.mapHotspots, self.sellingStationMapHotspot);
    end;
    if g_currentMission ~= nil then
        pcall(g_currentMission.removeMapHotspot, g_currentMission, self.sellingStationMapHotspot);
    end;
    if self.sellingStationMapHotspot.delete ~= nil then
        self.sellingStationMapHotspot:delete();
    end;
    self.sellingStationMapHotspot = nil;
end;

local function tfuReplaceSellingStation(self, newStation)
    if newStation == self.sellingStation then
        return true;
    end;

    if self.sellingStation ~= nil and self.sellingStation.missions ~= nil then
        self.sellingStation.missions[self] = nil;
    end;
    tfuRemoveSellingStationHotspot(self);
    self.sellingStation = nil;
    self.pendingSellingStationId = nil;
    if newStation ~= nil then
        self:setSellingStation(newStation);
        if self.isServer and self.status == MissionStatus.RUNNING then
            local expected = tonumber(self.expectedLiters) or 0;
            local required = expected * ChaffMission.data.ownTable.SUCCESS_FACTOR;
            if expected <= 0 or (tonumber(self.depositedLiters) or 0) < required then
                newStation.missions = newStation.missions or {};
                newStation.missions[self] = self;
            end;
        end;
    end;
    return self.sellingStation == newStation;
end;

local function tfuCreateProgressModifier(self)
    local fruitDesc = tfuGetFruitDesc(self);
    if fruitDesc == nil
        or fruitDesc.terrainDataPlaneId == nil
        or type(fruitDesc.startStateChannel) ~= "number"
        or type(fruitDesc.numStateChannels) ~= "number"
        or fruitDesc.numStateChannels <= 0
        or self.field == nil
        or self.field.getDensityMapPolygon == nil then
        return false;
    end;

    local minState = fruitDesc.minForageGrowthState;
    local maxState = fruitDesc.maxForageGrowthState;
    if type(minState) ~= "number" or type(maxState) ~= "number" or minState <= 0 or maxState < minState then
        return false;
    end;

    local modifier = DensityMapModifier.new(
        fruitDesc.terrainDataPlaneId,
        fruitDesc.startStateChannel,
        fruitDesc.numStateChannels,
        g_terrainNode
    );
    local filter = DensityMapFilter.new(modifier);
    filter:setValueCompareParams(DensityValueCompareType.BETWEEN, minState, maxState);
    local allFruitFilter = DensityMapFilter.new(modifier);
    allFruitFilter:setValueCompareParams(DensityValueCompareType.GREATER, 0);
    local cutStateFilters = {};
    local seenCutStates = {};
    local function addCutStateFilter(cutState)
        if type(cutState) ~= "number" or cutState <= 0 or seenCutStates[cutState] then return;end;
        seenCutStates[cutState] = true;
        local cutFilter = DensityMapFilter.new(modifier);
        cutFilter:setValueCompareParams(DensityValueCompareType.EQUAL, cutState);
        table.insert(cutStateFilters, cutFilter);
    end;
    if type(fruitDesc.cutStates) == "table" then
        for cutState, _ in pairs(fruitDesc.cutStates) do
            addCutStateFilter(cutState);
        end;
    end;
    addCutStateFilter(fruitDesc.cutState);
    self.field:getDensityMapPolygon():applyToModifier(modifier);

    self.tfuProgressModifier = modifier;
    self.tfuProgressFilter = filter;
    self.tfuAllFruitFilter = allFruitFilter;
    self.tfuCutStateFilters = cutStateFilters;
    self.tfuProgressMinState = minState;
    self.tfuProgressMaxState = maxState;

    -- The TFU counter does its own full-polygon scan. Leaving the original
    -- partition modifier disabled prevents the old cut-state counter from
    -- writing contradictory values into fieldPercentageDone.
    self.completionModifier = nil;
    self.completionFilter = nil;
    self.completionPartitions = nil;
    self.isFieldCompletionInitialized = true;
    self.currentPartitionCompletionIndex = nil;
    return true;
end;

local function tfuScanRemainingCrop(self, force)
    if not self.isServer then
        return self.tfuRemainingCropArea or 0, self.tfuPolygonPixelArea or 0, self.tfuProgressScanValid == true;
    end;

    local now = tfuGetTimeMs();
    if not force
        and type(self.tfuNextProgressScanTime) == "number"
        and now < self.tfuNextProgressScanTime
        and self.tfuProgressScanValid == true
        and type(self.tfuRemainingCropArea) == "number" then
        return self.tfuRemainingCropArea, self.tfuPolygonPixelArea or 0, true;
    end;

    if self.tfuProgressModifier == nil or self.tfuProgressFilter == nil then
        if not tfuCreateProgressModifier(self) then
            self.tfuProgressScanValid = false;
            return self.tfuRemainingCropArea or 0, self.tfuPolygonPixelArea or 0, false;
        end;
    end;

    local success, sumPixelsOrError, remainingArea, polygonArea = pcall(
        self.tfuProgressModifier.executeGet,
        self.tfuProgressModifier,
        self.tfuProgressFilter
    );
    if not success or type(remainingArea) ~= "number" or type(polygonArea) ~= "number" then
        self.tfuProgressScanValid = false;
        if not self.tfuProgressScanErrorLogged then
            self.tfuProgressScanErrorLogged = true;
            Logging.error(
                "Additional Contracts | TFU chaff progress scan failed: mission=%s field=%s fruit='%s' error=%s",
                tfuGetMissionId(self),
                tfuGetFieldId(self),
                tfuGetFruitName(self),
                tostring(success and "invalid return values" or sumPixelsOrError)
            );
        end;
        return self.tfuRemainingCropArea or 0, self.tfuPolygonPixelArea or 0, false;
    end;

    self.tfuProgressScanErrorLogged = false;
    self.tfuProgressScanValid = true;
    self.tfuRemainingCropArea = math.max(remainingArea, 0);
    self.tfuPolygonPixelArea = math.max(polygonArea, 0);
    self.tfuNextProgressScanTime = now + ChaffMission.TFU_PROGRESS_SCAN_INTERVAL_MS;
    return self.tfuRemainingCropArea, self.tfuPolygonPixelArea, true;
end;

local function tfuCanTreatZeroRemainingAsComplete(self, residualTolerance)
    if not self.isServer or self.tfuProgressModifier == nil or self.tfuAllFruitFilter == nil then
        return false;
    end;
    if self.getWasStarted ~= nil and not self:getWasStarted() then
        return false;
    end;
    if type(self.expectedLiters) ~= "number" or self.expectedLiters <= 0 then
        return false;
    end;

    local success, _, allFruitArea = pcall(
        self.tfuProgressModifier.executeGet,
        self.tfuProgressModifier,
        self.tfuAllFruitFilter
    );
    if not success or type(allFruitArea) ~= "number" then
        return false;
    end;

    local cutArea = 0;
    for _, cutFilter in ipairs(self.tfuCutStateFilters or {}) do
        local cutSuccess, _, area = pcall(
            self.tfuProgressModifier.executeGet,
            self.tfuProgressModifier,
            cutFilter
        );
        if not cutSuccess or type(area) ~= "number" then
            return false;
        end;
        cutArea = cutArea + math.max(area, 0);
    end;

    self.tfuAllFruitArea = math.max(allFruitArea, 0);
    self.tfuCutStateArea = math.max(cutArea, 0);
    local tolerance = math.max(residualTolerance or 4, 4);
    local nonCutFruitArea = math.max(self.tfuAllFruitArea - self.tfuCutStateArea, 0);
    if nonCutFruitArea <= tolerance then
        return true;
    end;

    -- Some multifruit maps do not declare the exact post-chop state in
    -- fruitDesc.cutStates. Once the exact forage-ready crop is gone and the
    -- mission's own 90% delivery requirement has been met, both independent
    -- objectives prove the legacy mission can be completed safely.
    local deliveryComplete = type(self.expectedLiters) == "number"
        and self.expectedLiters > 0
        and (self.depositedLiters or 0) >= self.expectedLiters * ChaffMission.data.ownTable.SUCCESS_FACTOR;
    if deliveryComplete then
        if not self.tfuDeliveryBackedCompletionLogged then
            self.tfuDeliveryBackedCompletionLogged = true;
            Logging.warning(
                "Additional Contracts | TFU chaff accepted zero remaining crop with completed delivery because the map post-cut state is not declared: mission=%s field=%s fruit='%s' allFruit=%.0f cutState=%.0f",
                tfuGetMissionId(self),
                tfuGetFieldId(self),
                tfuGetFruitName(self),
                self.tfuAllFruitArea,
                self.tfuCutStateArea
            );
        end;
        return true;
    end;
    return false;
end;

local function tfuDeriveBaselineFromExpected(self, remainingArea, polygonArea)
    if type(self.expectedLiters) ~= "number" or self.expectedLiters <= 0 then
        return nil;
    end;

    local converter = tfuGetForageConverter(self.fruitTypeIndex);
    local conversionFactor = converter ~= nil and converter.conversionFactor or nil;
    if type(conversionFactor) ~= "number" or conversionFactor <= 0 then
        return nil;
    end;

    local litersPerPixel = nil;
    if g_fruitTypeManager ~= nil and g_fruitTypeManager.getFruitTypeAreaLiters ~= nil then
        local success, value = pcall(g_fruitTypeManager.getFruitTypeAreaLiters, g_fruitTypeManager, self.fruitTypeIndex, 1, false);
        if success and type(value) == "number" and value > 0 then
            litersPerPixel = value;
        end;
    end;

    if litersPerPixel == nil then
        local fruitDesc = tfuGetFruitDesc(self);
        local pixelToSqm = g_currentMission ~= nil and g_currentMission.getFruitPixelsToSqm ~= nil and g_currentMission:getFruitPixelsToSqm() or nil;
        if fruitDesc ~= nil and type(fruitDesc.literPerSqm) == "number" and fruitDesc.literPerSqm > 0 and type(pixelToSqm) == "number" and pixelToSqm > 0 then
            litersPerPixel = fruitDesc.literPerSqm * pixelToSqm;
        end;
    end;

    if type(litersPerPixel) ~= "number" or litersPerPixel <= 0 then
        return nil;
    end;

    local multiplier = tfuGetHarvestScaleMultiplier(self);
    local baseline = self.expectedLiters / conversionFactor / litersPerPixel / math.max(multiplier, 0.0001);
    if type(polygonArea) == "number" and polygonArea > 0 then
        baseline = math.min(baseline, polygonArea);
    end;
    if type(remainingArea) == "number" then
        baseline = math.max(baseline, remainingArea);
    end;
    return baseline > 0 and baseline or nil;
end;

local function tfuInferRawFieldCompletion(self)
    local savedCompletion = type(self.completion) == "number" and self.completion or 0;
    if savedCompletion <= 0 then
        return 0;
    end;

    local deliveryCompletion = 0;
    if type(self.expectedLiters) == "number" and self.expectedLiters > 0 then
        deliveryCompletion = tfuClamp(
            (self.depositedLiters or 0) / self.expectedLiters / ChaffMission.data.ownTable.SUCCESS_FACTOR,
            0,
            1
        );
    end;

    local normalizedFieldCompletion = tfuClamp((savedCompletion - 0.2 * deliveryCompletion) / 0.8, 0, 1);
    local fieldThreshold = type(AbstractMission.SUCCESS_FACTOR) == "number" and AbstractMission.SUCCESS_FACTOR or 0.95;
    return tfuClamp(normalizedFieldCompletion * fieldThreshold, 0, 1);
end;

local function tfuEnsureBaseline(self, preferCurrentArea)
    local remainingArea, polygonArea, scanValid = tfuScanRemainingCrop(self, true);
    if type(self.tfuBaselineCropArea) == "number" and self.tfuBaselineCropArea > 0 then
        if scanValid then
            self.tfuBaselineCropArea = math.max(self.tfuBaselineCropArea, remainingArea or 0);
        end;
        return self.tfuBaselineCropArea;
    end;

    local baseline = nil;
    if preferCurrentArea and scanValid and type(remainingArea) == "number" and remainingArea > 0 then
        baseline = remainingArea;
    else
        baseline = tfuDeriveBaselineFromExpected(self, scanValid and remainingArea or nil, scanValid and polygonArea or nil);
        if baseline == nil and scanValid and type(remainingArea) == "number" and remainingArea > 0 then
            baseline = remainingArea;
        end;
    end;

    if type(baseline) == "number" and baseline > 0 then
        self.tfuBaselineCropArea = baseline;
        self.tfuProgressDataVersion = ChaffMission.TFU_PROGRESS_DATA_VERSION;
        return baseline;
    end;
    return nil;
end;

local function tfuCalculateFieldCompletion(self, force)
    if not self.isServer then
        local synced = self.tfuSyncedFieldCompletion;
        if type(synced) == "number" then
            self.fieldPercentageDone = tfuClamp(synced, 0, 1);
            return self.fieldPercentageDone;
        end;
        return tfuClamp(self.fieldPercentageDone or 0, 0, 1);
    end;

    local remainingArea, polygonArea, scanValid = tfuScanRemainingCrop(self, force == true);
    local baseline = self.tfuBaselineCropArea;
    if type(baseline) ~= "number" or baseline <= 0 then
        baseline = tfuEnsureBaseline(self, false);
    end;

    local rawCompletion = self.tfuFieldCompletion or 0;
    if scanValid and type(baseline) == "number" and baseline > 0 then
        baseline = math.max(baseline, remainingArea or 0);
        self.tfuBaselineCropArea = baseline;
        rawCompletion = tfuClamp((baseline - math.max(remainingArea or 0, 0)) / baseline, 0, 1);

        -- Density-map edge pixels can leave a tiny amount after a visually
        -- complete field. Treat less than 0.1% (minimum four pixels) as done.
        local residualTolerance = math.max(4, baseline * 0.001);
        if (remainingArea or 0) <= residualTolerance then
            if tfuCanTreatZeroRemainingAsComplete(self, residualTolerance) then
                rawCompletion = 1;
            else
                -- The zero result is ambiguous (for example growth moved out of
                -- the forage range). Preserve the last confirmed progress.
                rawCompletion = self.tfuFieldCompletion or 0;
                if not self.tfuUnsafeZeroRemainingLogged then
                    self.tfuUnsafeZeroRemainingLogged = true;
                    Logging.warning(
                    "Additional Contracts | TFU chaff zero-remaining guard kept previous progress: mission=%s field=%s fruit='%s' allFruit=%.0f cutState=%.0f",
                    tfuGetMissionId(self),
                    tfuGetFieldId(self),
                    tfuGetFruitName(self),
                    self.tfuAllFruitArea or 0,
                    self.tfuCutStateArea or 0
                    );
                end;
            end;
        else
            self.tfuUnsafeZeroRemainingLogged = false;
        end;
    end;

    -- Completion must never regress because of growth-map refreshes, Precision
    -- Farming multipliers, or a temporary failed scan. A failed scan never
    -- turns an unknown zero area into completed work.
    rawCompletion = math.max(rawCompletion, self.tfuFieldCompletion or 0, tfuInferRawFieldCompletion(self));
    self.tfuFieldCompletion = tfuClamp(rawCompletion, 0, 1);
    self.fieldPercentageDone = self.tfuFieldCompletion;
    self.tfuSyncedFieldCompletion = self.tfuFieldCompletion;
    return self.tfuFieldCompletion;
end;

local function tfuGetAuthoritativeCompletion(self)
    if self.status == MissionStatus.CREATED or self.status == MissionStatus.PREPARING then
        return tfuClamp(self.completion or 0, 0, 1);
    end;

    local deliveryCompletion = 0;
    if type(self.expectedLiters) == "number" and self.expectedLiters > 0 then
        deliveryCompletion = tfuClamp(
            (self.depositedLiters or 0) / self.expectedLiters / ChaffMission.data.ownTable.SUCCESS_FACTOR,
            0,
            1
        );
    end;
    local fieldThreshold = type(AbstractMission.SUCCESS_FACTOR) == "number" and AbstractMission.SUCCESS_FACTOR or 0.95;
    local fieldCompletion = tfuCalculateFieldCompletion(self, false) / math.max(fieldThreshold, 0.0001);
    return tfuClamp(0.8 * math.min(fieldCompletion, 1) + 0.2 * deliveryCompletion, 0, 1);
end;

local function tfuWriteProgressState(streamId, self, synchronizeStation)
    if streamWriteBool(streamId, synchronizeStation == true and self.sellingStation ~= nil) then
        NetworkUtil.writeNodeObject(streamId, self.sellingStation);
    end;
    streamWriteFloat32(streamId, self.expectedLiters or 0);
    streamWriteFloat32(streamId, self.depositedLiters or 0);
    streamWriteFloat32(streamId, self.completion or self.tfuAuthoritativeCompletion or 0);
    streamWriteFloat32(streamId, self.tfuFieldCompletion or self.fieldPercentageDone or 0);
    streamWriteFloat32(streamId, self.tfuBaselineCropArea or 0);
    streamWriteUInt8(streamId, self.tfuTargetGrowthState or 0);
end;

local function tfuReadProgressState(streamId, self)
    if streamReadBool(streamId) then
        local synchronizedStation = NetworkUtil.readNodeObject(streamId);
        -- Do not clear the original pending station when a node object is not
        -- available yet. The base mission stream resolves that case by ID.
        if synchronizedStation ~= nil and synchronizedStation ~= self.sellingStation then
            tfuReplaceSellingStation(self, synchronizedStation);
        end;
    end;
    self.expectedLiters = streamReadFloat32(streamId);
    self.depositedLiters = streamReadFloat32(streamId);
    self.tfuSyncedCompletion = tfuClamp(streamReadFloat32(streamId), 0, 1);
    self.tfuSyncedFieldCompletion = tfuClamp(streamReadFloat32(streamId), 0, 1);
    self.tfuBaselineCropArea = math.max(streamReadFloat32(streamId), 0);
    self.tfuTargetGrowthState = streamReadUInt8(streamId);
    self.completion = self.tfuSyncedCompletion;
    self.fieldPercentageDone = self.tfuSyncedFieldCompletion;
end;

-- Savegame schema -----------------------------------------------------------
local tfuPreviousRegisterSavegameXMLPaths = ChaffMission.registerSavegameXMLPaths;
function ChaffMission.registerSavegameXMLPaths(schema, key)
    tfuPreviousRegisterSavegameXMLPaths(schema, key);
    local harvestKey = string.format("%s.harvest", key);
    schema:register(XMLValueType.INT, harvestKey .. "#tfuProgressVersion", "TFU chaff progress data version");
    schema:register(XMLValueType.FLOAT, harvestKey .. "#tfuBaselineCropArea", "Initial forage-ready crop area in density pixels");
    schema:register(XMLValueType.FLOAT, harvestKey .. "#tfuHarvestScaleMultiplier", "Harvest scale multiplier used for baseline recovery");
    schema:register(XMLValueType.INT, harvestKey .. "#tfuTargetGrowthState", "Growth state recorded at contract start");
    schema:register(XMLValueType.FLOAT, harvestKey .. "#tfuFieldCompletion", "Last authoritative raw field completion");
end;

-- Mission construction ------------------------------------------------------
local tfuPreviousNew = ChaffMission.new;
function ChaffMission.new(isServer, isClient, customMt)
    local self = tfuPreviousNew(isServer, isClient, customMt);
    self.tfuIsDeliveryMission = true;
    self.tfuProgressDataVersion = ChaffMission.TFU_PROGRESS_DATA_VERSION;
    self.tfuBaselineCropArea = 0;
    self.tfuHarvestScaleMultiplier = 0;
    self.tfuRemainingCropArea = 0;
    self.tfuPolygonPixelArea = 0;
    self.tfuFieldCompletion = 0;
    self.tfuSyncedFieldCompletion = nil;
    self.tfuSyncedCompletion = nil;
    self.tfuNextProgressScanTime = 0;
    self.tfuNextProgressLogTime = 0;
    self.tfuNextProgressSyncTime = 0;
    self.tfuNextDestinationAuditTime = 0;
    self.tfuProgressScanValid = false;
    return self;
end;

-- Network synchronization --------------------------------------------------
local tfuPreviousWriteStream = ChaffMission.writeStream;
function ChaffMission:writeStream(streamId, connection)
    tfuPreviousWriteStream(self, streamId, connection);
    tfuWriteProgressState(streamId, self, false);
end;

local tfuPreviousReadStream = ChaffMission.readStream;
function ChaffMission:readStream(streamId, connection)
    tfuPreviousReadStream(self, streamId, connection);
    tfuReadProgressState(streamId, self);
end;

function ChaffMission:writeUpdateStream(streamId, connection, dirtyMask)
    ChaffMission:superClass().writeUpdateStream(self, streamId, connection, dirtyMask);
    if tfuConnectionIsServer(connection) ~= true then
        tfuWriteProgressState(streamId, self, true);
    end;
end;

function ChaffMission:readUpdateStream(streamId, timestamp, connection)
    ChaffMission:superClass().readUpdateStream(self, streamId, timestamp, connection);
    if tfuConnectionIsServer(connection) ~= false then
        tfuReadProgressState(streamId, self);
    end;
end;

-- Save/load persistence ----------------------------------------------------
local tfuPreviousSaveToXMLFile = ChaffMission.saveToXMLFile;
function ChaffMission:saveToXMLFile(xmlFile, key)
    tfuPreviousSaveToXMLFile(self, xmlFile, key);
    local harvestKey = string.format("%s.harvest", key);
    xmlFile:setValue(harvestKey .. "#tfuProgressVersion", self.tfuProgressDataVersion or ChaffMission.TFU_PROGRESS_DATA_VERSION);
    xmlFile:setValue(harvestKey .. "#tfuBaselineCropArea", self.tfuBaselineCropArea or 0);
    xmlFile:setValue(harvestKey .. "#tfuHarvestScaleMultiplier", self.tfuHarvestScaleMultiplier or 1);
    xmlFile:setValue(harvestKey .. "#tfuTargetGrowthState", self.tfuTargetGrowthState or 0);
    xmlFile:setValue(harvestKey .. "#tfuFieldCompletion", self.tfuFieldCompletion or self.fieldPercentageDone or 0);
end;

local tfuPreviousLoadFromXMLFile = ChaffMission.loadFromXMLFile;
function ChaffMission:loadFromXMLFile(xmlFile, key)
    if not tfuPreviousLoadFromXMLFile(self, xmlFile, key) then
        return false;
    end;
    local harvestKey = string.format("%s.harvest", key);
    self.tfuProgressDataVersion = xmlFile:getValue(harvestKey .. "#tfuProgressVersion", 0);
    self.tfuBaselineCropArea = xmlFile:getValue(harvestKey .. "#tfuBaselineCropArea", 0);
    self.tfuHarvestScaleMultiplier = xmlFile:getValue(harvestKey .. "#tfuHarvestScaleMultiplier", 0);
    self.tfuTargetGrowthState = xmlFile:getValue(harvestKey .. "#tfuTargetGrowthState", 0);
    self.tfuFieldCompletion = tfuClamp(xmlFile:getValue(harvestKey .. "#tfuFieldCompletion", 0), 0, 1);
    self.tfuSyncedFieldCompletion = self.tfuFieldCompletion;
    return true;
end;

-- Progress calculation -----------------------------------------------------
function ChaffMission:createModifier()
    tfuCreateProgressModifier(self);
end;

function ChaffMission:getPartitionCompletion(partitionIndex)
    -- Kept return-safe for any third-party code that calls this method. TFU5's
    -- getFieldCompletion performs the authoritative full-polygon scan.
    return 0, 0, 0;
end;

function ChaffMission:getFieldCompletion()
    return tfuCalculateFieldCompletion(self, false);
end;

function ChaffMission:getCompletion()
    if not self.isServer and type(self.tfuSyncedCompletion) == "number" then
        return self.tfuSyncedCompletion;
    end;
    local completion = tfuGetAuthoritativeCompletion(self);
    self.tfuAuthoritativeCompletion = completion;
    return completion;
end;

local tfuPreviousFinishedPreparing = ChaffMission.finishedPreparing;
function ChaffMission:finishedPreparing()
    tfuPreviousFinishedPreparing(self);
    if self.isServer and not self:getIsFinished() then
        tfuGetHarvestScaleMultiplier(self);
        local baseline = tfuEnsureBaseline(self, true);
        if baseline ~= nil then
            Logging.info(
                "Additional Contracts | TFU chaff baseline captured: mission=%s field=%s fruit='%s' area=%.0f expectedLiters=%.1f",
                tfuGetMissionId(self),
                tfuGetFieldId(self),
                tfuGetFruitName(self),
                baseline,
                self.expectedLiters or 0
            );
        end;
    end;
end;

-- Safe destination selection -----------------------------------------------
local tfuPreviousStart = ChaffMission.start;
function ChaffMission:start(spawnVehicles)
    if self.isServer and not tfuIsStationNeutral(self.sellingStation) then
        local oldOwner = tfuGetOwnerFarmId(self.sellingStation);
        local safeStation, highestPrice = tfuFindSafeSellingStation(self.fillTypeIndex or FillType.CHAFF);
        if safeStation == nil then
            Logging.error(
                "Additional Contracts | Chaff mission start blocked: mission=%s field=%s acceptingFarm=%s destinationOwner=%s; no neutral/NPC mission selling station accepts CHAFF",
                tfuGetMissionId(self),
                tfuGetFieldId(self),
                tostring(self.farmId),
                tostring(oldOwner)
            );
            return false;
        end;
        tfuReplaceSellingStation(self, safeStation);
        self.highestPrice = highestPrice;
        if self.missionDirtyFlag ~= nil then
            self:raiseDirtyFlags(self.missionDirtyFlag);
        end;
        Logging.info(
            "Additional Contracts | Chaff destination reassigned for safety: mission=%s field=%s acceptingFarm=%s oldOwner=%s newOwner=%s station='%s'",
            tfuGetMissionId(self),
            tfuGetFieldId(self),
            tostring(self.farmId),
            tostring(oldOwner),
            tostring(tfuGetOwnerFarmId(safeStation)),
            tostring(safeStation.getName ~= nil and safeStation:getName() or "unknown")
        );
    end;

    tfuGetHarvestScaleMultiplier(self);
    if type(self.tfuBaselineCropArea) ~= "number" or self.tfuBaselineCropArea <= 0 then
        tfuEnsureBaseline(self, true);
    end;

    return tfuPreviousStart(self, spawnVehicles);
end;

local tfuPreviousOnSavegameLoaded = ChaffMission.onSavegameLoaded;
function ChaffMission:onSavegameLoaded()
    tfuPreviousOnSavegameLoaded(self);
    if not self.isServer or self.field == nil or self:getIsFinished() then
        return;
    end;
    if self.sellingStation == nil then
        -- The compatibility loader may already have marked the mission for
        -- deletion because its destination no longer exists.
        return;
    end;

    local wasStarted = self.getWasStarted ~= nil and self:getWasStarted();
    if not tfuIsStationNeutral(self.sellingStation) then
        local oldOwner = tfuGetOwnerFarmId(self.sellingStation);
        local safeStation, highestPrice = tfuFindSafeSellingStation(self.fillTypeIndex or FillType.CHAFF);
        if safeStation == nil then
            if self.sellingStation ~= nil then
                self.sellingStation.missions = self.sellingStation.missions or {};
                if wasStarted then self.sellingStation.missions[self] = self;else self.sellingStation.missions[self] = nil;end;
            end;
            self.tfuDestinationBlocked = true;
            Logging.warning(
                "Additional Contracts | Chaff mission destination blocked: mission=%s field=%s running=%s destinationOwner=%s; no neutral/NPC bulk mission selling station accepts CHAFF",
                tfuGetMissionId(self),
                tfuGetFieldId(self),
                tostring(wasStarted),
                tostring(oldOwner)
            );
            if not wasStarted then g_missionManager:markMissionForDeletion(self);end;
            return;
        end;
        tfuReplaceSellingStation(self, safeStation);
        self.highestPrice = highestPrice;
        self.tfuDestinationBlocked = false;
        Logging.info(
            "Additional Contracts | Chaff destination migrated to neutral station: mission=%s field=%s running=%s oldOwner=%s station='%s'",
            tfuGetMissionId(self),
            tfuGetFieldId(self),
            tostring(wasStarted),
            tostring(oldOwner),
            tostring(safeStation.getName ~= nil and safeStation:getName() or "unknown")
        );
    end;

    tfuCreateProgressModifier(self);
    self.tfuFieldCompletion = math.max(self.tfuFieldCompletion or 0, tfuInferRawFieldCompletion(self));
    local baseline = tfuEnsureBaseline(self, false);
    local fieldCompletion = tfuCalculateFieldCompletion(self, true);
    local completion = tfuGetAuthoritativeCompletion(self);
    self.completion = completion;
    self.tfuSyncedCompletion = completion;

    if self.sellingStation ~= nil
        and type(self.expectedLiters) == "number"
        and self.expectedLiters > 0
        and (self.depositedLiters or 0) >= self.expectedLiters * ChaffMission.data.ownTable.SUCCESS_FACTOR then
        self.sellingStation.missions[self] = nil;
    end;

    Logging.info(
        "Additional Contracts | TFU chaff progress restored: mission=%s field=%s fruit='%s' baseline=%.0f remaining=%.0f field=%.4f delivered=%.1f/%.1f total=%.4f",
        tfuGetMissionId(self),
        tfuGetFieldId(self),
        tfuGetFruitName(self),
        baseline or 0,
        self.tfuRemainingCropArea or 0,
        fieldCompletion,
        self.depositedLiters or 0,
        self.expectedLiters or 0,
        completion
    );
end;

function ChaffMission:acceptsMissionFillType(fillTypeIndex)
    return self.fillTypeIndex ~= nil and self.fillTypeIndex == fillTypeIndex;
end;

function ChaffMission:getDeliveryRequirements()
    return {requireBulk=true};
end;

function ChaffMission:getTFUDeliveryRequirements()
    return {requireBulk=true};
end;

function ChaffMission:isDeliveryDestinationSafe(station, fillTypeIndex)
    local targetStation = station or self.sellingStation;
    local targetFillType = fillTypeIndex or self.fillTypeIndex or FillType.CHAFF;
    if g_additionalContractMapData ~= nil and g_additionalContractMapData.isMissionSafeSellingStation ~= nil then
        return g_additionalContractMapData:isMissionSafeSellingStation(targetStation, targetFillType, false, {requireBulk=true});
    end;
    return tfuIsStationNeutral(targetStation);
end;

function ChaffMission:isTFUDestinationSafe(station)
    return self:isDeliveryDestinationSafe(station, self.fillTypeIndex or FillType.CHAFF);
end;

function ChaffMission:getDeliverySuccessFactor()
    return ChaffMission.data.ownTable.SUCCESS_FACTOR;
end;

function ChaffMission:fillSold(fillDelta, fillTypeIndex)
    if self.status ~= MissionStatus.RUNNING then return 0;end;
    if self.tfuDestinationBlocked == true or not tfuIsStationNeutral(self.sellingStation) then return 0;end;
    if type(fillDelta) ~= "number" or fillDelta <= 0 then return 0;end;
    if fillTypeIndex ~= nil and self.fillTypeIndex ~= nil and fillTypeIndex ~= self.fillTypeIndex then return 0;end;

    local expectedLiters = math.max(self.expectedLiters or 0, 0);
    if expectedLiters <= 0 then return 0;end;
    local before = math.max(self.depositedLiters or 0, 0);
    self.depositedLiters = math.min(before + fillDelta, expectedLiters);
    local credited = self.depositedLiters - before;
    if credited <= 0 then return 0;end;

    -- Delivery success is mission-specific (90%). It must not depend on the
    -- configurable global field-completion threshold.
    local requiredLiters = expectedLiters * ChaffMission.data.ownTable.SUCCESS_FACTOR;
    if self.sellingStation ~= nil then
        self.sellingStation.missions = self.sellingStation.missions or {};
        if self.depositedLiters >= requiredLiters then
            self.sellingStation.missions[self] = nil;
        else
            self.sellingStation.missions[self] = self;
        end;
    end;

    self.lastSellChange = 30;
    if self.isServer then
        self.tfuNextProgressScanTime = 0;
        self.tfuAuthoritativeCompletion = tfuGetAuthoritativeCompletion(self);
        self.completion = self.tfuAuthoritativeCompletion;
        self.tfuSyncedCompletion = self.completion;
        if self.missionDirtyFlag ~= nil then self:raiseDirtyFlags(self.missionDirtyFlag);end;
        Logging.info(
            "Additional Contracts | TFU chaff delivery credited: mission=%s field=%s delta=%.1f deposited=%.1f/%.1f required=%.1f total=%.4f",
            tfuGetMissionId(self),
            tfuGetFieldId(self),
            credited,
            self.depositedLiters,
            expectedLiters,
            requiredLiters,
            self.completion
        );
    end;
    return credited;
end;

-- Server heartbeat, auto-finish and diagnostics ----------------------------
local tfuPreviousUpdate = ChaffMission.update;
function ChaffMission:update(dt)
    tfuPreviousUpdate(self, dt);
    if not self.isServer or self.status ~= MissionStatus.RUNNING then
        return;
    end;

    local completion = tfuGetAuthoritativeCompletion(self);
    self.completion = completion;
    self.tfuSyncedCompletion = completion;

    local now = tfuGetTimeMs();
    if now >= (self.tfuNextDestinationAuditTime or 0) then
        self.tfuNextDestinationAuditTime = now + 5000;
        if not tfuIsStationNeutral(self.sellingStation) then
            local oldOwner = tfuGetOwnerFarmId(self.sellingStation);
            local safeStation, highestPrice = tfuFindSafeSellingStation(self.fillTypeIndex or FillType.CHAFF);
            if safeStation ~= nil then
                tfuReplaceSellingStation(self, safeStation);
                self.highestPrice = highestPrice;
                self.tfuDestinationBlocked = false;
                Logging.info(
                    "Additional Contracts | Chaff runtime destination migrated: mission=%s field=%s oldOwner=%s station='%s'",
                    tfuGetMissionId(self),
                    tfuGetFieldId(self),
                    tostring(oldOwner),
                    tostring(safeStation.getName ~= nil and safeStation:getName() or "unknown")
                );
            else
                if self.sellingStation ~= nil then
                    self.sellingStation.missions = self.sellingStation.missions or {};
                    self.sellingStation.missions[self] = self;
                end;
                self.tfuDestinationBlocked = true;
            end;
        end;
    end;
    if now >= (self.tfuNextProgressSyncTime or 0) then
        self.tfuNextProgressSyncTime = now + ChaffMission.TFU_PROGRESS_SYNC_INTERVAL_MS;
        if self.missionDirtyFlag ~= nil then
            self:raiseDirtyFlags(self.missionDirtyFlag);
        end;
    end;

    if now >= (self.tfuNextProgressLogTime or 0) then
        self.tfuNextProgressLogTime = now + ChaffMission.TFU_PROGRESS_LOG_INTERVAL_MS;
        Logging.info(
            "Additional Contracts | TFU chaff progress: mission=%s field=%s fruit='%s' baseline=%.0f remaining=%.0f allFruit=%.0f cutState=%.0f rawField=%.4f delivered=%.1f/%.1f total=%.4f",
            tfuGetMissionId(self),
            tfuGetFieldId(self),
            tfuGetFruitName(self),
            self.tfuBaselineCropArea or 0,
            self.tfuRemainingCropArea or 0,
            self.tfuAllFruitArea or 0,
            self.tfuCutStateArea or 0,
            self.tfuFieldCompletion or 0,
            self.depositedLiters or 0,
            self.expectedLiters or 0,
            completion
        );
    end;

    if completion >= ChaffMission.TFU_PROGRESS_FINISH_VALUE and self.status == MissionStatus.RUNNING then
        Logging.info(
            "Additional Contracts | TFU chaff mission reached authoritative completion and is finishing: mission=%s field=%s completion=%.4f",
            tfuGetMissionId(self),
            tfuGetFieldId(self),
            completion
        );
        self:finish(MissionFinishState.SUCCESS);
    end;
end;

Logging.info("Additional Contracts | Installed TFU chaff progress/sync/destination safety fix v%s", tostring(ChaffMission.TFU_PROGRESS_FIX_VERSION));
