/*
 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#import "CKKSKeychainView.h"



#if OCTAGON
#import "CloudKitDependencies.h"
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
#endif

#import "CKKS.h"
#import "keychain/ckks/CKKSStates.h"
#import "OctagonAPSReceiver.h"
#import "CKKSIncomingQueueEntry.h"
#import "CKKSOutgoingQueueEntry.h"
#import "CKKSCurrentKeyPointer.h"
#import "CKKSKey.h"
#import "CKKSMirrorEntry.h"
#import "CKKSZoneStateEntry.h"
#import "CKKSItemEncrypter.h"
#import "CKKSIncomingQueueOperation.h"
#import "CKKSNewTLKOperation.h"
#import "CKKSProcessReceivedKeysOperation.h"
#import "CKKSFetchAllRecordZoneChangesOperation.h"
#import "keychain/ckks/CKKSHealKeyHierarchyOperation.h"
#import "CKKSReencryptOutgoingItemsOperation.h"
#import "CKKSScanLocalItemsOperation.h"
#import "CKKSSynchronizeOperation.h"
#import "CKKSRateLimiter.h"
#import "CKKSManifest.h"
#import "CKKSManifestLeafRecord.h"
#import "CKKSZoneChangeFetcher.h"
#import "CKKSAnalytics.h"
#import "keychain/analytics/CKKSLaunchSequence.h"
#import "keychain/ckks/CKKSCloudKitClassDependencies.h"
#import "keychain/ckks/CKKSDeviceStateEntry.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CKKSCurrentItemPointer.h"
#import "keychain/ckks/CKKSCreateCKZoneOperation.h"
#import "keychain/ckks/CKKSDeleteCKZoneOperation.h"
#import "keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h"
#import "keychain/ckks/CKKSUpdateDeviceStateOperation.h"
#import "keychain/ckks/CKKSNotifier.h"
#import "keychain/ckks/CloudKitCategories.h"
#import "keychain/ckks/CKKSTLKShareRecord.h"
#import "keychain/ckks/CKKSHealTLKSharesOperation.h"
#import "keychain/ckks/CKKSLocalSynchronizeOperation.h"
#import "keychain/ckks/CKKSPeerProvider.h"
#import "keychain/ckks/CKKSCheckKeyHierarchyOperation.h"
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/categories/NSError+UsefulConstructors.h"

#import "keychain/ckks/CKKSLocalResetOperation.h"

#import "keychain/ot/OTConstants.h"
#import "keychain/ot/OTDefines.h"
#import "keychain/ot/OctagonCKKSPeerAdapter.h"
#import "keychain/ot/ObjCImprovements.h"

#include <utilities/SecCFWrappers.h>
#include <utilities/SecTrace.h>
#include <utilities/SecDb.h>
#include "keychain/securityd/SecDbItem.h"
#include "keychain/securityd/SecItemDb.h"
#include "keychain/securityd/SecItemSchema.h"
#include "keychain/securityd/SecItemServer.h"
#include <Security/SecItemPriv.h>
#include "keychain/SecureObjectSync/SOSAccountTransaction.h"
#include <utilities/SecPLWrappers.h>
#include <os/transaction_private.h>

#import "keychain/trust/TrustedPeers/TPSyncingPolicy.h"
#import <Security/SecItemInternal.h>

#if OCTAGON

@interface CKKSKeychainView()

@property (readonly) Class<CKKSNotifier> notifierClass;

// Slows down all outgoing queue operations
@property CKKSNearFutureScheduler* outgoingQueueOperationScheduler;

@property CKKSResultOperation* processIncomingQueueAfterNextUnlockOperation;
@property CKKSResultOperation* resultsOfNextIncomingQueueOperationOperation;

// An extra queue for semaphore-waiting-based NSOperations
@property NSOperationQueue* waitingQueue;

// Scratch space for resyncs
@property (nullable) NSMutableSet<NSString*>* resyncRecordsSeen;



@property NSOperationQueue* operationQueue;
@property CKKSResultOperation* accountLoggedInDependency;
@property BOOL halted;

// Make these readwrite
@property NSArray<CKKSPeerProviderState*>* currentTrustStates;

@property NSMutableSet<CKKSFetchBecause*>* currentFetchReasons;
@end
#endif

@implementation CKKSKeychainView
#if OCTAGON

- (instancetype)initWithContainer:(CKContainer*)container
                         zoneName:(NSString*)zoneName
                   accountTracker:(CKKSAccountStateTracker*)accountTracker
                 lockStateTracker:(CKKSLockStateTracker*)lockStateTracker
              reachabilityTracker:(CKKSReachabilityTracker*)reachabilityTracker
                    changeFetcher:(CKKSZoneChangeFetcher*)fetcher
                     zoneModifier:(CKKSZoneModifier*)zoneModifier
                 savedTLKNotifier:(CKKSNearFutureScheduler*)savedTLKNotifier
        cloudKitClassDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies
{

    if((self = [super init])) {
        WEAKIFY(self);

        _container = container;
        _zoneName = zoneName;
        _accountTracker = accountTracker;
        _reachabilityTracker = reachabilityTracker;
        _cloudKitClassDependencies = cloudKitClassDependencies;

        _halted = NO;

        _database = [_container privateCloudDatabase];
        _zoneID = [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName];

        _accountStatus = CKKSAccountStatusUnknown;
        _accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in."];

        _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
        _operationQueue = [[NSOperationQueue alloc] init];


        _loggedIn = [[CKKSCondition alloc] init];
        _loggedOut = [[CKKSCondition alloc] init];
        _accountStateKnown = [[CKKSCondition alloc] init];

        _initiatedLocalScan = NO;

        _trustStatus = CKKSAccountStatusUnknown;

        _incomingQueueOperations = [NSHashTable weakObjectsHashTable];
        _outgoingQueueOperations = [NSHashTable weakObjectsHashTable];
        _scanLocalItemsOperations = [NSHashTable weakObjectsHashTable];

        _currentTrustStates = @[];

        _currentFetchReasons = [NSMutableSet set];

        _launch = [[CKKSLaunchSequence alloc] initWithRocketName:@"com.apple.security.ckks.launch"];
        [_launch addAttribute:@"view" value:zoneName];

        _zoneChangeFetcher = fetcher;
        [fetcher registerClient:self];

        _resyncRecordsSeen = nil;

        _notifierClass = cloudKitClassDependencies.notifierClass;
        _notifyViewChangedScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-notify-scheduler", self.zoneName]
                                                            initialDelay:250*NSEC_PER_MSEC
                                                         continuingDelay:1*NSEC_PER_SEC
                                                        keepProcessAlive:true
                                                          dependencyDescriptionCode:CKKSResultDescriptionPendingViewChangedScheduling
                                                                   block:^{
                                                                       STRONGIFY(self);
                                                                       [self.notifierClass post:[NSString stringWithFormat:@"com.apple.security.view-change.%@", self.zoneName]];
                                                                       [self.notifierClass post:[NSString stringWithUTF8String:kSecServerKeychainChangedNotification]];


                                                                       // Ugly, but: the Manatee and Engram views need to send a fake 'PCS' view change.
                                                                       // TODO: make this data-driven somehow
                                                                       if([self.zoneName isEqualToString:@"Manatee"] ||
                                                                          [self.zoneName isEqualToString:@"Engram"] ||
                                                                          [self.zoneName isEqualToString:@"ApplePay"] ||
                                                                          [self.zoneName isEqualToString:@"Home"] ||
                                                                          [self.zoneName isEqualToString:@"LimitedPeersAllowed"]) {
                                                                           [self.notifierClass post:@"com.apple.security.view-change.PCS"];
                                                                       }
                                                                   }];

        _notifyViewReadyScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-ready-scheduler", self.zoneName]
                                                                       initialDelay:250*NSEC_PER_MSEC
                                                                    continuingDelay:120*NSEC_PER_SEC
                                                                   keepProcessAlive:true
                                                          dependencyDescriptionCode:CKKSResultDescriptionPendingViewChangedScheduling
                                                                              block:^{
                                                                                  STRONGIFY(self);
                                                                                  NSDistributedNotificationCenter *center = [self.cloudKitClassDependencies.nsdistributednotificationCenterClass defaultCenter];

                                                                                  [center postNotificationName:@"com.apple.security.view-become-ready"
                                                                                                        object:nil
                                                                                                      userInfo:@{ @"view" : self.zoneName ?: @"unknown" }
                                                                                                       options:0];
                                                                              }];


        _lockStateTracker = lockStateTracker;

        _stateMachine = [[OctagonStateMachine alloc] initWithName:[NSString stringWithFormat:@"ckks-%@", self.zoneName]
                                                           states:[NSSet setWithArray:[CKKSZoneKeyStateMap() allKeys]]
                                                            flags:CKKSAllStateFlags()
                                                     initialState:SecCKKSZoneKeyStateWaitForCloudKitAccountStatus
                                                            queue:self.queue
                                                      stateEngine:self
                                                 lockStateTracker:lockStateTracker];
        [_stateMachine startOperation];

        _waitingQueue = [[NSOperationQueue alloc] init];
        _waitingQueue.maxConcurrentOperationCount = 5;

        _keyStateReadyDependency = [self createKeyStateReadyDependency: @"Key state has become ready for the first time."];

        dispatch_time_t initialOutgoingQueueDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 1;
        dispatch_time_t continuingOutgoingQueueDelay = SecCKKSReduceRateLimiting() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 30;
        _outgoingQueueOperationScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-outgoing-queue-scheduler", self.zoneName]
                                                                            initialDelay:initialOutgoingQueueDelay
                                                                         continuingDelay:continuingOutgoingQueueDelay
                                                                        keepProcessAlive:false
                                                               dependencyDescriptionCode:CKKSResultDescriptionPendingOutgoingQueueScheduling
                                                                                   block:^{}];

        _operationDependencies = [[CKKSOperationDependencies alloc] initWithZoneID:self.zoneID
                                                                      zoneModifier:zoneModifier
                                                                  ckoperationGroup:nil
                                                                       flagHandler:_stateMachine
                                                                    launchSequence:_launch
                                                                  lockStateTracker:_lockStateTracker
                                                               reachabilityTracker:reachabilityTracker
                                                                     peerProviders:@[]
                                                                  databaseProvider:self
                                                        notifyViewChangedScheduler:_notifyViewChangedScheduler
                                                                  savedTLKNotifier:savedTLKNotifier];
    }
    return self;
}

- (NSString*)description {
    return [NSString stringWithFormat:@"<%@: %@ (%@)>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState];
}

- (NSString*)debugDescription {
    return [NSString stringWithFormat:@"<%@: %@ (%@) %p>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState, self];
}

- (CKKSZoneKeyState*)keyHierarchyState {
    return self.stateMachine.currentState;
}

- (NSMutableDictionary<CKKSZoneKeyState*, CKKSCondition*>*)keyHierarchyConditions
{
    return self.stateMachine.stateConditions;
}

- (void)ensureKeyStateReadyDependency:(NSString*)resetMessage {
    NSOperation* oldKSRD = self.keyStateReadyDependency;
    self.keyStateReadyDependency = [self createKeyStateReadyDependency:resetMessage];
    if(oldKSRD) {
        [oldKSRD addDependency:self.keyStateReadyDependency];
        [self.waitingQueue addOperation:oldKSRD];
    }
}

- (CKKSResultOperation<OctagonStateTransitionOperationProtocol>*)performInitializedOperation
{
    WEAKIFY(self);
    return [OctagonStateTransitionOperation named:@"ckks-initialized-operation"
                                        intending:SecCKKSZoneKeyStateBecomeReady
                                       errorState:SecCKKSZoneKeyStateError
                              withBlockTakingSelf:^(OctagonStateTransitionOperation * _Nonnull op) {
        STRONGIFY(self);
        [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
            CKKSOutgoingQueueOperation* outgoingOperation = nil;
            CKKSIncomingQueueOperation* initialProcess = nil;
            CKKSScanLocalItemsOperation* initialScan = nil;

            CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];

            // Check if we believe we've synced this zone before.
            if(ckse.changeToken == nil) {
                self.operationDependencies.ckoperationGroup = [CKOperationGroup CKKSGroupWithName:@"initial-setup"];

                ckksnotice("ckks", self, "No existing change token; going to try to match local items with CloudKit ones.");

                // Onboard this keychain: there's likely items in it that we haven't synced yet.
                // But, there might be items in The Cloud that correspond to these items, with UUIDs that we don't know yet.
                // First, fetch all remote items.

                [self.currentFetchReasons addObject:CKKSFetchBecauseInitialStart];
                op.nextState = SecCKKSZoneKeyStateBeginFetch;

                // Next, try to process them (replacing local entries). This will wait for the key state to be ready.
                initialProcess = [self processIncomingQueue:true after:nil];

                // If all that succeeds, iterate through all keychain items and find the ones which need to be uploaded
                initialScan = [self scanLocalItems:@"initial-scan-operation"
                                  ckoperationGroup:self.operationDependencies.ckoperationGroup
                                             after:initialProcess];

            } else {
                // Likely a restart of securityd!

                // Are there any fixups to run first?
                self.lastFixupOperation = [CKKSFixups fixup:ckse.lastFixup for:self];
                if(self.lastFixupOperation) {
                    ckksnotice("ckksfixup", self, "We have a fixup to perform: %@", self.lastFixupOperation);
                    [self scheduleOperation:self.lastFixupOperation];
                    op.nextState = SecCKKSZoneKeyStateWaitForFixupOperation;
                    return CKKSDatabaseTransactionCommit;
                }

                // First off, are there any in-flight queue entries? If so, put them back into New.
                // If they're truly in-flight, we'll "conflict" with ourselves, but that should be fine.
                NSError* error = nil;
                [self _onqueueResetAllInflightOQE:&error];
                if(error) {
                    ckkserror("ckks", self, "Couldn't reset in-flight OQEs, bad behavior ahead: %@", error);
                }

                // Are there any entries waiting for reencryption? If so, set the flag.
                error = nil;
                NSArray<CKKSOutgoingQueueEntry*>* reencryptOQEs = [CKKSOutgoingQueueEntry allInState:SecCKKSStateReencrypt
                                                                                              zoneID:self.zoneID
                                                                                               error:&error];
                if(error) {
                    ckkserror("ckks", self, "Couldn't load reencrypt OQEs, bad behavior ahead: %@", error);
                }
                if(reencryptOQEs.count > 0) {
                    [self.stateMachine _onqueueHandleFlag:CKKSFlagItemReencryptionNeeded];
                }

                self.operationDependencies.ckoperationGroup = [CKOperationGroup CKKSGroupWithName:@"restart-setup"];

                // If it's been more than 24 hours since the last fetch, fetch and process everything.
                // Or, if we think we were interrupted in the middle of fetching, fetch some more.
                // Otherwise, just kick off the local queue processing.

                NSDate* now = [NSDate date];
                NSDateComponents* offset = [[NSDateComponents alloc] init];
                [offset setHour:-24];
                NSDate* deadline = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:now options:0];

                if(ckse.lastFetchTime == nil ||
                   [ckse.lastFetchTime compare: deadline] == NSOrderedAscending ||
                   ckse.moreRecordsInCloudKit) {

                    op.nextState = SecCKKSZoneKeyStateBeginFetch;

                } else {
                    // Check if we have an existing key hierarchy in keyset
                    CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
                    if(keyset.error && !([keyset.error.domain isEqual: @"securityd"] && keyset.error.code == errSecItemNotFound)) {
                        ckkserror("ckkskey", self, "Error examining existing key hierarchy: %@", keyset.error);
                    }

                    if(keyset.tlk && keyset.classA && keyset.classC && !keyset.error) {
                        // This is likely a restart of securityd, and we think we're ready. Double check.
                        op.nextState = SecCKKSZoneKeyStateBecomeReady;

                    } else {
                        ckksnotice("ckkskey", self, "No existing key hierarchy for %@. Check if there's one in CloudKit...", self.zoneID.zoneName);
                        op.nextState = SecCKKSZoneKeyStateBeginFetch;
                    }
                }

                if(ckse.lastLocalKeychainScanTime == nil || [ckse.lastLocalKeychainScanTime compare:deadline] == NSOrderedAscending) {
                    // TODO handle with a state flow
                    ckksnotice("ckksscan", self, "CKKS scan last occurred at %@; beginning a new one", ckse.lastLocalKeychainScanTime);
                    initialScan = [self scanLocalItems:ckse.lastLocalKeychainScanTime == nil ? @"initial-scan-operation" : @"24-hr-scan-operation"
                                      ckoperationGroup:self.operationDependencies.ckoperationGroup
                                                 after:nil];
                }

                // Process outgoing queue after re-start
                outgoingOperation = [self processOutgoingQueueAfter:nil ckoperationGroup:self.operationDependencies.ckoperationGroup];
            }

            /*
             * Launch time is determined by when the zone have:
             *  1. keystate have become ready
             *  2. scan local items (if needed)
             *  3. processed all outgoing item (if needed)
             * TODO: this should move, once queue processing becomes part of the state machine
             */

            WEAKIFY(self);
            NSBlockOperation *seemReady = [NSBlockOperation named:[NSString stringWithFormat:@"seemsReadyForSyncing-%@", self.zoneName] withBlock:^void{
                STRONGIFY(self);
                NSError *error = nil;
                ckksnotice("launch", self, "Launch complete");
                NSNumber *zoneSize = [CKKSMirrorEntry counts:self.zoneID error:&error];
                if (zoneSize) {
                    zoneSize = @(SecBucket1Significant([zoneSize longValue]));
                    [self.launch addAttribute:@"zonesize" value:zoneSize];
                }
                [self.launch launch];

                /*
                 * Since we think we are ready, signal to CK that its to check for PCS identities again, and create the
                 * since before we completed this operation, we would probably have failed with a timeout because
                 * we where busy downloading items from CloudKit and then processing them.
                 */
                [self.notifyViewReadyScheduler trigger];
            }];

            [seemReady addNullableDependency:self.keyStateReadyDependency];
            [seemReady addNullableDependency:outgoingOperation];
            [seemReady addNullableDependency:initialScan];
            [seemReady addNullableDependency:initialProcess];
            [self scheduleOperation:seemReady];

            return CKKSDatabaseTransactionCommit;
        }];
    }];
}

