//
//  MPAppDelegate.m
//  MasterPassword
//
//  Created by Maarten Billemont on 24/11/11.
//  Copyright (c) 2011 Lyndir. All rights reserved.
//

#import "MPAppDelegate_Store.h"

#if TARGET_OS_IPHONE
#define STORE_OPTIONS NSPersistentStoreFileProtectionKey : NSFileProtectionComplete,
#else
#define STORE_OPTIONS
#endif

#define MPCloudContainerIdentifier @"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"
#define MPMigrationLevelLocalStoreKey @"MPMigrationLevelLocalStoreKey"
#define MPMigrationLevelCloudStoreKey @"MPMigrationLevelCloudStoreKey"

typedef NS_ENUM(NSInteger, MPMigrationLevelLocalStore) {
    MPMigrationLevelLocalStoreV1,
    MPMigrationLevelLocalStoreV2,
    MPMigrationLevelLocalStoreCurrent = MPMigrationLevelLocalStoreV2,
};

typedef NS_ENUM(NSInteger, MPMigrationLevelCloudStore) {
    MPMigrationLevelCloudStoreV1,
    MPMigrationLevelCloudStoreV2,
    MPMigrationLevelCloudStoreV3,
    MPMigrationLevelCloudStoreCurrent = MPMigrationLevelCloudStoreV3,
};

@implementation MPAppDelegate_Shared(Store)
        PearlAssociatedObjectProperty(NSManagedObjectContext*, PrivateManagedObjectContext, privateManagedObjectContext);
PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, mainManagedObjectContext);


#pragma mark - Core Data setup

+ (NSManagedObjectContext *)managedObjectContextForMainThreadIfReady {

    NSAssert([[NSThread currentThread] isMainThread], @"Can only access main MOC from the main thread.");
    NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
    if (!mainManagedObjectContext || ![[NSThread currentThread] isMainThread])
        return nil;

    return mainManagedObjectContext;
}

+ (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *context))mocBlock {

    NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
    if (!mainManagedObjectContext)
        return NO;

    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    moc.parentContext = mainManagedObjectContext;
    [moc performBlock:^{
        mocBlock( moc );
    }];

    return YES;
}

+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock {

    NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
    if (!mainManagedObjectContext)
        return NO;

    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    moc.parentContext = mainManagedObjectContext;
    [moc performBlockAndWait:^{
        mocBlock( moc );
    }];

    return YES;
}

- (NSManagedObjectContext *)mainManagedObjectContextIfReady {

    [self storeManager];
    return self.mainManagedObjectContext;
}

- (NSManagedObjectContext *)privateManagedObjectContextIfReady {

    [self storeManager];
    return self.privateManagedObjectContext;
}

- (UbiquityStoreManager *)storeManager {

    static UbiquityStoreManager *storeManager = nil;
    if (storeManager)
        return storeManager;

    storeManager = [[UbiquityStoreManager alloc] initStoreNamed:nil withManagedObjectModel:nil localStoreURL:nil
                                            containerIdentifier:MPCloudContainerIdentifier
                                             storeConfiguration:nil storeOptions:@{ STORE_OPTIONS }
                                                       delegate:self];

#if TARGET_OS_IPHONE
    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:UIApp
                                                       queue:[NSOperationQueue mainQueue] usingBlock:
            ^(NSNotification *note) {
                [[self mainManagedObjectContext] saveToStore];
            }];
    [[NSNotificationCenter defaultCenter]
            addObserverForName:UIApplicationWillResignActiveNotification object:UIApp
                         queue:[NSOperationQueue mainQueue] usingBlock:
            ^(NSNotification *note) {
                [[self mainManagedObjectContext] saveToStore];
            }];
#else
    [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification object:NSApp
                                                       queue:[NSOperationQueue mainQueue] usingBlock:
     ^(NSNotification *note) {
         [self.mainManagedObjectContextIfReady saveToStore];
     }];
#endif

    return storeManager;
}

- (void)migrateStoreForManager:(UbiquityStoreManager *)manager isCloud:(BOOL)isCloudStore {

    [self migrateLocalStore];

    if (isCloudStore)
        [self migrateCloudStore];
}

