//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import SignalCoreKit

private let networkManagerQueue = DispatchQueue(
    label: "org.signal.network-manager",
    autoreleaseFrequency: .workItem)

class RESTNetworkManager {
    fileprivate typealias Success = (HTTPResponse) -> Void
    fileprivate typealias Failure = (OWSHTTPError) -> Void

    private let udSessionManagerPool = OWSSessionManagerPool()
    private let nonUdSessionManagerPool = OWSSessionManagerPool()

    init() {
        SwiftSingletons.register(self)
    }

    private func makeRequest(
        _ request: TSRequest,
        completionQueue: DispatchQueue,
        success: @escaping Success,
        failure: @escaping Failure
    ) {
        networkManagerQueue.async {
            self.makeRequestSync(request, completionQueue: completionQueue, success: success, failure: failure)
        }
    }

    private func makeRequestSync(
        _ request: TSRequest,
        completionQueue: DispatchQueue,
        success successParam: @escaping Success,
        failure failureParam: @escaping Failure
    ) {
        let isUdRequest = request.isUDRequest
        let label = isUdRequest ? "UD request" : "Non-UD request"
        if (isUdRequest) {
            owsAssert(!request.shouldHaveAuthorizationHeaders)
        }
        Logger.info("Making \(label): \(request)")

        let sessionManagerPool = isUdRequest ? self.udSessionManagerPool : self.nonUdSessionManagerPool
        let sessionManager = sessionManagerPool.get()

        let success = { (response: HTTPResponse) in
            #if TESTABLE_BUILD
            if DebugFlags.logCurlOnSuccess {
                HTTPUtils.logCurl(for: request as URLRequest)
            }
            #endif

            networkManagerQueue.async {
                sessionManagerPool.returnToPool(sessionManager)
            }
            completionQueue.async {
                Logger.info("\(label) succeeded (\(response.responseStatusCode)) : \(request)")
                successParam(response)
                OutageDetection.shared.reportConnectionSuccess()
            }
        }
        let failure = { (error: OWSHTTPError) in
            networkManagerQueue.async {
                sessionManagerPool.returnToPool(sessionManager)
            }
            completionQueue.async {
                failureParam(error)
            }
        }
        sessionManager.performRequest(request, success: success, failure: failure)
    }

    func makePromise(request: TSRequest) -> Promise<HTTPResponse> {
        let (promise, future) = Promise<HTTPResponse>.pending()
        makeRequest(request,
                    completionQueue: .global(),
                    success: { (response: HTTPResponse) in
                        future.resolve(response)
                    },
                    failure: { (error: OWSHTTPError) in
                        future.reject(error)
                    })
        return promise
    }
}

// MARK: -

private class RESTSessionManager {

    private let urlSession: OWSURLSessionProtocol
    public let createdDate = Date()

    init() {
        assertOnQueue(networkManagerQueue)
        urlSession = SSKEnvironment.shared.signalService.urlSessionForMainSignalService()
    }

    public func performRequest(_ request: TSRequest,
                               success: @escaping RESTNetworkManager.Success,
                               failure: @escaping RESTNetworkManager.Failure) {
        assertOnQueue(networkManagerQueue)
        owsAssertDebug(!FeatureFlags.deprecateREST)

        // We should only use the RESTSessionManager for requests to the Signal main service.
        let urlSession = self.urlSession
        owsAssertDebug(urlSession.unfrontedBaseUrl == URL(string: TSConstants.mainServiceIdentifiedURL))

        guard let requestUrl = request.url else {
            owsFailDebug("Missing requestUrl.")
            failure(.missingRequest)
            return
        }

        firstly {
            urlSession.promiseForTSRequest(request)
        }.done(on: DispatchQueue.global()) { (response: HTTPResponse) in
            success(response)
        }.catch(on: DispatchQueue.global()) { error in
            // OWSUrlSession should only throw OWSHTTPError or OWSAssertionError.
            if let httpError = error as? OWSHTTPError {
                HTTPUtils.applyHTTPError(httpError)

                if httpError.httpStatusCode == 401, request.shouldCheckDeregisteredOn401 {
                    networkManagerQueue.async {
                        self.makeIsDeregisteredRequest(
                            originalRequestFailureHandler: failure,
                            originalRequestFailure: httpError
                        )
                    }
                } else {
                    failure(httpError)
                }
            } else {
                owsFailDebug("Unexpected error: \(error)")

                failure(.invalidRequest(requestUrl: requestUrl))
            }
        }
    }