- (CKKSResultOperation*)resetLocalData {
    ckksnotice("ckksreset", self, "Requesting local data reset");

    return [self.stateMachine doWatchedStateMachineRPC:@"ckks-local-reset"
                                   sourceStates:[NSSet setWithArray:@[
                                       // TODO: possibly every state?
                                       SecCKKSZoneKeyStateReady,
                                       SecCKKSZoneKeyStateWaitForTLK,
                                       SecCKKSZoneKeyStateWaitForTrust,
                                       SecCKKSZoneKeyStateWaitForTLKUpload,
                                       SecCKKSZoneKeyStateLoggedOut,
                                   ]]
                                           path:[OctagonStateTransitionPath pathFromDictionary:@{
                                               SecCKKSZoneKeyStateResettingLocalData: @{
                                                   SecCKKSZoneKeyStateInitializing: @{
                                                       SecCKKSZoneKeyStateInitialized: [OctagonStateTransitionPathStep success],
                                                       SecCKKSZoneKeyStateLoggedOut: [OctagonStateTransitionPathStep success],
                                                   }
                                               }
                                           }]
                                                 reply:^(NSError * _Nonnull error) {}];
}

- (CKKSResultOperation*)resetCloudKitZone:(CKOperationGroup*)operationGroup
{
    [self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 10*NSEC_PER_SEC)];

    // Not overly thread-safe, but a single read is okay
    if(self.accountStatus != CKKSAccountStatusAvailable) {
        // No CK account? goodbye!
        ckksnotice("ckksreset", self, "Requesting reset of CK zone, but no CK account exists");
        CKKSResultOperation* errorOp = [CKKSResultOperation named:@"fail" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) {
            op.error = [NSError errorWithDomain:CKKSErrorDomain
                                          code:CKKSNotLoggedIn
                                   description:@"User is not signed into iCloud."];
        }];

        [self scheduleOperationWithoutDependencies:errorOp];
        return errorOp;
    }

    ckksnotice("ckksreset", self, "Requesting reset of CK zone (logged in)");

    NSDictionary* localResetPath = @{
        SecCKKSZoneKeyStateInitializing: @{
            SecCKKSZoneKeyStateInitialized: [OctagonStateTransitionPathStep success],
            SecCKKSZoneKeyStateLoggedOut: [OctagonStateTransitionPathStep success],
        },
    };

    // If the zone delete doesn't work, try it up to two more times

    return [self.stateMachine doWatchedStateMachineRPC:@"ckks-cloud-reset"
                                          sourceStates:[NSSet setWithArray:@[
                                              // TODO: possibly every state?
                                              SecCKKSZoneKeyStateReady,
                                              SecCKKSZoneKeyStateInitialized,
                                              SecCKKSZoneKeyStateFetchComplete,
                                              SecCKKSZoneKeyStateWaitForTLK,
                                              SecCKKSZoneKeyStateWaitForTrust,
                                              SecCKKSZoneKeyStateWaitForTLKUpload,
                                              SecCKKSZoneKeyStateLoggedOut,
                                          ]]
                                                  path:[OctagonStateTransitionPath pathFromDictionary:@{
                                                      SecCKKSZoneKeyStateResettingZone: @{
                                                          SecCKKSZoneKeyStateResettingLocalData: localResetPath,
                                                          SecCKKSZoneKeyStateResettingZone: @{
                                                              SecCKKSZoneKeyStateResettingLocalData: localResetPath,
                                                              SecCKKSZoneKeyStateResettingZone: @{
                                                                 SecCKKSZoneKeyStateResettingLocalData: localResetPath,
                                                              }
                                                          }
                                                      }
                                                  }]
                                                 reply:^(NSError * _Nonnull error) {}];
}

- (void)keyStateMachineRequestProcess {
    [self.stateMachine handleFlag:CKKSFlagKeyStateProcessRequested];
}

- (CKKSResultOperation*)createKeyStateReadyDependency:(NSString*)message {
    WEAKIFY(self);
    CKKSResultOperation* keyStateReadyDependency = [CKKSResultOperation operationWithBlock:^{
        STRONGIFY(self);
        ckksnotice("ckkskey", self, "CKKS became ready: %@", message);
    }];
    keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
    keyStateReadyDependency.descriptionErrorCode = CKKSResultDescriptionPendingKeyReady;
    return keyStateReadyDependency;
}

- (void)_onqueuePokeKeyStateMachine
{
    dispatch_assert_queue(self.queue);
    [self.stateMachine _onqueuePokeStateMachine];
}

- (CKKSResultOperation<OctagonStateTransitionOperationProtocol>* _Nullable)_onqueueNextStateMachineTransition:(OctagonState*)currentState
                                                                                                        flags:(OctagonFlags*)flags
                                                                                                 pendingFlags:(id<OctagonStateOnqueuePendingFlagHandler>)pendingFlagHandler
{
    dispatch_assert_queue(self.queue);

    // Resetting back to 'loggedout' takes all precedence.
    if([flags _onqueueContains:CKKSFlagCloudKitLoggedOut]) {
        [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedOut];
        ckksnotice("ckkskey", self, "CK account is not present");

        [self ensureKeyStateReadyDependency:@"cloudkit-account-not-present"];
        return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
                                                       intendedState:SecCKKSZoneKeyStateLoggedOut
                                                          errorState:SecCKKSZoneKeyStateError];
    }

    if([flags _onqueueContains:CKKSFlagCloudKitZoneMissing]) {
        [flags _onqueueRemoveFlag:CKKSFlagCloudKitZoneMissing];

        [self ensureKeyStateReadyDependency:@"cloudkit-zone-missing"];
        // The zone is gone! Let's reset our local state, which will feed into recreating the zone
        return [OctagonStateTransitionOperation named:@"ck-zone-missing"
                                             entering:SecCKKSZoneKeyStateResettingLocalData];
    }

    if([flags _onqueueContains:CKKSFlagChangeTokenExpired]) {
        [flags _onqueueRemoveFlag:CKKSFlagChangeTokenExpired];

        [self ensureKeyStateReadyDependency:@"cloudkit-change-token-expired"];
        // Our change token is invalid! We'll have to refetch the world, so let's delete everything locally.
        return [OctagonStateTransitionOperation named:@"ck-token-expired"
                                             entering:SecCKKSZoneKeyStateResettingLocalData];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateLoggedOut]) {
        if([flags _onqueueContains:CKKSFlagCloudKitLoggedIn] || self.accountStatus == CKKSAccountStatusAvailable) {
            [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedIn];

            ckksnotice("ckkskey", self, "CloudKit account now present");
            return [OctagonStateTransitionOperation named:@"ck-sign-in"
                                                 entering:SecCKKSZoneKeyStateInitializing];
        }

        if([flags _onqueueContains:CKKSFlag24hrNotification]) {
            [flags _onqueueRemoveFlag:CKKSFlag24hrNotification];
        }
        return nil;
    }

    if([currentState isEqualToString: SecCKKSZoneKeyStateWaitForCloudKitAccountStatus]) {
        if([flags _onqueueContains:CKKSFlagCloudKitLoggedIn] || self.accountStatus == CKKSAccountStatusAvailable) {
            [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedIn];

            ckksnotice("ckkskey", self, "CloudKit account now present");
            return [OctagonStateTransitionOperation named:@"ck-sign-in"
                                                 entering:SecCKKSZoneKeyStateInitializing];
        }

        if([flags _onqueueContains:CKKSFlagCloudKitLoggedOut]) {
            [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedOut];
            ckksnotice("ckkskey", self, "No account available");

            return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
                                                           intendedState:SecCKKSZoneKeyStateLoggedOut
                                                              errorState:SecCKKSZoneKeyStateError];
        }
        return nil;
    }

    [self.launch addEvent:currentState];

    if([currentState isEqual:SecCKKSZoneKeyStateInitializing]) {
        if(self.accountStatus == CKKSAccountStatusNoAccount) {
            ckksnotice("ckkskey", self, "CloudKit account is missing. Departing!");
            return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
                                                           intendedState:SecCKKSZoneKeyStateLoggedOut
                                                              errorState:SecCKKSZoneKeyStateError];
        }

        // Begin zone creation, but rate-limit it
        CKKSCreateCKZoneOperation* pendingInitializeOp = [[CKKSCreateCKZoneOperation alloc] initWithDependencies:self.operationDependencies
                                                                                                   intendedState:SecCKKSZoneKeyStateInitialized
                                                                                                      errorState:SecCKKSZoneKeyStateZoneCreationFailed];
        [pendingInitializeOp addNullableDependency:self.operationDependencies.zoneModifier.cloudkitRetryAfter.operationDependency];
        [self.operationDependencies.zoneModifier.cloudkitRetryAfter trigger];

        return pendingInitializeOp;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForFixupOperation]) {
        // TODO: fixup operations should become part of the state machine
        ckksnotice("ckkskey", self, "Waiting for the fixup operation: %@", self.lastFixupOperation);
        OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"wait-for-fixup" entering:SecCKKSZoneKeyStateInitialized];
        [op addNullableDependency:self.lastFixupOperation];
        return op;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateInitialized]) {
        // We're initialized and CloudKit is ready. If we're trusted, see what needs done. Otherwise, wait.
        return [self performInitializedOperation];
    }

    // In error? You probably aren't getting out.
    if([currentState isEqualToString:SecCKKSZoneKeyStateError]) {
        if([flags _onqueueContains:CKKSFlagCloudKitLoggedIn]) {
            [flags _onqueueRemoveFlag:CKKSFlagCloudKitLoggedIn];

            // Worth one last shot. Reset everything locally, and try again.
            return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
                                                           intendedState:SecCKKSZoneKeyStateInitializing
                                                              errorState:SecCKKSZoneKeyStateError];
        }

        ckkserror("ckkskey", self, "Staying in error state %@", currentState);
        return nil;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateResettingZone]) {
        ckksnotice("ckkskey", self, "Deleting the CloudKit Zone");

        [self ensureKeyStateReadyDependency:@"ck-zone-reset"];
        return [[CKKSDeleteCKZoneOperation alloc] initWithDependencies:self.operationDependencies
                                                         intendedState:SecCKKSZoneKeyStateResettingLocalData
                                                            errorState:SecCKKSZoneKeyStateResettingZone];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateResettingLocalData]) {
        ckksnotice("ckkskey", self, "Resetting local data");

        [self ensureKeyStateReadyDependency:@"local-data-reset"];
        return [[CKKSLocalResetOperation alloc] initWithDependencies:self.operationDependencies
                                                       intendedState:SecCKKSZoneKeyStateInitializing
                                                          errorState:SecCKKSZoneKeyStateError];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateZoneCreationFailed]) {
        //Prepare to go back into initializing, as soon as the cloudkitRetryAfter is happy
        OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"recover-from-cloudkit-failure" entering:SecCKKSZoneKeyStateInitializing];

        [op addNullableDependency:self.operationDependencies.zoneModifier.cloudkitRetryAfter.operationDependency];
        [self.operationDependencies.zoneModifier.cloudkitRetryAfter trigger];

        return op;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateLoseTrust]) {
        if([flags _onqueueContains:CKKSFlagBeginTrustedOperation]) {
            [flags _onqueueRemoveFlag:CKKSFlagBeginTrustedOperation];
            // This was likely a race between some operation and the beginTrustedOperation call! Skip changing state and try again.
            return [OctagonStateTransitionOperation named:@"begin-trusted-operation" entering:SecCKKSZoneKeyStateInitialized];
        }

        // If our current state is "trusted", fall out
        if(self.trustStatus == CKKSAccountStatusAvailable) {
            self.trustStatus = CKKSAccountStatusUnknown;
        }
        return [OctagonStateTransitionOperation named:@"trust-loss" entering:SecCKKSZoneKeyStateWaitForTrust];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
        if(self.trustStatus == CKKSAccountStatusAvailable) {
            ckksnotice("ckkskey", self, "Beginning trusted state machine operation");
            return [OctagonStateTransitionOperation named:@"begin-trusted-operation" entering:SecCKKSZoneKeyStateInitialized];
        }

        if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
            [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
            return [OctagonStateTransitionOperation named:@"begin-trusted-operation" entering:SecCKKSZoneKeyStateProcess];
        }

        if([flags _onqueueContains:CKKSFlag24hrNotification]) {
            [flags _onqueueRemoveFlag:CKKSFlag24hrNotification];
        }

        return nil;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateBecomeReady]) {
        return [[CKKSCheckKeyHierarchyOperation alloc] initWithDependencies:self.operationDependencies
                                                              intendedState:SecCKKSZoneKeyStateReady
                                                                 errorState:SecCKKSZoneKeyStateError];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateReady]) {
        // If we're ready, we can ignore the begin trusted flag
        [flags _onqueueRemoveFlag:CKKSFlagBeginTrustedOperation];

        if(self.keyStateFullRefetchRequested) {
            // In ready, but something has requested a full refetch.
            ckksnotice("ckkskey", self, "Kicking off a full key refetch based on request:%d", self.keyStateFullRefetchRequested);
            [self ensureKeyStateReadyDependency:@"key-state-full-refetch"];
            return [OctagonStateTransitionOperation named:@"full-refetch" entering:SecCKKSZoneKeyStateNeedFullRefetch];
        }

        if([flags _onqueueContains:CKKSFlagFetchRequested]) {
            [flags _onqueueRemoveFlag:CKKSFlagFetchRequested];
            ckksnotice("ckkskey", self, "Kicking off a key refetch based on request");
            [self ensureKeyStateReadyDependency:@"key-state-fetch"];
            return [OctagonStateTransitionOperation named:@"fetch-requested" entering:SecCKKSZoneKeyStateBeginFetch];
        }

        if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
            [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
            ckksnotice("ckkskey", self, "Kicking off a key reprocess based on request");
            [self ensureKeyStateReadyDependency:@"key-state-process"];
            return [OctagonStateTransitionOperation named:@"key-process" entering:SecCKKSZoneKeyStateProcess];
        }

        if(self.trustStatus != CKKSAccountStatusAvailable) {
            ckksnotice("ckkskey", self, "In ready, but there's no trust; going into waitfortrust");
            [self ensureKeyStateReadyDependency:@"trust loss"];
            return [OctagonStateTransitionOperation named:@"trust-gone" entering:SecCKKSZoneKeyStateLoseTrust];
        }

        if([flags _onqueueContains:CKKSFlagTrustedPeersSetChanged]) {
            [flags _onqueueRemoveFlag:CKKSFlagTrustedPeersSetChanged];
            ckksnotice("ckkskey", self, "Received a nudge that the trusted peers set might have changed! Reprocessing.");
            [self ensureKeyStateReadyDependency:@"Peer set changed"];
            return [OctagonStateTransitionOperation named:@"trusted-peers-changed" entering:SecCKKSZoneKeyStateProcess];
        }

        if([flags _onqueueContains:CKKSFlag24hrNotification]) {
            [flags _onqueueRemoveFlag:CKKSFlag24hrNotification];

            // We'd like to trigger our 24-hr backup fetch and scan.
            // That's currently part of the Initialized state, so head that way
            return [OctagonStateTransitionOperation named:@"24-hr-check" entering:SecCKKSZoneKeyStateInitialized];
        }

        if([flags _onqueueContains:CKKSFlagItemReencryptionNeeded]) {
            [flags _onqueueRemoveFlag:CKKSFlagItemReencryptionNeeded];

            // TODO: this should be part of the state machine
            CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithDependencies:self.operationDependencies
                                                                                                                   ckks:self
                                                                                                          intendedState:SecCKKSZoneKeyStateReady
                                                                                                             errorState:SecCKKSZoneKeyStateError];
            [self scheduleOperation:op];
            // fall through.
        }

        if([flags _onqueueContains:CKKSFlagProcessIncomingQueue]) {
            [flags _onqueueRemoveFlag:CKKSFlagProcessIncomingQueue];
            // TODO: this should be part of the state machine

            [self processIncomingQueue:true];
            //return [OctagonStateTransitionOperation named:@"process-outgoing" entering:SecCKKSZoneKeyStateProcessIncomingQueue];
        }

        if([flags _onqueueContains:CKKSFlagScanLocalItems]) {
            [flags _onqueueRemoveFlag:CKKSFlagScanLocalItems];
            ckksnotice("ckkskey", self, "Launching a scan operation to find dropped items");

            // TODO: this should be a state flow
            [self scanLocalItems:@"per-request"];
            // fall through
        }

        if([flags _onqueueContains:CKKSFlagProcessOutgoingQueue]) {
            [flags _onqueueRemoveFlag:CKKSFlagProcessOutgoingQueue];

            [self processOutgoingQueue:nil];
            // TODO: this should be a state flow.
            //return [OctagonStateTransitionOperation named:@"process-outgoing" entering:SecCKKSZoneKeyStateProcessOutgoingQueue];
            // fall through
        }

        // TODO: kick off a key roll if one has been requested


        // If we reach this point, we're in ready, and will stay there.
        // Tell the launch and the viewReadyScheduler about that.

        [self.launch launch];

        [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastKeystateReady zoneName:self.zoneName];
        if(self.keyStateReadyDependency) {
            [self scheduleOperation:self.keyStateReadyDependency];
            self.keyStateReadyDependency = nil;
        }

        return nil;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) {
        if([flags _onqueueContains:CKKSFlagDeviceUnlocked]) {
            [flags _onqueueRemoveFlag:CKKSFlagDeviceUnlocked];
            [self ensureKeyStateReadyDependency:@"Device unlocked"];
            return [OctagonStateTransitionOperation named:@"key-state-ready-after-unlock" entering:SecCKKSZoneKeyStateBecomeReady];
        }

        if([flags _onqueueContains:CKKSFlagProcessOutgoingQueue]) {
            [flags _onqueueRemoveFlag:CKKSFlagProcessOutgoingQueue];
            [self processOutgoingQueue:nil];
            // TODO: this should become part of the key state hierarchy
        }

        // Ready enough!

        [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastKeystateReady zoneName:self.zoneName];
        if(self.keyStateReadyDependency) {
            [self scheduleOperation:self.keyStateReadyDependency];
            self.keyStateReadyDependency = nil;
        }

        OctagonPendingFlag* unlocked = [[OctagonPendingFlag alloc] initWithFlag:CKKSFlagDeviceUnlocked
                                                                     conditions:OctagonPendingConditionsDeviceUnlocked];
        [pendingFlagHandler _onqueueHandlePendingFlag:unlocked];
        return nil;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateBeginFetch]) {
        ckksnotice("ckkskey", self, "Starting a key hierarchy fetch");
        [flags _onqueueRemoveFlag:CKKSFlagFetchComplete];

        WEAKIFY(self);

        NSSet<CKKSFetchBecause*>* fetchReasons = self.currentFetchReasons ?
            [self.currentFetchReasons setByAddingObject:CKKSFetchBecauseKeyHierarchy] :
            [NSSet setWithObject:CKKSFetchBecauseKeyHierarchy];

        CKKSResultOperation* fetchOp = [self.zoneChangeFetcher requestSuccessfulFetchForManyReasons:fetchReasons];
        CKKSResultOperation* flagOp = [CKKSResultOperation named:@"post-fetch"
                                                       withBlock:^{
            STRONGIFY(self);
            [self.stateMachine handleFlag:CKKSFlagFetchComplete];
        }];
        [flagOp addDependency:fetchOp];
        [self scheduleOperation:flagOp];

        return [OctagonStateTransitionOperation named:@"waiting-for-fetch" entering:SecCKKSZoneKeyStateFetch];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateFetch]) {
        if([flags _onqueueContains:CKKSFlagFetchComplete]) {
            [flags _onqueueRemoveFlag:CKKSFlagFetchComplete];
            return [OctagonStateTransitionOperation named:@"fetch-complete" entering:SecCKKSZoneKeyStateFetchComplete];
        }

        // The flags CKKSFlagCloudKitZoneMissing and CKKSFlagChangeTokenOutdated are both handled at the top of this function
        // So, we don't need to handle them here.

        return nil;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateNeedFullRefetch]) {
         ckksnotice("ckkskey", self, "Starting a key hierarchy full refetch");

         //TODO use states here instead of flags
         self.keyStateMachineRefetched = true;
         self.keyStateFullRefetchRequested = false;

         return [OctagonStateTransitionOperation named:@"fetch-complete" entering:SecCKKSZoneKeyStateResettingLocalData];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateFetchComplete]) {
        [self.launch addEvent:@"fetch-complete"];
        [self.currentFetchReasons removeAllObjects];

        return [OctagonStateTransitionOperation named:@"post-fetch-process" entering:SecCKKSZoneKeyStateProcess];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLKCreation]) {
        if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
            [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
            ckksnotice("ckkskey", self, "We believe we need to create TLKs but we also received a key nudge; moving to key state Process.");
            return [OctagonStateTransitionOperation named:@"wait-for-tlk-creation-process" entering:SecCKKSZoneKeyStateProcess];

        } else if([flags _onqueueContains:CKKSFlagFetchRequested]) {
            [flags _onqueueRemoveFlag:CKKSFlagFetchRequested];
            return [OctagonStateTransitionOperation named:@"fetch-requested" entering:SecCKKSZoneKeyStateBeginFetch];

        } else if([flags _onqueueContains:CKKSFlagTLKCreationRequested]) {
            [flags _onqueueRemoveFlag:CKKSFlagTLKCreationRequested];

            // It's very likely that we're already untrusted at this point. But, sometimes we will be trusted right now, and can lose trust while waiting for the upload.
            // This probably should be handled by a state increase.
            [flags _onqueueRemoveFlag:CKKSFlagEndTrustedOperation];

            ckksnotice("ckkskey", self, "TLK creation requested; kicking off operation");
            return [[CKKSNewTLKOperation alloc] initWithDependencies:self.operationDependencies
                                                                ckks:self];
        } else if(self.lastNewTLKOperation.keyset) {
            // This means that we _have_ created new TLKs, and should wait for them to be uploaded. This is ugly and should probably be done with more states.
            return [OctagonStateTransitionOperation named:@"" entering:SecCKKSZoneKeyStateWaitForTLKUpload];

        } else {
            ckksnotice("ckkskey", self, "We believe we need to create TLKs; waiting for Octagon (via %@)", self.suggestTLKUpload);
            [self.suggestTLKUpload trigger];
        }
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLKUpload]) {
        ckksnotice("ckkskey", self, "We believe we have TLKs that need uploading");

        if([flags _onqueueContains:CKKSFlagFetchRequested]) {
            ckksnotice("ckkskey", self, "Received a nudge to refetch CKKS");
            return [OctagonStateTransitionOperation named:@"tlk-upload-refetch" entering:SecCKKSZoneKeyStateBeginFetch];
        }

        if([flags _onqueueContains:CKKSFlagKeyStateTLKsUploaded]) {
            [flags _onqueueRemoveFlag:CKKSFlagKeyStateTLKsUploaded];

            return [OctagonStateTransitionOperation named:@"wait-for-tlk-upload-process" entering:SecCKKSZoneKeyStateProcess];
        }

        if([flags _onqueueContains:CKKSFlagEndTrustedOperation]) {
            [flags _onqueueRemoveFlag:CKKSFlagEndTrustedOperation];

            return [OctagonStateTransitionOperation named:@"trust-loss" entering:SecCKKSZoneKeyStateLoseTrust];
        }

        if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
            return [OctagonStateTransitionOperation named:@"wait-for-tlk-fetch-process" entering:SecCKKSZoneKeyStateProcess];
        }

        // This is quite the hack, but it'll do for now.
        [self.operationDependencies provideKeySet:self.lastNewTLKOperation.keyset];

        ckksnotice("ckkskey", self, "Notifying Octagon again, just in case");
        [self.suggestTLKUpload trigger];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateTLKMissing]) {
        return [self tlkMissingOperation:SecCKKSZoneKeyStateWaitForTLK];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLK]) {
        // We're in a hold state: waiting for the TLK bytes to arrive.

        if([flags _onqueueContains:CKKSFlagKeyStateProcessRequested]) {
            [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];
            // Someone has requsted a reprocess! Go to the correct state.
            ckksnotice("ckkskey", self, "Received a nudge that our TLK might be here! Reprocessing.");
            return [OctagonStateTransitionOperation named:@"wait-for-tlk-process" entering:SecCKKSZoneKeyStateProcess];

        } else if([flags _onqueueContains:CKKSFlagTrustedPeersSetChanged]) {
            [flags _onqueueRemoveFlag:CKKSFlagTrustedPeersSetChanged];

            // Hmm, maybe this trust set change will cause us to recover this TLK (due to a previously-untrusted share becoming trusted). Worth a shot!
            ckksnotice("ckkskey", self, "Received a nudge that the trusted peers set might have changed! Reprocessing.");
            return [OctagonStateTransitionOperation named:@"wait-for-tlk-peers" entering:SecCKKSZoneKeyStateProcess];
        }

        return nil;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateWaitForUnlock]) {
        ckksnotice("ckkskey", self, "Requested to enter waitforunlock");

        if([flags _onqueueContains:CKKSFlagDeviceUnlocked ]) {
            [flags _onqueueRemoveFlag:CKKSFlagDeviceUnlocked];
            return [OctagonStateTransitionOperation named:@"key-state-after-unlock" entering:SecCKKSZoneKeyStateInitialized];
        }

        OctagonPendingFlag* unlocked = [[OctagonPendingFlag alloc] initWithFlag:CKKSFlagDeviceUnlocked
                                                                     conditions:OctagonPendingConditionsDeviceUnlocked];
        [pendingFlagHandler _onqueueHandlePendingFlag:unlocked];

        return nil;
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateBadCurrentPointers]) {
        // The current key pointers are broken, but we're not sure why.
        ckksnotice("ckkskey", self, "Our current key pointers are reported broken. Attempting a fix!");
        return [[CKKSHealKeyHierarchyOperation alloc] initWithDependencies:self.operationDependencies
                                                                      ckks:self
                                                                 intending:SecCKKSZoneKeyStateBecomeReady
                                                                errorState:SecCKKSZoneKeyStateError];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateNewTLKsFailed]) {
        ckksnotice("ckkskey", self, "Creating new TLKs didn't work. Attempting to refetch!");
        return [OctagonStateTransitionOperation named:@"new-tlks-failed" entering:SecCKKSZoneKeyStateBeginFetch];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateHealTLKSharesFailed]) {
        ckksnotice("ckkskey", self, "Creating new TLK shares didn't work. Attempting to refetch!");
        return [OctagonStateTransitionOperation named:@"heal-tlks-failed" entering:SecCKKSZoneKeyStateBeginFetch];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
        if(self.trustStatus != CKKSAccountStatusAvailable) {
            ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy, but we're untrusted.");
            return [OctagonStateTransitionOperation named:@"unhealthy-lacking-trust" entering:SecCKKSZoneKeyStateLoseTrust];

        } else {
            ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
            return [[CKKSHealKeyHierarchyOperation alloc] initWithDependencies:self.operationDependencies
                                                                          ckks:self
                                                                     intending:SecCKKSZoneKeyStateBecomeReady
                                                                    errorState:SecCKKSZoneKeyStateError];
        }
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateHealTLKShares]) {
        ckksnotice("ckksshare", self, "Key hierarchy is okay, but not shared appropriately. Launching fix.");
        return [[CKKSHealTLKSharesOperation alloc] initWithOperationDependencies:self.operationDependencies
                                                                            ckks:self];
    }

    if([currentState isEqualToString:SecCKKSZoneKeyStateProcess]) {
        [flags _onqueueRemoveFlag:CKKSFlagKeyStateProcessRequested];

        ckksnotice("ckksshare", self, "Launching key state process");
        return [[CKKSProcessReceivedKeysOperation alloc] initWithDependencies:self.operationDependencies
                                                                intendedState:SecCKKSZoneKeyStateBecomeReady
                                                                   errorState:SecCKKSZoneKeyStateError];
    }

    return nil;
}

