//
/* NOTICE: All information contained herein is, and remains the property of Brightinsight Inc. or its customer.
The intellectual and technical concepts contained herein are proprietary to Brightinsight Inc. or its customer and may be covered by U.S. and Foreign Patents, patents in process, and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material is strictly forbidden unless prior written permission is obtained from Brightinsight Inc. or its customer. Access to the source code contained herein is hereby forbidden to anyone except current Brightinsight Inc. employees, managers or contractors who have executed
Confidentiality and Non-disclosure agreements explicitly covering such access.
*/
/**
* System Name : BISDKCore
* Component ID : BISDKApiRequest
* Description :
* Author: Walter José
* Copyright © : Brightinsight, Inc.
* -----------------------
* Revision Change
* -----------------------
* Author            Date             Version       Change-Description
*========================================================================================================
*/

import Foundation

public enum BISDKHTTPHeader: Hashable {
    case contentType
    case apiKey
    case authorization
    case custom(String)
    
    var keyName: String {
        switch self {
        case .contentType:
            return "Content-Type"
        case .apiKey:
            return "apikey"
        case .authorization:
            return "Authorization"
        case .custom(let customKey):
            return customKey
        }
    }
    public func hash(into hasher: inout Hasher) {
        hasher.combine(keyName)
    }
}

/**
 * Provide intefaces to build a URLSession, a part of `BISDKConfig`, to perform API requests.
 *
 * Supports request method, headers, parameters, request body. A base URL `base_url` is required.
 * Use a generic type for responses that conforms the `Codable` protocol.
 */
public class BISDKApiRequest <T: Codable> {
    /// Server configuration
    let config: BISDKConfig
    
    /// HTTP method
    public var method: String
    
    /// OMS base URL
    public var baseURL: URLComponents
    
    /// Request headers collection
    public var headers:[String: String] = [:]
    
    /// Request parameters collection
    public var parameters: [String: String] = [:]
    
    /// Request body
    public var body: Data? = nil
    
    /// Generic handler for server responses
    public var completion: ((T?, Error?) -> Void)? = nil
    
    /**
     * Initializer of the `BISDKApiRequest` builder.
     *
     * - Parameters:
     *      - config: `BISDKConfig` to indicate the base URL and the API Key.
     */
    public init(_ config: BISDKConfig) {
        self.config = config
        self.method = "GET"
        self.baseURL = URLComponents(string: config.baseURL)!
        self.headers[BISDKHTTPHeader.apiKey.keyName] = config.apiKey
        self.headers[BISDKHTTPHeader.contentType.keyName] = "application/json"
    }
    
    /**
     * Builder interface to set the base URL to perform the request.
     *
     * Note: Update the value of the initial config `baseUrl`
     *
     * - Parameters:
     *      - baseUrl: Base URL of the request. Please, provide also the protocol (HTTP, HTTPS)
     *
     * - Returns: Instance of the request including the provided base URL.
     */
    public func withURL(_ baseURL: String) -> BISDKApiRequest{
        self.baseURL = URLComponents(string: baseURL)!
        return self
    }
    
    
    /**
     * Callback triggered at the moment the response arrives from backend service.
     *
     * - Parameters:
     *      - completion: Handler that arrives from service including the generic reponse object.
     *
     * - Returns: Instance of the request including the completion handler.
     */
    public func onCompletion(_ completion: @escaping (T?, Error?) -> Void) -> BISDKApiRequest{
        self.completion = completion
        return self
    }
    
    /**
     * Add path to base URL
     *
     * - Parameters:
     *      - path: String representing the path to be added to the base URL.
     *
     * - Returns: Instance of the request including the path.
     */
    public func addPath(_ path: String) -> BISDKApiRequest{
        self.baseURL.path += path
        return self
    }
    
    /**
     * Interface to set the header properties to the request.
     *
     * - Parameters:
     *   - newHeaders: New headers to add into the request.
     *
     * - Returns: Instance of the request including new headers.
     */
    public func addHeaders(_ newHeaders: [BISDKHTTPHeader: String]) -> BISDKApiRequest{
        for (key,value) in newHeaders{
            self.headers.updateValue(value, forKey: key.keyName)
        }
        return self
    }
    
    /**
     * Interface to set the parameter properties to the request.
     *
     * - Parameters:
     *      - newParameters: New parameters to add to the request.
     *
     * - Returns: instance of the request including new parameters.
     */
    public func addParameters(_ newParameters: [String: String]) -> BISDKApiRequest{
        for (key,value) in newParameters{
            self.parameters.updateValue(value, forKey: key)
        }
        return self
    }
    
    /**
     * Interface to set the body to the current request receiving a `Dictionary` of values
     *
     * - Parameters:
     *      - body: New body of the request.
     *
     * - Returns: Instance of the request including the body.
     */
    public func withBody(_ body: [String: Any]) -> BISDKApiRequest{
        self.body = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
        return self
    }
    
    /**
     * Interface to set the body to the current request receiving an `Encodable` object.
     *
     * - Parameters:
     *      - body: New body of the request.
     *
     * - Returns: instance of the request including the body.
     */
    public func withBody(_ body: Encodable) -> BISDKApiRequest{
        self.body = try? JSONSerialization.data(withJSONObject: body.asDictionary(), options: .prettyPrinted)
        return self
    }
    
