<commit_before>//
//  UserManager.swift
//  MetabolicCompass
//
//  Created by <NAME> on 12/13/15.
//  Copyright © 2015 <NAME>, <NAME>. All rights reserved.
//

import Foundation
import Alamofire
import Locksmith
import Stormpath
import CryptoSwift
import SwiftDate
import Async
import AsyncKit
import JWTDecode

private let UMPrimaryUserKey = "UMPrimaryUserKey"
private let UMUserHashKey    = "UMUserHashKey"

// Profile entry keys
private let UMPHotwordKey    = "UMPHotwordKey"
private let UMPFrequencyKey  = "UMPFrequencyKey"

private let HMHRangeStartKey = "HKHRStart"
private let HMHRangeEndKey   = "HKHREnd"
private let HMHRangeMinKey   = "HKHRMin"

private let profileExcludes  = ["userid"]

public let UMConsentInfoString = "Retrieved consent"
public let UMPhotoInfoString   = "Retrieved photo"


/**
 This manages the users for Metabolic Compass. We need to enable users to maintain access to their data and to delete themselves from the study if they so desire. In addition we maintain, in this class, the ability to do this securely, using OAuth and our third party authenticator (Stormpath)

 - note: We use Stormpath and tokens for authentication
 */
public class UserManager {

    public static let sharedManager = UserManager()

    // Constants.
    public static let maxTokenRetries  = 2
    public static let defaultRefreshFrequency = 30
    public static let defaultHotwords = "food log"

    // Primary user
    public var userId: String? {
        get {
            if let dictionary = Locksmith.loadDataForUserAccount(UMPrimaryUserKey)
            {
                return dictionary["userId"] as? String
            } else {
                return nil
            }
        }
        set(newUser) {
            do {
                if let _newUser = newUser {
                    if let _ = Locksmith.loadDataForUserAccount(UMPrimaryUserKey)
                    {
                        try Locksmith.updateData(["userId" : _newUser], forUserAccount: UMPrimaryUserKey)
                    } else {
                        try Locksmith.saveData(["userId" : _newUser], forUserAccount: UMPrimaryUserKey)
                    }
                }
                else {
                    if let _ = Locksmith.loadDataForUserAccount(UMPrimaryUserKey) {
                        try Locksmith.deleteDataForUserAccount(UMPrimaryUserKey)
                    }
                }
            } catch {
                log.error("userId.set: \(error)")
            }
        }
    }

    var tokenExpiry : NSTimeInterval = NSDate().timeIntervalSince1970   // Expiry in time interval since 1970.

    private(set) public var profileCache : [String: AnyObject] = [:]                        // Stormpath account dictionary cache.

    // Account component dictionary cache.
    private(set) public var componentCache : [AccountComponent: [String: AnyObject]] = [
        .Consent      : [:],
        .Photo        : [:],
        .LastAcquired : [:],
        .Profile      : [:],
        .Settings     : [:]
    ]

    // Track when the remote account component was last retrieved
    var lastComponentLoadDate : [AccountComponent: NSDate?] = [
        .Consent      : nil,
        .Photo        : nil,
        .LastAcquired : nil,
        .Profile      : nil,
        .Settings     : nil
    ]

    // Batched synchronization for account components.
    // These are async optional and double pairs, with the double representing the batch delay.
    var requestAsyncs : [AccountComponent: (Async?, Double)] = [
        .Consent      : (nil, 1.0),
        .Photo        : (nil, 1.0),
        .LastAcquired : (nil, 1.0),
        .Profile      : (nil, 1.0),
        .Settings     : (nil, 1.0)
    ]

    init() {
        Stormpath.setUpWithURL(MCRouter.baseURLString)
        self.profileCache = [:]
    }

    // MARK: - Account status, and authentication

    public func hasUserId() -> Bool {
        if let user = userId {
            return !user.isEmpty
        }
        return false
    }

    public func getUserId() -> String?     { return userId }
    public func setUserId(userId: String)  { self.userId = userId }
    public func resetUserId()              { self.userId = nil }

    // MARK: - Account metadata accessors for fields stored in keychain.

    public func getAccountData() -> [String:AnyObject]? {
        if let user = userId {
            let account = UserAccount(username: user, password: "")
            let lockbox = account.readFromSecureStore()
            return lockbox?.data
        }
        return nil
    }

    public func setAccountData(items: [String:AnyObject]) {
        withUserId { user in
            if var datadict = Locksmith.loadDataForUserAccount(user) {
                for (k,v) in items { datadict[k] = v }
                do {
                    try Locksmith.updateData(datadict, forUserAccount: user)
                } catch {
                    log.error("setAccountData: \(error)")
                }
            }
        }
    }

    public func hasAccount() -> Bool {
        if let user = userId {
            let account = UserAccount(username: user, password: "")
            return account.readFromSecureStore() != nil
        }
        return false
    }

