What the work is
A copy of production is raised on staging - the whole database, not a
sample, because it always breaks on the row nobody expected. The upgrade
runs there first, and the list of breakages is fixed in modules, not by
patching data.
Then the same run is repeated until it is boring. Only after that does the
live system move, on an evening chosen by you, with the old version kept
ready to come back to.
Where the cost comes from
Not from the number of versions between you and the target. From how much
of what you built lives in code someone can read: modules with a git
history are cheap to carry over, clicked-together automations are not.
A rough answer takes one call with the module list in front of us. A firm
one takes the first staging run, and that run is the estimate.
What you end up with
The same system on a supported version, with your data, your customisations
and a written list of what changed. Plus the staging environment, which
stays - the next upgrade starts from there rather than from scratch.
If the honest answer is that the migration is not worth it yet, you will
hear that on the first call rather than after the first invoice.