DiseasePlayerLogic = {}

local function nz(v, d)
    if v == nil then
        return d
    end
    return v
end

function DiseasePlayerLogic:advanceFieldForNewDay(core, farmlandId, entry, field, defs, tuning)
    if core == nil or entry == nil or field == nil then
        return 0
    end

    local oldState = nz(entry.state, 0)
    if oldState <= 0 then
        return 0
    end

    local newState = math.min(oldState + 1, 3)
    local promoted = 0
    local threatId = defs and defs.getThreatId and defs:getThreatId(entry.threatKey) or 0

    if newState > oldState and DiseaseGrid ~= nil then
        promoted = DiseaseGrid:promoteThreatOnField(field, newState, threatId) or 0
        entry.state = newState
        entry.neglectDays = nz(entry.neglectDays, 0) + 1
        core:beginThreatStage(entry, newState, field)
        local profile = core:getFieldBehaviorProfile(field, tuning)
        local spreadSeeds = math.max(2, math.floor(
            (nz(tuning.escalationSpreadSpotsBase, 2) + nz(newState, 1) * nz(tuning.escalationSpreadSpotsPerLevel, 2))
                * nz(profile.spreadSeedsMultiplier, 1.0) * 1.35 + 0.5
        ))
        local spreadSpawn = math.max(2, math.floor(
            (nz(tuning.escalationSpawnNewSpotsBase, 2) + math.max(0, nz(newState, 1) - 1) * nz(tuning.escalationSpawnNewSpotsPerLevel, 1))
                * nz(profile.spreadSpawnMultiplier, 1.0) * 1.35 + 0.5
        ))
        core:queueThreatSpreadJob(
            field,
            farmlandId,
            entry,
            "daily stage reinforce",
            entry.stageHours,
            threatId,
            newState,
            spreadSeeds,
            spreadSpawn,
            nz(tuning.escalationSpreadRadiusMin, 4.5) * nz(tuning.escalationSpreadRadiusMultiplier, 2.0) * 1.15,
            nz(tuning.escalationSpreadRadiusMax, 7.0) * nz(tuning.escalationSpreadRadiusMultiplier, 2.0) * 1.15,
            nz(tuning.escalationSpawnOffsetMin, 6.0),
            nz(tuning.escalationSpawnOffsetMax, 14.0),
            nz(tuning.escalationSpawnRadiusMin, 2.5) * 1.10,
            nz(tuning.escalationSpawnRadiusMax, 5.0) * 1.10
        )
        core:debugLog("daily stage rise: farmland=%s oldState=%d newState=%d",
            tostring(farmlandId), oldState, nz(entry.state, 0))
    end

    return promoted
end