    func createAccount(userId: String, userPass: String) {
        let account = UserAccount(username: userId, password: <PASSWORD>)
        do {
            try account.createInSecureStore()
        } catch {
            log.error("createAccount: \(error)")
        }
    }

    func validateAccount(userPass: String) -> Bool {
        if let pass = getPassword() { return pass == userPass }
        return false
    }

    func resetAccount() {
        withUserId { user in
            let account = UserAccount(username: user, password: "")
            do {
                try account.deleteFromSecureStore()
            } catch {
                log.warning("resetAccount: \(error)")
            }
        }
    }

    public func getPassword() -> String? {
        if let data = getAccountData(), pass = data["password"] as? String {
            return pass
        }
        return nil
    }

    public func hasPassword() -> Bool {
        if let data = getAccountData(), pass = data["password"] as? String {
            return !pass.isEmpty
        }
        return false
    }

    public func setPassword(userPass: String) {
        if hasAccount() {
            setAccountData(["password": user<PASSWORD>])
        } else {
            withUserId { user in self.createAccount(user, userPass: <PASSWORD>) }
        }
    }

    // Set a username and password in keychain, invoking a completion with an error status.
    public func ensureUserPass(user: String?, pass: String?, completion: Bool -> Void) {
        if let u = user, p = pass {
            guard !(u.isEmpty || p.isEmpty) else {
                completion(true)
                return
            }
            UserManager.sharedManager.setUserId(u)
            UserManager.sharedManager.setPassword(p)
            completion(false)
        }
    }

    // Set the username and password in keychain.
    public func overrideUserPass(user: String?, pass: String?) {
        withUserPass(user, password: <PASSWORD>) { (newUser, newPass) in
            UserManager.sharedManager.setUserId(newUser)
            UserManager.sharedManager.setPassword(<PASSWORD>)
        }
    }


    // MARK: - Stormpath-based account creation and authentication

    public func loginWithCompletion(completion: SvcStringCompletion) {
        withUserPass (getPassword()) { (user, pass) in
            Stormpath.login(username: user, password: <PASSWORD>, completionHandler: {
                (accessToken, err) -> Void in
                guard err == nil else {
                    log.error("Stormpath login failed: \(err!.localizedDescription)")
                    self.resetFull()
                    completion(true, err!.localizedDescription)
                    return
                }

                log.verbose("Access token: \(Stormpath.accessToken)")
                MCRouter.updateAuthToken(Stormpath.accessToken)
                completion(false, nil)
            })
        }
    }

    public func loginWithPush(profile: [String: AnyObject], completion: SvcStringCompletion) {
        loginWithCompletion { (error, why) in
            guard !error else {
                completion(true, why)
                return
            }
            self.pushProfile(profile, completion: completion)
        }
    }

    public func loginWithPull(completion: SvcStringCompletion) {
        loginWithCompletion { (error, why) in
            guard !error else {
                completion(true, why)
                return
            }
            self.pullProfile { error, _ in
                if !error { self.pullConsent { (consentError, _) in
                    completion(consentError, consentError ? nil : UMConsentInfoString)
                } }
                else { completion(error, nil) }
            }
        }
    }

    public func login(userPass: String, completion: SvcStringCompletion) {
        withUserId { user in
            if !self.validateAccount(userPass) {
                self.resetAccount()
                self.createAccount(user, userPass: <PASSWORD>)
            }
            self.loginWithPull(completion)
        }
    }

    public func logoutWithCompletion(completion: (Void -> Void)?) {
        Stormpath.logout(completionHandler: { (error) -> Void in
            guard error == nil else {
                log.error("Error logging out of Stormpath: \(error)")
                return
            }
        })
        MCRouter.updateAuthToken(nil)
        resetUser()
        if let comp = completion { comp() }
    }

    public func logout() {
        logoutWithCompletion(nil)
    }

    public func register(firstName: String, lastName: String, completion: ((NSDictionary?, Bool, String?) -> Void))
    {
        withUserPass(getPassword()) { (user,pass) in
            let stormpathAccountDict : [String:String] = [
                "email": user,
                "password": <PASSWORD>,
                "givenName": firstName,
                "surname": lastName
            ]

            print("account dict: \(stormpathAccountDict)")

            Stormpath.register(userDictionary: stormpathAccountDict, completionHandler: {
                (registerDict, error) -> Void in
                if error != nil { log.error("Register failed: \(error)") }
                completion(registerDict, error != nil, error?.localizedDescription)
            })
        }
    }

    public func withdraw(completion: (Bool -> Void)) {
        Service.string(MCRouter.DeleteAccount, statusCode: 200..<300, tag: "WITHDRAW") {
            _, response, result in
            if result.isSuccess { self.resetFull() }
            completion(result.isSuccess)
        }
    }


