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

import Foundation
import PromiseKit
import SessionMetadataKit

@objc
public enum RequestMakerUDAuthError: Int, Error {
    case udAuthFailure
}

public enum RequestMakerError: Error {
    case websocketRequestError(statusCode : Int, responseData : Data?, underlyingError : Error)
}

@objc(OWSRequestMakerResult)
public class RequestMakerResult: NSObject {
    @objc
    public let responseObject: Any?

    @objc
    public let wasSentByUD: Bool

    @objc
    public let wasSentByWebsocket: Bool

    @objc
    public init(responseObject: Any?,
                wasSentByUD: Bool,
                wasSentByWebsocket: Bool) {
        self.responseObject = responseObject
        self.wasSentByUD = wasSentByUD
        self.wasSentByWebsocket = wasSentByWebsocket
    }
}

// A utility class that handles:
//
// * UD auth-to-Non-UD auth failover.
// * Websocket-to-REST failover.
@objc(OWSRequestMaker)
public class RequestMaker: NSObject {

    public typealias RequestFactoryBlock = (SMKUDAccessKey?) -> TSRequest
    public typealias UDAuthFailureBlock = () -> Void
    public typealias WebsocketFailureBlock = () -> Void

    private let label: String
    private let requestFactoryBlock: RequestFactoryBlock
    private let udAuthFailureBlock: UDAuthFailureBlock
    private let websocketFailureBlock: WebsocketFailureBlock
    private let recipientId: String
    private let udAccess: OWSUDAccess?
    private let canFailoverUDAuth: Bool

    @objc
    public init(label: String,
                requestFactoryBlock : @escaping RequestFactoryBlock,
                udAuthFailureBlock : @escaping UDAuthFailureBlock,
                websocketFailureBlock : @escaping WebsocketFailureBlock,
                recipientId: String,
                udAccess: OWSUDAccess?,
                canFailoverUDAuth: Bool) {
        self.label = label
        self.requestFactoryBlock = requestFactoryBlock
        self.udAuthFailureBlock = udAuthFailureBlock
        self.websocketFailureBlock = websocketFailureBlock
        self.recipientId = recipientId
        self.udAccess = udAccess
        self.canFailoverUDAuth = canFailoverUDAuth
    }

    // MARK: - Dependencies

    private var socketManager: TSSocketManager {
        return SSKEnvironment.shared.socketManager
    }

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

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

    private var profileManager: ProfileManagerProtocol {
        return SSKEnvironment.shared.profileManager
    }

    // MARK: -

    @objc
    public func makeRequestObjc() -> AnyPromise {
        let promise = makeRequest()
            .recover(on: DispatchQueue.global()) { (error: Error) -> Promise<RequestMakerResult> in
                switch error {
                case NetworkManagerError.taskError(_, let underlyingError):
                    throw underlyingError
                default:
                    throw error
                }
            }
        let anyPromise = AnyPromise(promise)
        anyPromise.retainUntilComplete()
        return anyPromise
    }

    public func makeRequest() -> Promise<RequestMakerResult> {
        return makeRequestInternal(skipUD: false, skipWebsocket: false)
    }