- (void)migrateLocalStore {

    MPMigrationLevelLocalStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey];
    if (migrationLevel >= MPMigrationLevelLocalStoreCurrent)
            // Local store up-to-date.
        return;

    inf(@"Local store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelLocalStoreCurrent);
    if (migrationLevel <= MPMigrationLevelLocalStoreV1) if (![self migrateV1LocalStore]) {
        inf(@"Failed to migrate old V1 to new local store.");
        return;
    }

    [[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelLocalStoreCurrent forKey:MPMigrationLevelLocalStoreKey];
    inf(@"Successfully migrated old to new local store.");
}

- (void)migrateCloudStore {

    MPMigrationLevelCloudStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelCloudStoreKey];
    if (migrationLevel >= MPMigrationLevelCloudStoreCurrent)
            // Cloud store up-to-date.
        return;

    inf(@"Cloud store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelCloudStoreCurrent);
    if (migrationLevel <= MPMigrationLevelCloudStoreV1) {
        if (![self migrateV1CloudStore]) {
            inf(@"Failed to migrate old V1 to new cloud store.");
            return;
        }
    }
    else if (migrationLevel <= MPMigrationLevelCloudStoreV2) {
        if (![self migrateV2CloudStore]) {
            inf(@"Failed to migrate old V2 to new cloud store.");
            return;
        }
    }

    [[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelCloudStoreCurrent forKey:MPMigrationLevelCloudStoreKey];
    inf(@"Successfully migrated old to new cloud store.");
}

- (BOOL)migrateV1CloudStore {

    // Migrate cloud enabled preference.
    NSNumber *oldCloudEnabled = [[NSUserDefaults standardUserDefaults] objectForKey:@"iCloudEnabledKey"];
    if ([oldCloudEnabled boolValue])
        [[NSUserDefaults standardUserDefaults] setBool:YES forKey:USMCloudEnabledKey];

    // Migrate cloud store.
    NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:@"LocalUUIDKey"];
    if (!uuid) {
        inf(@"No V1 cloud store to migrate.");
        return YES;
    }

    inf(@"Migrating V1 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"]);
    NSURL *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier];
    NSURL *oldCloudContentURL = [[cloudContainerURL
            URLByAppendingPathComponent:@"Data" isDirectory:YES]
            URLByAppendingPathComponent:uuid isDirectory:YES];
    NSURL *oldCloudStoreURL = [[[cloudContainerURL
            URLByAppendingPathComponent:@"Database.nosync" isDirectory:YES]
            URLByAppendingPathComponent:uuid isDirectory:NO] URLByAppendingPathExtension:@"sqlite"];

    return [self migrateFromCloudStore:oldCloudStoreURL cloudContent:oldCloudContentURL];
}

- (BOOL)migrateV2CloudStore {

    // Migrate cloud store.
    NSString *uuid = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:@"USMStoreUUIDKey"];
    if (!uuid) {
        inf(@"No V2 cloud store to migrate.");
        return YES;
    }

    inf(@"Migrating V2 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"]);
    NSURL *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier];
    NSURL *oldCloudContentURL = [[cloudContainerURL
            URLByAppendingPathComponent:@"CloudLogs" isDirectory:YES]
            URLByAppendingPathComponent:uuid isDirectory:YES];
    NSURL *oldCloudStoreURL = [[[cloudContainerURL
            URLByAppendingPathComponent:@"CloudStore.nosync" isDirectory:YES]
            URLByAppendingPathComponent:uuid isDirectory:NO] URLByAppendingPathExtension:@"sqlite"];

    return [self migrateFromCloudStore:oldCloudStoreURL cloudContent:oldCloudContentURL];
}