    public func resetPassword(email: String, completion: ((Bool, String?) -> Void)) {
        Stormpath.resetPassword(email: email, completionHandler: { (error) -> Void in
            if error == nil {
                completion(true, nil)
            }
            else {
                log.error("Reset Password failed: \(error)")
                completion(false, error!.localizedDescription)
            }
        })
    }

    // MARK: - Stormpath token management.

    public func getAccessToken() -> String? {
        return Stormpath.accessToken
    }

    public func ensureAccessToken(tried: Int, completion: (Bool -> Void)) {
        guard tried < UserManager.maxTokenRetries else {
            log.error("Failed to get access token within \(UserManager.maxTokenRetries) iterations")
            // Get the expiry time locally from the token if available.
            // TODO: this is a temporary workaround for issue #30, to support auto-login:
            // https://github.com/yanif/circator/issues/30
            var doReset = true
            if let token = Stormpath.accessToken {
                do {
                    let jwt = try decode(token)
                    if let expiry = jwt.expiresAt?.timeIntervalSince1970 {
                        doReset = false
                        self.tokenExpiry = expiry
                        log.info("Setting expiry as \(expiry)")
                    }
                } catch {}
            }
            if doReset { self.resetFull() }
            completion(doReset)
            return
        }

        Service.json(MCRouter.TokenExpiry([:]), statusCode: 200..<300, tag: "ACCTOK") {
            _, response, result in
                guard result.isSuccess else {
                    self.refreshAccessToken(tried, completion: completion)
                    return
                }
                guard let jwtDict = result.value as? [String:[String:AnyObject]],
                    expiry  = jwtDict["body"]?["exp"] as? NSTimeInterval
                    where expiry > NSDate().timeIntervalSince1970 else
                {
                    self.refreshAccessToken(tried, completion: completion)
                    return
                }
                self.tokenExpiry = expiry
                completion(false)
        }
    }

    public func ensureAccessToken(completion: (Bool -> Void)) {
        if (MCRouter.tokenExpireTime - NSDate().timeIntervalSince1970 > 0) {
            completion(false)
        }
        else {
            self.refreshAccessToken(completion)
        }

        // temporary disabled, waiting for server api clarification
        // ensureAccessToken(0, completion: completion)
    }

    public func refreshAccessToken(tried: Int, completion: (Bool -> Void)) {
        Stormpath.refreshAccesToken { (_, error) in
            guard error == nil else {
                log.warning("Refresh failed: \(error!.localizedDescription)")
                log.warning("Attempting login: \(self.hasAccount()) \(self.hasPassword())")

                if self.hasAccount() && self.hasPassword() {
                    self.loginWithPull { (error,_) in completion(error) }
                } else {
                    completion(true)
                }
                return
            }

            if let token = Stormpath.accessToken {
                log.verbose("Refreshed token: \(token)")
                MCRouter.updateAuthToken(Stormpath.accessToken)

                self.ensureAccessToken(tried+1, completion: completion)
            } else {
                log.error("RefreshAccessToken failed, please login manually.")
                completion(true)
            }
        }
    }

    public func refreshAccessToken(completion: (Bool -> Void)) {
        refreshAccessToken(0, completion: completion)
    }


    // MARK: - Account component accessors

    // Retrieves the currently cached account component and pushes it to the backend.
    // The component may be modified by an extractor to transform the component data.
    private func syncAccountComponent(component: AccountComponent,
                                      extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                      completion: SvcStringCompletion)
    {
        var params : [String:AnyObject] = [:]
        if let fn = extractor { params = fn(componentCache[component]!) }
        else { params = componentCache[component]! }
        Service.string(MCRouter.SetUserAccountData(component, params), statusCode: 200..<300, tag: "SACC\(component)") {
            _, response, result in completion(!result.isSuccess, result.value)
        }
    }

    private func deferredSyncOnAccountComponent(component: AccountComponent,
                                                extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                                sync: Bool)
    {
        if sync {
            requestAsyncs[component]!.0?.cancel()
            let componentDelay = requestAsyncs[component]!.1
            let newAsync = Async.background(after: componentDelay) {
                self.syncAccountComponent(component, extractor: extractor) { _ in () }
            }
            requestAsyncs[component] = (newAsync, componentDelay)
        }
    }

    // Sets the component data in the cache as requested, and then synchronizes with the backend.
    private func pushAccountComponent(component: AccountComponent,
                                      refresh: Bool,
                                      parameters: [String:AnyObject],
                                      extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                      completion: SvcStringCompletion)
    {
        // Refresh cache, and post to the backend.
        // TOD: refresh arg value or extracted value?
        if refresh { refreshComponentCache(component, parameters: parameters) }
        syncAccountComponent(component, extractor: extractor, completion: completion)
    }

