//
//  OSSNetworking.m
//  oss_ios_sdk
//
//  Created by zhouzhuo on 8/16/15.
//  Copyright (c) 2015 aliyun.com. All rights reserved.
//

#import "OSSDefine.h"
#import "OSSNetworking.h"
#import "OSSBolts.h"
#import "OSSModel.h"
#import "OSSUtil.h"
#import "OSSLog.h"
#import "OSSXMLDictionary.h"
#import "OSSInputStreamHelper.h"
#import "OSSNetworkingRequestDelegate.h"
#import "OSSURLRequestRetryHandler.h"
#import "OSSHttpResponseParser.h"

@implementation OSSNetworkingConfiguration
@end


@implementation OSSNetworking

- (instancetype)initWithConfiguration:(OSSNetworkingConfiguration *)configuration {
    if (self = [super init]) {
        self.configuration = configuration;
        NSURLSessionConfiguration * conf = nil;
        NSOperationQueue *delegateQueue = [NSOperationQueue new];

        if (configuration.enableBackgroundTransmitService) {
            conf = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:self.configuration.backgroundSessionIdentifier];
        } else {
            conf = [NSURLSessionConfiguration defaultSessionConfiguration];
        }
        conf.URLCache = nil;

        if (configuration.timeoutIntervalForRequest > 0) {
            conf.timeoutIntervalForRequest = configuration.timeoutIntervalForRequest;
        }
        if (configuration.timeoutIntervalForResource > 0) {
            conf.timeoutIntervalForResource = configuration.timeoutIntervalForResource;
        }
        if (configuration.HTTPMaximumConnectionsPerHost > 0) {
            conf.HTTPMaximumConnectionsPerHost = configuration.HTTPMaximumConnectionsPerHost;
        }
        
        if (configuration.proxyHost && configuration.proxyPort) {
            // Create an NSURLSessionConfiguration that uses the proxy
            NSDictionary *proxyDict = @{
                                        @"HTTPEnable"  : [NSNumber numberWithInt:1],
                                        (NSString *)kCFStreamPropertyHTTPProxyHost  : configuration.proxyHost,
                                        (NSString *)kCFStreamPropertyHTTPProxyPort  : configuration.proxyPort,

                                        @"HTTPSEnable" : [NSNumber numberWithInt:1],
                                        (NSString *)kCFStreamPropertyHTTPSProxyHost : configuration.proxyHost,
                                        (NSString *)kCFStreamPropertyHTTPSProxyPort : configuration.proxyPort,
                                        };
            conf.connectionProxyDictionary = proxyDict;
        }

        _session = [NSURLSession sessionWithConfiguration:conf
                                                 delegate:self
                                            delegateQueue:delegateQueue];

        self.isUsingBackgroundSession = configuration.enableBackgroundTransmitService;
        _sessionDelagateManager = [OSSSyncMutableDictionary new];
        if (configuration.enableResetRetryCount) {
            [_sessionDelagateManager addObserverForResetCurrentRetryCount];
        }

        NSOperationQueue * operationQueue = [NSOperationQueue new];
        if (configuration.maxConcurrentRequestCount > 0) {
            operationQueue.maxConcurrentOperationCount = configuration.maxConcurrentRequestCount;
        }
        self.taskExecutor = [OSSExecutor executorWithOperationQueue: operationQueue];
    }
    return self;
}

- (OSSTask *)sendRequest:(OSSNetworkingRequestDelegate *)request {
    OSSLogVerbose(@"send request --------");
    if (self.configuration.proxyHost && self.configuration.proxyPort) {
        request.isAccessViaProxy = YES;
    }

    /* set maximum retry */
    request.retryHandler.maxRetryCount = self.configuration.maxRetryCount;

    OSSTaskCompletionSource * taskCompletionSource = [OSSTaskCompletionSource taskCompletionSource];

    __weak OSSNetworkingRequestDelegate *weakRequest= request;
    request.completionHandler = ^(id responseObject, NSError * error) {
        [weakRequest reset];
        
        // 1.判断是否出错，如果出错的话，直接设置错误信息
        if (error)
        {
            if (weakRequest.metrics) {
                NSMutableDictionary *userInfo = error.userInfo ? [NSMutableDictionary dictionaryWithDictionary:error.userInfo] : [NSMutableDictionary dictionary];
                [userInfo oss_setObject:weakRequest.metrics forKey:OSSNetworkTaskMetrics];
                [taskCompletionSource setError:[NSError errorWithDomain:error.domain
                                                                   code:error.code
                                                               userInfo:userInfo]];
            } else {
                [taskCompletionSource setError:error];
            }
        }else
        {
            [self checkForCrc64WithResult:responseObject
                          requestDelegate:weakRequest
                     taskCompletionSource:taskCompletionSource];
        }
    };
    [self dataTaskWithDelegate:request];
    return taskCompletionSource.task;
}

