//
//  APIRequest.swift
//
//  The template was designed by Hưng Nguyễn. 
//  Website, application (native/flutter), hosting, SEO.
//  Email: hungnguyen.it36@gmail.com
//

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

enum HttpMethod:String{
    case post = "POST"
    case get = "GET"
    case put = "PUT"
    case del = "DELETE"
}

public enum ContentType: String {
    case urlFormEncoded = "application/x-www-form-urlencoded"
    case applicationJson = "application/json"
    case applicationRaw = "application/x-www-form-urlencoded; charset=utf-8"
}

final class APIRequest {
    static let shared = APIRequest()
    
    var enableSSL:Bool = false //_USING_HTTPS_PROTOCOL_
    var boolEncoding = BoolEncoding.numeric
    var arrayEncoding = ArrayEncoding.brackets
    let ctrl = "\r\n"
    lazy var globalQueue:OperationQueue = {
        let queue = OperationQueue()
        queue.name = "com.visikard.APIRequest.OperationQueue"
        return queue
    }()
    
    func request(urlString: String, params: [String: Any], method:HttpMethod, contenType: ContentType = .urlFormEncoded, allHTTPHeaderFields:[String:String] = ["Content-Type":"application/x-www-form-urlencoded"], completed: @escaping(_ result: Data?, _ error: Error?)->()) {
        switch  method {
        case .get:
            getAPI(urlString: urlString, method: .get, allHTTPHeaderFields: allHTTPHeaderFields, params: params) { (result, error) in
                completed(result, error)
            }
        case .post:
            postAPI(urlString: urlString, method: .post, contenType: contenType, allHTTPHeaderFields: allHTTPHeaderFields, params: params) { (result, error) in
                completed(result, error)
            }
        case .put:
            postAPI(urlString: urlString, method: .put, contenType: contenType, allHTTPHeaderFields: allHTTPHeaderFields, params: params) { (result, error) in
                completed(result, error)
            }
        case .del:
            postAPI(urlString: urlString, method: .del, contenType: contenType, allHTTPHeaderFields: allHTTPHeaderFields, params: params) { (result, error) in
                completed(result, error)
            }
        }
    }
    
    private func getAPI(urlString: String, method: HttpMethod, allHTTPHeaderFields: [String:String], params: [String: Any],_ completed:@escaping(_ result: Data?, _ error: Error?)->()) {
        let urlComponents = NSURLComponents(string: urlString)
        var items = [URLQueryItem]()
        for (key,value) in params {
               items.append(URLQueryItem(name: key, value: ("\(value)")))
        }
        urlComponents?.queryItems = items
        
        guard let urlRequest =  urlComponents?.url else {
            let error = NSError(domain:"1000", code:404, userInfo:nil)
            APIError.shared.handleError(error: error)
            completed(nil, error)
            return
        }

        var request = URLRequest(url: urlRequest)
        request.httpMethod = method.rawValue
        request.allHTTPHeaderFields = allHTTPHeaderFields
//        request.addValue(X_TENANT, forHTTPHeaderField: "X-TENANT")
//        request.addValue(UserDataManager.shared().deviceID, forHTTPHeaderField: "deviceId")
//        request.addValue(UserDataManager.shared().token, forHTTPHeaderField: "token")
//        request.addValue(SupportFunction.getAppVersionString(), forHTTPHeaderField: "appVersion")
//        request.addValue(APP_PLATFORM, forHTTPHeaderField: "app-platform")
        request.timeoutInterval = 300
        
        datatashAPI(request: request) { (result, error) in
            completed(result, error)
        }
    }
    
    private func postAPI(urlString: String, method: HttpMethod, contenType: ContentType = .urlFormEncoded, allHTTPHeaderFields: [String:String], params: [String: Any], completed:@escaping(_ result: Data?, _ error: Error?)->()){
        guard let urlRequest =  URL(string: urlString) else {
            let error = NSError(domain:"1000", code:404, userInfo:nil)
            APIError.shared.handleError(error: error)
            completed(nil, error)
            return
            
        }
        
        var data: Data?
        if contenType == .applicationJson {
            data = try?  JSONSerialization.data(withJSONObject: params, options: [])
        } else {
            data = query(params).data(using: .utf8, allowLossyConversion: false)
        }
        
        var allHeader = allHTTPHeaderFields
        allHeader["Content-Type"] = contenType.rawValue
        var request = URLRequest(url: urlRequest)
        request.httpMethod = method.rawValue
        request.httpBody =  data
        request.allHTTPHeaderFields = allHeader
//        request.addValue(X_TENANT, forHTTPHeaderField: "X-TENANT")
//        request.addValue(UserDataManager.shared().deviceID, forHTTPHeaderField: "deviceId")
//        request.addValue(UserDataManager.shared().token, forHTTPHeaderField: "token")
//        request.addValue(SupportFunction.getAppVersionString(), forHTTPHeaderField: "appVersion")
//        request.addValue(APP_PLATFORM, forHTTPHeaderField: "app-platform")
        request.timeoutInterval = 300
        
        datatashAPI(request: request) { (result, error) in
            completed(result, error)
        }
    }
    