    // Sets the component data in the cache as requested, and batches synchronization requests.
    private func deferredPushOnAccountComponent(component: AccountComponent,
                                                refresh: Bool,
                                                sync: Bool,
                                                parameters: [String:AnyObject],
                                                extractor: ([String:AnyObject] -> [String:AnyObject])?)
    {
        if refresh { refreshComponentCache(component, parameters: parameters) }
        deferredSyncOnAccountComponent(component, extractor: extractor, sync: sync)
    }

    // A helper function for a binary account component that retrieves the component data from a file.
    // This is common to both the consent pdf and the profile pic.
    private func pushBinaryFileAccountComponent(
                    filePath: String?,
                    fieldWrapper: String,
                    component: AccountComponent,
                    refresh: Bool,
                    extractor: ([String:AnyObject] -> [String:AnyObject])?,
                    completion: SvcStringCompletion)
    {
        if let path = filePath {
            if let data = NSData(contentsOfFile: path) {
                let cache = [fieldWrapper: data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions())]
                pushAccountComponent(component, refresh: true, parameters: cache, extractor: nil, completion: completion)
            } else {
                log.error("Failed to read \(component) file at: \(path)")
                completion(true, nil)
            }
        } else {
            log.error("Invalid \(component) file path: \(filePath)")
            completion(true, nil)
        }
    }

    // Retrieves an account component from the backend service.
    private func pullAccountComponent(component: AccountComponent,
                                      extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                      completion: SvcObjectCompletion)
    {
        print("!!! pullAccountComponent \(component)")
        Service.json(MCRouter.GetUserAccountData(component), statusCode: 200..<300, tag: "GACC\(component)") {
            _, _, result in
            if result.isSuccess {
                // All account component routes return a JSON object.
                // Use this to refresh the component cache.
                if let dict = result.value as? [String: AnyObject] {
                    let refreshVal = extractor == nil ? dict : extractor!(dict)
                    self.refreshComponentCache(component, parameters: refreshVal)
                    self.lastComponentLoadDate[component] = NSDate()
                }
            }
            completion(!result.isSuccess, result.value)
        }
    }

    // Retrieves an account component if it is stale.
    private func pullAccountComponentIfNeeded(component: AccountComponent,
                                              extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                              completion: SvcObjectCompletion)
    {
        if isAccountComponentOutdated(component) {
            pullAccountComponent(component, extractor: extractor, completion: completion)
        } else {
            print("pull component \(component) skipped")
            completion(false, nil)
        }
    }

    private func isAccountComponentOutdated(component: AccountComponent) -> Bool {
        if let lastDateOpt = lastComponentLoadDate[component], lastDate = lastDateOpt {
            return lastDate.timeIntervalSinceNow < -300.0 // sec
        } else {
            return true
        }
    }

    private func refreshComponentCache(component: AccountComponent, parameters: [String:AnyObject]) {
        if var cache = componentCache[component] {
            for (k,v) in parameters {
                cache.updateValue(v, forKey: k)
            }
            componentCache[component] = cache
        }
    }

    private func getCachedComponent(component: AccountComponent) -> [String: AnyObject] {
        return componentCache[component]!
    }

    private func resetCachedComponent(component: AccountComponent) {
        componentCache.updateValue([:], forKey: component)
    }

    // Batch reset of all components.
    private func resetAccountComponents(components: [AccountComponent]) {
        for component in components { resetCachedComponent(component) }
    }

    // Batch accessor, as an async parallel set of requests to each component's web endpoint.
    private func pullAccountComponents(components: [(AccountComponent, ([String:AnyObject] -> [String:AnyObject])?)],
                                       completion: SvcStringCompletion)
    {
        let async = AsyncKit<AccountComponent, AccountComponent>()
        async.parallel(components.map { (component, extractor) in
            return { done in
                self.pullAccountComponent(component, extractor: extractor) {
                    (error, _) in
                    if error { done(.Failure(component)) }
                    else { done(.Success(component)) }
                }
            }
        }) { result in
            switch result {
            case .Success(_):
                completion(false, nil)
            case .Failure(let component):
                completion(true, "Failed to pull account component \(component)")
            }
        }
    }


    // MARK: - Consent accessors<commit_msg>Added pullFullAccount to retrieve all user account components in parallel. Fixed error handling strings for componentized access.<commit_after>//
//  UserManager.swift
//  MetabolicCompass
//
//  Created by <NAME> on 12/13/15.
//  Copyright © 2015 <NAME>, <NAME>. All rights reserved.
//

import Foundation
import Alamofire
import Locksmith
import Stormpath
import CryptoSwift
import SwiftDate
import Async
import AsyncKit
import JWTDecode

private let UMPrimaryUserKey = "UMPrimaryUserKey"
private let UMUserHashKey    = "UMUserHashKey"