    private func makeRequestInternal(skipUD: Bool, skipWebsocket: Bool) -> Promise<RequestMakerResult> {
        var udAccessForRequest: OWSUDAccess?
        if !skipUD {
            udAccessForRequest = udAccess
        }
        let isUDRequest: Bool = udAccessForRequest != nil
        let request: TSRequest = requestFactoryBlock(udAccessForRequest?.udAccessKey)
        let canMakeWebsocketRequests = (socketManager.canMakeRequests() && !skipWebsocket && !isUDRequest)

        if canMakeWebsocketRequests {
            return Promise { resolver in
                socketManager.make(request, success: { (responseObject: Any?) in
                    if self.udManager.isUDVerboseLoggingEnabled() {
                        if isUDRequest {
                            Logger.debug("UD websocket request '\(self.label)' succeeded.")
                        } else {
                            Logger.debug("Non-UD websocket request '\(self.label)' succeeded.")
                        }
                    }

                    self.requestSucceeded(udAccess: udAccessForRequest)

                    resolver.fulfill(RequestMakerResult(responseObject: responseObject,
                                                        wasSentByUD: isUDRequest,
                                                        wasSentByWebsocket: true))
                }) { (statusCode: Int, responseData: Data?, error: Error) in
                    resolver.reject(RequestMakerError.websocketRequestError(statusCode: statusCode, responseData: responseData, underlyingError: error))
                }
                }.recover { (error: Error) -> Promise<RequestMakerResult> in
                    switch error {
                    case RequestMakerError.websocketRequestError(let statusCode, _, _):
                        if isUDRequest && (statusCode == 401 || statusCode == 403) {
                            // If a UD request fails due to service response (as opposed to network
                            // failure), mark recipient as _not_ in UD mode, then retry.
                            self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId)
                            self.profileManager.fetchProfile(forRecipientId: self.recipientId)
                            self.udAuthFailureBlock()

                            if self.canFailoverUDAuth {
                                Logger.info("UD websocket request '\(self.label)' auth failed; failing over to non-UD websocket request.")
                                return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket)
                            } else {
                                Logger.info("UD websocket request '\(self.label)' auth failed; aborting.")
                                throw RequestMakerUDAuthError.udAuthFailure
                            }
                        }
                        break
                    default:
                        break
                    }

                    self.websocketFailureBlock()
                    if isUDRequest {
                        Logger.info("UD Web socket request '\(self.label)' failed; failing over to REST request: \(error).")
                    } else {
                        Logger.info("Non-UD Web socket request '\(self.label)' failed; failing over to REST request: \(error).")
                    }
                    return self.makeRequestInternal(skipUD: skipUD, skipWebsocket: true)
                }
        } else {
            return self.networkManager.makePromise(request: request)
                .map(on: DispatchQueue.global()) { (networkManagerResult: TSNetworkManager.NetworkManagerResult) -> RequestMakerResult in
                    if self.udManager.isUDVerboseLoggingEnabled() {
                        if isUDRequest {
                            Logger.debug("UD REST request '\(self.label)' succeeded.")
                        } else {
                            Logger.debug("Non-UD REST request '\(self.label)' succeeded.")
                        }
                    }

                    self.requestSucceeded(udAccess: udAccessForRequest)

                    // Unwrap the network manager promise into a request maker promise.
                    return RequestMakerResult(responseObject: networkManagerResult.responseObject,
                                              wasSentByUD: isUDRequest,
                                              wasSentByWebsocket: false)
                }.recover { (error: Error) -> Promise<RequestMakerResult> in
                    switch error {
                    case NetworkManagerError.taskError(let task, _):
                        let statusCode = task.statusCode()
                        if isUDRequest && (statusCode == 401 || statusCode == 403) {
                            // If a UD request fails due to service response (as opposed to network
                            // failure), mark recipient as _not_ in UD mode, then retry.
                            self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId)
                            self.profileManager.fetchProfile(forRecipientId: self.recipientId)
                            self.udAuthFailureBlock()

                            if self.canFailoverUDAuth {
                                Logger.info("UD REST request '\(self.label)' auth failed; failing over to non-UD REST request.")
                                return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket)
                            } else {
                                Logger.info("UD REST request '\(self.label)' auth failed; aborting.")
                                throw RequestMakerUDAuthError.udAuthFailure
                            }
                        }
                        break
                    default:
                        break
                    }

                    if isUDRequest {
                        Logger.debug("UD REST request '\(self.label)' failed: \(error).")
                    } else {
                        Logger.debug("Non-UD REST request '\(self.label)' failed: \(error).")
                    }
                    throw error
                }
        }
    }

    private func requestSucceeded(udAccess: OWSUDAccess?) {
        // If this was a UD request...
        guard let udAccess = udAccess else {
            return
        }
        // ...made for a user in "unknown" UD access mode...
        guard udAccess.udAccessMode == .unknown else {
            return
        }

        if udAccess.isRandomKey {
            // If a UD request succeeds for an unknown user with a random key,
            // mark recipient as .unrestricted.
            udManager.setUnidentifiedAccessMode(.unrestricted, recipientId: recipientId)
        } else {
            // If a UD request succeeds for an unknown user with a non-random key,
            // mark recipient as .enabled.
            udManager.setUnidentifiedAccessMode(.enabled, recipientId: recipientId)
        }
        DispatchQueue.main.async {
            self.profileManager.fetchProfile(forRecipientId: self.recipientId)
        }
    }
}