function DiseasePlayerLogic:advanceFieldTimers(core, farmlandId, entry, field, defs, tuning, deltaHours, currentHoursInDay, lastHoursInDay, currentDayToken)
    if core == nil or entry == nil or field == nil or defs == nil or deltaHours == nil or deltaHours <= 0 or DiseaseGrid == nil then
        return
    end

    local profile = core:getFieldBehaviorProfile(field, tuning)
    entry.stageHours = nz(entry.stageHours, 0) + deltaHours

    local threatId = defs and defs.getThreatId and defs:getThreatId(entry.threatKey) or 0
    if nz(entry.state, 0) < 3 and entry.stageHours >= nz(entry.stageExpandDelayHours, 3.0) then
        local spreadSeeds = math.max(1, math.floor(
            (nz(tuning.escalationSpreadSpotsBase, 2) + nz(entry.state, 1) * nz(tuning.escalationSpreadSpotsPerLevel, 2))
                * nz(profile.spreadSeedsMultiplier, 1.0) + 0.5
        ))
        local spreadSpawn = math.max(1, math.floor(
            (nz(tuning.escalationSpawnNewSpotsBase, 2) + math.max(0, nz(entry.state, 1) - 1) * nz(tuning.escalationSpawnNewSpotsPerLevel, 1))
                * nz(profile.spreadSpawnMultiplier, 1.0) + 0.5
        ))
        local queued = core:queueThreatSpreadJob(
            field,
            farmlandId,
            entry,
            "stage expanded",
            entry.stageHours,
            threatId,
            entry.state,
            spreadSeeds,
            spreadSpawn,
            nz(tuning.escalationSpreadRadiusMin, 4.5) * nz(tuning.escalationSpreadRadiusMultiplier, 2.0),
            nz(tuning.escalationSpreadRadiusMax, 7.0) * nz(tuning.escalationSpreadRadiusMultiplier, 2.0),
            nz(tuning.escalationSpawnOffsetMin, 6.0),
            nz(tuning.escalationSpawnOffsetMax, 14.0),
            nz(tuning.escalationSpawnRadiusMin, 2.5),
            nz(tuning.escalationSpawnRadiusMax, 5.0)
        )
        if queued then
            entry.stageExpanded = true
            entry.stageExpandDelayHours = nz(entry.stageHours, 0) + MathUtil.randomFloat(3.0, 6.0)
        else
            entry.stageExpandDelayHours = nz(entry.stageHours, 0) + MathUtil.randomFloat(0.5, 1.25)
            local retryReason = core:hasPendingFieldLayerJob(farmlandId) and "jobBusy" or "noSeeds"
            core:debugLog("stage expand retry scheduled: farmland=%s state=%d afterHours=%.2f reason=%s retryAt=%.2f",
                tostring(farmlandId), nz(entry.state, 0), nz(entry.stageHours, 0), retryReason, nz(entry.stageExpandDelayHours, 0))
        end
    end

    local currentState = nz(entry.state, 0)
    if currentState < 2 then
        return
    end

    if currentState >= 3 and nz(entry.stage3NextExpandHours, 0) <= 0 then
        entry.stage3NextExpandHours = MathUtil.randomFloat(
            nz(profile.stage3RepeatExpandHoursMin, 3.0),
            nz(profile.stage3RepeatExpandHoursMax, 5.0)
        )
    end

    if currentState >= 3 and entry.stageHours >= nz(entry.stage3NextExpandHours, 0) and nz(entry.stage3NextExpandHours, 0) > 0 then
        local spreadSeeds = math.max(1, math.floor(
            (nz(tuning.escalationSpreadSpotsBase, 2) + nz(entry.state, 1) * nz(tuning.escalationSpreadSpotsPerLevel, 2))
                * nz(profile.spreadSeedsMultiplier, 1.0) + 0.5
        ))
        local spreadSpawn = math.max(1, math.floor(
            (nz(tuning.escalationSpawnNewSpotsBase, 2) + math.max(0, nz(entry.state, 1) - 1) * nz(tuning.escalationSpawnNewSpotsPerLevel, 1))
                * nz(profile.spreadSpawnMultiplier, 1.0) + 0.5
        ))
        local queued = core:queueThreatSpreadJob(
            field,
            farmlandId,
            entry,
            "stage3 repeat spread",
            entry.stageHours,
            threatId,
            entry.state,
            spreadSeeds,
            spreadSpawn,
            nz(tuning.escalationSpreadRadiusMin, 4.5) * nz(tuning.escalationSpreadRadiusMultiplier, 2.0),
            nz(tuning.escalationSpreadRadiusMax, 7.0) * nz(tuning.escalationSpreadRadiusMultiplier, 2.0),
            nz(tuning.escalationSpawnOffsetMin, 6.0),
            nz(tuning.escalationSpawnOffsetMax, 14.0),
            nz(tuning.escalationSpawnRadiusMin, 2.5),
            nz(tuning.escalationSpawnRadiusMax, 5.0)
        )
        if queued then
            entry.stage3NextExpandHours = nz(entry.stageHours, 0) + MathUtil.randomFloat(2.8, 3.2)
        else
            entry.stage3NextExpandHours = nz(entry.stageHours, 0) + MathUtil.randomFloat(0.5, 1.2)
            local retryReason = core:hasPendingFieldLayerJob(farmlandId) and "jobBusy" or "noSeeds"
            core:debugLog("stage3 repeat spread retry: farmland=%s state=%d afterHours=%.2f reason=%s retryAt=%.2f",
                tostring(farmlandId), nz(entry.state, 0), nz(entry.stageHours, 0), retryReason, nz(entry.stage3NextExpandHours, 0))
        end
    end

    local defaultDamageTick = currentState >= 3
        and nz(profile.stage3DamageTickHours, 6.0)
        or nz(tuning.stage2DamageTickHours, 12.0)
    local tickHours = nz(entry.stage3DamageDelayHours, defaultDamageTick)
    if tickHours <= 0 then
        tickHours = defaultDamageTick
        entry.stage3DamageDelayHours = tickHours
    end

    local dayToken = currentDayToken or core:getCurrentDayToken()
    if entry.stage3DamageDay ~= dayToken then
        entry.stage3DamageDay = dayToken
        entry.stage3DamageHoursToday = 0
    end

    local elapsedToday = deltaHours
    if currentHoursInDay ~= nil and lastHoursInDay ~= nil and currentHoursInDay < lastHoursInDay then
        elapsedToday = currentHoursInDay
    end
    entry.stage3DamageHoursToday = nz(entry.stage3DamageHoursToday, 0) + math.max(0, elapsedToday or 0)

    if tickHours > 0 and entry.stage3DamageHoursToday >= tickHours then
        entry.stage3DamageHoursToday = entry.stage3DamageHoursToday - tickHours
        entry.damageReady = true
        core:applyTimedDamage(field, entry, threatId, farmlandId,
            currentState >= 3 and "stage3 damage tick" or "stage2 damage tick")
    end
end