- (OctagonStateTransitionOperation*)tlkMissingOperation:(CKKSZoneKeyState*)newState
{
    WEAKIFY(self);
    return [OctagonStateTransitionOperation named:@"tlk-missing"
                                        intending:newState
                                       errorState:SecCKKSZoneKeyStateError
                              withBlockTakingSelf:^(OctagonStateTransitionOperation * _Nonnull op) {
        STRONGIFY(self);

        NSArray<CKKSPeerProviderState*>* trustStates = self.operationDependencies.currentTrustStates;

        [self.operationDependencies.databaseProvider dispatchSyncWithReadOnlySQLTransaction:^{
            CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];

            if(keyset.error) {
                ckkserror("ckkskey", self, "Unable to load keyset: %@", keyset.error);
                op.nextState = newState;

                [self.operationDependencies provideKeySet:keyset];
                return;
            }

            if(!keyset.currentTLKPointer.currentKeyUUID) {
                // In this case, there's no current TLK at all. Go into "wait for tlkcreation";
                op.nextState = SecCKKSZoneKeyStateWaitForTLKCreation;
                [self.operationDependencies provideKeySet:keyset];
                return;
            }

            if(self.trustStatus != CKKSAccountStatusAvailable) {
                ckksnotice("ckkskey", self, "TLK is missing, but no trust is present.");
                op.nextState = SecCKKSZoneKeyStateLoseTrust;

                [self.operationDependencies provideKeySet:keyset];
                return;
            }

            bool otherDevicesPresent = [self _onqueueOtherDevicesReportHavingTLKs:keyset
                                                                      trustStates:trustStates];
            if(otherDevicesPresent) {
                // We expect this keyset to continue to exist. Send it to our listeners.
                [self.operationDependencies provideKeySet:keyset];

                op.nextState = newState;
            } else {
                ckksnotice("ckkskey", self, "No other devices claim to have the TLK. Resetting zone...");
                op.nextState = SecCKKSZoneKeyStateResettingZone;
            }
            return;
        }];
    }];
}