- (void)checkForCrc64WithResult:(nonnull id)response requestDelegate:(OSSNetworkingRequestDelegate *)delegate taskCompletionSource:(OSSTaskCompletionSource *)source
{
    OSSResult *result = (OSSResult *)response;
    BOOL hasRange = [delegate.internalRequest valueForHTTPHeaderField:@"Range"] != nil;
    NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:delegate.internalRequest.URL resolvingAgainstBaseURL:YES];
    BOOL hasXOSSProcess = [urlComponents.query containsString:@"x-oss-process"];
    BOOL enableCRC = delegate.crc64Verifiable;
    // 3.判断如果未开启crc校验,或者headerFields里面有Range字段或者参数表中存在
    //   x-oss-process字段,都将不进行crc校验
    if (!enableCRC || hasRange || hasXOSSProcess)
    {
        [source setResult:response];
    }
    else
    {
        OSSLogVerbose(@"--- checkForCrc64WithResult --- ");
        // 如果服务端未返回crc信息，默认是成功的
        OSSLogVerbose(@"result.remoteCRC64ecma : %@",result.remoteCRC64ecma);
        OSSLogVerbose(@"if result.localCRC64ecma : %@",result.localCRC64ecma);
        if (!result.remoteCRC64ecma.oss_isNotEmpty)
        {
            [source setResult:response];
            return;
        }
        // getObject 操作的crc数值会在delegate.responseParser consumeHttpResponseBody 进行计算。
        // upload & put 操作在上传成功后再计算。
        // 如果用户设置onReceiveData block。无法计算localCRC64ecma
        if (!result.localCRC64ecma.oss_isNotEmpty)
        {
            OSSLogVerbose(@"delegate.uploadingFileURL : %@",delegate.uploadingFileURL);
            if (delegate.uploadingFileURL)
            {
                OSSInputStreamHelper *helper = [[OSSInputStreamHelper alloc] initWithURL:delegate.uploadingFileURL];
                [helper syncReadBuffers];
                if (helper.crc64 != 0) {
                    result.localCRC64ecma = [NSString stringWithFormat:@"%llu",helper.crc64];
                }
            }
            else
            {
                result.localCRC64ecma = delegate.contentCRC;
            }
            OSSLogVerbose(@"finally result.localCRC64ecma : %@",result.localCRC64ecma);
        }

        
        // 针对append接口，需要多次计算crc值
        if ([delegate.lastCRC oss_isNotEmpty] && [result.localCRC64ecma oss_isNotEmpty])
        {
            uint64_t last_crc64,local_crc64;
            NSScanner *scanner = [NSScanner scannerWithString:delegate.lastCRC];
            [scanner scanUnsignedLongLong:&last_crc64];
            
            scanner = [NSScanner scannerWithString:result.localCRC64ecma];
            [scanner scanUnsignedLongLong:&local_crc64];
            
            NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:delegate.internalRequest.URL resolvingAgainstBaseURL:YES];
            NSArray<NSString *> *params = [urlComponents.query componentsSeparatedByString:@"&"];
            
            __block NSString *positionValue;
            [params enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) {
                if ([obj rangeOfString:@"position="].location == 0)
                {
                    *stop = YES;
                    positionValue = [obj substringFromIndex:9];
                }
            }];
            
            uint64_t position = [positionValue longLongValue];
            NSString *next_append_position = [result.httpResponseHeaderFields objectForKey:@"x-oss-next-append-position"];
            uint64_t length = [next_append_position longLongValue] - position;
            
            uint64_t crc_local = [OSSUtil crc64ForCombineCRC1:last_crc64 CRC2:local_crc64 length:(size_t)length];
            result.localCRC64ecma = [NSString stringWithFormat:@"%llu",crc_local];
            OSSLogVerbose(@"crc_local: %llu, crc_remote: %@,last_position: %llu,nextAppendPosition: %llu,length:  %llu",crc_local,result.remoteCRC64ecma,position,[next_append_position longLongValue],length);
        }
        //如果服务器和本机计算的crc值不一致,则报crc校验失败;否则,认为上传任务执行成功
        if (result.remoteCRC64ecma.oss_isNotEmpty && result.localCRC64ecma.oss_isNotEmpty)
        {
            if ([result.remoteCRC64ecma isEqualToString:result.localCRC64ecma])
            {
                [source setResult:response];
            }else
            {
                NSString *errorMessage = [NSString stringWithFormat:@"crc validation fails(local_crc64ecma: %@,remote_crc64ecma: %@)",result.localCRC64ecma,result.remoteCRC64ecma];
                
                NSError *crcError = [NSError errorWithDomain:OSSClientErrorDomain
                                                        code:OSSClientErrorCodeInvalidCRC
                                                    userInfo:@{OSSErrorMessageTOKEN:errorMessage}];
                [source setError:crcError];
            }
        }
        else
        {
            [source setResult:response];
        }
    }
}

