//
//  XPSDKRequest.swift
//  XProtect
//
//  Created by Ivelin Tsankov on 1/13/17.
//  Copyright © 2017 Milestone Systems A/S. All rights reserved.
//

import Foundation

enum RequestError: Error {
    case xmlParse
    
    var description: String {
        switch self {
        case .xmlParse:
            return "RequestError->XMLParse"
        }
    }
}

enum XPConnectionError: Int {
    case noPublicKey = -2
    case wrongConnectionId = 12
}

protocol XPSDKRequestDelegate {
    func receiveChallengeForTrust(_ challenge: URLAuthenticationChallenge, connection: XPSDKConnection?)
    func wrongConnectionIdWithError(_ error: NSError)
}

open class XPSDKRequest: NSObject, URLSessionDelegate {
    //MARK: public properties
    open internal(set) var commandName = ""
    
    //MARK: protected properties
    static var sequenceID = -1
    var parameters: [String: String]?
    var successHandler: SuccessResponse?
    var failureHandler: FailureBlock?
    var encrypted = false
    var response: XPSDKResponse?
    let defaultError = NSError(domain: XPSDKRequestDefaultError, code: -1, userInfo: nil)
    var delegate: XPSDKRequestDelegate! = nil
    var connection: XPSDKConnection! = nil
    var session: Foundation.URLSession! = nil
    var receivedChallenge: URLAuthenticationChallenge! = nil
    
    //MARK: init
    convenience init(forConnection connection: XPSDKConnection) {
        self.init()
        self.connection = connection
        self.connection.requests.add(self)
        self.delegate = self.connection
    }
    
    convenience init(withCommandName commandName: String, parameters: [String: String]?, successHandler: SuccessResponse?, failureHandler: FailureBlock?) {
        self.init()
        
        self.commandName = commandName
        self.parameters = parameters
        self.successHandler = successHandler
        self.failureHandler = failureHandler
    }
    
    //MARK: public
    public func cancelSession() {
        if self.session != nil {
            self.session.invalidateAndCancel()
            self.session = nil
        }
    }
    
    //MARK: protected
    func execute() {
        self.addDefaultParameters()
        
        XPSDKRequest.sequenceID = XPSDKRequest.sequenceID + 1
        
        if let request = self.request() {
            let session = self.createSession()
            
            UIApplication.shared.isNetworkActivityIndicatorVisible = true
            
            let dataTask = session.dataTask(with: request, completionHandler: { (data, response, error) in
                if self.session == nil {
                    self.callFailureHandler(withError: NSError())
                    return
                }
                
                if self.connection.requests.count > 0 {
                    UIApplication.shared.isNetworkActivityIndicatorVisible = false
                }
                
                if error != nil {
                    log.error(error)
                    if (error as NSError?)?.code == NSURLErrorCancelled {
                        if self.receivedChallenge != nil {
                            self.delegate.receiveChallengeForTrust(self.receivedChallenge, connection: self.connection)
                        }
                    } else {
                        self.callFailureHandler(withError: error! as NSError)
                    }
                    return
                }
                
                let httpResponse = response as! HTTPURLResponse
                if httpResponse.statusCode == 200 {
                    do {
                        let parsedData = try self.parseData(data!)
                        if parsedData == nil {
                            log.error("Error: no parsed data")
                            self.callFailureHandler(withError: NSError(domain: XPSDKRequestDefaultError, code: 0, userInfo: nil))
                        } else {
                            let response = XPSDKResponse(forRequest: self, response: parsedData!)
                            self.response = response
                            
                            if response.error != nil {
                                if response.error?.domain == XPSDKResponseErrorDomain {
                                    if response.error?.code == XPConnectionError.wrongConnectionId.rawValue {
                                        if self.commandName != XPSDKDisconnect {
                                            self.delegate.wrongConnectionIdWithError(response.error!)
                                        }
                                    }
                                }
                                
                                log.error(response.error)
                                
                                self.callFailureHandler(withError: response.error!)
                            } else {
                                self.callSuccessHandler(withResponse: response)
                            }
                        }
                    } catch _ {
                        let error = NSError(domain: XPSDKRequestXMLParse, code: 0, userInfo: nil)
                        log.error(error)
                        
                        self.callFailureHandler(withError: error)
                    }
                } else {
                    self.callFailureHandler(withError: self.defaultError)
                }
            })
            
            
            dataTask.resume()
        } else {
            self.callFailureHandler(withError: self.defaultError)
        }
    }
    
    //MARK: SSL
    open func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        
        self.receivedChallenge = challenge
        
        var forceTrust = false
        
        if self.commandName == XPSDKDisconnect {
            forceTrust = true
        }
        