- (bool)_onqueueOtherDevicesReportHavingTLKs:(CKKSCurrentKeySet*)keyset
                                 trustStates:(NSArray<CKKSPeerProviderState*>*)trustStates
{
    //Has there been any activity indicating that other trusted devices have keys in the past 45 days, or untrusted devices in the past 4?
    // (We chose 4 as devices attempt to upload their device state every 3 days. If a device is unceremoniously kicked out of circle, we normally won't immediately reset.)
    NSDate* now = [NSDate date];
    NSDateComponents* trustedOffset = [[NSDateComponents alloc] init];
    [trustedOffset setDay:-45];
    NSDate* trustedDeadline = [[NSCalendar currentCalendar] dateByAddingComponents:trustedOffset toDate:now options:0];

    NSDateComponents* untrustedOffset = [[NSDateComponents alloc] init];
    [untrustedOffset setDay:-4];
    NSDate* untrustedDeadline = [[NSCalendar currentCalendar] dateByAddingComponents:untrustedOffset toDate:now options:0];


    NSMutableSet<NSString*>* trustedPeerIDs = [NSMutableSet set];
    for(CKKSPeerProviderState* trustState in trustStates) {
        for(id<CKKSPeer> peer in trustState.currentTrustedPeers) {
            [trustedPeerIDs addObject:peer.peerID];
        }
    }

    NSError* localerror = nil;

    NSArray<CKKSDeviceStateEntry*>* allDeviceStates = [CKKSDeviceStateEntry allInZone:keyset.currentTLKPointer.zoneID error:&localerror];
    if(localerror) {
        ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
        localerror = nil;
        return true;
    }
    for(CKKSDeviceStateEntry* device in allDeviceStates) {
        // The peerIDs in CDSEs aren't written with the peer prefix. Make sure we match both.
        NSString* sosPeerID = device.circlePeerID ? [CKKSSOSPeerPrefix stringByAppendingString:device.circlePeerID] : nil;

        if([trustedPeerIDs containsObject:device.circlePeerID] ||
           [trustedPeerIDs containsObject:sosPeerID] ||
           [trustedPeerIDs containsObject:device.octagonPeerID]) {
            // Is this a recent DSE? If it's older than the deadline, skip it
            if([device.storedCKRecord.modificationDate compare:trustedDeadline] == NSOrderedAscending) {
                ckksnotice("ckkskey", self, "Trusted device state (%@) is too old; ignoring", device);
                continue;
            }
        } else {
            // Device is untrusted. How does it fare with the untrustedDeadline?
            if([device.storedCKRecord.modificationDate compare:untrustedDeadline] == NSOrderedAscending) {
                ckksnotice("ckkskey", self, "Device (%@) is not trusted and from too long ago; ignoring device state (%@)", device.circlePeerID, device);
                continue;
            } else {
                ckksnotice("ckkskey", self, "Device (%@) is not trusted, but very recent. Including in heuristic: %@", device.circlePeerID, device);
            }
        }

        if([device.keyState isEqualToString:SecCKKSZoneKeyStateReady] ||
           [device.keyState isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) {
            ckksnotice("ckkskey", self, "Other device (%@) has keys; it should send them to us", device);
            return true;
        }
    }

    NSArray<CKKSTLKShareRecord*>* tlkShares = [CKKSTLKShareRecord allForUUID:keyset.currentTLKPointer.currentKeyUUID
                                                                      zoneID:keyset.currentTLKPointer.zoneID
                                                                       error:&localerror];
    if(localerror) {
        ckkserror("ckkskey", self, "Error fetching device states: %@", localerror);
        localerror = nil;
        return false;
    }

    for(CKKSTLKShareRecord* tlkShare in tlkShares) {
        if([trustedPeerIDs containsObject:tlkShare.senderPeerID] &&
           [tlkShare.storedCKRecord.modificationDate compare:trustedDeadline] == NSOrderedDescending) {
            ckksnotice("ckkskey", self, "Trusted TLK Share (%@) created recently; other devices have keys and should send them to us", tlkShare);
            return true;
        }
    }

    // Okay, how about the untrusted deadline?
    for(CKKSTLKShareRecord* tlkShare in tlkShares) {
        if([tlkShare.storedCKRecord.modificationDate compare:untrustedDeadline] == NSOrderedDescending) {
            ckksnotice("ckkskey", self, "Untrusted TLK Share (%@) created very recently; other devices might have keys and should rejoin the circle (and send them to us)", tlkShare);
            return true;
        }
    }

    return false;
}

- (void)handleKeychainEventDbConnection:(SecDbConnectionRef) dbconn
                                 source:(SecDbTransactionSource)txionSource
                                  added:(SecDbItemRef) added
                                deleted:(SecDbItemRef) deleted
                            rateLimiter:(CKKSRateLimiter*) rateLimiter
{
    if(!SecCKKSIsEnabled()) {
        ckksnotice("ckks", self, "Skipping handleKeychainEventDbConnection due to disabled CKKS");
        return;
    }

    __block NSError* error = nil;

    // Tombstones come in as item modifications or item adds. Handle modifications here.
    bool addedTombstone   = added   && SecDbItemIsTombstone(added);
    bool deletedTombstone = deleted && SecDbItemIsTombstone(deleted);

    bool addedSync   = added   && SecDbItemIsSyncable(added);
    bool deletedSync = deleted && SecDbItemIsSyncable(deleted);

    bool isTombstoneModification = addedTombstone && deletedTombstone;
    bool isAdd    = ( added && !deleted) || (added && deleted && !addedTombstone &&  deletedTombstone) || (added && deleted &&  addedSync && !deletedSync);
    bool isDelete = (!added &&  deleted) || (added && deleted &&  addedTombstone && !deletedTombstone) || (added && deleted && !addedSync &&  deletedSync);
    bool isModify = ( added &&  deleted) && (!isAdd) && (!isDelete);

    // On an update that changes an item's primary key, SecDb modifies the existing item, then adds a new tombstone to replace the old primary key.
    // Therefore, we might receive an added tombstone here with no deleted item to accompany it. This should be considered a deletion.
    if(addedTombstone && !deleted) {
        isAdd = false;
        isDelete = true;
        isModify = false;

        // Passed to withItem: below
        deleted = added;
    }

    // If neither item is syncable, don't proceed further in the syncing system
    bool proceed = addedSync || deletedSync;

    if(!proceed) {
        ckksnotice("ckks", self, "skipping sync of non-sync item (%d, %d)", addedSync, deletedSync);
        return;
    }

    if(isTombstoneModification) {
        ckksnotice("ckks", self, "skipping syncing update of tombstone item (%d, %d)", addedTombstone, deletedTombstone);
        return;
    }

    // It's possible to ask for an item to be deleted without adding a corresponding tombstone.
    // This is arguably a bug, as it generates an out-of-sync state, but it is in the API contract.
    // CKKS should ignore these, but log very upset messages.
    if(isDelete && !addedTombstone) {
        ckksnotice("ckks", self, "Client has asked for an item deletion to not sync. Keychain is now out of sync with account");
        return;
    }

    // Only synchronize items which can transfer between devices
    NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(added ? added : deleted, kSecAttrAccessible);
    if(! ([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked] ||
          [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock] ||
          [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate])) {
        ckksnotice("ckks", self, "skipping sync of device-bound(%@) item", protection);
        return;
    }

    if(txionSource == kSecDbSOSTransaction) {
        NSString* addedUUID = (__bridge NSString*)SecDbItemGetValue(added, &v10itemuuid, NULL);
        ckksnotice("ckks", self, "Received an incoming %@ from SOS (%@)",
                   isAdd ? @"addition" : (isModify ? @"modification" : @"deletion"),
                   addedUUID);
    }

    // Our caller gave us a database connection. We must get on the local queue to ensure atomicity
    // Note that we're at the mercy of the surrounding db transaction, so don't try to rollback here
    [self dispatchSyncWithConnection:dbconn
                      readWriteTxion:YES
                               block:^CKKSDatabaseTransactionResult {
        // Schedule a "view changed" notification
        [self.notifyViewChangedScheduler trigger];

        if(self.accountStatus == CKKSAccountStatusNoAccount) {
            // No account; CKKS shouldn't attempt anything.
            [self.stateMachine _onqueueHandleFlag:CKKSFlagScanLocalItems];
            ckksnotice("ckks", self, "Dropping sync item modification due to CK account state; will scan to find changes later");

            // We're positively not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
            NSString* uuid = (__bridge NSString*)SecDbItemGetValue(added ? added : deleted, &v10itemuuid, NULL);

            SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:uuid];
            if(syncCallback) {
                [CKKSViewManager callSyncCallbackWithErrorNoAccount: syncCallback];
            }

            return CKKSDatabaseTransactionCommit;
        }

        CKKSOutgoingQueueEntry* oqe = nil;
        if       (isAdd) {
            oqe = [CKKSOutgoingQueueEntry withItem: added   action: SecCKKSActionAdd    zoneID:self.zoneID error: &error];
        } else if(isDelete) {
            oqe = [CKKSOutgoingQueueEntry withItem: deleted action: SecCKKSActionDelete zoneID:self.zoneID error: &error];
        } else if(isModify) {
            oqe = [CKKSOutgoingQueueEntry withItem: added   action: SecCKKSActionModify zoneID:self.zoneID error: &error];
        } else {
            ckkserror("ckks", self, "processKeychainEventItemAdded given garbage: %@ %@", added, deleted);
            return CKKSDatabaseTransactionCommit;
        }

        if(!self.itemSyncingEnabled) {
            // Call any callback now; they're not likely to get the sync they wanted
            SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
            if(syncCallback) {
                syncCallback(false, [NSError errorWithDomain:CKKSErrorDomain
                                                        code:CKKSErrorViewIsPaused
                                                 description:@"View is paused; item is not expected to sync"]);
            }
        }

        CKOperationGroup* operationGroup = txionSource == kSecDbSOSTransaction
            ? [CKOperationGroup CKKSGroupWithName:@"sos-incoming-item"]
            : [CKOperationGroup CKKSGroupWithName:@"keychain-api-use"];

        if(error) {
            ckkserror("ckks", self, "Couldn't create outgoing queue entry: %@", error);
            [self.stateMachine _onqueueHandleFlag:CKKSFlagScanLocalItems];

            // If the problem is 'couldn't load key', tell the key hierarchy state machine to fix it
            if([error.domain isEqualToString:CKKSErrorDomain] && error.code == errSecItemNotFound) {
                [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
            }

            return CKKSDatabaseTransactionCommit;
        } else if(!oqe) {
            ckkserror("ckks", self, "Decided that no operation needs to occur for %@", error);
            return CKKSDatabaseTransactionCommit;
        }

        if(rateLimiter) {
            NSDate* limit = nil;
            NSInteger value = [rateLimiter judge:oqe at:[NSDate date] limitTime:&limit];
            if(limit) {
                oqe.waitUntil = limit;
                SecPLLogRegisteredEvent(@"CKKSSyncing", @{ @"ratelimit" : @(value), @"accessgroup" : oqe.accessgroup});
            }
        }

        [oqe saveToDatabaseWithConnection: dbconn error: &error];
        if(error) {
            ckkserror("ckks", self, "Couldn't save outgoing queue entry to database: %@", error);
            return CKKSDatabaseTransactionCommit;
        } else {
            ckksnotice("ckks", self, "Saved %@ to outgoing queue", oqe);
        }

        // This update supercedes all other local modifications to this item (_except_ those in-flight).
        // Delete all items in reencrypt or error.
        NSArray<CKKSOutgoingQueueEntry*>* siblings = [CKKSOutgoingQueueEntry allWithUUID:oqe.uuid
                                                                                  states:@[SecCKKSStateReencrypt, SecCKKSStateError]
                                                                                  zoneID:self.zoneID
                                                                                   error:&error];
        if(error) {
            ckkserror("ckks", self, "Couldn't load OQE siblings for %@: %@", oqe, error);
        }

        for(CKKSOutgoingQueueEntry* oqeSibling in siblings) {
            NSError* deletionError = nil;
            [oqeSibling deleteFromDatabase:&deletionError];
            if(deletionError) {
                ckkserror("ckks", self, "Couldn't delete OQE sibling(%@) for %@: %@", oqeSibling, oqe.uuid, deletionError);
            }
        }

        // This update also supercedes any remote changes that are pending.
        NSError* iqeError = nil;
        CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:oqe.uuid zoneID:self.zoneID error:&iqeError];
        if(iqeError) {
            ckkserror("ckks", self, "Couldn't find IQE matching %@: %@", oqe.uuid, error);
        } else if(iqe) {
            [iqe deleteFromDatabase:&iqeError];
            if(iqeError) {
                ckkserror("ckks", self, "Couldn't delete IQE matching %@: %@", oqe.uuid, error);
            } else {
                ckksnotice("ckks", self, "Deleted IQE matching changed item %@", oqe.uuid);
            }
        }

        [self processOutgoingQueue:operationGroup];

        return CKKSDatabaseTransactionCommit;
    }];
}

-(void)setCurrentItemForAccessGroup:(NSData* _Nonnull)newItemPersistentRef
                               hash:(NSData*)newItemSHA1
                        accessGroup:(NSString*)accessGroup
                         identifier:(NSString*)identifier
                          replacing:(NSData* _Nullable)oldCurrentItemPersistentRef
                               hash:(NSData*)oldItemSHA1
                           complete:(void (^) (NSError* operror)) complete
{
    if(accessGroup == nil || identifier == nil) {
        NSError* error = [NSError errorWithDomain:CKKSErrorDomain
                                             code:errSecParam
                                      description:@"No access group or identifier given"];
        ckkserror("ckkscurrent", self, "Cancelling request: %@", error);
        complete(error);
        return;
    }

    // Not being in a CloudKit account is an automatic failure.
    // But, wait a good long while for the CloudKit account state to be known (in the case of daemon startup)
    [self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 30*NSEC_PER_SEC)];

    if(self.accountStatus != CKKSAccountStatusAvailable) {
        NSError* error = [NSError errorWithDomain:CKKSErrorDomain
                                             code:CKKSNotLoggedIn
                                      description:@"User is not signed into iCloud."];
        ckksnotice("ckkscurrent", self, "Rejecting current item pointer set since we don't have an iCloud account.");
        complete(error);
        return;
    }

    ckksnotice("ckkscurrent", self, "Starting change current pointer operation for %@-%@", accessGroup, identifier);
    CKKSUpdateCurrentItemPointerOperation* ucipo = [[CKKSUpdateCurrentItemPointerOperation alloc] initWithCKKSKeychainView:self
                                                                                                                   newItem:newItemPersistentRef
                                                                                                                      hash:newItemSHA1
                                                                                                               accessGroup:accessGroup
                                                                                                                identifier:identifier
                                                                                                                 replacing:oldCurrentItemPersistentRef
                                                                                                                      hash:oldItemSHA1
                                                                                                          ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"currentitem-api"]];

    WEAKIFY(self);
    CKKSResultOperation* returnCallback = [CKKSResultOperation operationWithBlock:^{
        STRONGIFY(self);

        if(ucipo.error) {
            ckkserror("ckkscurrent", self, "Failed setting a current item pointer for %@ with %@", ucipo.currentPointerIdentifier, ucipo.error);
        } else {
            ckksnotice("ckkscurrent", self, "Finished setting a current item pointer for %@", ucipo.currentPointerIdentifier);
        }
        complete(ucipo.error);
    }];
    returnCallback.name = @"setCurrentItem-return-callback";
    [returnCallback addDependency: ucipo];
    [self scheduleOperation: returnCallback];

    // Now, schedule ucipo. It modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
    // Then, we won't have simultaneous zone-modifying operations.
    [ucipo linearDependencies:self.outgoingQueueOperations];

    // If this operation hasn't started within 60 seconds, cancel it and return a "timed out" error.
    [ucipo timeout:60*NSEC_PER_SEC];

    [self scheduleOperation:ucipo];
    return;
}

-(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
                         identifier:(NSString*)identifier
                    fetchCloudValue:(bool)fetchCloudValue
                           complete:(void (^) (NSString* uuid, NSError* operror)) complete
{
    if(accessGroup == nil || identifier == nil) {
        ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since no access group(%@) or identifier(%@) given", accessGroup, identifier);
        complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
                                           code:errSecParam
                                    description:@"No access group or identifier given"]);
        return;
    }

    // Not being in a CloudKit account is an automatic failure.
    // But, wait a good long while for the CloudKit account state to be known (in the case of daemon startup)
    [self.accountStateKnown wait:(SecCKKSTestsEnabled() ? 1*NSEC_PER_SEC : 30*NSEC_PER_SEC)];

    if(self.accountStatus != CKKSAccountStatusAvailable) {
        ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since we don't have an iCloud account.");
        complete(NULL, [NSError errorWithDomain:CKKSErrorDomain
                                           code:CKKSNotLoggedIn
                                    description:@"User is not signed into iCloud."]);
        return;
    }

    CKKSResultOperation* fetchAndProcess = nil;
    if(fetchCloudValue) {
        fetchAndProcess = [self fetchAndProcessCKChanges:CKKSFetchBecauseCurrentItemFetchRequest];
    }

    WEAKIFY(self);
    CKKSResultOperation* getCurrentItem = [CKKSResultOperation named:@"get-current-item-pointer" withBlock:^{
        if(fetchAndProcess.error) {
            ckksnotice("ckkscurrent", self, "Rejecting current item pointer get since fetch failed: %@", fetchAndProcess.error);
            complete(NULL, fetchAndProcess.error);
            return;
        }

        STRONGIFY(self);

        [self dispatchSyncWithReadOnlySQLTransaction:^{
            NSError* error = nil;
            NSString* currentIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];

            CKKSCurrentItemPointer* cip = [CKKSCurrentItemPointer fromDatabase:currentIdentifier
                                                                         state:SecCKKSProcessedStateLocal
                                                                        zoneID:self.zoneID
                                                                         error:&error];
            if(!cip || error) {
                if([error.domain isEqualToString:@"securityd"] && error.code == errSecItemNotFound) {
                    // This error is common and very, very noisy. Shorten it and don't log here (the framework should log for us)
                    ckksinfo("ckkscurrent", self, "No current item pointer for %@", currentIdentifier);
                    error = [NSError errorWithDomain:@"securityd" code:errSecItemNotFound description:[NSString stringWithFormat:@"No current item pointer found for %@", currentIdentifier]];
                } else {
                    ckkserror("ckkscurrent", self, "No current item pointer for %@", currentIdentifier);
                }
                complete(nil, error);
                return;
            }

            if(!cip.currentItemUUID) {
                ckkserror("ckkscurrent", self, "Current item pointer is empty %@", cip);
                complete(nil, [NSError errorWithDomain:CKKSErrorDomain
                                                  code:errSecInternalError
                                           description:@"Current item pointer is empty"]);
                return;
            }

            ckksinfo("ckkscurrent", self, "Retrieved current item pointer: %@", cip);
            complete(cip.currentItemUUID, NULL);
            return;
        }];
    }];

    [getCurrentItem addNullableDependency:fetchAndProcess];
    [self scheduleOperation: getCurrentItem];
}

- (CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*)findKeySet:(BOOL)refetchBeforeReturningKeySet
{
    __block CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = nil;
    __block BOOL moveFromWaitForTrust = NO;

    [self dispatchSyncWithReadOnlySQLTransaction:^{
        keysetOp = (CKKSProvideKeySetOperation*)[self findFirstPendingOperation:self.operationDependencies.keysetProviderOperations];
        if(!keysetOp) {
            keysetOp = [[CKKSProvideKeySetOperation alloc] initWithZoneName:self.zoneName];
            [self.operationDependencies.keysetProviderOperations addObject:keysetOp];

            // This is an abuse of operations: they should generally run when added to a queue, not wait, but this allows recipients to set timeouts
            [self scheduleOperationWithoutDependencies:keysetOp];
        }

        if(refetchBeforeReturningKeySet) {
            ckksnotice("ckks", self, "Refetch requested before returning key set!");

            [self.stateMachine _onqueueHandleFlag:CKKSFlagFetchRequested];
            [self.stateMachine _onqueueHandleFlag:CKKSFlagTLKCreationRequested];

            if([self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
                moveFromWaitForTrust = YES;
            }
            return;
        }

        CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
        if(keyset.currentTLKPointer.currentKeyUUID &&
           (keyset.tlk.uuid ||
            [self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust] ||
            [self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTLK])) {
            ckksnotice("ckks", self, "Already have keyset %@", keyset);

            [keysetOp provideKeySet:keyset];
            return;

        } else if([self.stateMachine.currentState isEqualToString:SecCKKSZoneKeyStateWaitForTrust]) {
            // No keyset exists, but we're in waitfortrust? Seems like a bug. Move us out of this state...

            ckksnotice("ckks", self, "Received a keyset request in an odd state; forwarding to state machine");
            [self.stateMachine _onqueueHandleFlag:CKKSFlagTLKCreationRequested];
            moveFromWaitForTrust = YES;

        } else {
            // The key state machine will know what to do.
            [self.stateMachine _onqueueHandleFlag:CKKSFlagTLKCreationRequested];
        };
    }];

    if(moveFromWaitForTrust) {
        [self.stateMachine handleExternalRequest:[[OctagonStateTransitionRequest alloc] init:@"fix-bug"
                                                                                sourceStates:[NSSet setWithObject:SecCKKSZoneKeyStateWaitForTrust]
                                                                                 serialQueue:self.queue
                                                                                     timeout:5 * NSEC_PER_SEC
                                                                                transitionOp:[OctagonStateTransitionOperation named:@"fix-bug"
                                                                                                                           entering:SecCKKSZoneKeyStateWaitForTLKCreation]]];
    }

    return keysetOp;
}

- (void)receiveTLKUploadRecords:(NSArray<CKRecord*>*)records
{
    // First, filter for records matching this zone
    NSMutableArray<CKRecord*>* zoneRecords = [NSMutableArray array];
    for(CKRecord* record in records) {
        if([record.recordID.zoneID isEqual:self.zoneID]) {
            [zoneRecords addObject:record];
        }
    }

    ckksnotice("ckkskey", self, "Received a set of %lu TLK upload records", (unsigned long)zoneRecords.count);

    if(!zoneRecords || zoneRecords.count == 0) {
        return;
    }

    [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
        for(CKRecord* record in zoneRecords) {
            [self _onqueueCKRecordChanged:record resync:false];
        }

        [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateTLKsUploaded];

        return CKKSDatabaseTransactionCommit;
    }];
}