- (void)dataTaskWithDelegate:(OSSNetworkingRequestDelegate *)requestDelegate {

    [[[[[OSSTask taskWithResult:nil] continueWithExecutor:self.taskExecutor withSuccessBlock:^id(OSSTask *task) {
        OSSLogVerbose(@"start to intercept request");
        for (id<OSSRequestInterceptor> interceptor in requestDelegate.interceptors) {
            task = [interceptor interceptRequestMessage:requestDelegate.allNeededMessage];
            if (task.error) {
                return task;
            }
        }
        return task;
    }] continueWithSuccessBlock:^id(OSSTask *task) {
        return [requestDelegate buildInternalHttpRequest];
    }] continueWithSuccessBlock:^id(OSSTask *task) {
        NSURLSessionDataTask * sessionTask = nil;
        if (self.configuration.timeoutIntervalForRequest > 0) {
            requestDelegate.internalRequest.timeoutInterval = self.configuration.timeoutIntervalForRequest;
        }

        if (requestDelegate.uploadingData) {
            [requestDelegate.internalRequest setHTTPBody:requestDelegate.uploadingData];
            sessionTask = [_session dataTaskWithRequest:requestDelegate.internalRequest];
        } else if (requestDelegate.uploadingFileURL) {
            sessionTask = [_session uploadTaskWithRequest:requestDelegate.internalRequest fromFile:requestDelegate.uploadingFileURL];

                requestDelegate.isBackgroundUploadFileTask = self.isUsingBackgroundSession;
        } else { // not upload request
            sessionTask = [_session dataTaskWithRequest:requestDelegate.internalRequest];
        }

        requestDelegate.currentSessionTask = sessionTask;
        requestDelegate.httpRequestNotSuccessResponseBody = [NSMutableData new];
        [self.sessionDelagateManager setObject:requestDelegate forKey:@(sessionTask.taskIdentifier)];
        if (requestDelegate.isRequestCancelled) {
            return [OSSTask taskWithError:[NSError errorWithDomain:OSSClientErrorDomain
                                                              code:OSSClientErrorCodeTaskCancelled
                                                          userInfo:nil]];
        }
        [sessionTask resume];
      
        return task;
    }] continueWithBlock:^id(OSSTask *task) {

        // if error occurs before created sessionTask
        if (task.error) {
            requestDelegate.completionHandler(nil, task.error);
        } else if (task.isFaulted) {
            requestDelegate.completionHandler(nil, [NSError errorWithDomain:OSSClientErrorDomain
                                                                       code:OSSClientErrorCodeExcpetionCatched
                                                                   userInfo:@{OSSErrorMessageTOKEN: [NSString stringWithFormat:@"Catch exception - %@", task.exception]}]);
        }
        return nil;
    }];
}

