//
//  OAuthSwiftHTTPRequest.swift
//  OAuthSwift
//
//  Created by Dongri Jin on 6/21/14.
//  Copyright (c) 2014 Dongri Jin. All rights reserved.
//

import Foundation

let kHTTPHeaderContentType = "Content-Type"

open class OAuthSwiftHTTPRequest: NSObject, OAuthSwiftRequestHandle {

    public typealias SuccessHandler = (_ response: OAuthSwiftResponse) -> Void
    public typealias FailureHandler = (_ error: OAuthSwiftError) -> Void

    /// HTTP request method
    /// https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
    public enum Method: String {
        case GET, POST, PUT, DELETE, PATCH, HEAD //, OPTIONS, TRACE, CONNECT

        var isBody: Bool {
            return self == .POST || self == .PUT || self == .PATCH
        }
    }

    /// Where the additional parameters will be injected
    @objc public enum ParamsLocation: Int {
        case authorizationHeader, /*FormEncodedBody,*/ requestURIQuery
    }

    public var config: Config

    private var request: URLRequest?
    private var task: URLSessionTask?
    private var session: URLSession!

    fileprivate var cancelRequested = false

    open static var executionContext: (@escaping () -> Void) -> Void = { block in
        return DispatchQueue.main.async(execute: block)
    }

    // MARK: INIT

    convenience init(url: URL, method: Method = .GET, parameters: OAuthSwift.Parameters = [:], paramsLocation: ParamsLocation = .authorizationHeader, httpBody: Data? = nil, headers: OAuthSwift.Headers = [:], sessionFactory: URLSessionFactory = .default) {
        self.init(config: Config(url: url, httpMethod: method, httpBody: httpBody, headers: headers, parameters: parameters, paramsLocation: paramsLocation, sessionFactory: sessionFactory))
    }

    convenience init(request: URLRequest, paramsLocation: ParamsLocation = .authorizationHeader, sessionFactory: URLSessionFactory = .default) {
        self.init(config: Config(urlRequest: request, paramsLocation: paramsLocation, sessionFactory: sessionFactory))
    }

    init(config: Config) {
        self.config = config
    }

    /// START request
    func start(success: SuccessHandler?, failure: FailureHandler?) {
        guard request == nil else { return } // Don't start the same request twice!

        let successHandler = success
        let failureHandler = failure

        do {
            self.request = try self.makeRequest()
        } catch let error as NSError {
            failureHandler?(OAuthSwiftError.requestCreation(message: error.localizedDescription))
            self.request = nil
            return
        }

        OAuthSwiftHTTPRequest.executionContext {
            // perform lock here to prevent cancel calls on another thread while creating the request
            objc_sync_enter(self)
            defer { objc_sync_exit(self) }
            if self.cancelRequested {
                return
            }

            self.session = self.config.sessionFactory.build()
            let usedRequest = self.request!

            if self.config.sessionFactory.useDataTaskClosure {
                let completionHandler: (Data?, URLResponse?, Error?) -> Void = { data, resp, error in
                    OAuthSwiftHTTPRequest.completionHandler(successHandler: success,
                                                            failureHandler: failure,
                                                            request: usedRequest,
                                                            data: data,
                                                            resp: resp,
                                                            error: error)
                }
                self.task = self.session.dataTask(with: usedRequest, completionHandler: completionHandler)
            } else {
                self.task = self.session.dataTask(with: usedRequest)
            }

            self.task?.resume()
            self.session.finishTasksAndInvalidate()

            #if os(iOS)
                #if !OAUTH_APP_EXTENSIONS
                    UIApplication.shared.isNetworkActivityIndicatorVisible = self.config.sessionFactory.isNetworkActivityIndicatorVisible
                #endif
            #endif
        }
    }