        if forceTrust {
            completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
        } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
            completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
        } else {
            if self.connection.authenticationComplete {
                // Authentication complete. We trust this server.
                let trust = challenge.protectionSpace.serverTrust
                let credential = URLCredential(trust: trust!)
                
                completionHandler(.useCredential, credential)
            } else {
                completionHandler(Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
            }
        }
    }
    
    func connection(_ connection:NSURLConnection!, willSendRequestForAuthenticationChallenge challenge:URLAuthenticationChallenge!) {
        
    }
    
    //MARK: private
    fileprivate func addDefaultParameters() {
        self.parameters?["ProcessingMessage"] = XPSDKPublicNo
    }
    
    fileprivate func callSuccessHandler(withResponse response: XPSDKResponse) {
        if  self.connection.requests.contains(self) {
            self.connection.requests.remove(self)
        }
        
        self.successHandler?(response)
    }
    
    fileprivate func callFailureHandler(withError error: NSError) {
        self.connection.requests.remove(self)
        
        self.failureHandler?(error)
    }
    
    //MARK: generate object and data
    fileprivate func createSession() -> Foundation.URLSession {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 99999
        
        self.session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue:OperationQueue.main)
        
        return session
    }
    
    fileprivate func request() -> URLRequest? {
        if let url = self.connection.url {
            log.info("request url", url)
            let request = NSMutableURLRequest(url: url as URL)
            request.addValue("application/xml", forHTTPHeaderField: "Content-Type")
            request.addValue("application/xml; charset=utf-8", forHTTPHeaderField: "Accept")
            request.httpMethod = "POST"
            
            let body = self.HTTPBody()
            request.httpBody = body
            
            return request as URLRequest
        }
        
        return nil
    }
    
    fileprivate func HTTPBody() -> Data {
        if encrypted {
            return self.encryptedHTTPBody()
        } else {
            return self.unencryptedHTTPBody()
        }
    }
    
    fileprivate func encryptedHTTPBody() -> Data {
        let inputParams = NSMutableString()
        
        for (key, value) in self.parameters! {
            inputParams.appendFormat("<Param Name=\"%@\" Value=\"%@\" />", key, value)
        }
        
        let stringForEncrypt = NSString(format: "<Command SequenceId=\"%lu\"><Type>Request</Type><Name>%@</Name><InputParams>%@</InputParams><OutputParams /></Command>", XPSDKRequest.sequenceID, self.commandName, inputParams)
        
        let dataString = NSMutableString()
        
        dataString.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
        dataString.append("<Communication xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">")
        
        if let id = self.connection.id {
            dataString.appendFormat("<ConnectionId>%@</ConnectionId>", id)
        }
                
        dataString.appendFormat("<EncryptedCommand>%@</EncryptedCommand >", (self.connection.dhe?.encrypt(stringForEncrypt as String))!)
        dataString.append("</Communication>\r\n\r\n")
        
        log.info(dataString)
        
        return dataString.data(using: String.Encoding.utf8.rawValue)!
    }
    
    fileprivate func unencryptedHTTPBody() -> Data {
        let inputParams = NSMutableString()
        
        if self.parameters != nil {
            for (key, value) in self.parameters! {
                inputParams.appendFormat("<Param Name=\"%@\" Value=\"%@\" />", key, value)
            }
        }
        
        let dataString = NSMutableString()
        
        dataString.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
        dataString.append("<Communication xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">")
        
        if let id = self.connection.id {
            dataString.appendFormat("<ConnectionId>%@</ConnectionId>", id)
        }
        
        dataString.appendFormat("<Command SequenceId=\"%lu\">", XPSDKRequest.sequenceID)
        dataString.append("<Type>Request</Type>")
        dataString.appendFormat("<Name>%@</Name>", self.commandName)
        dataString.appendFormat("<InputParams>%@</InputParams>", inputParams)
        dataString.append("<OutputParams />")
        dataString.append("</Command>")
        dataString.append("</Communication>\r\n\r\n")
        
        log.info(dataString)
        
        return dataString.data(using: String.Encoding.utf8.rawValue)!
    }
    
    //MARK: parse
    fileprivate func parseData(_ d: Data) throws -> NSDictionary? {
        let crlf = "\r\n\r\n"
        let CRLFCRLF = Data(bytes: UnsafePointer<UInt8>(crlf), count: 4)
        let data = d as NSData
        
        var range: NSRange? = nil
        
        repeat {
            range = data.range(of: CRLFCRLF, options: NSData.SearchOptions(rawValue: UInt(0)), in: NSMakeRange(0, data.length))
            //            range = data.range(of: CRLFCRLF, options: NSData.SearchOptions(rawValue: UInt(0)), in: 0 ..< data.count)!
            if range?.location != NSNotFound {
                let xmlData: NSData? = data.subdata(with: NSMakeRange(0, range!.location)) as NSData?
                
                if xmlData != nil {
                    do {
                        let xmlDictionary = try KPXMLParser.dictionary(forXMLData: xmlData as Data!)
                        
                        return xmlDictionary as NSDictionary?
                    } catch _ {
                        log.error(RequestError.xmlParse.description)
                        throw RequestError.xmlParse
                    }
                }
            }
            
        } while data.length > 0 && range?.location != NSNotFound
        
        return nil
    }
}