#pragma mark - NSURLSessionTaskDelegate Methods

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)sessionTask didCompleteWithError:(NSError *)error
{
    if (error) {
        OSSLogError(@"%@,error: %@", NSStringFromSelector(_cmd), error);
    }
    
    OSSNetworkingRequestDelegate * delegate = [self.sessionDelagateManager objectForKey:@(sessionTask.taskIdentifier)];
    [self.sessionDelagateManager removeObjectForKey:@(sessionTask.taskIdentifier)];

    NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)sessionTask.response;
    if (delegate == nil) {
        OSSLogVerbose(@"delegate: %@", delegate);
        /* if the background transfer service is enable, may recieve the previous task complete callback */
        /* for now, we ignore it */
        return ;
    }

    /* background upload task will not call back didRecieveResponse */
    if (delegate.isBackgroundUploadFileTask) {
        OSSLogVerbose(@"backgroud upload task did recieve response: %@", httpResponse);
        if (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 && httpResponse.statusCode != 203) {
            [delegate.responseParser consumeHttpResponse:httpResponse];
        } else {
            delegate.isHttpRequestNotSuccessResponse = YES;
        }
    }

    [[[[OSSTask taskWithResult:nil] continueWithSuccessBlock:^id(OSSTask * task) {
        if (!delegate.error) {
            delegate.error = error;
        }
        if (delegate.error) {
            OSSLogDebug(@"networking request completed with error: %@", error);
            if ([delegate.error.domain isEqualToString:NSURLErrorDomain] && delegate.error.code == NSURLErrorCancelled) {
                return [OSSTask taskWithError:[NSError errorWithDomain:OSSClientErrorDomain
                                                                 code:OSSClientErrorCodeTaskCancelled
                                                             userInfo:[error userInfo]]];
            } else {
                NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithDictionary:[error userInfo]];
                [userInfo setObject:[NSString stringWithFormat:@"%ld", (long)error.code] forKey:@"OriginErrorCode"];
                return [OSSTask taskWithError:[NSError errorWithDomain:OSSClientErrorDomain
                                                                 code:OSSClientErrorCodeNetworkError
                                                             userInfo:userInfo]];
            }
        }
        return task;
    }] continueWithSuccessBlock:^id(OSSTask *task) {
        if (delegate.isHttpRequestNotSuccessResponse) {
            if (httpResponse.statusCode == 0) {
                return [OSSTask taskWithError:[NSError errorWithDomain:OSSClientErrorDomain
                                                                 code:OSSClientErrorCodeNetworkingFailWithResponseCode0
                                                             userInfo:@{OSSErrorMessageTOKEN: @"Request failed, response code 0"}]];
            }
            NSString * notSuccessResponseBody = [[NSString alloc] initWithData:delegate.httpRequestNotSuccessResponseBody encoding:NSUTF8StringEncoding];
            OSSLogError(@"http error response: %@", notSuccessResponseBody);
            NSDictionary * dict = [NSDictionary oss_dictionaryWithXMLString:notSuccessResponseBody];

            return [OSSTask taskWithError:[NSError errorWithDomain:OSSServerErrorDomain
                                                             code:(-1 * httpResponse.statusCode)
                                                         userInfo:dict]];
        }
        return task;
    }] continueWithBlock:^id(OSSTask *task) {
        if (task.error) {
            
            
            OSSNetworkingRetryType retryType = [delegate.retryHandler shouldRetry:delegate.currentRetryCount
                                                                  requestDelegate:delegate
                                                                         response:httpResponse
                                                                            error:task.error];
            OSSLogVerbose(@"current retry count: %u, retry type: %d", delegate.currentRetryCount, (int)retryType);

            switch (retryType) {

                case OSSNetworkingRetryTypeShouldNotRetry: {
                    delegate.completionHandler(nil, task.error);
                    return nil;
                }

                case OSSNetworkingRetryTypeShouldCorrectClockSkewAndRetry: {
                    /* correct clock skew */
                    NSString * dateStr = [[httpResponse allHeaderFields] objectForKey:@"Date"];
                    if ([dateStr length] > 0) {
                        NSDate * serverTime = [NSDate oss_dateFromString:dateStr];
                        NSDate * deviceTime = [NSDate date];
                        NSTimeInterval skewTime = [deviceTime timeIntervalSinceDate:serverTime];
                        [NSDate oss_setClockSkew:skewTime];
                    } else if (!error) {
                        // The response header does not have the 'Date' field.
                        // This should not happen.
                        OSSLogError(@"Date header does not exist, unable to fix the clock skew");
                    }
                    
                    break;
                }

                default:
                    break;
            }

            /* now, should retry */
            NSTimeInterval suspendTime = [delegate.retryHandler timeIntervalForRetry:delegate.currentRetryCount retryType:retryType];
            delegate.currentRetryCount++;
            [NSThread sleepForTimeInterval:suspendTime];
            
            if(delegate.retryCallback){
                delegate.retryCallback();
            }
            

            /* retry recursively */
            [delegate reset];
            
            [self dataTaskWithDelegate:delegate];
        } else {
            OSSResult *result = [delegate.responseParser constructResultObject];
            result.metrics = delegate.metrics;
            delegate.completionHandler(result, nil);
        }
        return nil;
    }];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
    OSSNetworkingRequestDelegate * delegate = [self.sessionDelagateManager objectForKey:@(task.taskIdentifier)];
    if (delegate.uploadProgress)
    {
        delegate.uploadProgress(bytesSent, totalBytesSent, totalBytesExpectedToSend);
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
    if (!challenge) {
        return;
    }
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;
    
    /*
     * Gets the host name
     */
    
    NSString * host = [[task.currentRequest allHTTPHeaderFields] objectForKey:@"Host"];
    if (!host) {
        host = task.currentRequest.URL.host;
    }
    
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    // Uses the default evaluation for other challenges.
    completionHandler(disposition,credential);
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                     willPerformHTTPRedirection:(NSHTTPURLResponse *)response
                                     newRequest:(NSURLRequest *)request
                              completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    if (self.configuration.enableFollowRedirects) {
        completionHandler(request);
    } else {
        completionHandler(nil);
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
    if (self.configuration.enableNetworkMetricInfo) {
        OSSNetworkingRequestDelegate *delegate = [self.sessionDelagateManager objectForKey:@(task.taskIdentifier)];
        delegate.metrics = metrics;
        OSSLogDebug(@"%@", metrics);
    }
}

#pragma mark - NSURLSessionDataDelegate Methods

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    /* background upload task will not call back didRecieveResponse */
    OSSLogVerbose(@"%@,response: %@", NSStringFromSelector(_cmd), response);
    
    OSSNetworkingRequestDelegate * delegate = [self.sessionDelagateManager objectForKey:@(dataTask.taskIdentifier)];
    NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
    if (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 && httpResponse.statusCode != 203) {
        [delegate.responseParser consumeHttpResponse:httpResponse];
    } else {
        delegate.isHttpRequestNotSuccessResponse = YES;
    }
    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    OSSNetworkingRequestDelegate * delegate = [self.sessionDelagateManager objectForKey:@(dataTask.taskIdentifier)];

    /* background upload task will not call back didRecieveResponse.
       so if we recieve response data after background uploading file,
       we consider it as error response message since a successful uploading request will not response any data */
    if (delegate.isBackgroundUploadFileTask)
    {
        //判断当前的statuscode是否成功
        NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)dataTask.response;
        if (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 && httpResponse.statusCode != 203) {
            [delegate.responseParser consumeHttpResponse:httpResponse];
            delegate.isHttpRequestNotSuccessResponse = NO;
        }else
        {
            delegate.isHttpRequestNotSuccessResponse = YES;
        }
    }
    
    if (delegate.isHttpRequestNotSuccessResponse) {
        [delegate.httpRequestNotSuccessResponseBody appendData:data];
    }
    else {
        if (delegate.onRecieveData) {
            delegate.onRecieveData(data);
        } else {
            OSSTask * consumeTask = [delegate.responseParser consumeHttpResponseBody:data];
            if (consumeTask.error) {
                OSSLogError(@"consume data error: %@", consumeTask.error);
                delegate.error = consumeTask.error;
                [dataTask cancel];
            }
        }
    }

    if (!delegate.isHttpRequestNotSuccessResponse && delegate.downloadProgress) {
        int64_t bytesWritten = [data length];
        delegate.payloadTotalBytesWritten += bytesWritten;
        int64_t totalBytesExpectedToWrite = dataTask.response.expectedContentLength;
        delegate.downloadProgress(bytesWritten, delegate.payloadTotalBytesWritten, totalBytesExpectedToWrite);
    }
}

#pragma mark - Private Methods

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
    /*
     * Creates the policies for certificate verification.
     */
    NSMutableArray *policies = [NSMutableArray array];
    if (domain) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    
    /*
     * Sets the policies to server's certificate
     */
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
    
    
    /*
     * Evaulates if the current serverTrust is trustable.
     * It's officially suggested that the serverTrust could be passed when result = kSecTrustResultUnspecified or kSecTrustResultProceed.
     * For more information checks out https://developer.apple.com/library/ios/technotes/tn2232/_index.html
     * For detail information about SecTrustResultType, checks out SecTrust.h
     */
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    
    return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}

@end