    /// Function called when receiving data from server.
    public static func completionHandler(successHandler: SuccessHandler?, failureHandler: FailureHandler?, request: URLRequest, data: Data?, resp: URLResponse?, error: Error?) {
        #if os(iOS)
        #if !OAUTH_APP_EXTENSIONS
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
        #endif
        #endif

        // MARK: failure error returned by server
        if let error = error {
            var oauthError: OAuthSwiftError = .requestError(error: error, request: request)
            let nsError = error as NSError
            if nsError.code == NSURLErrorCancelled {
                oauthError = .cancelled
            } else if nsError.isExpiredToken {
                oauthError = .tokenExpired(error: error)
            }

            failureHandler?(oauthError)
            return
        }

        // MARK: failure no response or data returned by server
        guard let response = resp as? HTTPURLResponse, let responseData = data else {
            let badRequestCode = 400
            let localizedDescription = OAuthSwiftHTTPRequest.descriptionForHTTPStatus(badRequestCode, responseString: "")
            var userInfo: [String: Any] = [
                NSLocalizedDescriptionKey: localizedDescription
            ]
            if let response = resp { // there is only no data
                userInfo[OAuthSwiftError.ResponseKey] = response
            }
            if let response = resp as? HTTPURLResponse {
                userInfo["Response-Headers"] = response.allHeaderFields
            }
            let error = NSError(domain: OAuthSwiftError.Domain, code: badRequestCode, userInfo: userInfo)
            failureHandler?(.requestError(error:error, request: request))
            return
        }

        // MARK: failure code > 400
        guard response.statusCode < 400 else {
            var localizedDescription = String()
            let responseString = String(data: responseData, encoding: OAuthSwiftDataEncoding)

            // Try to get error information from data as json
            let responseJSON = try? JSONSerialization.jsonObject(with: responseData, options: .mutableContainers)
            if let responseJSON = responseJSON as? OAuthSwift.Parameters {
                if let code = responseJSON["error"] as? String, let description = responseJSON["error_description"] as? String {

                    localizedDescription = NSLocalizedString("\(code) \(description)", comment: "")
                    if code == "authorization_pending" {
                        failureHandler?(.authorizationPending)
                        return
                    }
                }
            } else {
                localizedDescription = OAuthSwiftHTTPRequest.descriptionForHTTPStatus(response.statusCode, responseString: String(data: responseData, encoding: OAuthSwiftDataEncoding)!)
            }

            var userInfo: [String: Any] = [
                NSLocalizedDescriptionKey: localizedDescription,
                "Response-Headers": response.allHeaderFields,
                OAuthSwiftError.ResponseKey: response,
                OAuthSwiftError.ResponseDataKey: responseData
            ]
            if let string = responseString {
                userInfo["Response-Body"] = string
            }
            if let urlString = response.url?.absoluteString {
                userInfo[NSURLErrorFailingURLErrorKey] = urlString
            }

            let error = NSError(domain: NSURLErrorDomain, code: response.statusCode, userInfo: userInfo)
            if error.isExpiredToken {
                failureHandler?(.tokenExpired(error: error))
            } else {
                failureHandler?(.requestError(error: error, request: request))
            }
            return
        }

        // MARK: success
        successHandler?(OAuthSwiftResponse(data: responseData, response: response, request: request))
    }