- (BOOL)migrateV1LocalStore {

    NSURL *applicationFilesDirectory = [[[NSFileManager defaultManager]
            URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *oldLocalStoreURL = [[applicationFilesDirectory
            URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO] URLByAppendingPathExtension:@"sqlite"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NO]) {
        inf(@"No V1 local store to migrate.");
        return YES;
    }

    inf(@"Migrating V1 local store");
    return [self migrateFromLocalStore:oldLocalStoreURL];
}

- (BOOL)migrateFromLocalStore:(NSURL *)oldLocalStoreURL {

    NSURL *newLocalStoreURL = [self.storeManager URLForLocalStore];
    if ([[NSFileManager defaultManager] fileExistsAtPath:newLocalStoreURL.path isDirectory:NO]) {
        wrn(@"Can't migrate local store: A new local store already exists.");
        return YES;
    }

    if (![self.storeManager migrateStore:oldLocalStoreURL withOptions:nil
                                 toStore:newLocalStoreURL withOptions:nil
                                strategy:0 error:nil cause:nil context:nil]) {
        self.storeManager.localStoreURL = oldLocalStoreURL;
        return NO;
    }

    inf(@"Successfully migrated to new local store.");
    return YES;
}

- (BOOL)migrateFromCloudStore:(NSURL *)oldCloudStoreURL cloudContent:(NSURL *)oldCloudContentURL {

    if (![self.storeManager cloudSafeForSeeding]) {
        inf(@"Can't migrate cloud store: A new cloud store already exists.");
        return YES;
    }

    NSURL *newCloudStoreURL = [self.storeManager URLForCloudStore];
    if (![self.storeManager migrateStore:oldCloudStoreURL withOptions:nil
                                 toStore:newCloudStoreURL withOptions:nil
                                strategy:0 error:nil cause:nil context:nil])
        return NO;

    inf(@"Successfully migrated to new cloud store.");
    return YES;
}

#pragma mark - UbiquityStoreManagerDelegate

- (NSManagedObjectContext *)ubiquityStoreManager:(UbiquityStoreManager *)manager
          managedObjectContextForUbiquityChanges:(NSNotification *)note {

    return [self mainManagedObjectContextIfReady];
}

- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {

    inf(@"[StoreManager] %@", message);
}

- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore {

    NSManagedObjectContext *moc = [self mainManagedObjectContextIfReady];
    [moc performBlockAndWait:^{
        [moc saveToStore];
        [moc reset];

        self.privateManagedObjectContext = nil;
        self.mainManagedObjectContext = nil;
    }];

    [self migrateStoreForManager:manager isCloud:isCloudStore];
}

- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator
                     isCloud:(BOOL)isCloudStore {

    inf(@"Using iCloud? %@", @(isCloudStore));
    MPCheckpoint( MPCheckpointCloud, @{
            @"enabled" : @(isCloudStore)
    } );

    // Create our contexts.
    NSManagedObjectContext
            *privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [privateManagedObjectContext performBlockAndWait:^{
        privateManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
        privateManagedObjectContext.persistentStoreCoordinator = coordinator;

//        dbg(@"===");
//        NSError *error;
//        for (NSEntityDescription *entityDescription in [coordinator.managedObjectModel entities]) {
//            dbg(@"Entities: %@", entityDescription.name);
//            NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:entityDescription.name];
//            NSArray *entities = [privateManagedObjectContext executeFetchRequest:request error:&error];
//            if (!entities)
//            err(@"  - Error: %@", error);
//            else
//                for (id entity in entities)
//                        dbg(@"  - %@", [entity debugDescription]);
//        }
//        dbg(@"===");
    }];

    NSManagedObjectContext *mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    mainManagedObjectContext.parentContext = privateManagedObjectContext;

    self.privateManagedObjectContext = privateManagedObjectContext;
    self.mainManagedObjectContext = mainManagedObjectContext;
}

- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreErrorCause)cause
                     context:(id)context {

    err(@"[StoreManager] ERROR: cause=%@, context=%@, error=%@", NSStringFromUSMCause( cause ), context, error);
    MPCheckpoint( MPCheckpointMPErrorUbiquity, @{
            @"cause"        : @(cause),
            @"error.code"   : @(error.code),
            @"error.domain" : NilToNSNull(error.domain),
            @"error.reason" : NilToNSNull(IfNotNilElse( [error localizedFailureReason], [error localizedDescription] )),
    } );
}

#pragma mark - Utilities

- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion {

    if (![siteName length]) {
        completion( nil );
        return;
    }

    [MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
        MPUserEntity *activeUser = [self activeUserInContext:context];
        NSAssert(activeUser, @"Missing user.");
        if (!activeUser)
            return;

        MPElementType type = activeUser.defaultType;
        if (!type)
            type = activeUser.defaultType = MPElementTypeGeneratedLong;
        NSString *typeEntityClassName = [MPAlgorithmDefault classNameOfType:type];

        MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:typeEntityClassName
                                                                 inManagedObjectContext:context];

        element.name = siteName;
        element.user = activeUser;
        element.type = type;
        element.lastUsed = [NSDate date];
        element.version = MPAlgorithmDefaultVersion;
        [context saveToStore];

        NSError *error = nil;
        if (element.objectID.isTemporaryID && ![context obtainPermanentIDsForObjects:@[ element ] error:&error])
        err(@"Failed to obtain a permanent object ID after creating new element: %@", error);

        NSManagedObjectID *elementOID = [element objectID];
        dispatch_async( dispatch_get_main_queue(), ^{
            completion(
                    (MPElementEntity *)[[MPAppDelegate_Shared managedObjectContextForMainThreadIfReady] objectRegisteredForID:elementOID] );
        } );
    }];
}

- (MPElementEntity *)changeElement:(MPElementEntity *)element inContext:(NSManagedObjectContext *)context toType:(MPElementType)type {

    if ([element.algorithm classOfType:type] == element.typeClass)
        element.type = type;

    else {
        // Type requires a different class of element.  Recreate the element.
        MPElementEntity *newElement
                = [NSEntityDescription insertNewObjectForEntityForName:[element.algorithm classNameOfType:type]
                                                inManagedObjectContext:context];
        newElement.type = type;
        newElement.name = element.name;
        newElement.user = element.user;
        newElement.uses = element.uses;
        newElement.lastUsed = element.lastUsed;
        newElement.version = element.version;
        newElement.loginName = element.loginName;

        [context deleteObject:element];
        [context saveToStore];

        NSError *error;
        if (![context obtainPermanentIDsForObjects:@[ newElement ] error:&error])
        err(@"Failed to obtain a permanent object ID after changing object type: %@", error);

        element = newElement;
    }

    [[NSNotificationCenter defaultCenter] postNotificationName:MPElementUpdatedNotification object:element.objectID];
    return element;
}

