You _can_ turn this kind of scary "basement" code into something that's maintainable, but it's often a multiyear slog. I've salvaged C++ horrors that were virtually all basement code, intricate and fragile and dating back to Win16 code written in 1993.
Here's a recipe that may help you get started on your adventure:
1. Work incrementally. Never try to fix the whole program at once. Overhauling the whole program inevitably introduces lots of disruption for users and delays the addition of new features. This will not be appreciated.
2. Make sure you have tests. Lots and lots of tests. Any time you touch a module, write some tests for that module.
3. Build better module boundaries. This can be a mechanical process. For example, to separate disk I/O code out of a checkout register, declare a RegisterIO class. Every time the GUI code touches the disk, add a new method to the RegisterIO class and move the code there. You'll wind up with a _really_ ugly RegisterIO class containing a lot of ad hoc IO functions. But the RegisterIO class will improve with time.
4. Reimplement small modules one at a time. You may want to run the old and new versions of a module in parallel for a while (I once had a program with two different text rendering subsystems), or run the same unit tests against both the old the new module.
Eight years of work later, you'll have a reasonably shiny new program with some odd architectural decisions and a really ancient logging subsystem.:-) But at least you will have been shipping new versions and earning revenue every step of the way.
Michael Feathers's _Working Effectively with Legacy Code_ is entirely about dealing with this sort of code, especially on carefully retrofitting tests so you can safely do deeper restructuring.
Here's a recipe that may help you get started on your adventure:
1. Work incrementally. Never try to fix the whole program at once. Overhauling the whole program inevitably introduces lots of disruption for users and delays the addition of new features. This will not be appreciated.
2. Make sure you have tests. Lots and lots of tests. Any time you touch a module, write some tests for that module.
3. Build better module boundaries. This can be a mechanical process. For example, to separate disk I/O code out of a checkout register, declare a RegisterIO class. Every time the GUI code touches the disk, add a new method to the RegisterIO class and move the code there. You'll wind up with a _really_ ugly RegisterIO class containing a lot of ad hoc IO functions. But the RegisterIO class will improve with time.
4. Reimplement small modules one at a time. You may want to run the old and new versions of a module in parallel for a while (I once had a program with two different text rendering subsystems), or run the same unit tests against both the old the new module.
Eight years of work later, you'll have a reasonably shiny new program with some odd architectural decisions and a really ancient logging subsystem.:-) But at least you will have been shipping new versions and earning revenue every step of the way.