- (BOOL)requiresTLKUpload
{
    __block BOOL requiresUpload = NO;
    dispatch_sync(self.queue, ^{
        // We want to return true only if we're in a state that immediately requires an upload.
        if(([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLKUpload] ||
            [self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLKCreation])) {
            requiresUpload = YES;
        }
    });

    return requiresUpload;
}

// Use the following method to find the first pending operation in a weak collection
- (NSOperation*)findFirstPendingOperation: (NSHashTable*) table {
    return [self findFirstPendingOperation:table ofClass:nil];
}

// Use the following method to find the first pending operation in a weak collection
- (NSOperation*)findFirstPendingOperation: (NSHashTable*) table ofClass:(Class)class {
    @synchronized(table) {
        for(NSOperation* op in table) {
            if(op != nil && [op isPending] && (class == nil || [op isKindOfClass: class])) {
                return op;
            }
        }
        return nil;
    }
}

- (CKKSOutgoingQueueOperation*)processOutgoingQueue:(CKOperationGroup* _Nullable)ckoperationGroup {
    return [self processOutgoingQueueAfter:nil ckoperationGroup:ckoperationGroup];
}

- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation* _Nullable)after
                                        ckoperationGroup:(CKOperationGroup* _Nullable)ckoperationGroup {
    return [self processOutgoingQueueAfter:after requiredDelay:DISPATCH_TIME_FOREVER ckoperationGroup:ckoperationGroup];
}

- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation* _Nullable)after
                                           requiredDelay:(uint64_t)requiredDelay
                                        ckoperationGroup:(CKOperationGroup* _Nullable)ckoperationGroup
{
    CKKSOutgoingQueueOperation* outgoingop =
            (CKKSOutgoingQueueOperation*) [self findFirstPendingOperation:self.outgoingQueueOperations
                                                                  ofClass:[CKKSOutgoingQueueOperation class]];
    if(outgoingop) {
        if(after) {
            [outgoingop addDependency: after];
        }
        if([outgoingop isPending]) {
            if(!outgoingop.ckoperationGroup && ckoperationGroup) {
                outgoingop.ckoperationGroup = ckoperationGroup;
            } else if(ckoperationGroup) {
                ckkserror("ckks", self, "Throwing away CKOperationGroup(%@) in favor of (%@)", ckoperationGroup.name, outgoingop.ckoperationGroup.name);
            }

            // Will log any pending dependencies as well
            ckksinfo("ckksoutgoing", self, "Returning existing %@", outgoingop);

            // Shouldn't be necessary, but can't hurt
            [self.outgoingQueueOperationScheduler triggerAt:requiredDelay];
            return outgoingop;
        }
    }

    CKKSOutgoingQueueOperation* op = [[CKKSOutgoingQueueOperation alloc] initWithDependencies:self.operationDependencies
                                                                                         ckks:self
                                                                                    intending:SecCKKSZoneKeyStateReady
                                                                                   errorState:SecCKKSZoneKeyStateUnhealthy
                                                                             ckoperationGroup:ckoperationGroup];
    op.name = @"outgoing-queue-operation";
    [op addNullableDependency:after];
    [op addNullableDependency:self.outgoingQueueOperationScheduler.operationDependency];

    [self.outgoingQueueOperationScheduler triggerAt:requiredDelay];

    [op linearDependencies:self.outgoingQueueOperations];

    [self scheduleOperation: op];
    ckksnotice("ckksoutgoing", self, "Scheduled %@", op);
    return op;
}

- (void)processIncomingQueueAfterNextUnlock {
    // Thread races aren't so important here; we might end up with two or three copies of this operation, but that's okay.
    if(![self.processIncomingQueueAfterNextUnlockOperation isPending]) {
        WEAKIFY(self);

        CKKSResultOperation* restartIncomingQueueOperation = [CKKSResultOperation operationWithBlock:^{
            STRONGIFY(self);
            // This IQO shouldn't error if the keybag has locked again. It will simply try again later.
            [self processIncomingQueue:false];
        }];

        restartIncomingQueueOperation.name = @"reprocess-incoming-queue-after-unlock";
        self.processIncomingQueueAfterNextUnlockOperation = restartIncomingQueueOperation;

        [restartIncomingQueueOperation addNullableDependency:self.lockStateTracker.unlockDependency];
        [self scheduleOperation: restartIncomingQueueOperation];
    }
}

- (CKKSResultOperation*)resultsOfNextProcessIncomingQueueOperation {
    if(self.resultsOfNextIncomingQueueOperationOperation && [self.resultsOfNextIncomingQueueOperationOperation isPending]) {
        return self.resultsOfNextIncomingQueueOperationOperation;
    }

    // Else, make a new one.
    self.resultsOfNextIncomingQueueOperationOperation = [CKKSResultOperation named:[NSString stringWithFormat:@"wait-for-next-incoming-queue-operation-%@", self.zoneName] withBlock:^{}];
    return self.resultsOfNextIncomingQueueOperationOperation;
}

- (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA {
    return [self processIncomingQueue:failOnClassA after: nil];
}

- (CKKSIncomingQueueOperation*) processIncomingQueue:(bool)failOnClassA after: (CKKSResultOperation*) after {
    return [self processIncomingQueue:failOnClassA after:after policyConsideredAuthoritative:false];
}

- (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA
                                              after:(CKKSResultOperation*)after
                      policyConsideredAuthoritative:(bool)policyConsideredAuthoritative
{
    CKKSIncomingQueueOperation* incomingop = (CKKSIncomingQueueOperation*) [self findFirstPendingOperation:self.incomingQueueOperations];
    if(incomingop) {
        ckksinfo("ckks", self, "Skipping processIncomingQueue due to at least one pending instance");
        if(after) {
            [incomingop addNullableDependency: after];
        }

        // check (again) for race condition; if the op has started we need to add another (for the dependency)
        if([incomingop isPending]) {
            incomingop.errorOnClassAFailure |= failOnClassA;
            incomingop.handleMismatchedViewItems |= policyConsideredAuthoritative;
            return incomingop;
        }
    }

    CKKSIncomingQueueOperation* op = [[CKKSIncomingQueueOperation alloc] initWithDependencies:self.operationDependencies
                                                                                         ckks:self
                                                                                    intending:SecCKKSZoneKeyStateReady
                                                                                   errorState:SecCKKSZoneKeyStateUnhealthy
                                                                         errorOnClassAFailure:failOnClassA
                                                                        handleMismatchedViewItems:policyConsideredAuthoritative];
    op.name = @"incoming-queue-operation";
    if(after != nil) {
        [op addSuccessDependency: after];
    }

    if(self.resultsOfNextIncomingQueueOperationOperation) {
        [self.resultsOfNextIncomingQueueOperationOperation addSuccessDependency:op];
        [self scheduleOperation:self.resultsOfNextIncomingQueueOperationOperation];
        self.resultsOfNextIncomingQueueOperationOperation = nil;
    }

    [self scheduleOperation: op];
    return op;
}

- (CKKSScanLocalItemsOperation*)scanLocalItems:(NSString*)operationName {
    return [self scanLocalItems:operationName ckoperationGroup:nil after:nil];
}

- (CKKSScanLocalItemsOperation*)scanLocalItems:(NSString*)operationName
                              ckoperationGroup:(CKOperationGroup*)operationGroup
                                         after:(NSOperation*)after
{
    CKKSScanLocalItemsOperation* scanOperation = (CKKSScanLocalItemsOperation*)[self findFirstPendingOperation:self.scanLocalItemsOperations];

    if(scanOperation) {
        [scanOperation addNullableDependency:after];

        // check (again) for race condition; if the op has started we need to add another (for the dependency)
        if([scanOperation isPending]) {
            scanOperation.ckoperationGroup = operationGroup;

            scanOperation.name = [NSString stringWithFormat:@"%@::%@", scanOperation.name, operationName];
            return scanOperation;
        }
    }

    scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithDependencies:self.operationDependencies
                                                                         ckks:self
                                                                    intending:SecCKKSZoneKeyStateReady
                                                                   errorState:SecCKKSZoneKeyStateError
                                                             ckoperationGroup:operationGroup];
    scanOperation.name = operationName;

    [scanOperation addNullableDependency:self.lastFixupOperation];
    [scanOperation addNullableDependency:self.lockStateTracker.unlockDependency];
    [scanOperation addNullableDependency:self.keyStateReadyDependency];
    [scanOperation addNullableDependency:after];

    [scanOperation linearDependencies:self.scanLocalItemsOperations];

    // This might generate items for upload. Make sure that any uploads wait until the scan is complete, so we know what to upload
    [scanOperation linearDependencies:self.outgoingQueueOperations];

    [self scheduleOperation:scanOperation];
    self.initiatedLocalScan = YES;
    return scanOperation;
}

- (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit
                   waitForKeyHierarchyInitialization:(uint64_t)timeout
                                    ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
    // If securityd just started, the key state might be in some transient early state. Wait a bit.
    OctagonStateMultiStateArrivalWatcher* waitForTransient = [[OctagonStateMultiStateArrivalWatcher alloc] initNamed:@"rpc-watcher"
                                                                                                         serialQueue:self.queue
                                                                                                              states:CKKSKeyStateNonTransientStates()];
    [waitForTransient timeout:timeout];
    [self.stateMachine registerMultiStateArrivalWatcher:waitForTransient];

    CKKSUpdateDeviceStateOperation* op = [[CKKSUpdateDeviceStateOperation alloc] initWithCKKSKeychainView:self rateLimit:rateLimit ckoperationGroup:ckoperationGroup];
    op.name = @"device-state-operation";

    [op addDependency:waitForTransient.result];

    // op modifies the CloudKit zone, so it should insert itself into the list of OutgoingQueueOperations.
    // Then, we won't have simultaneous zone-modifying operations and confuse ourselves.
    // However, since we might have pending OQOs, it should try to insert itself at the beginning of the linearized list
    [op linearDependenciesWithSelfFirst:self.outgoingQueueOperations];

    // CKKSUpdateDeviceStateOperations are special: they should fire even if we don't believe we're in an iCloud account.
    // They also shouldn't block or be blocked by any other operation; our wait operation above will handle that
    [self scheduleOperationWithoutDependencies:op];
    return op;
}

- (void)xpc24HrNotification
{
    // Called roughly once every 24hrs
    [self.stateMachine handleFlag:CKKSFlag24hrNotification];
}

// There are some errors which won't be reported but will be reflected in the CDSE; any error coming out of here is fatal
- (CKKSDeviceStateEntry*)_onqueueCurrentDeviceStateEntry: (NSError* __autoreleasing*)error {
    dispatch_assert_queue(self.queue);
    NSError* localerror = nil;

    CKKSAccountStateTracker* accountTracker = self.accountTracker;
    CKKSAccountStatus hsa2Status = accountTracker.hsa2iCloudAccountStatus;

    // We must have an HSA2 iCloud account and a CloudKit account to even create one of these
    if(hsa2Status != CKKSAccountStatusAvailable ||
       accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable) {
        ckkserror("ckksdevice", self, "No iCloud account active: %@ hsa2 account:%@",
                  accountTracker.currentCKAccountInfo,
                  CKKSAccountStatusToString(hsa2Status));
        localerror = [NSError errorWithDomain:@"securityd"
                                         code:errSecInternalError
                                     userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No active HSA2 iCloud account: %@", accountTracker.currentCKAccountInfo]}];
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    NSString* ckdeviceID = accountTracker.ckdeviceID;
    if(ckdeviceID == nil) {
        ckkserror("ckksdevice", self, "No CK device ID available; cannot make device state entry");
        localerror = [NSError errorWithDomain:CKKSErrorDomain
                                         code:CKKSNotLoggedIn
                                     userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No CK device ID: %@", accountTracker.currentCKAccountInfo]}];
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    CKKSDeviceStateEntry* oldcdse = [CKKSDeviceStateEntry tryFromDatabase:ckdeviceID zoneID:self.zoneID error:&localerror];
    if(localerror) {
        ckkserror("ckksdevice", self, "Couldn't read old CKKSDeviceStateEntry from database: %@", localerror);
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    // Find out what we think the current keys are
    CKKSCurrentKeyPointer* currentTLKPointer    = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassTLK zoneID:self.zoneID error:&localerror];
    CKKSCurrentKeyPointer* currentClassAPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassA   zoneID:self.zoneID error:&localerror];
    CKKSCurrentKeyPointer* currentClassCPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassC   zoneID:self.zoneID error:&localerror];
    if(localerror) {
        // Things is broken, but the whole point of this record is to share the brokenness. Continue.
        ckkserror("ckksdevice", self, "Couldn't read current key pointers from database: %@; proceeding", localerror);
        localerror = nil;
    }

    CKKSKey* suggestedTLK       = currentTLKPointer.currentKeyUUID    ? [CKKSKey tryFromDatabase:currentTLKPointer.currentKeyUUID    zoneID:self.zoneID error:&localerror] : nil;
    CKKSKey* suggestedClassAKey = currentClassAPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassAPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;
    CKKSKey* suggestedClassCKey = currentClassCPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:currentClassCPointer.currentKeyUUID zoneID:self.zoneID error:&localerror] : nil;

    if(localerror) {
        // Things is broken, but the whole point of this record is to share the brokenness. Continue.
        ckkserror("ckksdevice", self, "Couldn't read keys from database: %@; proceeding", localerror);
        localerror = nil;
    }

    // Check if we posess the keys in the keychain
    [suggestedTLK ensureKeyLoaded:&localerror];
    if(localerror && [self.lockStateTracker isLockedError:localerror]) {
        ckkserror("ckksdevice", self, "Device is locked; couldn't read TLK from keychain. Assuming it is present and continuing; error was %@", localerror);
        localerror = nil;
    } else if(localerror) {
        ckkserror("ckksdevice", self, "Couldn't read TLK from keychain. We do not have a current TLK. Error was %@", localerror);
        suggestedTLK = nil;
    }

    [suggestedClassAKey ensureKeyLoaded:&localerror];
    if(localerror && [self.lockStateTracker isLockedError:localerror]) {
        ckkserror("ckksdevice", self, "Device is locked; couldn't read ClassA key from keychain. Assuming it is present and continuing; error was %@", localerror);
        localerror = nil;
    } else if(localerror) {
        ckkserror("ckksdevice", self, "Couldn't read ClassA key from keychain. We do not have a current ClassA key. Error was %@", localerror);
        suggestedClassAKey = nil;
    }

    [suggestedClassCKey ensureKeyLoaded:&localerror];
    // class C keys are stored class C, so uh, don't check lock state.
    if(localerror) {
        ckkserror("ckksdevice", self, "Couldn't read ClassC key from keychain. We do not have a current ClassC key. Error was %@", localerror);
        suggestedClassCKey = nil;
    }

    // We'd like to have the circle peer ID. Give the account state tracker a fighting chance, but not having it is not an error
    // But, if the platform doesn't have SOS, don't bother
    if(OctagonPlatformSupportsSOS() && [accountTracker.accountCirclePeerIDInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.accountCirclePeerID) {
        ckkserror("ckksdevice", self, "No SOS peer ID available");
    }

    // We'd also like the Octagon status
    if([accountTracker.octagonInformationInitialized wait:500*NSEC_PER_MSEC] != 0 && !accountTracker.octagonPeerID) {
        ckkserror("ckksdevice", self, "No octagon peer ID available");
    }

    // Reset the last unlock time to 'day' granularity in UTC
    NSCalendar* calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
    calendar.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
    NSDate* lastUnlockDay = self.lockStateTracker.lastUnlockTime;
    lastUnlockDay = lastUnlockDay ? [calendar startOfDayForDate:lastUnlockDay] : nil;

    // We only really want the oldcdse for its encodedCKRecord, so make a new cdse here
    CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initForDevice:ckdeviceID
                                                                      osVersion:SecCKKSHostOSVersion()
                                                                 lastUnlockTime:lastUnlockDay
                                                                  octagonPeerID:accountTracker.octagonPeerID
                                                                  octagonStatus:accountTracker.octagonStatus
                                                                   circlePeerID:accountTracker.accountCirclePeerID
                                                                   circleStatus:accountTracker.currentCircleStatus.status
                                                                       keyState:self.keyHierarchyState
                                                                 currentTLKUUID:suggestedTLK.uuid
                                                              currentClassAUUID:suggestedClassAKey.uuid
                                                              currentClassCUUID:suggestedClassCKey.uuid
                                                                         zoneID:self.zoneID
                                                                encodedCKRecord:oldcdse.encodedCKRecord];
    return newcdse;
}

- (CKKSSynchronizeOperation*) resyncWithCloud {
    CKKSSynchronizeOperation* op = [[CKKSSynchronizeOperation alloc] initWithCKKSKeychainView: self];
    [self scheduleOperation: op];
    return op;
}

- (CKKSLocalSynchronizeOperation*)resyncLocal {
    CKKSLocalSynchronizeOperation* op = [[CKKSLocalSynchronizeOperation alloc] initWithCKKSKeychainView:self];
    [self scheduleOperation: op];
    return op;
}

- (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because
{
     if(!SecCKKSIsEnabled()) {
        ckksinfo("ckks", self, "Skipping fetchAndProcessCKChanges due to disabled CKKS");
        return nil;
    }

     // We fetched some changes; try to process them!
    return [self processIncomingQueue:false after:[self.zoneChangeFetcher requestSuccessfulFetch:because]];
}

// Lets the view know about a failed CloudKit write. If the error is "already have one of these records", it will
// store the new records and kick off the new processing
//
// Note that you need to tell this function the records you wanted to save, so it can determine what needs deletion
- (bool)_onqueueCKWriteFailed:(NSError*)ckerror attemptedRecordsChanged:(NSDictionary<CKRecordID*, CKRecord*>*)savedRecords {
    dispatch_assert_queue(self.queue);

    NSDictionary<CKRecordID*,NSError*>* partialErrors = ckerror.userInfo[CKPartialErrorsByItemIDKey];
    if([ckerror.domain isEqual:CKErrorDomain] && ckerror.code == CKErrorPartialFailure && partialErrors) {
        // Check if this error was "you're out of date"
        bool recordChanged = true;

        for(NSError* error in partialErrors.allValues) {
            if((![error.domain isEqual:CKErrorDomain]) || (error.code != CKErrorBatchRequestFailed && error.code != CKErrorServerRecordChanged && error.code != CKErrorUnknownItem)) {
                // There's an error in there that isn't CKErrorServerRecordChanged, CKErrorBatchRequestFailed, or CKErrorUnknownItem. Don't handle nicely...
                recordChanged = false;
            }
        }

        if(recordChanged) {
            ckksnotice("ckks", self, "Received a ServerRecordChanged error, attempting to update new records and delete unknown ones");

            bool updatedRecord = false;

            for(CKRecordID* recordID in partialErrors.allKeys) {
                NSError* error = partialErrors[recordID];
                if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorServerRecordChanged) {
                    CKRecord* newRecord = error.userInfo[CKRecordChangedErrorServerRecordKey];
                    ckksnotice("ckks", self, "On error: updating our idea of: %@", newRecord);

                    updatedRecord |= [self _onqueueCKRecordChanged:newRecord resync:true];
                } else if([error.domain isEqual:CKErrorDomain] && error.code == CKErrorUnknownItem) {
                    CKRecord* record = savedRecords[recordID];
                    ckksnotice("ckks", self, "On error: handling an unexpected delete of: %@ %@", recordID, record);

                    updatedRecord |= [self _onqueueCKRecordDeleted:recordID recordType:record.recordType resync:true];
                }
            }

            if(updatedRecord) {
                [self processIncomingQueue:false];
                return true;
            }
        }

        // Check if this error was the CKKS server extension rejecting the write
        for(CKRecordID* recordID in partialErrors.allKeys) {
            NSError* error = partialErrors[recordID];

            NSError* underlyingError = error.userInfo[NSUnderlyingErrorKey];
            NSError* thirdLevelError = underlyingError.userInfo[NSUnderlyingErrorKey];
            ckksnotice("ckks", self, "Examining 'write failed' error: %@ %@ %@", error, underlyingError, thirdLevelError);

            if([error.domain isEqualToString:CKErrorDomain] && error.code == CKErrorServerRejectedRequest &&
               underlyingError && [underlyingError.domain isEqualToString:CKInternalErrorDomain] && underlyingError.code == CKErrorInternalPluginError &&
               thirdLevelError && [thirdLevelError.domain isEqualToString:@"CloudkitKeychainService"]) {

                if(thirdLevelError.code == CKKSServerUnexpectedSyncKeyInChain) {
                    // The server thinks the classA/C synckeys don't wrap directly the to top TLK, but we don't (otherwise, we would have fixed it).
                    // Issue a key hierarchy fetch and see what's what.
                    ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@; requesting refetch and reprocess of key hierarchy", thirdLevelError, recordID);
                    [self.stateMachine _onqueueHandleFlag:CKKSFlagFetchRequested];

                } else if(thirdLevelError.code == CKKSServerMissingRecord) {
                    // The server is concerned that there's a missing record somewhere.
                    // Issue a key hierarchy fetch and see what's happening
                    ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@; requesting refetch and reprocess of key hierarchy", thirdLevelError, recordID);
                    [self.stateMachine _onqueueHandleFlag:CKKSFlagFetchRequested];

                } else {
                    ckkserror("ckks", self, "CKKS Server extension has told us about %@ for record %@, but we don't currently handle this error", thirdLevelError, recordID);
                }
            }
        }
    }

    return false;
}

- (bool)_onqueueCKRecordDeleted:(CKRecordID*)recordID recordType:(NSString*)recordType resync:(bool)resync {
    dispatch_assert_queue(self.queue);

    // TODO: resync doesn't really mean much here; what does it mean for a record to be 'deleted' if you're fetching from scratch?

    if([recordType isEqual: SecCKRecordItemType]) {
        ckksnotice("ckks", self, "CloudKit notification: deleted record(%@): %@", recordType, recordID);
        NSError* error = nil;
        NSError* iqeerror = nil;
        CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase: [recordID recordName] zoneID:self.zoneID error: &error];

        // Deletes always succeed, not matter the generation count
        if(ckme) {
            [ckme deleteFromDatabase:&error];

            CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item action:SecCKKSActionDelete state:SecCKKSStateNew];
            [iqe saveToDatabase:&iqeerror];
            if(iqeerror) {
                ckkserror("ckks", self, "Couldn't save incoming queue entry: %@", iqeerror);
            }

            // Delete any pending local changes; this delete wins
            NSArray<CKKSOutgoingQueueEntry*>* siblings = [CKKSOutgoingQueueEntry allWithUUID:iqe.uuid
                                                                                      states:@[SecCKKSStateNew,
                                                                                               SecCKKSStateReencrypt,
                                                                                               SecCKKSStateError]
                                                                                      zoneID:self.zoneID
                                                                                       error:&error];
            if(error) {
                ckkserror("ckks", self, "Couldn't load OQE sibling for %@: %@", iqe.uuid, error);
            }

            for(CKKSOutgoingQueueEntry* oqe in siblings) {
                NSError* deletionError = nil;
                [oqe deleteFromDatabase:&deletionError];
                if(deletionError) {
                    ckkserror("ckks", self, "Couldn't delete OQE sibling(%@) for %@: %@", oqe, iqe.uuid, deletionError);
                }
            }
        }
        ckksinfo("ckks", self, "CKKSMirrorEntry was deleted: %@ %@ error: %@", recordID, ckme, error);
        // TODO: actually pass error back up
        return (error == nil);

    } else if([recordType isEqual: SecCKRecordCurrentItemType]) {
        ckksinfo("ckks", self, "CloudKit notification: deleted current item pointer(%@): %@", recordType, recordID);
        NSError* error = nil;

        [[CKKSCurrentItemPointer tryFromDatabase:[recordID recordName] state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&error] deleteFromDatabase:&error];
        [[CKKSCurrentItemPointer fromDatabase:[recordID recordName]    state:SecCKKSProcessedStateLocal  zoneID:self.zoneID error:&error] deleteFromDatabase:&error];

        ckksinfo("ckks", self, "CKKSCurrentItemPointer was deleted: %@ error: %@", recordID, error);
        return (error == nil);

    } else if([recordType isEqual: SecCKRecordIntermediateKeyType]) {
        // TODO: handle in some interesting way
        return true;
    } else if([recordType isEqual: SecCKRecordTLKShareType]) {
        NSError* error = nil;
        ckksinfo("ckks", self, "CloudKit notification: deleted tlk share record(%@): %@", recordType, recordID);
        CKKSTLKShareRecord* share = [CKKSTLKShareRecord tryFromDatabaseFromCKRecordID:recordID error:&error];
        [share deleteFromDatabase:&error];

        if(error) {
            ckkserror("ckks", self, "CK notification: Couldn't delete deleted TLKShare: %@ %@", recordID,  error);
        }
        return (error == nil);

    } else if([recordType isEqual: SecCKRecordDeviceStateType]) {
        NSError* error = nil;
        ckksinfo("ckks", self, "CloudKit notification: deleted device state record(%@): %@", recordType, recordID);

        CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry tryFromDatabaseFromCKRecordID:recordID error:&error];
        [cdse deleteFromDatabase: &error];
        ckksinfo("ckks", self, "CKKSCurrentItemPointer(%@) was deleted: %@ error: %@", cdse, recordID, error);

        return (error == nil);

    } else if ([recordType isEqualToString:SecCKRecordManifestType]) {
        ckksinfo("ckks", self, "CloudKit notification: deleted manifest record (%@): %@", recordType, recordID);
        
        NSError* error = nil;
        CKKSManifest* manifest = [CKKSManifest manifestForRecordName:recordID.recordName error:&error];
        if (manifest) {
            [manifest deleteFromDatabase:&error];
        }
        
        ckksinfo("ckks", self, "CKKSManifest was deleted: %@ %@ error: %@", recordID, manifest, error);
        // TODO: actually pass error back up
        return error == nil;
    }

    else {
        ckkserror("ckksfetch", self, "unknown record type: %@ %@", recordType, recordID);
        return false;
    }
}

