import Foundation;

// Trust SSLs even if invalid
extension Tor:URLSessionDelegate {
    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        //Trust the certificate even if not valid
        let urlCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
        completionHandler(.useCredential, urlCredential)
    }
}

extension DispatchQueue {
    static func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
        DispatchQueue.global(qos: .background).async {
            background?()
            if let completion = completion {
                DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
                    completion()
                })
            }
        }
    }
}




class ObserverSwift {
    public let onSuccess: ((String) -> Void)
    public let onError: ((String) -> Void)
    init(onSuccess: @escaping ((String) -> Void), onError: @escaping ((String) -> Void),target:String) {
        self.onSuccess = onSuccess
        self.onError = onError
    }
}

@objc(Tor)
class Tor: RCTEventEmitter {
    var service:Optional<OpaquePointer> = nil;
    var proxySocksPort:Optional<UInt16> = nil;
    var starting:Bool = false;
    var streams:Dictionary<String,OpaquePointer> = [:];
    var hasLnser = false;
    var clienTimeout:TimeInterval = 60;
    
    func getProxiedClient(headers:Optional<NSDictionary>,socksPort:UInt16,trustInvalidSSL: Bool = false)->URLSession{
        let config = URLSessionConfiguration.default;
        config.requestCachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData;
        config.connectionProxyDictionary = [AnyHashable: Any]();
        config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1;
        config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyHost as String] = "127.0.0.1";
        config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyPort as String] = socksPort;
        config.connectionProxyDictionary?[kCFProxyTypeSOCKS as String] = 1;
        config.timeoutIntervalForRequest = clienTimeout;
        config.timeoutIntervalForResource = clienTimeout;

        if let headersPassed = headers {
            config.httpAdditionalHeaders = headersPassed as? [AnyHashable : Any]
        }
        if(trustInvalidSSL){
            return URLSession.init(configuration: config, delegate: self, delegateQueue: nil)
        } else {
            return URLSession.init(configuration: config, delegate: nil, delegateQueue: OperationQueue.current)
        }
    }
    
    func resolveObjResp (data:Data,resp:HTTPURLResponse,resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock)->Void{
        let jsonObject: NSMutableDictionary = NSMutableDictionary()
        jsonObject.setValue(data.base64EncodedString(), forKey: "b64Data")
        jsonObject.setValue(resp.mimeType, forKey: "mimeType")
        jsonObject.setValue(resp.allHeaderFields, forKey: "headers")
        jsonObject.setValue(resp.statusCode, forKey: "respCode")
        // parse json if that's what we have
        if let mimeType = resp.mimeType {
            if mimeType == "application/json" || mimeType == "application/javascript" {
                do{
                    let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
                    jsonObject.setValue(json, forKey: "json")
                } catch {
                    print("prepareObjResp errorParsingJson!",error);
                }
            }
        }
        
        
        if 200...299 ~= resp.statusCode {
            resolve(jsonObject as NSObject )
        }else{
            var msg:Optional<String> = nil;
            if let errorMessage = String(data:data,encoding: .utf8) {
                msg = errorMessage
            }
            reject(
                "TOR.REQUEST","Resp Code: \(resp.statusCode) : \(msg)",NSError.init(domain: "TOR.REQUEST", code: resp.statusCode,userInfo:["data" : msg]));
        }
    }
    
    @objc(request:method:jsonBody:headers:trustInvalidCert:resolver:rejecter:)
    func request(url: String, method: String, jsonBody: String, headers: NSDictionary, trustInvalidCert:Bool, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock){
        
        if service == nil {
            reject("TOR.SERVICE","Tor Service NOT Running. Call `startDaemon` first.",NSError.init(domain: "TOR.DAEMON", code: 99));
            return;
        }
        
        let session = getProxiedClient(headers:headers,socksPort: proxySocksPort!,trustInvalidSSL:trustInvalidCert);
        guard let _url = URL(string:url) else {
            reject("TOR.URL","Could not parse url",NSError.init(domain: "TOR", code: 404));
            return;
        }
        
        do{
            switch method{
            case "get":
                session.dataTask(with: _url) {  data, resp, error in
                    guard let dataResp = data , error == nil, let respData = resp else {
                        reject("TOR.NETWORK.GET",error?.localizedDescription,error);
                        return;
                    }
                    self.resolveObjResp(data:dataResp,resp:respData as! HTTPURLResponse,resolve:resolve,reject:reject);
                }.resume();
            case "delete":
                var request = URLRequest(url:_url);
                request.httpMethod = "DELETE";
                session.dataTask(with: request) {  data, resp, error in
                    guard let dataResp = data , error == nil, let respData = resp else {
                        reject("TOR.NETWORK.DELETE",error?.localizedDescription,error);
                        return;
                    }
                    self.resolveObjResp(data:dataResp,resp:respData as! HTTPURLResponse,resolve:resolve,reject:reject);
                    
                }.resume();
            case "post":
                var request = URLRequest(url:_url);
                request.httpMethod = "POST";
                session.uploadTask(with: request, from: jsonBody.data(using: .utf8)) {  data, resp, error in
                    guard let dataResp = data , let respData = resp , error == nil  else {
                        reject("TOR.NETWORK.POST",error?.localizedDescription,error);
                        return;
                    }
                    self.resolveObjResp(data:dataResp,resp:respData as! HTTPURLResponse,resolve:resolve,reject:reject);
                }.resume();
                
            default:
                throw NSError.init(domain:"TOR.REQUEST_METHOD",code:400)
            }
            
        } catch{
            reject("TOR.REQUEST",error.localizedDescription,error);
        }
    }
    
    
    @objc(startDaemon:clientTimeoutSec:resolver:rejecter:)
    func startDaemon(timeoutMs:NSNumber, clientTimeoutSec:NSNumber, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock)->Void{
        if service != nil || starting {
            reject("TOR.START","Tor Service Already Running. Call `stopDaemon` first.",NSError.init(domain: "TOR.START", code: 01));
            return;
        }
        clienTimeout = clientTimeoutSec.doubleValue;
        starting = true;
        do {
            
            let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(),isDirectory: true)
            // FIXME pass this and check if avalible
            let socksPort:UInt16 = 19032;
            // this gives file:///Users/.../tmp/ so we remove the file:// prefix and trailing slash
            let path = String(temporaryDirectoryURL.absoluteString.dropFirst(7).dropLast());
            
            // Rust will start Tor daemon thread and block until boostrapped, so run as dispatched async task so not to block this thread
            DispatchQueue.background(background: {
                defer {
                    self.starting = false;
                }
                // FIXME here make the timeout a param
                // better way to automatically have the JS promise handle a fail ?
                let call_result = get_owned_TorService(path, socksPort,timeoutMs.uint64Value).pointee;
                switch(call_result.message.tag){
                case Success:
                    self.service = Optional.some(call_result.result);
                    self.proxySocksPort = socksPort;
                    resolve(socksPort);
                    return;
                case Error:
                    // Convert RustByteSlice to String
                    if let error_body = call_result.message.error {
                        let error_string = String.init(cString: error_body);
                        reject("TOR.START",error_string,NSError.init(domain: "TOR", code: 0))
                    } else {
                        reject("TOR.START","Unknown daemon startup error",NSError.init(domain: "TOR", code: 99));
                    }
                    return;
                default:
                    reject("TOR.START","unknown startup result",NSError.init(domain: "TOR", code: 99));
                    return;
                }
            }, completion:{
            })
        }
    }
    
    @objc(getDaemonStatus:rejecter:)
    func getDaemonStatus(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock)->Void {
        guard let daemon = service else {
            if(starting)
            {
                resolve("STARTING")
            }else {
                resolve("NOTINIT");
            }
            return;
        }
        
        if let status = get_status_of_owned_TorService(daemon) {
            defer {
                destroy_cstr(status);
            }
            let status_string = String.init(cString: status);
            resolve(status_string)
        } else {
            reject("TOR.STATUS","UNKNOWN",NSError.init(domain: "TOR", code: 99));
        }
        
    }
    
    @objc(stopDaemon:rejecter:)
    func stopDaemon( resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock)->Void {
        if let hasSevice = service {
            // if we have streams, shut them down
            for key in streams.keys {
                if let stream = streams[key] {
                    // set it here in case eof callback gets called
                    // while looping
                    streams[key] = nil;
                    tcp_stream_destroy(stream);
                }
            }
            shutdown_owned_TorService(hasSevice);
            service = nil
            proxySocksPort = nil
        }
        resolve(true);
    }
    
    override func startObserving(){
        self.hasLnser = true;
    }
    override func stopObserving(){
        self.hasLnser = false;
    }
    
    // FIXME here it needs to support, so i guess we can use
    override func supportedEvents() -> [String]! {
        ["torTcpStreamData","torTcpStreamError"]
    }
    
    @objc(startTcpConn:timeoutMs:resolver:rejecter:)
    func startTcpConn(target:String,timeoutMs:NSNumber,resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock){
        guard let socksProxy = self.proxySocksPort else {
            reject("TOR.TCPCONN.startTcpConn","SocksProxy not detected, make sure Tor is started",NSError.init(domain: "TOR", code: 99));
            return;
        }
        
        let uuid = UUID().uuidString;
        
        let call_result = tcp_stream_start(target, "127.0.0.1:\(socksProxy)",timeoutMs.uint64Value).pointee;
        switch(call_result.message.tag){
        case Success:
            let stream = call_result.result;
            self.streams[uuid] = stream;
            // Create swift observer wrapper to store context
            let observerWrapper = ObserverSwift(onSuccess:{ (data) in
                self.sendEvent(withName: "torTcpStreamData", body: "\(uuid)||\(data)")
            }, onError:{ (data) in
                // On Eof destrory stream and remove from map
                // TODO update this when streaming streams
                if(data == "EOF"){
                    guard let stream = self.streams[uuid] else{
                        print("Note: EOF but stream already destroyed, returning...")
                        return;
                    }
                    tcp_stream_destroy(stream);
                    self.streams[uuid] = nil;
                } else if (data.contains("NotConnected")){
                    guard self.streams[uuid] != nil else{
                        print("Note: EOF but stream already destroyed, returning...")
                        return;
                    }
                    // Stream pointer could be already delocated from rust side here
                    // so don't try to destory it just remove it from map of references.
                    // Worst case we create a new one and the memory leak gets recycled
                    // when app restarts
                    // TODO way to better coordinate this.
                    // tcp_stream_destroy(stream);
                    self.streams[uuid] = nil;
                } else {
                    print("Got observerWrapper event but not EOF",data )
                    
                }
                self.sendEvent(withName: "torTcpStreamError", body: "\(uuid)||\(data)")
            },target:target);
            // Prepare pointer to context and observer callbacks as Retained
            let owner = UnsafeMutableRawPointer(Unmanaged.passRetained(observerWrapper).toOpaque());
            
            let onSuccess:@convention(c) (UnsafeMutablePointer<Int8>?, UnsafeRawPointer?) -> Void = { (data, context) in
                // take unretained so we don't clear it
                let obv = Unmanaged<ObserverSwift>.fromOpaque(context!).takeUnretainedValue();
                obv.onSuccess(String(cString: data!));
                destroy_cstr(data);
                
            }
            let onError:@convention(c) (UnsafeMutablePointer<Int8>?, UnsafeRawPointer?) -> Void = { (data, context) in
                let obv = Unmanaged<ObserverSwift>.fromOpaque(context!).takeUnretainedValue();
                obv.onError(String(cString: data!));
                destroy_cstr(data);
            }
            let obv = Observer(context: owner, on_success: onSuccess, on_err:onError);
            tcp_stream_on_data(stream,obv);
            resolve(uuid);
            return;
        case Error:
            // Convert RustByteSlice to String
            if let error_body = call_result.message.error {
                let error_string = String.init(cString: error_body);
                reject("TOR.TCPCONN.startTcpConn",error_string,NSError.init(domain: "TOR", code: 0))
            } else {
                reject("TOR.TCPCONN.startTcpConn","Unknown tcpStream startup error",NSError.init(domain: "TOR", code: 99));
            }
            return;
        default:
            reject("TOR.startTcpConn","unknown startup result",NSError.init(domain: "TOR", code: 99));
            return;
        }
        
    }
    
    @objc(sendTcpConnMsg:msg:timeoutSec:resolver:rejecter:)
    func sendTcpConnMsg(target:String,msg:String,timeoutSec:NSNumber,resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock){
        guard let _ = self.service else {
            reject("TOR.TCPCONN.sendTcpConnMsg","Service not detected, make sure Tor is started",NSError.init(domain: "TOR", code: 99));
            return;
        }
        guard let stream = self.streams[target] else{
            reject("TOR.TCPCONN.sendTcpConnMsg","Stream not detected",NSError.init(domain: "TOR", code: 99));
            return;
        }
        let result = tcp_stream_send_msg(stream, msg,timeoutSec.uint64Value).pointee;
        switch(result.tag){
        case Success:
            resolve(true);
            return;
        case Error:
            if let error_body = result.error {
                let error_string = String.init(cString: error_body);
                reject("TOR.TCPCONN.sendTcpConnMsg",error_string,NSError.init(domain: "TOR", code: 0))
            } else {
                reject("TOR.TCPCONN.sendTcpConnMsg","Unknown tcpStream startup error",NSError.init(domain: "TOR", code: 99));
            }
            return;
        default:
            reject("TOR.TCPCONN.sendTcpConnMsg","unknown tcp send message result",NSError.init(domain: "TOR", code: 99));
            return;
        }
    }
    
    @objc(stopTcpConn:resolver:rejecter:)
    func stopTcpConn(target:String,resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock){
        guard let stream = self.streams[target] else{
            reject("TOR.TCPCONN.stopTcpConn","Stream not detected",NSError.init(domain: "TOR", code: 99));
            return;
        }
        self.streams[target] = nil;
        tcp_stream_destroy(stream);
        resolve(true);
    }
}