    private func makeIsDeregisteredRequest(
        originalRequestFailureHandler: @escaping RESTNetworkManager.Failure,
        originalRequestFailure: OWSHTTPError
    ) {
        let isDeregisteredRequest = WhoAmIRequestFactory.amIDeregisteredRequest()

        let handleDeregisteredResponse: (WhoAmIRequestFactory.Responses.AmIDeregistered?) -> Void = { response in
            switch response {
            case .deregistered:
                Logger.warn("AmIDeregistered response says we are deregistered, marking as such.")
                DependenciesBridge.shared.db.write { tx in
                    DependenciesBridge.shared.registrationStateChangeManager.setIsDeregisteredOrDelinked(true, tx: tx)
                }
            case .notDeregistered:
                Logger.info("AmIDeregistered response says not deregistered; account probably disabled. Doing nothing.")
            case .none, .unexpectedError:
                Logger.error("Got unexpected AmIDeregistered response. Doing nothing.")
            }
        }

        self.performRequest(
            isDeregisteredRequest,
            success: { rawResponse in
                let response = WhoAmIRequestFactory.Responses.AmIDeregistered(rawValue: rawResponse.responseStatusCode)
                handleDeregisteredResponse(response)
                originalRequestFailureHandler(originalRequestFailure)
            }, failure: { rawFailure in
                let response = WhoAmIRequestFactory.Responses.AmIDeregistered(rawValue: rawFailure.responseStatusCode)
                handleDeregisteredResponse(response)
                originalRequestFailureHandler(originalRequestFailure)
            }
        )
    }
}

// MARK: -

// Session managers are stateful (e.g. the headers in the requestSerializer).
// Concurrent requests can interfere with each other. Therefore we use a pool
// do not re-use a session manager until its request succeeds or fails.
private class OWSSessionManagerPool {
    private let maxSessionManagerAge = 5 * kMinuteInterval

    // must only be accessed from the networkManagerQueue for thread-safety
    private var pool: [RESTSessionManager] = []

    // accessed from both networkManagerQueue and the main thread so needs a lock
    @Atomic private var lastDiscardDate: Date?

    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(OWSSessionManagerPool.isCensorshipCircumventionActiveDidChange),
            name: Notification.Name.isCensorshipCircumventionActiveDidChange,
            object: nil)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(OWSSessionManagerPool.isSignalProxyReadyDidChange),
            name: Notification.Name.isSignalProxyReadyDidChange,
            object: nil)
    }

    @objc
    private func isCensorshipCircumventionActiveDidChange() {
        AssertIsOnMainThread()
        lastDiscardDate = Date()
    }

    @objc
    private func isSignalProxyReadyDidChange() {
        AssertIsOnMainThread()
        lastDiscardDate = Date()
    }

    func get() -> RESTSessionManager {
        assertOnQueue(networkManagerQueue)

        // Iterate over the pool, discarding expired session managers
        // until we find an unexpired session manager in the pool or
        // drain the pool and create a new session manager.
        while true {
            guard let sessionManager = pool.popLast() else {
                return RESTSessionManager()
            }
            if shouldDiscardSessionManager(sessionManager) {
                continue
            }
            return sessionManager
        }
    }

    func returnToPool(_ sessionManager: RESTSessionManager) {
        assertOnQueue(networkManagerQueue)

        let maxPoolSize = CurrentAppContext().isNSE ? 5 : 32
        guard pool.count < maxPoolSize && !shouldDiscardSessionManager(sessionManager) else {
            return
        }
        pool.append(sessionManager)
    }

    private func shouldDiscardSessionManager(_ sessionManager: RESTSessionManager) -> Bool {
        if lastDiscardDate?.isAfter(sessionManager.createdDate) ?? false {
            return true
        }
        return fabs(sessionManager.createdDate.timeIntervalSinceNow) > maxSessionManagerAge
    }
}