// Profile entry keys
private let UMPHotwordKey    = "UMPHotwordKey"
private let UMPFrequencyKey  = "UMPFrequencyKey"

private let HMHRangeStartKey = "HKHRStart"
private let HMHRangeEndKey   = "HKHREnd"
private let HMHRangeMinKey   = "HKHRMin"

private let profileExcludes  = ["userid"]

public let UMConsentInfoString         = "Retrieved consent"
public let UMPhotoInfoString           = "Retrieved photo"
public let UMPullComponentsInfoString  = "Retrieved account components"
public let UMPullFullAccountInfoString = "Retrieved full account"

// Error generators.
// These are public to allow other components to recreate and check error messages.
public let UMPushInvalidBinaryFileError : (AccountComponent, String?) -> String = { (component, path) in
    return "Invalid \(component) file at: \(path)"
}

public let UMPushReadBinaryFileError : (AccountComponent, String) -> String = { (component, path) in
    return "Failed to read \(component) file at: \(path)"
}

public let UMPullComponentError : AccountComponent -> String = { component in
    return "Failed to pull account component \(component)"
}

/**
 This manages the users for Metabolic Compass. We need to enable users to maintain access to their data and to delete themselves from the study if they so desire. In addition we maintain, in this class, the ability to do this securely, using OAuth and our third party authenticator (Stormpath)

 - note: We use Stormpath and tokens for authentication
 */
public class UserManager {

    public static let sharedManager = UserManager()

    // Constants.
    public static let maxTokenRetries  = 2
    public static let defaultRefreshFrequency = 30
    public static let defaultHotwords = "food log"

    // Primary user
    public var userId: String? {
        get {
            if let dictionary = Locksmith.loadDataForUserAccount(UMPrimaryUserKey)
            {
                return dictionary["userId"] as? String
            } else {
                return nil
            }
        }
        set(newUser) {
            do {
                if let _newUser = newUser {
                    if let _ = Locksmith.loadDataForUserAccount(UMPrimaryUserKey)
                    {
                        try Locksmith.updateData(["userId" : _newUser], forUserAccount: UMPrimaryUserKey)
                    } else {
                        try Locksmith.saveData(["userId" : _newUser], forUserAccount: UMPrimaryUserKey)
                    }
                }
                else {
                    if let _ = Locksmith.loadDataForUserAccount(UMPrimaryUserKey) {
                        try Locksmith.deleteDataForUserAccount(UMPrimaryUserKey)
                    }
                }
            } catch {
                log.error("userId.set: \(error)")
            }
        }
    }

    var tokenExpiry : NSTimeInterval = NSDate().timeIntervalSince1970   // Expiry in time interval since 1970.

    // Account component dictionary cache.
    private(set) public var componentCache : [AccountComponent: [String: AnyObject]] = [
        .Consent      : [:],
        .Photo        : [:],
        .LastAcquired : [:],
        .Profile      : [:],
        .Settings     : [:]
    ]

    // Track when the remote account component was last retrieved
    var lastComponentLoadDate : [AccountComponent: NSDate?] = [
        .Consent      : nil,
        .Photo        : nil,
        .LastAcquired : nil,
        .Profile      : nil,
        .Settings     : nil
    ]

    // Batched synchronization for account components.
    // These are async optional and double pairs, with the double representing the batch delay.
    var requestAsyncs : [AccountComponent: (Async?, Double)] = [
        .Consent      : (nil, 1.0),
        .Photo        : (nil, 1.0),
        .LastAcquired : (nil, 1.0),
        .Profile      : (nil, 1.0),
        .Settings     : (nil, 1.0)
    ]

    init() {
        Stormpath.setUpWithURL(MCRouter.baseURLString)
    }

    // MARK: - Account status, and authentication

    public func hasUserId() -> Bool {
        if let user = userId {
            return !user.isEmpty
        }
        return false
    }

    public func getUserId() -> String?     { return userId }
    public func setUserId(userId: String)  { self.userId = userId }
    public func resetUserId()              { self.userId = nil }

    // MARK: - Account metadata accessors for fields stored in keychain.

    public func getAccountData() -> [String:AnyObject]? {
        if let user = userId {
            let account = UserAccount(username: user, password: "")
            let lockbox = account.readFromSecureStore()
            return lockbox?.data
        }
        return nil
    }

    public func setAccountData(items: [String:AnyObject]) {
        withUserId { user in
            if var datadict = Locksmith.loadDataForUserAccount(user) {
                for (k,v) in items { datadict[k] = v }
                do {
                    try Locksmith.updateData(datadict, forUserAccount: user)
                } catch {
                    log.error("setAccountData: \(error)")
                }
            }
        }
    }

    public func hasAccount() -> Bool {
        if let user = userId {
            let account = UserAccount(username: user, password: "")
            return account.readFromSecureStore() != nil
        }
        return false
    }