- (bool)_onqueueCKRecordChanged:(CKRecord*)record resync:(bool)resync {
    dispatch_assert_queue(self.queue);

    @autoreleasepool {
        ckksnotice("ckksfetch", self, "Processing record modification(%@): %@", record.recordType, record);

        if([[record recordType] isEqual: SecCKRecordItemType]) {
            [self _onqueueCKRecordItemChanged:record resync:resync];
            return true;
        } else if([[record recordType] isEqual: SecCKRecordCurrentItemType]) {
            [self _onqueueCKRecordCurrentItemPointerChanged:record resync:resync];
            return true;
        } else if([[record recordType] isEqual: SecCKRecordIntermediateKeyType]) {
            [self _onqueueCKRecordKeyChanged:record resync:resync];
            return true;
        } else if ([[record recordType] isEqual: SecCKRecordTLKShareType]) {
            [self _onqueueCKRecordTLKShareChanged:record resync:resync];
            return true;
        } else if([[record recordType] isEqualToString: SecCKRecordCurrentKeyType]) {
            [self _onqueueCKRecordCurrentKeyPointerChanged:record resync:resync];
            return true;
        } else if ([[record recordType] isEqualToString:SecCKRecordManifestType]) {
            [self _onqueueCKRecordManifestChanged:record resync:resync];
            return true;
        } else if ([[record recordType] isEqualToString:SecCKRecordManifestLeafType]) {
            [self _onqueueCKRecordManifestLeafChanged:record resync:resync];
            return true;
        } else if ([[record recordType] isEqualToString:SecCKRecordDeviceStateType]) {
            [self _onqueueCKRecordDeviceStateChanged:record resync:resync];
            return true;
        } else {
            ckkserror("ckksfetch", self, "unknown record type: %@ %@", [record recordType], record);
            return false;
        }
    }
}

- (void)_onqueueCKRecordItemChanged:(CKRecord*)record resync:(bool)resync {
    dispatch_assert_queue(self.queue);

    NSError* error = nil;
    // Find if we knew about this record in the past
    bool update = false;
    CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: [[record recordID] recordName] zoneID:self.zoneID error:&error];

    if(error) {
        ckkserror("ckks", self, "error loading a CKKSMirrorEntry from database: %@", error);
        // TODO: quit?
    }

    if(resync) {
        if(!ckme) {
            ckkserror("ckksresync", self, "BUG: No local item matching resynced CloudKit record: %@", record);
        } else if(![ckme matchesCKRecord:record]) {
            ckkserror("ckksresync", self, "BUG: Local item doesn't match resynced CloudKit record: %@ %@", ckme, record);
        } else {
            ckksnotice("ckksresync", self, "Already know about this item record, updating anyway: %@", record.recordID);
        }
    }

    if(ckme && ckme.item && ckme.item.generationCount > [record[SecCKRecordGenerationCountKey] unsignedLongLongValue]) {
        ckkserror("ckks", self, "received a record from CloudKit with a bad generation count: %@ (%ld > %@)", ckme.uuid,
                 (long) ckme.item.generationCount,
                 record[SecCKRecordGenerationCountKey]);
        // Abort processing this record.
        return;
    }

    // If we found an old version in the database; this might be an update
    if(ckme) {
        if([ckme matchesCKRecord:record] && !resync) {
            // This is almost certainly a record we uploaded; CKFetchChanges sends them back as new records
            ckksnotice("ckks", self, "CloudKit has told us of record we already know about for %@; skipping update", ckme.uuid);
            return;
        }

        update = true;
        // Set the CKKSMirrorEntry's fields to be whatever this record holds
        [ckme setFromCKRecord: record];
    } else {
        // Have to make a new CKKSMirrorEntry
        ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
    }

    [ckme saveToDatabase: &error];

    if(error) {
        ckkserror("ckks", self, "couldn't save new CKRecord to database: %@ %@", record, error);
    } else {
        ckksinfo("ckks", self, "CKKSMirrorEntry was created: %@", ckme);
    }

    NSError* iqeerror = nil;
    CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:ckme.item
                                                                            action:(update ? SecCKKSActionModify : SecCKKSActionAdd)
                                                                             state:SecCKKSStateNew];
    [iqe saveToDatabase:&iqeerror];
    if(iqeerror) {
        ckkserror("ckks", self, "Couldn't save modified incoming queue entry: %@", iqeerror);
    } else {
        ckksinfo("ckks", self, "CKKSIncomingQueueEntry was created: %@", iqe);
    }

    // A remote change has occured for this record. Delete any pending local changes; they will be overwritten.
    NSArray<CKKSOutgoingQueueEntry*>* siblings = [CKKSOutgoingQueueEntry allWithUUID:iqe.uuid
                                                                              states:@[SecCKKSStateNew,
                                                                                       SecCKKSStateReencrypt,
                                                                                       SecCKKSStateError]
                                                                              zoneID:self.zoneID
                                                                               error:&error];
    if(error) {
        ckkserror("ckks", self, "Couldn't load OQE sibling for %@: %@", iqe.uuid, error);
    }

    for(CKKSOutgoingQueueEntry* oqe in siblings) {
        NSError* deletionError = nil;
        [oqe deleteFromDatabase:&deletionError];
        if(deletionError) {
            ckkserror("ckks", self, "Couldn't delete OQE sibling(%@) for %@: %@", oqe, iqe.uuid, deletionError);
        }
    }
}

- (void)_onqueueCKRecordKeyChanged:(CKRecord*)record resync:(bool)resync {
    dispatch_assert_queue(self.queue);

    NSError* error = nil;

    if(resync) {
        NSError* resyncerror = nil;

        CKKSKey* key = [CKKSKey tryFromDatabaseAnyState:record.recordID.recordName zoneID:self.zoneID error:&resyncerror];
        if(resyncerror) {
            ckkserror("ckksresync", self, "error loading key: %@", resyncerror);
        }
        if(!key) {
            ckkserror("ckksresync", self, "BUG: No sync key matching resynced CloudKit record: %@", record);
        } else if(![key matchesCKRecord:record]) {
            ckkserror("ckksresync", self, "BUG: Local sync key doesn't match resynced CloudKit record(s): %@ %@", key, record);
        } else {
            ckksnotice("ckksresync", self, "Already know about this sync key, skipping update: %@", record);
            return;
        }
    }

    CKKSKey* remotekey = [[CKKSKey alloc] initWithCKRecord: record];

    // Do we already know about this key?
    CKKSKey* possibleLocalKey = [CKKSKey tryFromDatabase:remotekey.uuid zoneID:self.zoneID error:&error];
    if(error) {
        ckkserror("ckkskey", self, "Error findibg exsiting local key for %@: %@", remotekey, error);
        // Go on, assuming there isn't a local key
    } else if(possibleLocalKey && [possibleLocalKey matchesCKRecord:record]) {
        // Okay, nothing new here. Update the CKRecord and move on.
        // Note: If the new record doesn't match the local copy, we have to go through the whole dance below
        possibleLocalKey.storedCKRecord = record;
        [possibleLocalKey saveToDatabase:&error];

        if(error) {
            ckkserror("ckkskey", self, "Couldn't update existing key: %@: %@", possibleLocalKey, error);
        }
        return;
    }

    // Drop into the synckeys table as a 'remote' key, then ask for a rekey operation.
    remotekey.state = SecCKKSProcessedStateRemote;
    remotekey.currentkey = false;

    [remotekey saveToDatabase:&error];
    if(error) {
        ckkserror("ckkskey", self, "Couldn't save key record to database: %@: %@", remotekey, error);
        ckksinfo("ckkskey", self, "CKRecord was %@", record);
    }

    // We've saved a new key in the database; trigger a rekey operation.
    [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
}

- (void)_onqueueCKRecordTLKShareChanged:(CKRecord*)record resync:(bool)resync {
    dispatch_assert_queue(self.queue);

    NSError* error = nil;
    if(resync) {
        // TODO fill in
    }

    // CKKSTLKShares get saved with no modification
    CKKSTLKShareRecord* share = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
    [share saveToDatabase:&error];
    if(error) {
        ckkserror("ckksshare", self, "Couldn't save new TLK share to database: %@ %@", share, error);
    }

    [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
}

- (void)_onqueueCKRecordCurrentKeyPointerChanged:(CKRecord*)record resync:(bool)resync {
    dispatch_assert_queue(self.queue);

    // Pull out the old CKP, if it exists
    NSError* ckperror = nil;
    CKKSCurrentKeyPointer* oldckp = [CKKSCurrentKeyPointer tryFromDatabase:((CKKSKeyClass*) record.recordID.recordName) zoneID:self.zoneID error:&ckperror];
    if(ckperror) {
        ckkserror("ckkskey", self, "error loading ckp: %@", ckperror);
    }

    if(resync) {
        if(!oldckp) {
            ckkserror("ckksresync", self, "BUG: No current key pointer matching resynced CloudKit record: %@", record);
        } else if(![oldckp matchesCKRecord:record]) {
            ckkserror("ckksresync", self, "BUG: Local current key pointer doesn't match resynced CloudKit record: %@ %@", oldckp, record);
        } else {
            ckksnotice("ckksresync", self, "Current key pointer has 'changed', but it matches our local copy: %@", record);
        }
    }

    NSError* error = nil;
    CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: record];

    [currentkey saveToDatabase: &error];
    if(error) {
        ckkserror("ckkskey", self, "Couldn't save current key pointer to database: %@: %@", currentkey, error);
        ckksinfo("ckkskey", self, "CKRecord was %@", record);
    }

    if([oldckp matchesCKRecord:record]) {
        ckksnotice("ckkskey", self, "Current key pointer modification doesn't change anything interesting; skipping reprocess: %@", record);
    } else {
        // We've saved a new key in the database; trigger a rekey operation.
        [self.stateMachine _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested];
    }
}

- (void)_onqueueCKRecordCurrentItemPointerChanged:(CKRecord*)record resync:(bool)resync {
    dispatch_assert_queue(self.queue);

    if(resync) {
        NSError* ciperror = nil;
        CKKSCurrentItemPointer* localcip  = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateLocal  zoneID:self.zoneID error:&ciperror];
        CKKSCurrentItemPointer* remotecip = [CKKSCurrentItemPointer tryFromDatabase:record.recordID.recordName state:SecCKKSProcessedStateRemote zoneID:self.zoneID error:&ciperror];
        if(ciperror) {
            ckkserror("ckksresync", self, "error loading cip: %@", ciperror);
        }
        if(!(localcip || remotecip)) {
            ckkserror("ckksresync", self, "BUG: No current item pointer matching resynced CloudKit record: %@", record);
        } else if(! ([localcip matchesCKRecord:record] || [remotecip matchesCKRecord:record]) ) {
            ckkserror("ckksresync", self, "BUG: Local current item pointer doesn't match resynced CloudKit record(s): %@ %@ %@", localcip, remotecip, record);
        } else {
            ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
            return;
        }
    }

    NSError* error = nil;
    CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initWithCKRecord: record];
    cip.state = SecCKKSProcessedStateRemote;

    [cip saveToDatabase: &error];
    if(error) {
        ckkserror("currentitem", self, "Couldn't save current item pointer to database: %@: %@ %@", cip, error, record);
    }
}