    open func cancel() {
        // perform lock here to prevent cancel calls on another thread while creating the request
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }
        // either cancel the request if it's already running or set the flag to prohibit creation of the request
        if let task = task {
            task.cancel()
        } else {
            cancelRequested = true
        }
    }

    open func makeRequest() throws -> URLRequest {
        return try OAuthSwiftHTTPRequest.makeRequest(config: self.config)
    }

    open class func makeRequest(config: Config) throws -> URLRequest {
        var request = config.urlRequest
        return try setupRequestForOAuth(request: &request,
                                        parameters: config.parameters,
                                        dataEncoding: config.dataEncoding,
                                        paramsLocation: config.paramsLocation
        )
    }

    open class func makeRequest(
        url: Foundation.URL,
        method: Method,
        headers: OAuthSwift.Headers,
        parameters: OAuthSwift.Parameters,
        dataEncoding: String.Encoding,
        body: Data? = nil,
        paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {

        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        for (key, value) in headers {
            request.setValue(value, forHTTPHeaderField: key)
        }

        return try setupRequestForOAuth(
            request: &request,
            parameters: parameters,
            dataEncoding: dataEncoding,
            body: body,
            paramsLocation: paramsLocation
        )
    }

    open class func setupRequestForOAuth(
        request: inout URLRequest,
        parameters: OAuthSwift.Parameters,
        dataEncoding: String.Encoding = OAuthSwiftDataEncoding,
        body: Data? = nil,
        paramsLocation: ParamsLocation = .authorizationHeader) throws -> URLRequest {

        let finalParameters: OAuthSwift.Parameters
        switch paramsLocation {
        case .authorizationHeader:
            finalParameters = parameters.filter { key, _ in !key.hasPrefix("oauth_") }
        case .requestURIQuery:
            finalParameters = parameters
        }

        if let b = body {
            request.httpBody = b
        } else {
            if !finalParameters.isEmpty {
                let charset = dataEncoding.charset
                let headers = request.allHTTPHeaderFields ?? [:]
                if request.httpMethod == "GET" || request.httpMethod == "HEAD" || request.httpMethod == "DELETE" {
                    let queryString = finalParameters.urlEncodedQuery
                    let url = request.url!
                    request.url = url.urlByAppending(queryString: queryString)
                    if headers[kHTTPHeaderContentType] == nil {
                        request.setValue("application/x-www-form-urlencoded; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
                    }
                } else {
                    if let contentType = headers[kHTTPHeaderContentType], contentType.contains("application/json") {
                        let jsonData = try JSONSerialization.data(withJSONObject: finalParameters, options: [])
                        request.setValue("application/json; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
                        request.httpBody = jsonData
                    } else if let contentType = headers[kHTTPHeaderContentType], contentType.contains("multipart/form-data") {
                    // snip
                    } else {
                        request.setValue("application/x-www-form-urlencoded; charset=\(charset)", forHTTPHeaderField: kHTTPHeaderContentType)
                        let queryString = finalParameters.urlEncodedQuery
                        request.httpBody = queryString.data(using: dataEncoding, allowLossyConversion: true)
                    }
                }
            }
        }
        return request
    }

}

// MARK: - Request configuraiton
extension OAuthSwiftHTTPRequest {

    /// Configuration for request
    public struct Config {

        /// URLRequest (url, method, ...)
        public var urlRequest: URLRequest
        /// These parameters are either added to the query string for GET, HEAD and DELETE requests or
        /// used as the http body in case of POST, PUT or PATCH requests.
        ///
        /// If used in the body they are either encoded as JSON or as encoded plaintext based on the Content-Type header field.
        public var parameters: OAuthSwift.Parameters
        public let paramsLocation: ParamsLocation
        public let dataEncoding: String.Encoding
        public let sessionFactory: URLSessionFactory

        /// Shortcut
        public var httpMethod: Method {
            if let requestMethod = urlRequest.httpMethod {
                return Method(rawValue: requestMethod) ?? .GET
            }
            return .GET
        }

        public var url: Foundation.URL? {
            return urlRequest.url
        }

        // MARK: init
        public init(url: URL, httpMethod: Method = .GET, httpBody: Data? = nil, headers: OAuthSwift.Headers = [:], timeoutInterval: TimeInterval = 60, httpShouldHandleCookies: Bool = false, parameters: OAuthSwift.Parameters, paramsLocation: ParamsLocation = .authorizationHeader, dataEncoding: String.Encoding = OAuthSwiftDataEncoding, sessionFactory: URLSessionFactory = .default) {
            var urlRequest = URLRequest(url: url)
            urlRequest.httpMethod = httpMethod.rawValue
            urlRequest.httpBody = httpBody
            urlRequest.allHTTPHeaderFields = headers
            urlRequest.timeoutInterval = timeoutInterval
            urlRequest.httpShouldHandleCookies = httpShouldHandleCookies
            self.init(urlRequest: urlRequest, parameters: parameters, paramsLocation: paramsLocation, dataEncoding: dataEncoding, sessionFactory: sessionFactory)
        }

        public init(urlRequest: URLRequest, parameters: OAuthSwift.Parameters = [:], paramsLocation: ParamsLocation = .authorizationHeader, dataEncoding: String.Encoding = OAuthSwiftDataEncoding, sessionFactory: URLSessionFactory = .default) {
            self.urlRequest = urlRequest
            self.parameters = parameters
            self.paramsLocation = paramsLocation
            self.dataEncoding = dataEncoding
            self.sessionFactory = sessionFactory
        }

        /// Modify request with authentification
        public mutating func updateRequest(credential: OAuthSwiftCredential) {
            let method = self.httpMethod
            let url = self.urlRequest.url!
            let headers: OAuthSwift.Headers = self.urlRequest.allHTTPHeaderFields ?? [:]
            let paramsLocation = self.paramsLocation
            let parameters = self.parameters

            var signatureUrl = url
            var signatureParameters = parameters

            // Check if body must be hashed (oauth1)
            let body: Data? = nil
            if method.isBody {
                if let contentType = headers[kHTTPHeaderContentType]?.lowercased() {

                    if contentType.contains("application/json") {
                        // TODO: oauth_body_hash create body before signing if implementing body hashing
                        /*do {
                         let jsonData: Data = try JSONSerialization.jsonObject(parameters, options: [])
                         request.HTTPBody = jsonData
                         requestHeaders["Content-Length"] = "\(jsonData.length)"
                         body = jsonData
                         }
                         catch {
                         }*/

                        signatureParameters = [:] // parameters are not used for general signature (could only be used for body hashing
                    }
                    // else other type are not supported, see setupRequestForOAuth()
                }
            }

            // Need to account for the fact that some consumers will have additional parameters on the
            // querystring, including in the case of fetching a request token. Especially in the case of
            // additional parameters on the request, authorize, or access token exchanges, we need to
            // normalize the URL and add to the parametes collection.

            var queryStringParameters = OAuthSwift.Parameters()
            var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false )
            if let queryItems = urlComponents?.queryItems {
                for queryItem in queryItems {
                    let value = queryItem.value?.safeStringByRemovingPercentEncoding ?? ""
                    queryStringParameters.updateValue(value, forKey: queryItem.name)
                }
            }

            // According to the OAuth1.0a spec, the url used for signing is ONLY scheme, path, and query
            if !queryStringParameters.isEmpty {
                urlComponents?.query = nil
                // This is safe to unwrap because these just came from an NSURL
                signatureUrl = urlComponents?.url ?? url
            }
            signatureParameters = signatureParameters.join(queryStringParameters)

            var requestHeaders = OAuthSwift.Headers()
            switch paramsLocation {
            case .authorizationHeader:
                //Add oauth parameters in the Authorization header
                requestHeaders += credential.makeHeaders(signatureUrl, method: method, parameters: signatureParameters, body: body)
            case .requestURIQuery:
                //Add oauth parameters as request parameters
                self.parameters += credential.authorizationParametersWithSignature(method: method, url: signatureUrl, parameters: signatureParameters, body: body)
            }

            self.urlRequest.allHTTPHeaderFields = requestHeaders + headers
        }

    }
}

// MARK: - session configuration

/// configure how URLSession is initialized
public struct URLSessionFactory {

    public static let `default` = URLSessionFactory()

    public var configuration = URLSessionConfiguration.default
    public var queue = OperationQueue.main
    /// An optional delegate for the URLSession
    public weak var delegate: URLSessionDelegate?

    /// Monitor session: see UIApplication.shared.isNetworkActivityIndicatorVisible
    public var isNetworkActivityIndicatorVisible = true

    /// By default use a closure to receive data from server.
    /// If you set to false, you must in `delegate` take care of server response.
    /// and maybe call in delegate `OAuthSwiftHTTPRequest.completionHandler`
    public var useDataTaskClosure = true

    /// Create a new URLSession
    func build() -> URLSession {
        return URLSession(configuration: self.configuration, delegate: self.delegate, delegateQueue: self.queue)
    }
}

// MARK: - status code mapping

extension OAuthSwiftHTTPRequest {

    class func descriptionForHTTPStatus(_ status: Int, responseString: String) -> String {

        var s = "HTTP Status \(status)"

        var description: String?
        // http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
        if status == 400 { description = "Bad Request" }
        if status == 401 { description = "Unauthorized" }
        if status == 402 { description = "Payment Required" }
        if status == 403 { description = "Forbidden" }
        if status == 404 { description = "Not Found" }
        if status == 405 { description = "Method Not Allowed" }
        if status == 406 { description = "Not Acceptable" }
        if status == 407 { description = "Proxy Authentication Required" }
        if status == 408 { description = "Request Timeout" }
        if status == 409 { description = "Conflict" }
        if status == 410 { description = "Gone" }
        if status == 411 { description = "Length Required" }
        if status == 412 { description = "Precondition Failed" }
        if status == 413 { description = "Payload Too Large" }
        if status == 414 { description = "URI Too Long" }
        if status == 415 { description = "Unsupported Media Type" }
        if status == 416 { description = "Requested Range Not Satisfiable" }
        if status == 417 { description = "Expectation Failed" }
        if status == 422 { description = "Unprocessable Entity" }
        if status == 423 { description = "Locked" }
        if status == 424 { description = "Failed Dependency" }
        if status == 425 { description = "Unassigned" }
        if status == 426 { description = "Upgrade Required" }
        if status == 427 { description = "Unassigned" }
        if status == 428 { description = "Precondition Required" }
        if status == 429 { description = "Too Many Requests" }
        if status == 430 { description = "Unassigned" }
        if status == 431 { description = "Request Header Fields Too Large" }
        if status == 432 { description = "Unassigned" }
        if status == 500 { description = "Internal Server Error" }
        if status == 501 { description = "Not Implemented" }
        if status == 502 { description = "Bad Gateway" }
        if status == 503 { description = "Service Unavailable" }
        if status == 504 { description = "Gateway Timeout" }
        if status == 505 { description = "HTTP Version Not Supported" }
        if status == 506 { description = "Variant Also Negotiates" }
        if status == 507 { description = "Insufficient Storage" }
        if status == 508 { description = "Loop Detected" }
        if status == 509 { description = "Unassigned" }
        if status == 510 { description = "Not Extended" }
        if status == 511 { description = "Network Authentication Required" }

        if description != nil {
            s += ": " + description! + ", Response: " + responseString
        }

        return s
    }

}
