class CoreStateDownloader {

    boolean downloadSnapshot( CatchupAddressProvider addressProvider )
            throws LifecycleException, IOException, DatabaseShutdownException
    {
        /* Extract some key properties before shutting it down. */
        boolean isEmptyStore = localDatabase.isEmpty();

        /*
         *  There is no reason to try to recover if there are no transaction logs and in fact it is
         *  also problematic for the initial transaction pull during the snapshot download because the
         *  kernel will create a transaction log with a header where previous index points to the same
         *  index as that written down into the metadata store. This is problematic because we have no
         *  guarantee that there are later transactions and we need at least one transaction in
         *  the log to figure out the Raft log index (see {@link RecoverConsensusLogIndex}).
         */
        if ( commitStateHelper.hasTxLogs( localDatabase.databaseLayout() ) )
        {
            log.info( "Recovering local database" );
            ensure( localDatabase::start, "start local database" );
            ensure( localDatabase::stop, "stop local database" );
        }

        AdvertisedSocketAddress primary;
        StoreId remoteStoreId;
        try
        {
            primary = addressProvider.primary();
            remoteStoreId = remoteStore.getStoreId( primary );
        }
        catch ( CatchupAddressResolutionException | StoreIdDownloadFailedException e )
        {
            log.warn( "Store copy failed", e );
            return false;
        }

        if ( !isEmptyStore && !remoteStoreId.equals( localDatabase.storeId() ) )
        {
            log.error( "Store copy failed due to store ID mismatch" );
            return false;
        }

        ensure( suspendOnStoreCopy::disable, "disable auxiliary services before store copy" );
        ensure( localDatabase::stopForStoreCopy, "stop local database for store copy" );

        log.info( "Downloading snapshot from core server at %s", primary );

        /* The core snapshot must be copied before the store, because the store has a dependency on
         * the state of the state machines. The store will thus be at or ahead of the state machines,
         * in consensus log index, and application of commands will bring them in sync. Any such commands
         * that carry transactions will thus be ignored by the transaction/token state machines, since they
         * are ahead, and the correct decisions for their applicability have already been taken as encapsulated
         * in the copied store. */

        CoreSnapshot coreSnapshot;
        try
        {
            coreSnapshot = catchUpClient.makeBlockingRequest( primary, new CoreSnapshotRequest(),
                    new CatchUpResponseAdaptor<CoreSnapshot>()
                    {
                        @Override
                        public void onCoreSnapshot( CompletableFuture<CoreSnapshot> signal, CoreSnapshot response )
                        {
                            signal.complete( response );
                        }
                    } );
        }
        catch ( CatchUpClientException e )
        {
            log.warn( "Store copy failed", e );
            return false;
        }

        if ( !isEmptyStore )
        {
            StoreId localStoreId = localDatabase.storeId();
            CatchupResult catchupResult;
            try
            {
                catchupResult = remoteStore.tryCatchingUp( primary, localStoreId, localDatabase.databaseLayout(), false, false );
            }
            catch ( StoreCopyFailedException e )
            {
                log.warn( "Failed to catch up", e );
                return false;
            }

            if ( catchupResult == E_TRANSACTION_PRUNED )
            {
                log.warn( format( "Failed to pull transactions from (%s). They may have been pruned away", primary ) );
                localDatabase.delete();
                isEmptyStore = true;
            }
            else if ( catchupResult != SUCCESS_END_OF_STREAM )
            {
                log.warn( format( "Unexpected catchup operation result %s from %s", catchupResult, primary ) );
                return false;
            }
        }

        if ( isEmptyStore )
        {
            try
            {
                storeCopyProcess.replaceWithStoreFrom( addressProvider, remoteStoreId );
            }
            catch ( StoreCopyFailedException e )
            {
                log.warn( "Failed to copy and replace store", e );
                return false;
            }
        }

        /* We install the snapshot after the store has been downloaded,
         * so that we are not left with a state ahead of the store. */
        snapshotService.installSnapshot( coreSnapshot );
        log.info( "Core snapshot installed: " + coreSnapshot );

        /* Starting the database will invoke the commit process factory in
         * the EnterpriseCoreEditionModule, which has important side-effects. */
        log.info( "Starting local database" );
        ensure( localDatabase::start, "start local database after store copy" );

        coreStateMachines.installCommitProcess( localDatabase.getCommitProcess() );
        ensure( suspendOnStoreCopy::enable, "enable auxiliary services after store copy" );

        return true;
    }

    public  CoreStateDownloader( LocalDatabase localDatabase, Suspendable suspendOnStoreCopy, RemoteStore remoteStore,
                                CatchUpClient catchUpClient, LogProvider logProvider, StoreCopyProcess storeCopyProcess,
                                CoreStateMachines coreStateMachines, CoreSnapshotService snapshotService,
                                CommitStateHelper commitStateHelper );

}