- (void)_onqueueCKRecordManifestChanged:(CKRecord*)record resync:(bool)resync
{
    dispatch_assert_queue(self.queue);
    NSError* error = nil;
    CKKSPendingManifest* manifest = [[CKKSPendingManifest alloc] initWithCKRecord:record];
    [manifest saveToDatabase:&error];
    if (error) {
        ckkserror("CKKS", self, "Failed to save fetched manifest record to database: %@: %@", manifest, error);
        ckksinfo("CKKS", self, "manifest CKRecord was %@", record);
    }
}

- (void)_onqueueCKRecordManifestLeafChanged:(CKRecord*)record resync:(bool)resync
{
    dispatch_assert_queue(self.queue);
    NSError* error = nil;
    CKKSManifestLeafRecord* manifestLeaf = [[CKKSManifestPendingLeafRecord alloc] initWithCKRecord:record];
    [manifestLeaf saveToDatabase:&error];
    if (error) {
        ckkserror("CKKS", self, "Failed to save fetched manifest leaf record to database: %@: %@", manifestLeaf, error);
        ckksinfo("CKKS", self, "manifest leaf CKRecord was %@", record);
    }
}

- (void)_onqueueCKRecordDeviceStateChanged:(CKRecord*)record resync:(bool)resync {
    dispatch_assert_queue(self.queue);
    if(resync) {
        NSError* dserror = nil;
        CKKSDeviceStateEntry* cdse  = [CKKSDeviceStateEntry tryFromDatabase:record.recordID.recordName zoneID:self.zoneID error:&dserror];
        if(dserror) {
            ckkserror("ckksresync", self, "error loading cdse: %@", dserror);
        }
        if(!cdse) {
            ckkserror("ckksresync", self, "BUG: No current device state entry matching resynced CloudKit record: %@", record);
        } else if(![cdse matchesCKRecord:record]) {
            ckkserror("ckksresync", self, "BUG: Local current device state entry doesn't match resynced CloudKit record(s): %@ %@", cdse, record);
        } else {
            ckksnotice("ckksresync", self, "Already know about this current item pointer, skipping update: %@", record);
            return;
        }
    }

    NSError* error = nil;
    CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
    [cdse saveToDatabase:&error];
    if (error) {
        ckkserror("ckksdevice", self, "Failed to save device record to database: %@: %@ %@", cdse, error, record);
    }
}

- (bool)_onqueueResetAllInflightOQE:(NSError**)error {
    dispatch_assert_queue(self.queue);
    NSError* localError = nil;

    while(true) {
        NSArray<CKKSOutgoingQueueEntry*> * inflightQueueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce
                                                                                          state:SecCKKSStateInFlight
                                                                                         zoneID:self.zoneID
                                                                                          error:&localError];

        if(localError != nil) {
            ckkserror("ckks", self, "Error finding inflight outgoing queue records: %@", localError);
            if(error) {
                *error = localError;
            }
            return false;
        }

        if([inflightQueueEntries count] == 0u) {
            break;
        }

        for(CKKSOutgoingQueueEntry* oqe in inflightQueueEntries) {
            [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateNew error:&localError];

            if(localError) {
                ckkserror("ckks", self, "Error fixing up inflight OQE(%@): %@", oqe, localError);
                if(error) {
                    *error = localError;
                }
                return false;
            }
        }
    }

    return true;
}

- (bool)_onqueueChangeOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe toState: (NSString*) state error: (NSError* __autoreleasing*) error {
    dispatch_assert_queue(self.queue);

    NSError* localerror = nil;

    if([state isEqualToString: SecCKKSStateDeleted]) {
        // Hurray, this must be a success
        SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
        if(syncCallback) {
            syncCallback(true, nil);
        }

        [oqe deleteFromDatabase: &localerror];
        if(localerror) {
            ckkserror("ckks", self, "Couldn't delete %@: %@", oqe, localerror);
        }

    } else if([oqe.state isEqualToString:SecCKKSStateInFlight] && [state isEqualToString:SecCKKSStateNew]) {
        // An in-flight OQE is moving to new? See if it's been superceded
        CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateNew zoneID:self.zoneID error:&localerror];
        if(localerror) {
            ckkserror("ckksoutgoing", self, "Couldn't fetch an overwriting OQE, assuming one doesn't exist: %@", localerror);
            newOQE = nil;
        }

        if(newOQE) {
            ckksnotice("ckksoutgoing", self, "New modification has come in behind inflight %@; dropping failed change", oqe);
            // recurse for that lovely code reuse
            [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&localerror];
            if(localerror) {
                ckkserror("ckksoutgoing", self, "Couldn't delete in-flight OQE: %@", localerror);
                if(error) {
                    *error = localerror;
                }
            }
        } else {
            oqe.state = state;
            [oqe saveToDatabase: &localerror];
            if(localerror) {
                ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
            }
        }

    } else {
        oqe.state = state;
        [oqe saveToDatabase: &localerror];
        if(localerror) {
            ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
        }
    }

    if(error && localerror) {
        *error = localerror;
    }
    return localerror == nil;
}

- (bool)_onqueueErrorOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe itemError: (NSError*) itemError error: (NSError* __autoreleasing*) error {
    dispatch_assert_queue(self.queue);

    SecBoolNSErrorCallback callback = [[CKKSViewManager manager] claimCallbackForUUID:oqe.uuid];
    if(callback) {
        callback(false, itemError);
    }
    NSError* localerror = nil;

    // Now, delete the OQE: it's never coming back
    [oqe deleteFromDatabase:&localerror];
    if(localerror) {
        ckkserror("ckks", self, "Couldn't delete %@ (due to error %@): %@", oqe, itemError, localerror);
    }

    if(error && localerror) {
        *error = localerror;
    }
    return localerror == nil;
}

- (bool)dispatchSyncWithConnection:(SecDbConnectionRef _Nonnull)dbconn
                    readWriteTxion:(BOOL)readWriteTxion
                             block:(CKKSDatabaseTransactionResult (^)(void))block
{
    CFErrorRef cferror = NULL;

    // Take the DB transaction, then get on the local queue.
    // In the case of exclusive DB transactions, we don't really _need_ the local queue, but, it's here for future use.

    SecDbTransactionType txtionType = readWriteTxion ? kSecDbExclusiveRemoteCKKSTransactionType : kSecDbNormalTransactionType;
    bool ret = kc_transaction_type(dbconn, txtionType, &cferror, ^bool{
        __block CKKSDatabaseTransactionResult result = CKKSDatabaseTransactionRollback;

        CKKSSQLInTransaction = true;
        if(readWriteTxion) {
            CKKSSQLInWriteTransaction = true;
        }

        dispatch_sync(self.queue, ^{
            result = block();
        });

        if(readWriteTxion) {
            CKKSSQLInWriteTransaction = false;
        }
        CKKSSQLInTransaction = false;
        return result == CKKSDatabaseTransactionCommit;
    });

    if(cferror) {
        ckkserror("ckks", self, "error doing database transaction, major problems ahead: %@", cferror);
    }
    return ret;
}

- (void)dispatchSyncWithSQLTransaction:(CKKSDatabaseTransactionResult (^)(void))block
{
    // important enough to block this thread. Must get a connection first, though!

    // Please don't jetsam us...
    os_transaction_t transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.ckks.%@", self.zoneName] UTF8String]);

    CFErrorRef cferror = NULL;
    kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
        return [self dispatchSyncWithConnection:dbt
                                 readWriteTxion:YES
                                          block:block];

    });
    if(cferror) {
        ckkserror("ckks", self, "error getting database connection, major problems ahead: %@", cferror);
    }

    (void)transaction;
}

- (void)dispatchSyncWithReadOnlySQLTransaction:(void (^)(void))block
{
    // Please don't jetsam us...
    os_transaction_t transaction = os_transaction_create([[NSString stringWithFormat:@"com.apple.securityd.ckks.%@", self.zoneName] UTF8String]);

    CFErrorRef cferror = NULL;

    // Note: we are lying to kc_with_dbt here about whether we're read-and-write or read-only.
    // This is because the SOS engine's queue are broken: SOSEngineSetNotifyPhaseBlock attempts
    // to take the SOS engine's queue while a SecDb transaction is still ongoing. But, in
    // SOSEngineCopyPeerConfirmedDigests, SOS takes the engine queue, then calls dsCopyManifestWithViewNameSet()
    // which attempts to get a read-only SecDb connection.
    //
    // The issue manifests when many CKKS read-only transactions are in-flight, and starve out
    // the pool of read-only connections. Then, a deadlock forms.
    //
    // By claiming to be a read-write connection here, we'll contend on the pool of writer threads,
    // and shouldn't starve SOS of its read thread.
    //
    // But, since we pass NO to readWriteTxion, the SQLite transaction will be of type
    // kSecDbNormalTransactionType, which won't block other readers.

    kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
        return [self dispatchSyncWithConnection:dbt
                                 readWriteTxion:NO
                                          block:^CKKSDatabaseTransactionResult {
            block();
            return CKKSDatabaseTransactionCommit;
        }];

    });
    if(cferror) {
        ckkserror("ckks", self, "error getting database connection, major problems ahead: %@", cferror);
    }

    (void)transaction;
}

- (BOOL)insideSQLTransaction
{
    return CKKSSQLInTransaction;
}

#pragma mark - CKKSZone operations

- (void)beginCloudKitOperation
{
    [self.accountTracker registerForNotificationsOfCloudKitAccountStatusChange:self];
}

- (CKKSResultOperation*)createAccountLoggedInDependency:(NSString*)message
{
    WEAKIFY(self);
    CKKSResultOperation* accountLoggedInDependency = [CKKSResultOperation named:@"account-logged-in-dependency" withBlock:^{
        STRONGIFY(self);
        ckksnotice("ckkszone", self, "%@", message);
    }];
    accountLoggedInDependency.descriptionErrorCode = CKKSResultDescriptionPendingAccountLoggedIn;
    return accountLoggedInDependency;
}

#pragma mark - CKKSZoneUpdateReceiverProtocol

- (CKKSAccountStatus)accountStatusFromCKAccountInfo:(CKAccountInfo*)info
{
    if(!info) {
        return CKKSAccountStatusUnknown;
    }
    if(info.accountStatus == CKAccountStatusAvailable &&
       info.hasValidCredentials) {
        return CKKSAccountStatusAvailable;
    } else {
        return CKKSAccountStatusNoAccount;
    }
}

- (void)cloudkitAccountStateChange:(CKAccountInfo* _Nullable)oldAccountInfo to:(CKAccountInfo*)currentAccountInfo
{
    ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
               self.zoneID.zoneName,
               oldAccountInfo,
               currentAccountInfo);

    // Filter for device2device encryption and cloudkit grey mode
    CKKSAccountStatus oldStatus = [self accountStatusFromCKAccountInfo:oldAccountInfo];
    CKKSAccountStatus currentStatus = [self accountStatusFromCKAccountInfo:currentAccountInfo];

    if(oldStatus == currentStatus) {
        ckksnotice("ckkszone", self, "Computed status of new CK account info is same as old status: %@", [CKKSAccountStateTracker stringFromAccountStatus:currentStatus]);
        return;
    }

    switch(currentStatus) {
        case CKKSAccountStatusAvailable: {
            ckksnotice("ckkszone", self, "Logged into iCloud.");
            [self handleCKLogin];

            if(self.accountLoggedInDependency) {
                [self.operationQueue addOperation:self.accountLoggedInDependency];
                self.accountLoggedInDependency = nil;
            };
        }
            break;

        case CKKSAccountStatusNoAccount: {
            ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");

            if(!self.accountLoggedInDependency) {
                self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in again."];
            }

            [self handleCKLogout];
        }
            break;

        case CKKSAccountStatusUnknown: {
            // We really don't expect to receive this as a notification, but, okay!
            ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);

            if(!self.accountLoggedInDependency) {
                self.accountLoggedInDependency = [self createAccountLoggedInDependency:@"CloudKit account logged in again."];
            }

            [self handleCKLogout];
        }
            break;
    }
}

- (void)handleCKLogin
{
    ckksnotice("ckks", self, "received a notification of CK login");
    if(!SecCKKSIsEnabled()) {
        ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS");
        return;
    }

    dispatch_sync(self.queue, ^{
        ckksinfo("ckkszone", self, "received a notification of CK login");

        // Change our condition variables to reflect that we think we're logged in
        self.accountStatus = CKKSAccountStatusAvailable;
        self.loggedOut = [[CKKSCondition alloc] initToChain:self.loggedOut];
        [self.loggedIn fulfill];
    });

    [self.stateMachine handleFlag:CKKSFlagCloudKitLoggedIn];

    [self.accountStateKnown fulfill];
}

- (void)handleCKLogout
{
    dispatch_sync(self.queue, ^{
        ckksinfo("ckkszone", self, "received a notification of CK logout");

        self.accountStatus = CKKSAccountStatusNoAccount;
        self.loggedIn = [[CKKSCondition alloc] initToChain:self.loggedIn];
        [self.loggedOut fulfill];
    });

    [self.stateMachine handleFlag:CKKSFlagCloudKitLoggedOut];

    [self.accountStateKnown fulfill];
}

#pragma mark - Trust operations

- (void)beginTrustedOperation:(NSArray<id<CKKSPeerProvider>>*)peerProviders
             suggestTLKUpload:(CKKSNearFutureScheduler*)suggestTLKUpload
           requestPolicyCheck:(CKKSNearFutureScheduler*)requestPolicyCheck
{
    for(id<CKKSPeerProvider> peerProvider in peerProviders) {
        [peerProvider registerForPeerChangeUpdates:self];
    }

    [self.launch addEvent:@"beginTrusted"];

    dispatch_sync(self.queue, ^{
        ckksnotice("ckkstrust", self, "Beginning trusted operation");
        self.operationDependencies.peerProviders = peerProviders;

        CKKSAccountStatus oldTrustStatus = self.trustStatus;

        self.suggestTLKUpload = suggestTLKUpload;
        self.requestPolicyCheck = requestPolicyCheck;

        self.trustStatus = CKKSAccountStatusAvailable;
        [self.stateMachine _onqueueHandleFlag:CKKSFlagBeginTrustedOperation];

        if(oldTrustStatus == CKKSAccountStatusNoAccount) {
            ckksnotice("ckkstrust", self, "Moving from an untrusted status; we need to process incoming queue and scan for any new items");

            [self.stateMachine _onqueueHandleFlag:CKKSFlagProcessIncomingQueue];
            [self.stateMachine _onqueueHandleFlag:CKKSFlagScanLocalItems];
        }
    });
}

- (void)endTrustedOperation
{
    [self.launch addEvent:@"endTrusted"];

    dispatch_sync(self.queue, ^{
        ckksnotice("ckkstrust", self, "Ending trusted operation");

        self.operationDependencies.peerProviders = @[];

        self.suggestTLKUpload = nil;

        self.trustStatus = CKKSAccountStatusNoAccount;
        [self.stateMachine _onqueueHandleFlag:CKKSFlagEndTrustedOperation];
    });
}