    func createAccount(userId: String, userPass: String) {
        let account = UserAccount(username: userId, password: <PASSWORD>)
        do {
            try account.createInSecureStore()
        } catch {
            log.error("createAccount: \(error)")
        }
    }

    func validateAccount(userPass: String) -> Bool {
        if let pass = getPassword() { return pass == userPass }
        return false
    }

    func resetAccount() {
        withUserId { user in
            let account = UserAccount(username: user, password: "")
            do {
                try account.deleteFromSecureStore()
            } catch {
                log.warning("resetAccount: \(error)")
            }
        }
    }

    public func getPassword() -> String? {
        if let data = getAccountData(), pass = data["password"] as? String {
            return pass
        }
        return nil
    }

    public func hasPassword() -> Bool {
        if let data = getAccountData(), pass = data["password"] as? String {
            return !pass.isEmpty
        }
        return false
    }

    public func setPassword(userPass: String) {
        if hasAccount() {
            setAccountData(["password": userPass])
        } else {
            withUserId { user in self.createAccount(user, userPass: userPass) }
        }
    }

    // Set a username and password in keychain, invoking a completion with an error status.
    public func ensureUserPass(user: String?, pass: String?, completion: Bool -> Void) {
        if let u = user, p = pass {
            guard !(u.isEmpty || p.isEmpty) else {
                completion(true)
                return
            }
            UserManager.sharedManager.setUserId(u)
            UserManager.sharedManager.setPassword(p)
            completion(false)
        }
    }

    // Set the username and password in keychain.
    public func overrideUserPass(user: String?, pass: String?) {
        withUserPass(user, password: pass) { (newUser, newPass) in
            UserManager.sharedManager.setUserId(newUser)
            UserManager.sharedManager.setPassword(<PASSWORD>)
        }
    }


    // MARK: - Stormpath-based account creation and authentication

    public func loginWithCompletion(completion: SvcStringCompletion) {
        withUserPass (getPassword()) { (user, pass) in
            Stormpath.login(username: user, password: pass, completionHandler: {
                (accessToken, err) -> Void in
                guard err == nil else {
                    log.error("Stormpath login failed: \(err!.localizedDescription)")
                    self.resetFull()
                    completion(true, err!.localizedDescription)
                    return
                }

                log.verbose("Access token: \(Stormpath.accessToken)")
                MCRouter.updateAuthToken(Stormpath.accessToken)
                completion(false, nil)
            })
        }
    }

    public func loginWithPush(profile: [String: AnyObject], completion: SvcStringCompletion) {
        loginWithCompletion { (error, why) in
            guard !error else {
                completion(true, why)
                return
            }
            self.pushProfile(profile, completion: completion)
        }
    }

    public func loginWithPull(completion: SvcStringCompletion) {
        loginWithCompletion { (error, why) in
            guard !error else {
                completion(true, why)
                return
            }
            self.pullFullAccount(completion)
        }
    }

    public func login(userPass: String, completion: SvcStringCompletion) {
        withUserId { user in
            if !self.validateAccount(userPass) {
                self.resetAccount()
                self.createAccount(user, userPass: userPass)
            }
            self.loginWithPull(completion)
        }
    }

    public func logoutWithCompletion(completion: (Void -> Void)?) {
        Stormpath.logout(completionHandler: { (error) -> Void in
            guard error == nil else {
                log.error("Error logging out of Stormpath: \(error)")
                return
            }
        })
        MCRouter.updateAuthToken(nil)
        resetUser()
        if let comp = completion { comp() }
    }

    public func logout() {
        logoutWithCompletion(nil)
    }

    public func register(firstName: String, lastName: String, completion: ((NSDictionary?, Bool, String?) -> Void))
    {
        withUserPass(getPassword()) { (user,pass) in
            let stormpathAccountDict : [String:String] = [
                "email": user,
                "password": <PASSWORD>,
                "givenName": firstName,
                "surname": lastName
            ]

            print("account dict: \(stormpathAccountDict)")

            Stormpath.register(userDictionary: stormpathAccountDict, completionHandler: {
                (registerDict, error) -> Void in
                if error != nil { log.error("Register failed: \(error)") }
                completion(registerDict, error != nil, error?.localizedDescription)
            })
        }
    }

    public func withdraw(completion: (Bool -> Void)) {
        Service.string(MCRouter.DeleteAccount, statusCode: 200..<300, tag: "WITHDRAW") {
            _, response, result in
            if result.isSuccess { self.resetFull() }
            completion(result.isSuccess)
        }
    }


    public func resetPassword(email: String, completion: ((Bool, String?) -> Void)) {
        Stormpath.resetPassword(email: email, completionHandler: { (error) -> Void in
            if error == nil {
                completion(true, nil)
            }
            else {
                log.error("Reset Password failed: \(error)")
                completion(false, error!.localizedDescription)
            }
        })
    }

