//
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//

import Foundation
import PromiseKit
import SessionServiceKit
import SessionMetadataKit

@objc
public class ProfileFetcherJob: NSObject {

    // This property is only accessed on the main queue.
    static var fetchDateMap = [String: Date]()

    let ignoreThrottling: Bool

    var backgroundTask: OWSBackgroundTask?

    @objc
    public class func run(thread: TSThread) {
        guard CurrentAppContext().isMainApp else {
            return
        }

        ProfileFetcherJob().run(recipientIds: thread.recipientIdentifiers)
    }

    @objc
    public class func run(recipientId: String, ignoreThrottling: Bool) {
        guard CurrentAppContext().isMainApp else {
            return
        }

        ProfileFetcherJob(ignoreThrottling: ignoreThrottling).run(recipientIds: [recipientId])
    }

    public init(ignoreThrottling: Bool = false) {
        self.ignoreThrottling = ignoreThrottling
    }

    // MARK: - Dependencies

    private var networkManager: TSNetworkManager {
        return SSKEnvironment.shared.networkManager
    }

    private var socketManager: TSSocketManager {
        return TSSocketManager.shared
    }

    private var primaryStorage: OWSPrimaryStorage {
        return SSKEnvironment.shared.primaryStorage
    }

    private var udManager: OWSUDManager {
        return SSKEnvironment.shared.udManager
    }

    private var profileManager: OWSProfileManager {
        return OWSProfileManager.shared()
    }

    private var identityManager: OWSIdentityManager {
        return SSKEnvironment.shared.identityManager
    }

    private var signalServiceClient: SignalServiceClient {
        // TODO hang on SSKEnvironment
        return SignalServiceRestClient()
    }

    private var tsAccountManager: TSAccountManager {
        return SSKEnvironment.shared.tsAccountManager
    }

    // MARK: -

    public func run(recipientIds: [String]) {
        AssertIsOnMainThread()

        /* Loki: Original code
         * Disabled as we don't have an endpoint for fetching profiles
         * ================
        guard CurrentAppContext().isMainApp else {
            // Only refresh profiles in the MainApp to decrease the chance of missed SN notifications
            // in the AppExtension for our users who choose not to verify contacts.
            owsFailDebug("Should only fetch profiles in the main app")
            return
        }

        backgroundTask = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in
            AssertIsOnMainThread()

            guard status == .expired else {
                return
            }
            guard let _ = self else {
                return
            }
            Logger.error("background task time ran out before profile fetch completed.")
        })

        DispatchQueue.global().async {
            for recipientId in recipientIds {
                self.getAndUpdateProfile(recipientId: recipientId)
            }
        }
         * ================
         */
    }

    enum ProfileFetcherJobError: Error {
        case throttled(lastTimeInterval: TimeInterval)
    }

    public func getAndUpdateProfile(recipientId: String, remainingRetries: Int = 3) {
        self.getProfile(recipientId: recipientId).map(on: DispatchQueue.global()) { profile in
            self.updateProfile(signalServiceProfile: profile)
        }.catch(on: DispatchQueue.global()) { error in
            switch error {
            case ProfileFetcherJobError.throttled(let lastTimeInterval):
                // skipping
                break
            case let error as SignalServiceProfile.ValidationError:
                Logger.warn("skipping updateProfile retry. Invalid profile for: \(recipientId) error: \(error)")
            default:
                if remainingRetries > 0 {
                    self.getAndUpdateProfile(recipientId: recipientId, remainingRetries: remainingRetries - 1)
                } else {
                    Logger.error("failed to get profile with error: \(error)")
                }
            }
        }.retainUntilComplete()
    }