- (BOOL)itemSyncingEnabled
{
    if(!self.operationDependencies.syncingPolicy) {
        ckksnotice("ckks", self, "No syncing policy loaded; item syncing is disabled");
        return NO;
    } else {
        return [self.operationDependencies.syncingPolicy isSyncingEnabledForView:self.zoneName];
    }
}

- (void)setCurrentSyncingPolicy:(TPSyncingPolicy*)syncingPolicy policyIsFresh:(BOOL)policyIsFresh
{
    dispatch_sync(self.queue, ^{
        BOOL oldEnabled = [self itemSyncingEnabled];

        self.operationDependencies.syncingPolicy = syncingPolicy;

        BOOL enabled = [self itemSyncingEnabled];
        if(enabled != oldEnabled) {
            ckksnotice("ckks", self, "Syncing for this view is now %@ (policy: %@)", enabled ? @"enabled" : @"paused", self.operationDependencies.syncingPolicy);
        }

        if(enabled) {
            CKKSResultOperation* incomingOp = [self processIncomingQueue:false after:nil policyConsideredAuthoritative:policyIsFresh];
            [self processOutgoingQueueAfter:incomingOp ckoperationGroup:nil];
        }
    });
}

- (void)receivedItemForWrongView
{
    [self.requestPolicyCheck trigger];
}

#pragma mark - CKKSChangeFetcherClient

- (BOOL)zoneIsReadyForFetching
{
    __block BOOL ready = NO;

    [self dispatchSyncWithReadOnlySQLTransaction:^{
        ready = (bool)[self _onQueueZoneIsReadyForFetching];
    }];

    return ready;
}

- (BOOL)_onQueueZoneIsReadyForFetching
{
    dispatch_assert_queue(self.queue);
    if(self.accountStatus != CKKSAccountStatusAvailable) {
        ckksnotice("ckksfetch", self, "Not participating in fetch: not logged in");
        return NO;
    }

    CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.operationDependencies.zoneID.zoneName];

    if(!ckse.ckzonecreated) {
        ckksnotice("ckksfetch", self, "Not participating in fetch: zone not created yet");
        return NO;
    }
    return YES;
}

- (CKKSCloudKitFetchRequest*)participateInFetch
{
    __block CKKSCloudKitFetchRequest* request = [[CKKSCloudKitFetchRequest alloc] init];

    [self dispatchSyncWithReadOnlySQLTransaction:^{
        if (![self _onQueueZoneIsReadyForFetching]) {
            ckksnotice("ckksfetch", self, "skipping fetch since zones are not ready");
            return;
        }

        request.participateInFetch = true;
        [self.launch addEvent:@"fetch"];

        if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateNeedFullRefetch]) {
            // We want to return a nil change tag (to force a resync)
            ckksnotice("ckksfetch", self, "Beginning refetch");
            request.changeToken = nil;
            request.resync = true;
        } else {
            CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];
            if(!ckse) {
                ckkserror("ckksfetch", self, "couldn't fetch zone change token for %@", self.zoneName);
                return;
            }
            request.changeToken = ckse.changeToken;
        }
    }];

    if (request.changeToken == nil) {
        self.launch.firstLaunch = true;
    }

    return request;
}

- (void)changesFetched:(NSArray<CKRecord*>*)changedRecords
      deletedRecordIDs:(NSArray<CKKSCloudKitDeletion*>*)deletedRecords
        newChangeToken:(CKServerChangeToken*)newChangeToken
            moreComing:(BOOL)moreComing
                resync:(BOOL)resync
{
    [self.launch addEvent:@"changes-fetched"];

    if(changedRecords.count == 0 && deletedRecords.count == 0 && !moreComing && !resync) {
        // Early-exit, so we don't pick up the account keys or kick off an IncomingQueue operation for no changes
        [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
            ckksinfo("ckksfetch", self, "No record changes in this fetch");

            NSError* error = nil;
            CKKSZoneStateEntry* state = [CKKSZoneStateEntry state:self.zoneName];
            state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
            state.changeToken = newChangeToken;
            state.moreRecordsInCloudKit = moreComing;
            [state saveToDatabase:&error];
            if(error) {
                ckkserror("ckksfetch", self, "Couldn't save new server change token: %@", error);
            }
            return CKKSDatabaseTransactionCommit;
        }];
        return;
    }

    [self dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
        for (CKRecord* record in changedRecords) {
            [self _onqueueCKRecordChanged:record resync:resync];
        }

        for (CKKSCloudKitDeletion* deletion in deletedRecords) {
            [self _onqueueCKRecordDeleted:deletion.recordID recordType:deletion.recordType resync:resync];
        }

        NSError* error = nil;
        if(resync) {
            // If we're performing a resync, we need to keep track of everything that's actively in
            // CloudKit during the fetch, (so that we can find anything that's on-disk and not in CloudKit).
            // Please note that if, during a resync, the fetch errors, we won't be notified. If a record is in
            // the first refetch but not the second, it'll be added to our set, and the second resync will not
            // delete the record (which is a consistency violation, but only with actively changing records).
            // A third resync should correctly delete that record.

            if(self.resyncRecordsSeen == nil) {
                self.resyncRecordsSeen = [NSMutableSet set];
            }
            for(CKRecord* r in changedRecords) {
                [self.resyncRecordsSeen addObject:r.recordID.recordName];
            }

            // Is there More Coming? If not, self.resyncRecordsSeen contains everything in CloudKit. Inspect for anything extra!
            if(moreComing) {
                ckksnotice("ckksresync", self, "In a resync, but there's More Coming. Waiting to scan for extra items.");

            } else {
                // Scan through all CKMirrorEntries and determine if any exist that CloudKit didn't tell us about
                ckksnotice("ckksresync", self, "Comparing local UUIDs against the CloudKit list");
                NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs:self.zoneID error:&error] mutableCopy];

                for(NSString* uuid in uuids) {
                    if([self.resyncRecordsSeen containsObject:uuid]) {
                        ckksnotice("ckksresync", self, "UUID %@ is still in CloudKit; carry on.", uuid);
                    } else {
                        CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:self.zoneID error:&error];
                        if(error != nil) {
                            ckkserror("ckksresync", self, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
                            continue;
                        }
                        if(!ckme) {
                            ckkserror("ckksresync", self, "Couldn't read ckme(%@) from database; continuing", uuid);
                            continue;
                        }

                        ckkserror("ckksresync", self, "BUG: Local item %@ not found in CloudKit, deleting", uuid);
                        [self _onqueueCKRecordDeleted:ckme.item.storedCKRecord.recordID recordType:ckme.item.storedCKRecord.recordType resync:resync];
                    }
                }

                // Now that we've inspected resyncRecordsSeen, reset it for the next time through
                self.resyncRecordsSeen = nil;
            }
        }

        CKKSZoneStateEntry* state = [CKKSZoneStateEntry state:self.zoneName];
        state.lastFetchTime = [NSDate date]; // The last fetch happened right now!
        state.changeToken = newChangeToken;
        state.moreRecordsInCloudKit = moreComing;
        [state saveToDatabase:&error];
        if(error) {
            ckkserror("ckksfetch", self, "Couldn't save new server change token: %@", error);
        }

        if(!moreComing) {
            // Might as well kick off a IQO!
            [self processIncomingQueue:false];
            ckksnotice("ckksfetch", self, "Beginning incoming processing for %@", self.zoneID);
        }

        ckksnotice("ckksfetch", self, "Finished processing changes for %@", self.zoneID);

        return CKKSDatabaseTransactionCommit;
    }];
}

- (bool)ckErrorOrPartialError:(NSError *)error isError:(CKErrorCode)errorCode
{
    if((error.code == errorCode) && [error.domain isEqualToString:CKErrorDomain]) {
        return true;
    } else if((error.code == CKErrorPartialFailure) && [error.domain isEqualToString:CKErrorDomain]) {
        NSDictionary* partialErrors = error.userInfo[CKPartialErrorsByItemIDKey];

        NSError* partialError = partialErrors[self.zoneID];
        if ((partialError.code == errorCode) && [partialError.domain isEqualToString:CKErrorDomain]) {
            return true;
        }
    }
    return false;
}

- (bool)shouldRetryAfterFetchError:(NSError*)error {

    bool isChangeTokenExpiredError = [self ckErrorOrPartialError:error isError:CKErrorChangeTokenExpired];
    if(isChangeTokenExpiredError) {
        ckkserror("ckks", self, "Received notice that our change token is out of date (for %@). Resetting local data...", self.zoneID);

        [self.stateMachine handleFlag:CKKSFlagChangeTokenExpired];
        return true;
    }

    bool isDeletedZoneError = [self ckErrorOrPartialError:error isError:CKErrorZoneNotFound];
    if(isDeletedZoneError) {
        ckkserror("ckks", self, "Received notice that our zone(%@) does not exist. Resetting local data.", self.zoneID);

        [self.stateMachine handleFlag:CKKSFlagCloudKitZoneMissing];
        return false;
    }

    if([error.domain isEqualToString:CKErrorDomain] && (error.code == CKErrorBadContainer)) {
        ckkserror("ckks", self, "Received notice that our container does not exist. Nothing to do.");
        return false;
    }

    return true;
}

#pragma mark CKKSPeerUpdateListener

- (void)selfPeerChanged:(id<CKKSPeerProvider>)provider
{
    // Currently, we have no idea what to do with this. Kick off a key reprocess?
    ckkserror("ckks", self, "Received update that our self identity has changed");
    [self keyStateMachineRequestProcess];
}

- (void)trustedPeerSetChanged:(id<CKKSPeerProvider>)provider
{
    // We might need to share the TLK to some new people, or we might now trust the TLKs we have.
    // The key state machine should handle that, so poke it.
    ckkserror("ckks", self, "Received update that the trust set has changed");

    [self.stateMachine handleFlag:CKKSFlagTrustedPeersSetChanged];
}

#pragma mark - Test Support

- (bool) outgoingQueueEmpty: (NSError * __autoreleasing *) error {
    __block bool ret = false;
    [self dispatchSyncWithReadOnlySQLTransaction:^{
        NSArray* queueEntries = [CKKSOutgoingQueueEntry all: error];
        ret = queueEntries && ([queueEntries count] == 0);
    }];

    return ret;
}

- (void)waitForFetchAndIncomingQueueProcessing
{
    [[self.zoneChangeFetcher inflightFetch] waitUntilFinished];
    [self waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
}

- (void)waitForKeyHierarchyReadiness {
    if(self.keyStateReadyDependency) {
        [self.keyStateReadyDependency waitUntilFinished];
    }
}

#pragma mark - NSOperation assistance

- (void)scheduleOperation:(NSOperation*)op
{
    if(self.halted) {
        ckkserror("ckkszone", self, "attempted to schedule an operation on a halted zone, ignoring");
        return;
    }

    [op addNullableDependency:self.accountLoggedInDependency];
    [self.operationQueue addOperation: op];
}

// to be used rarely, if at all
- (bool)scheduleOperationWithoutDependencies:(NSOperation*)op
{
    if(self.halted) {
        ckkserror("ckkszone", self, "attempted to schedule an non-dependent operation on a halted zone, ignoring");
        return false;
    }

    [self.operationQueue addOperation: op];
    return true;
}

- (void)waitUntilAllOperationsAreFinished
{
    [self.operationQueue waitUntilAllOperationsAreFinished];
}

- (void)waitForOperationsOfClass:(Class)operationClass
{
    NSArray* operations = [self.operationQueue.operations copy];
    for(NSOperation* op in operations) {
        if([op isKindOfClass:operationClass]) {
            [op waitUntilFinished];
        }
    }
}

- (void)cancelPendingOperations {
    @synchronized(self.outgoingQueueOperations) {
        for(NSOperation* op in self.outgoingQueueOperations) {
            [op cancel];
        }
        [self.outgoingQueueOperations removeAllObjects];
    }

    @synchronized(self.incomingQueueOperations) {
        for(NSOperation* op in self.incomingQueueOperations) {
            [op cancel];
        }
        [self.incomingQueueOperations removeAllObjects];
    }

    @synchronized(self.scanLocalItemsOperations) {
        for(NSOperation* op in self.scanLocalItemsOperations) {
            [op cancel];
        }
        [self.scanLocalItemsOperations removeAllObjects];
    }
}

- (void)cancelAllOperations {
    [self.keyStateReadyDependency cancel];
    [self.zoneChangeFetcher cancel];
    [self.notifyViewChangedScheduler cancel];

    [self cancelPendingOperations];
    [self.operationQueue cancelAllOperations];
}

- (void)halt {
    [self.stateMachine haltOperation];

    // Synchronously set the 'halted' bit
    dispatch_sync(self.queue, ^{
        self.halted = true;
    });

    // Bring all operations down, too
    [self cancelAllOperations];

    // And now, wait for all operations that are running
    for(NSOperation* op in self.operationQueue.operations) {
        if(op.isExecuting) {
            [op waitUntilFinished];
        }
    }

    // Don't send any more notifications, either
    _notifierClass = nil;
}

- (NSDictionary*)status {
#define stringify(obj) CKKSNilToNSNull([obj description])
#define boolstr(obj) (!!(obj) ? @"yes" : @"no")
    __block NSMutableDictionary* ret = nil;
    __block NSError* error = nil;

    ret = [[self fastStatus] mutableCopy];

    [self dispatchSyncWithReadOnlySQLTransaction:^{
        CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.zoneID];
        if(keyset.error) {
            error = keyset.error;
        }

        if(error) {
            ckkserror("ckks", self, "error during status: %@", error);
        }
        // We actually don't care about this error, especially if it's "no current key pointers"...
        error = nil;

        // Map deviceStates to strings to avoid NSXPC issues. Obj-c, why is this so hard?
        NSArray* deviceStates = [CKKSDeviceStateEntry allInZone:self.zoneID error:&error];
        NSMutableArray<NSString*>* mutDeviceStates = [[NSMutableArray alloc] init];
        [deviceStates enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            [mutDeviceStates addObject: [obj description]];
        }];

        NSArray* tlkShares = [CKKSTLKShareRecord allForUUID:keyset.currentTLKPointer.currentKeyUUID zoneID:self.zoneID error:&error];
        NSMutableArray<NSString*>* mutTLKShares = [[NSMutableArray alloc] init];
        [tlkShares enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            [mutTLKShares addObject: [obj description]];
        }];

        [ret addEntriesFromDictionary:@{
                 @"statusError":         stringify(error),
                 @"oqe":                 CKKSNilToNSNull([CKKSOutgoingQueueEntry countsByStateInZone:self.zoneID error:&error]),
                 @"iqe":                 CKKSNilToNSNull([CKKSIncomingQueueEntry countsByStateInZone:self.zoneID error:&error]),
                 @"ckmirror":            CKKSNilToNSNull([CKKSMirrorEntry        countsByParentKey:self.zoneID error:&error]),
                 @"devicestates":        CKKSNilToNSNull(mutDeviceStates),
                 @"tlkshares":           CKKSNilToNSNull(mutTLKShares),
                 @"keys":                CKKSNilToNSNull([CKKSKey countsByClass:self.zoneID error:&error]),
                 @"currentTLK":          CKKSNilToNSNull(keyset.tlk.uuid),
                 @"currentClassA":       CKKSNilToNSNull(keyset.classA.uuid),
                 @"currentClassC":       CKKSNilToNSNull(keyset.classC.uuid),
                 @"currentTLKPtr":       CKKSNilToNSNull(keyset.currentTLKPointer.currentKeyUUID),
                 @"currentClassAPtr":    CKKSNilToNSNull(keyset.currentClassAPointer.currentKeyUUID),
                 @"currentClassCPtr":    CKKSNilToNSNull(keyset.currentClassCPointer.currentKeyUUID),
                 @"itemsyncing":         self.itemSyncingEnabled ? @"enabled" : @"paused",
            }];
    }];
    return ret;
}

- (NSDictionary*)fastStatus {

    __block NSDictionary* ret = nil;

    [self dispatchSyncWithReadOnlySQLTransaction:^{
        CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.zoneName];

        ret = @{
            @"view":                CKKSNilToNSNull(self.zoneName),
            @"ckaccountstatus":     self.accountStatus == CKAccountStatusCouldNotDetermine ? @"could not determine" :
                self.accountStatus == CKAccountStatusAvailable         ? @"logged in" :
                self.accountStatus == CKAccountStatusRestricted        ? @"restricted" :
                self.accountStatus == CKAccountStatusNoAccount         ? @"logged out" : @"unknown",
            @"accounttracker":      stringify(self.accountTracker),
            @"fetcher":             stringify(self.zoneChangeFetcher),
            @"zoneCreated":         boolstr(ckse.ckzonecreated),
            @"zoneSubscribed":      boolstr(ckse.ckzonesubscribed),
            @"keystate":            CKKSNilToNSNull(self.keyHierarchyState),
            @"statusError":         [NSNull null],
            @"launchSequence":      CKKSNilToNSNull([self.launch eventsByTime]),

            @"lastIncomingQueueOperation":         stringify(self.lastIncomingQueueOperation),
            @"lastNewTLKOperation":                stringify(self.lastNewTLKOperation),
            @"lastOutgoingQueueOperation":         stringify(self.lastOutgoingQueueOperation),
            @"lastProcessReceivedKeysOperation":   stringify(self.lastProcessReceivedKeysOperation),
            @"lastReencryptOutgoingItemsOperation":stringify(self.lastReencryptOutgoingItemsOperation),
        };
    }];

    return ret;
}

#endif /* OCTAGON */
@end