    // MARK: - Stormpath token management.

    public func getAccessToken() -> String? {
        return Stormpath.accessToken
    }

    public func ensureAccessToken(tried: Int, completion: (Bool -> Void)) {
        guard tried < UserManager.maxTokenRetries else {
            log.error("Failed to get access token within \(UserManager.maxTokenRetries) iterations")
            // Get the expiry time locally from the token if available.
            // TODO: this is a temporary workaround for issue #30, to support auto-login:
            // https://github.com/yanif/circator/issues/30
            var doReset = true
            if let token = Stormpath.accessToken {
                do {
                    let jwt = try decode(token)
                    if let expiry = jwt.expiresAt?.timeIntervalSince1970 {
                        doReset = false
                        self.tokenExpiry = expiry
                        log.info("Setting expiry as \(expiry)")
                    }
                } catch {}
            }
            if doReset { self.resetFull() }
            completion(doReset)
            return
        }

        Service.json(MCRouter.TokenExpiry([:]), statusCode: 200..<300, tag: "ACCTOK") {
            _, response, result in
                guard result.isSuccess else {
                    self.refreshAccessToken(tried, completion: completion)
                    return
                }
                guard let jwtDict = result.value as? [String:[String:AnyObject]],
                    expiry  = jwtDict["body"]?["exp"] as? NSTimeInterval
                    where expiry > NSDate().timeIntervalSince1970 else
                {
                    self.refreshAccessToken(tried, completion: completion)
                    return
                }
                self.tokenExpiry = expiry
                completion(false)
        }
    }

    public func ensureAccessToken(completion: (Bool -> Void)) {
        if (MCRouter.tokenExpireTime - NSDate().timeIntervalSince1970 > 0) {
            completion(false)
        }
        else {
            self.refreshAccessToken(completion)
        }

        // temporary disabled, waiting for server api clarification
        // ensureAccessToken(0, completion: completion)
    }

    public func refreshAccessToken(tried: Int, completion: (Bool -> Void)) {
        Stormpath.refreshAccesToken { (_, error) in
            guard error == nil else {
                log.warning("Refresh failed: \(error!.localizedDescription)")
                log.warning("Attempting login: \(self.hasAccount()) \(self.hasPassword())")

                if self.hasAccount() && self.hasPassword() {
                    self.loginWithPull { (error,_) in completion(error) }
                } else {
                    completion(true)
                }
                return
            }

            if let token = Stormpath.accessToken {
                log.verbose("Refreshed token: \(token)")
                MCRouter.updateAuthToken(Stormpath.accessToken)

                self.ensureAccessToken(tried+1, completion: completion)
            } else {
                log.error("RefreshAccessToken failed, please login manually.")
                completion(true)
            }
        }
    }

    public func refreshAccessToken(completion: (Bool -> Void)) {
        refreshAccessToken(0, completion: completion)
    }


    // MARK: - Account component accessors

    // Retrieves the currently cached account component and pushes it to the backend.
    // The component may be modified by an extractor to transform the component data.
    private func syncAccountComponent(component: AccountComponent,
                                      extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                      completion: SvcStringCompletion)
    {
        var params : [String:AnyObject] = [:]
        if let fn = extractor { params = fn(componentCache[component]!) }
        else { params = componentCache[component]! }
        Service.string(MCRouter.SetUserAccountData(component, params), statusCode: 200..<300, tag: "SACC\(component)") {
            _, response, result in completion(!result.isSuccess, result.value)
        }
    }

    private func deferredSyncOnAccountComponent(component: AccountComponent,
                                                extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                                sync: Bool)
    {
        if sync {
            requestAsyncs[component]!.0?.cancel()
            let componentDelay = requestAsyncs[component]!.1
            let newAsync = Async.background(after: componentDelay) {
                self.syncAccountComponent(component, extractor: extractor) { _ in () }
            }
            requestAsyncs[component] = (newAsync, componentDelay)
        }
    }

    // Sets the component data in the cache as requested, and then synchronizes with the backend.
    private func pushAccountComponent(component: AccountComponent,
                                      refresh: Bool,
                                      parameters: [String:AnyObject],
                                      extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                      completion: SvcStringCompletion)
    {
        // Refresh cache, and post to the backend.
        // TOD: refresh arg value or extracted value?
        if refresh { refreshComponentCache(component, parameters: parameters) }
        syncAccountComponent(component, extractor: extractor, completion: completion)
    }

    // Sets the component data in the cache as requested, and batches synchronization requests.
    private func deferredPushOnAccountComponent(component: AccountComponent,
                                                refresh: Bool,
                                                sync: Bool,
                                                parameters: [String:AnyObject],
                                                extractor: ([String:AnyObject] -> [String:AnyObject])?)
    {
        if refresh { refreshComponentCache(component, parameters: parameters) }
        deferredSyncOnAccountComponent(component, extractor: extractor, sync: sync)
    }