    public func getProfile(recipientId: String) -> Promise<SignalServiceProfile> {
        if !ignoreThrottling {
            if let lastDate = ProfileFetcherJob.fetchDateMap[recipientId] {
                let lastTimeInterval = fabs(lastDate.timeIntervalSinceNow)
                // Don't check a profile more often than every N seconds.
                //
                // Throttle less in debug to make it easier to test problems
                // with our fetching logic.
                let kGetProfileMaxFrequencySeconds = _isDebugAssertConfiguration() ? 60 : 60.0 * 5.0
                guard lastTimeInterval > kGetProfileMaxFrequencySeconds else {
                    return Promise(error: ProfileFetcherJobError.throttled(lastTimeInterval: lastTimeInterval))
                }
            }
        }
        ProfileFetcherJob.fetchDateMap[recipientId] = Date()

        Logger.error("getProfile: \(recipientId)")

        // Don't use UD for "self" profile fetches.
        var udAccess: OWSUDAccess?
        if recipientId != tsAccountManager.localNumber() {
            udAccess = udManager.udAccess(forRecipientId: recipientId,
                                          requireSyncAccess: false)
        }

        return requestProfile(recipientId: recipientId,
                              udAccess: udAccess,
                              canFailoverUDAuth: true)
    }

    private func requestProfile(recipientId: String,
                                udAccess: OWSUDAccess?,
                                canFailoverUDAuth: Bool) -> Promise<SignalServiceProfile> {
        let requestMaker = RequestMaker(label: "Profile Fetch",
                                        requestFactoryBlock: { (udAccessKeyForRequest) -> TSRequest in
            return OWSRequestFactory.getProfileRequest(recipientId: recipientId, udAccessKey: udAccessKeyForRequest)
        }, udAuthFailureBlock: {
            // Do nothing
        }, websocketFailureBlock: {
            // Do nothing
        }, recipientId: recipientId,
           udAccess: udAccess,
           canFailoverUDAuth: canFailoverUDAuth)
        return requestMaker.makeRequest()
            .map(on: DispatchQueue.global()) { (result: RequestMakerResult) -> SignalServiceProfile in
                try SignalServiceProfile(recipientId: recipientId, responseObject: result.responseObject)
        }
    }

    private func updateProfile(signalServiceProfile: SignalServiceProfile) {
        let recipientId = signalServiceProfile.recipientId
        verifyIdentityUpToDateAsync(recipientId: recipientId, latestIdentityKey: signalServiceProfile.identityKey)

        profileManager.updateProfile(forRecipientId: recipientId,
                                     profileNameEncrypted: signalServiceProfile.profileNameEncrypted,
                                     avatarUrlPath: signalServiceProfile.avatarUrlPath)

        updateUnidentifiedAccess(recipientId: recipientId,
                                 verifier: signalServiceProfile.unidentifiedAccessVerifier,
                                 hasUnrestrictedAccess: signalServiceProfile.hasUnrestrictedUnidentifiedAccess)
    }

    private func updateUnidentifiedAccess(recipientId: String, verifier: Data?, hasUnrestrictedAccess: Bool) {
        guard let verifier = verifier else {
            // If there is no verifier, at least one of this user's devices
            // do not support UD.
            udManager.setUnidentifiedAccessMode(.disabled, recipientId: recipientId)
            return
        }

        if hasUnrestrictedAccess {
            udManager.setUnidentifiedAccessMode(.unrestricted, recipientId: recipientId)
            return
        }

        guard let udAccessKey = udManager.udAccessKey(forRecipientId: recipientId) else {
            udManager.setUnidentifiedAccessMode(.disabled, recipientId: recipientId)
            return
        }

        let dataToVerify = Data(count: 32)
        guard let expectedVerifier = Cryptography.computeSHA256HMAC(dataToVerify, withHMACKey: udAccessKey.keyData) else {
            owsFailDebug("could not compute verification")
            udManager.setUnidentifiedAccessMode(.disabled, recipientId: recipientId)
            return
        }

        guard expectedVerifier.ows_constantTimeIsEqual(to: verifier) else {
            Logger.verbose("verifier mismatch, new profile key?")
            udManager.setUnidentifiedAccessMode(.disabled, recipientId: recipientId)
            return
        }

        udManager.setUnidentifiedAccessMode(.enabled, recipientId: recipientId)
    }

    private func verifyIdentityUpToDateAsync(recipientId: String, latestIdentityKey: Data) {
        primaryStorage.newDatabaseConnection().asyncReadWrite { (transaction) in
            if self.identityManager.saveRemoteIdentity(latestIdentityKey, recipientId: recipientId, protocolContext: transaction) {
                Logger.info("updated identity key with fetched profile for recipient: \(recipientId)")
                self.primaryStorage.archiveAllSessions(forContact: recipientId, protocolContext: transaction)
            } else {
                // no change in identity.
            }
        }
    }
}