- (MPImportResult)importSites:(NSString *)importedSitesString
            askImportPassword:(NSString *(^)(NSString *userName))importPassword
              askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword {

    NSAssert(![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread.");

    __block MPImportResult result = MPImportResultCancelled;
    do {
        if ([MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
            result = [self importSites:importedSitesString askImportPassword:importPassword askUserPassword:userPassword
                         saveInContext:context];
        }])
            break;
        usleep( (useconds_t)(USEC_PER_SEC * 0.2) );
    } while (YES);

    return result;
}

- (MPImportResult)importSites:(NSString *)importedSitesString
            askImportPassword:(NSString *(^)(NSString *userName))askImportPassword
              askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword
                saveInContext:(NSManagedObjectContext *)context {

    // Compile patterns.
    static NSRegularExpression *headerPattern, *sitePattern;
    NSError *error = nil;
    if (!headerPattern) {
        headerPattern = [[NSRegularExpression alloc]
                initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
                        options:(NSRegularExpressionOptions)0 error:&error];
        if (error) {
            err(@"Error loading the header pattern: %@", error);
            return MPImportResultInternalError;
        }
    }
    if (!sitePattern) {
        sitePattern = [[NSRegularExpression alloc]
                initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)"
                        options:(NSRegularExpressionOptions)0 error:&error];
        if (error) {
            err(@"Error loading the site pattern: %@", error);
            return MPImportResultInternalError;
        }
    }

    // Parse import data.
    inf(@"Importing sites.");
    __block MPUserEntity *user = nil;
    id<MPAlgorithm> importAlgorithm = nil;
    NSString *importBundleVersion = nil, *importUserName = nil;
    NSData *importKeyID = nil;
    BOOL headerStarted = NO, headerEnded = NO, clearText = NO;
    NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
    NSMutableSet *elementsToDelete = [NSMutableSet set];
    NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
    NSFetchRequest *elementFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )];
    for (NSString *importedSiteLine in importedSiteLines) {
        if ([importedSiteLine hasPrefix:@"#"]) {
            // Comment or header
            if (!headerStarted) {
                if ([importedSiteLine isEqualToString:@"##"])
                    headerStarted = YES;
                continue;
            }
            if (headerEnded)
                continue;
            if ([importedSiteLine isEqualToString:@"##"]) {
                headerEnded = YES;
                continue;
            }

            // Header
            if ([headerPattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
                                                 range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
                err(@"Invalid header format in line: %@", importedSiteLine);
                return MPImportResultMalformedInput;
            }
            NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
                                                                             range:NSMakeRange( 0, [importedSiteLine length] )] lastObject];
            NSString *headerName = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
            NSString *headerValue = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
            if ([headerName isEqualToString:@"User Name"]) {
                importUserName = headerValue;

                NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
                userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName];
                NSArray *users = [context executeFetchRequest:userFetchRequest error:&error];
                if (!users) {
                    err(@"While looking for user: %@, error: %@", importUserName, error);
                    return MPImportResultInternalError;
                }
                if ([users count] > 1) {
                    err(@"While looking for user: %@, found more than one: %lu", importUserName, (unsigned long)[users count]);
                    return MPImportResultInternalError;
                }

                user = [users count]? [users lastObject]: nil;
                dbg(@"Found user: %@", [user debugDescription]);
            }
            if ([headerName isEqualToString:@"Key ID"])
                importKeyID = [headerValue decodeHex];
            if ([headerName isEqualToString:@"Version"]) {
                importBundleVersion = headerValue;
                importAlgorithm = MPAlgorithmDefaultForBundleVersion( importBundleVersion );
            }
            if ([headerName isEqualToString:@"Passwords"]) {
                if ([headerValue isEqualToString:@"VISIBLE"])
                    clearText = YES;
            }

            continue;
        }
        if (!headerEnded)
            continue;
        if (!importKeyID || ![importUserName length])
            return MPImportResultMalformedInput;
        if (![importedSiteLine length])
            continue;

        // Site
        if ([sitePattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
                                           range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
            err(@"Invalid site format in line: %@", importedSiteLine);
            return MPImportResultMalformedInput;
        }
        NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
                                                                     range:NSMakeRange( 0, [importedSiteLine length] )] lastObject];
        NSString *lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
        NSString *uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
        NSString *type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
        NSString *version = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
        if ([version length])
            version = [version substringFromIndex:1]; // Strip the leading colon.
        NSString *name = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
        NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:6]];

        // Find existing site.
        if (user) {
            elementFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
            NSArray *existingSites = [context executeFetchRequest:elementFetchRequest error:&error];
            if (!existingSites) {
                err(@"Lookup of existing sites failed for site: %@, user: %@, error: %@", name, user.userID, error);
                return MPImportResultInternalError;
            }
            if ([existingSites count]) {
                dbg(@"Existing sites: %@", existingSites);
                [elementsToDelete addObjectsFromArray:existingSites];
            }
        }
        [importedSiteElements addObject:@[ lastUsed, uses, type, version, name, exportContent ]];
        dbg(@"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, name=%@, exportContent=%@",
        lastUsed, uses, type, version, name, exportContent);
    }

    // Ask for confirmation to import these sites and the master password of the user.
    inf(@"Importing %lu sites, deleting %lu sites, for user: %@", (unsigned long)[importedSiteElements count], (unsigned long)[elementsToDelete count], [MPUserEntity idFor:importUserName]);
    NSString *userMasterPassword = askUserPassword( user? user.name: importUserName, [importedSiteElements count],
            [elementsToDelete count] );
    if (!userMasterPassword) {
        inf(@"Import cancelled.");
        return MPImportResultCancelled;
    }
    MPKey *userKey = [MPAlgorithmDefault keyForPassword:userMasterPassword ofUserNamed:user? user.name: importUserName];
    if (user && ![userKey.keyID isEqualToData:user.keyID])
        return MPImportResultInvalidPassword;
    __block MPKey *importKey = userKey;
    if (![importKey.keyID isEqualToData:importKeyID])
        importKey = [importAlgorithm keyForPassword:askImportPassword( importUserName ) ofUserNamed:importUserName];
    if (![importKey.keyID isEqualToData:importKeyID])
        return MPImportResultInvalidPassword;


    // Delete existing sites.
    if (elementsToDelete.count)
        [elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
            inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]);
            [context deleteObject:obj];
        }];

    // Make sure there is a user.
    if (!user) {
        user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] )
                                             inManagedObjectContext:context];
        user.name = importUserName;
        user.keyID = importKeyID;
        dbg(@"Created User: %@", [user debugDescription]);
    }

    // Import new sites.
    for (NSArray *siteElements in importedSiteElements) {
        NSDate *lastUsed = [[NSDateFormatter rfc3339DateFormatter] dateFromString:siteElements[0]];
        NSUInteger uses = (unsigned)[siteElements[1] integerValue];
        MPElementType type = (MPElementType)[siteElements[2] integerValue];
        NSUInteger version = (unsigned)[siteElements[3] integerValue];
        NSString *name = siteElements[4];
        NSString *exportContent = siteElements[5];

        // Create new site.
        MPElementEntity
                *element = [NSEntityDescription insertNewObjectForEntityForName:[MPAlgorithmForVersion( version ) classNameOfType:type]
                                                         inManagedObjectContext:context];
        element.name = name;
        element.user = user;
        element.type = type;
        element.uses = uses;
        element.lastUsed = lastUsed;
        element.version = version;
        if ([exportContent length]) {
            if (clearText)
                [element.algorithm importClearTextContent:exportContent intoElement:element usingKey:userKey];
            else
                [element.algorithm importProtectedContent:exportContent protectedByKey:importKey intoElement:element usingKey:userKey];
        }

        dbg(@"Created Element: %@", [element debugDescription]);
    }

    if (![context saveToStore])
        return MPImportResultInternalError;

    inf(@"Import completed successfully.");
    MPCheckpoint( MPCheckpointSitesImported, nil );

    [[NSNotificationCenter defaultCenter] postNotificationName:MPSitesImportedNotification object:nil userInfo:@{
            MPSitesImportedNotificationUserKey : user
    }];

    return MPImportResultSuccess;
}

- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {

    MPUserEntity *activeUser = [self activeUserForMainThread];
    inf(@"Exporting sites, %@, for: %@", showPasswords? @"showing passwords": @"omitting passwords", activeUser.userID);

    // Header.
    NSMutableString *export = [NSMutableString new];
    [export appendFormat:@"# Master Password site export\n"];
    if (showPasswords)
        [export appendFormat:@"#     Export of site names and passwords in clear-text.\n"];
    else
        [export appendFormat:@"#     Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"];
    [export appendFormat:@"# \n"];
    [export appendFormat:@"##\n"];
    [export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
    [export appendFormat:@"# User Name: %@\n", activeUser.name];
    [export appendFormat:@"# Key ID: %@\n", [activeUser.keyID encodeHex]];
    [export appendFormat:@"# Date: %@\n", [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]]];
    if (showPasswords)
        [export appendFormat:@"# Passwords: VISIBLE\n"];
    else
        [export appendFormat:@"# Passwords: PROTECTED\n"];
    [export appendFormat:@"##\n"];
    [export appendFormat:@"#\n"];
    [export appendFormat:@"#               Last     Times  Password                  Site\tSite\n"];
    [export appendFormat:@"#               used      used      type                  name\tpassword\n"];

    // Sites.
    for (MPElementEntity *element in activeUser.elements) {
        NSDate *lastUsed = element.lastUsed;
        NSUInteger uses = element.uses;
        MPElementType type = element.type;
        NSUInteger version = element.version;
        NSString *name = element.name;
        NSString *content = nil;

        // Determine the content to export.
        if (!(type & MPElementFeatureDevicePrivate)) {
            if (showPasswords)
                content = [element.algorithm resolveContentForElement:element usingKey:self.key];
            else if (type & MPElementFeatureExportContent)
                content = [element.algorithm exportContentForElement:element usingKey:self.key];
        }

        [export appendFormat:@"%@  %8ld  %8s  %20s\t%@\n",
                             [[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], (long)uses,
                             [PearlString( @"%u:%lu", type, (unsigned long)version ) UTF8String], [name UTF8String], content
                                                                                                                     ? content: @""];
    }

    MPCheckpoint( MPCheckpointSitesExported, @{
            @"showPasswords" : @(showPasswords)
    } );

    return export;
}

@end