    // A helper function for a binary account component that retrieves the component data from a file.
    // This is common to both the consent pdf and the profile pic.
    private func pushBinaryFileAccountComponent(
                    filePath: String?,
                    fieldWrapper: String,
                    component: AccountComponent,
                    refresh: Bool,
                    extractor: ([String:AnyObject] -> [String:AnyObject])?,
                    completion: SvcStringCompletion)
    {
        if let path = filePath {
            if let data = NSData(contentsOfFile: path) {
                let cache = [fieldWrapper: data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions())]
                pushAccountComponent(component, refresh: true, parameters: cache, extractor: nil, completion: completion)
            } else {
                let msg = UMPushReadBinaryFileError(component, path)
                log.error(msg)
                completion(true, msg)
            }
        } else {
            let msg = UMPushInvalidBinaryFileError(component, filePath)
            log.error(msg)
            completion(true, msg)
        }
    }

    // Retrieves an account component from the backend service.
    private func pullAccountComponent(component: AccountComponent,
                                      extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                      completion: SvcObjectCompletion)
    {
        print("!!! pullAccountComponent \(component)")
        Service.json(MCRouter.GetUserAccountData(component), statusCode: 200..<300, tag: "GACC\(component)") {
            _, _, result in
            if result.isSuccess {
                // All account component routes return a JSON object.
                // Use this to refresh the component cache.
                if let dict = result.value as? [String: AnyObject] {
                    let refreshVal = extractor == nil ? dict : extractor!(dict)
                    self.refreshComponentCache(component, parameters: refreshVal)
                    self.lastComponentLoadDate[component] = NSDate()
                }
            }
            completion(!result.isSuccess, result.value)
        }
    }

    // Retrieves an account component if it is stale.
    private func pullAccountComponentIfNeeded(component: AccountComponent,
                                              extractor: ([String:AnyObject] -> [String:AnyObject])?,
                                              completion: SvcObjectCompletion)
    {
        if isAccountComponentOutdated(component) {
            pullAccountComponent(component, extractor: extractor, completion: completion)
        } else {
            print("pull component \(component) skipped")
            completion(false, nil)
        }
    }

    private func isAccountComponentOutdated(component: AccountComponent) -> Bool {
        if let lastDateOpt = lastComponentLoadDate[component], lastDate = lastDateOpt {
            return lastDate.timeIntervalSinceNow < -300.0 // sec
        } else {
            return true
        }
    }

    private func refreshComponentCache(component: AccountComponent, parameters: [String:AnyObject]) {
        if var cache = componentCache[component] {
            for (k,v) in parameters {
                cache.updateValue(v, forKey: k)
            }
            componentCache[component] = cache
        }
    }

    private func getCachedComponent(component: AccountComponent) -> [String: AnyObject] {
        return componentCache[component]!
    }

    private func resetCachedComponent(component: AccountComponent) {
        componentCache.updateValue([:], forKey: component)
    }

    // Batch reset of all components.
    private func resetAccountComponents(components: [AccountComponent]) {
        for component in components { resetCachedComponent(component) }
    }

    // Batch accessor, as an async parallel set of requests to each component's web endpoint.
    private func pullAccountComponents(components: [(AccountComponent, ([String:AnyObject] -> [String:AnyObject])?)],
                                       completion: SvcStringCompletion)
    {
        let async = AsyncKit<AccountComponent, AccountComponent>()
        async.parallel(components.map { (component, extractor) in
            return { done in
                self.pullAccountComponent(component, extractor: extractor) {
                    (error, _) in
                    if error { done(.Failure(component)) }
                    else { done(.Success(component)) }
                }
            }
        }) { result in
            switch result {
            case .Success(_):
                completion(false, UMPullComponentsInfoString)
            case .Failure(let component):
                completion(true, UMPullComponentError(component))
            }
        }
    }

    public func pullFullAccount(completion: SvcStringCompletion) {
        pullAccountComponents([.Consent, .Photo, .Profile, .Settings, .ArchiveSpan, .LastAcquired]) {
            (error, msg) in
            completion(error, (!error && msg == UMPullComponentsInfoString) ? UMPullComponentsInfoString : msg)
        }
    }

    // MARK: - Consent accessors
{'fraction_non_alphanumeric': 0.09028506470793984, 'fraction_numerical': 0.0037163343826512765, 'mean_word_length': 1.8021439509954058, 'pattern_counts': {'":': 10, '<': 34, '<?xml version=': 0, '>': 91, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 50, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7232517', 'n_tokens_mistral': 11685, 'n_tokens_neox': 11019, 'n_words': 3025}