    private func datatashAPI(request: URLRequest, completed: @escaping(_ result: Data?, _ error: Error?)->()) {
        
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config, delegate: nil, delegateQueue:globalQueue)
        
        print("curl command: " + request.cURL)
        
        session.dataTask(with: request) { (data, response, err) in
            if let err = err {
                print(err.localizedDescription)
                APIError.shared.handleError(error: err)
                completed(nil, err)
                return
            }
            guard let response = response as? HTTPURLResponse else {
                let error = NSError(domain: request.url?.path ?? "", code: 404, userInfo: nil)
                APIError.shared.handleError(error: error)
                completed(nil, error)
                return
            }
            guard response.statusCode == 200 else {
                let error = NSError(domain: request.url?.path ?? "", code: response.statusCode, userInfo: nil)
                APIError.shared.handleError(error: error)
                completed(nil, error)
                return
            }
            guard let data = data else {
                let error = NSError(domain: request.url?.path ?? "", code: 404, userInfo: nil)
                APIError.shared.handleError(error: error)
                completed(nil, error)
                return
            }
            DispatchQueue.main.async {
                print("result: \n" + (String(data: data, encoding: .utf8) ?? "null"))
                completed(data, nil)
            }
        }.resume()
    }
}

extension URLRequest {
    /// Returns a cURL command for a request
    /// - return A String object that contains cURL command or "" if an URL is not properly initalized.
    var cURL: String {
        guard
            let url = url,
            let httpMethod = httpMethod,
            url.absoluteString.utf8.count > 0
            else {
                return ""
        }
       var curlCommand = "curl"
        // Method
        curlCommand = curlCommand.appendingFormat(" -X %@ \\\n", httpMethod)
        // URL
        curlCommand = curlCommand.appendingFormat(" '%@' \\\n", url.absoluteString)
        
        // Headers
        let allHeadersFields = allHTTPHeaderFields!
        let allHeadersKeys = Array(allHeadersFields.keys)
        let sortedHeadersKeys  = allHeadersKeys.sorted(by: <)
        for key in sortedHeadersKeys {
            curlCommand = curlCommand.appendingFormat(" -H '%@: %@' \\\n", key, self.value(forHTTPHeaderField: key)!)
        }
        // HTTP body
        if let httpBody = httpBody, httpBody.count > 0 {
            let httpBodyString = String(data: httpBody, encoding: String.Encoding.utf8)!
            let escapedHttpBody = URLRequest.escapeAllSingleQuotes(httpBodyString)
            curlCommand = curlCommand.appendingFormat(" --data '%@' \\\n", escapedHttpBody)
        }
        return curlCommand
    }
    /// Escapes all single quotes for shell from a given string.
    static func escapeAllSingleQuotes(_ value: String) -> String {
        return value.replacingOccurrences(of: "'", with: "'\\''")
    }
}


//MARK: Encode
extension APIRequest {
    private func query(_ parameters: [String: Any]) -> String {
        var components: [(String, String)] = []
        
        for key in parameters.keys.sorted(by: <) {
            let value = parameters[key]!
            components += queryComponents(fromKey: key, value: value)
        }
        return components.map { "\($0)=\($1)" }.joined(separator: "&")
    }
    
    public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
        var components: [(String, String)] = []
        
        if let dictionary = value as? [String: Any] {
            for (nestedKey, value) in dictionary {
                components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
            }
        } else if let array = value as? [Any] {
            for value in array {
                components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
            }
        } else if let value = value as? NSNumber {
            if value.isBool {
                components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
            } else {
                components.append((escape(key), escape("\(value)")))
            }
        } else if let bool = value as? Bool {
            components.append((escape(key), escape(boolEncoding.encode(value: bool))))
        } else {
            components.append((escape(key), escape("\(value)")))
        }
        
        return components
    }
    public func escape(_ string: String) -> String {
        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = "!$&'()*+,;="
        
        var allowedCharacterSet = CharacterSet.urlQueryAllowed
        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
        
        var escaped = ""
        
        //==========================================================================================================
        //
        //  Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
        //  hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
        //  longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
        //  info, please refer to:
        //
        //      - https://github.com/Alamofire/Alamofire/issues/206
        //
        //==========================================================================================================
        
        if #available(iOS 8.3, *) {
            escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
        } else {
            let batchSize = 50
            var index = string.startIndex
            
            while index != string.endIndex {
                let startIndex = index
                let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
                let range = startIndex..<endIndex
                
                let substring = string[range]
                
                escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? String(substring)
                
                index = endIndex
            }
        }
        
        return escaped
    }
    
    public enum BoolEncoding {
        case numeric, literal
        
        func encode(value: Bool) -> String {
            switch self {
            case .numeric:
                return value ? "1" : "0"
            case .literal:
                return value ? "true" : "false"
            }
        }
    }
    
    public enum ArrayEncoding {
        case brackets, noBrackets
        
        func encode(key: String) -> String {
            switch self {
            case .brackets:
                return "\(key)[]"
            case .noBrackets:
                return key
            }
        }
    }
}

extension NSNumber {
    fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
}