    /**
     * Interface to set the body to the current request receiving a `Data` object.
     *
     * - Parameters:
     *    - body: New body of the request.
     *
     * - Returns: instance of the request including the body.
     */
    public func withBody(_ body: Data) -> BISDKApiRequest{
        self.body = body
        return self
    }
    
    /**
     * Set request method of the current request. For instance: GET, POST, DELETE, PUT, PATH, etc.
     *
     * The default value is GET.
     *
     * - Parameters:
     *      - method: method of the request.
     *
     * - Returns: Instance of the request including the request method.
     */
    public func withMethod(_ method: String) -> BISDKApiRequest{
        self.method = method
        return self
    }
    
    /**
     * Build an `URLSessionDataTask` object, using the parameters defined by the builder.
     *
     * - Returns: `URLSessionDataTask` generated a part of the initial configuration and the properties defined when building.
     */
    public func build(handler: ((Data?, URLResponse?, Error?) ->Void)? = nil) -> URLSessionDataTask{
        let request = buildRequest()
        return URLSession.shared.dataTask(with: request,completionHandler: handler ?? buildHandler())
    }
    
    /**
     * Build an `URLSessionDataTask` object, using the parameters defined by the builder.
     *
     * - Returns: `URLSessionDataTask` generated a part of the initial configuration and the properties defined when building.
     */
    
    public func buildAsFormURLEncoded(handler: ((Data?, URLResponse?, Error?) ->Void)? = nil) -> URLSessionDataTask{
        let request = buildRequestAsFormURLEncoded()
        let session = buildSession()
        return session.dataTask(with: request,completionHandler: handler ?? buildHandler())
    }
    
    public func buildAsFormURLEncodedNoRedirect(handler: ((Data?, URLResponse?, Error?) ->Void)? = nil) -> URLSessionDataTask{
        let request = buildRequestAsFormURLEncoded()
        let redirectHandler = BISDKRedirect()
        return redirectHandler.makeRequest(request: request, callback: handler ?? buildHandler())
    }
    
    /**
     * Private method to build the request with the parameters and properties defined by the developer.
     * Including base url, query paramters, headers, body and HTTP method.
     *
     * - Returns: A `URLRequest` object with the request properties defined.
     */
    private func buildRequest() -> URLRequest{
        self.baseURL.queryItems = self.parameters.map{URLQueryItem(name: $0.key, value: $0.value)}
        var request = URLRequest(url: self.baseURL.url!)
        request.httpMethod = method
        request.allHTTPHeaderFields = headers
        request.httpBody = body
        return request
    }
    
    /**
     * Private method to build a new URLSession.
     *
     * - Returns: A `URLSession` object 
     */
    private func buildSession() -> URLSession{
        let urlconfig = URLSessionConfiguration.default
        ///SERVICE CONNECTION TIMEOUT GID-1714421
        urlconfig.timeoutIntervalForRequest = TimeInterval(320)
        urlconfig.timeoutIntervalForResource = TimeInterval(320)
        return URLSession(configuration: urlconfig, delegate: self as? URLSessionDelegate, delegateQueue: OperationQueue.main)
    }
    
    /**
     * Private method to build the request with the parameters and properties defined by the developer.
     * Including base url,  queryItems, headers, body and HTTP method.
     * This method is for content type x-www-form-urlencoded
     *
     * - Returns: A `URLRequest` object with the request properties defined.
     */
    private func buildRequestAsFormURLEncoded() -> URLRequest{
        var requestComponents = URLComponents()
        requestComponents.queryItems = self.parameters.map{URLQueryItem(name: $0.key, value: $0.value)}
        var request = URLRequest(url: self.baseURL.url!)
        request.httpMethod = method
        request.allHTTPHeaderFields = headers
        request.httpBody = requestComponents.query?.data(using: .utf8)
        return request
    }
    
    /**
     * Private method to handle the server responses.
     *
     * - Returns: Handler with the response `Data`, `URLResponse`and the `Error` (in case error is present). All of them are optional.
     */
    private func buildHandler() -> ((Data?, URLResponse?, Error?) -> Void) {
        /*
         The next variable is copied so that this `BISDKApiRequest` instance can be freed from memory.
         If you access `self` directly inside the handler, one of the two things will happen:
         
         A. `self` will be `nil` (by using `[weak self]` in case the parent didn't hold a copy of the `BISDKApiRequest` in memory, and `completion` will never be called
         B. `self` will be forever in memory causing a memory leak
         
         TL;DR -> Don't touch the next two lines.
         */
        let completion = self.completion
        
        return { data, response, error in
            guard
                let httpResponse = response as? HTTPURLResponse
            else {
                completion?(nil, BISDKError.runtimeError(sdkPrefix: BISDKError.coreModulePrefix, errorMessage: "Expected URLResponse to be of type 'HTTPURLResponse'."))
                return
            }
            
            guard
                httpResponse.statusCode >= 200,
                httpResponse.statusCode <= 299,
                let validData = data,
                let parsedData = try? JSONDecoder().decode(T.self, from: validData)
            else {
                completion?(nil, BISDKError.platformError(data: data, response: httpResponse, error: error))
                return
            }
            
            completion?(parsedData, nil)
        }
    }
}
