
#import "{{classPrefix}}Logger.h"
#import "{{classPrefix}}ApiClient.h"
#import "{{classPrefix}}JSONRequestSerializer.h"
#import "{{classPrefix}}QueryParamCollection.h"
#import "{{classPrefix}}DefaultConfiguration.h"
#import "FHTLog.h"

NSString *const {{classPrefix}}ResponseObjectErrorKey = @"{{classPrefix}}ResponseObject";

static NSString * const k{{classPrefix}}ContentDispositionKey = @"Content-Disposition";

static NSDictionary * {{classPrefix}}__headerFieldsForResponse(NSURLResponse *response) {
    if(![response isKindOfClass:[NSHTTPURLResponse class]]) {
        return nil;
    }
    return ((NSHTTPURLResponse*)response).allHeaderFields;
}

static NSString * {{classPrefix}}__fileNameForResponse(NSURLResponse *response) {
    NSDictionary * headers = {{classPrefix}}__headerFieldsForResponse(response);
    if(!headers[k{{classPrefix}}ContentDispositionKey]) {
        return [NSString stringWithFormat:@"%@", [[NSProcessInfo processInfo] globallyUniqueString]];
    }
    NSString *pattern = @"filename=['\"]?([^'\"\\s]+)['\"]?";
    NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    NSString *contentDispositionHeader = headers[k{{classPrefix}}ContentDispositionKey];
    NSTextCheckingResult *match = [regexp firstMatchInString:contentDispositionHeader options:0 range:NSMakeRange(0, [contentDispositionHeader length])];
    return [contentDispositionHeader substringWithRange:[match rangeAtIndex:1]];
}


@interface {{classPrefix}}ApiClient ()

@property (nonatomic, strong, readwrite) id<{{classPrefix}}Configuration> configuration;
@property (nonatomic, strong) NSArray<NSString*>* downloadTaskResponseTypes;

@end

@implementation {{classPrefix}}ApiClient

#pragma mark - Singleton Methods

+ (instancetype)sharedClient {
    static {{classPrefix}}ApiClient *sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedClient = [[self alloc] init];
    });
    return sharedClient;
}

#pragma mark - Initialize Methods

- (instancetype)init {
    return [self initWithConfiguration:[{{classPrefix}}DefaultConfiguration sharedConfig]];
}

- (instancetype)initWithBaseURL:(NSURL *)url {
    return [self initWithBaseURL:url configuration:[{{classPrefix}}DefaultConfiguration sharedConfig]];
}

- (instancetype)initWithConfiguration:(id<{{classPrefix}}Configuration>)configuration {
    return [self initWithBaseURL:[NSURL URLWithString:configuration.host] configuration:configuration];
}

- (instancetype)initWithBaseURL:(NSURL *)url 
                  configuration:(id<{{classPrefix}}Configuration>)configuration {
    self = [super initWithBaseURL:url];
    if (self) {
        _configuration = configuration;
        _timeoutInterval = 60;
        _responseDeserializer = [[{{classPrefix}}ResponseDeserializer alloc] init];
        _sanitizer = [[{{classPrefix}}Sanitizer alloc] init];
        _downloadTaskResponseTypes = @[@"NSURL*", @"NSURL"];
        self.securityPolicy = [self createSecurityPolicy];
        self.responseSerializer = [AFHTTPResponseSerializer serializer];
    }
    return self;
}

#pragma mark - Task Methods

- (NSURLSessionDataTask*)taskWithCompletionBlock:(NSURLRequest *)request 
                                 completionBlock:(void(^)(id data, NSError *error, FhtResponseCacheStatus responseCacheStatus))completionBlock {
    NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
    NSHTTPURLResponse *httpCacheResponse = [cachedResponse.response isKindOfClass:[NSHTTPURLResponse class]]? (NSHTTPURLResponse *)cachedResponse.response: nil;
    __block NSURLSessionDataTask *task = [self dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        FhtDebugLogResponse(response, responseObject,request,error);
        if(!error) {
            FhtResponseCacheStatus responseCacheStatus = FhtResponseCacheStatus_NewFromServer;
            //see http://andrewmarinov.com/ioss-corenetwork-lying/
            if(httpCacheResponse && [response isKindOfClass:[NSHTTPURLResponse class]]){
                NSHTTPURLResponse *httpResponse =(NSHTTPURLResponse *)response;
                BOOL hasEtag = httpResponse.allHeaderFields[@"Etag"] != nil;
                if(hasEtag){
                    BOOL sameEtag = [httpResponse.allHeaderFields[@"Etag"] isEqualToString: httpCacheResponse.allHeaderFields[@"Etag"]];
                    if(sameEtag){
                        BOOL sameTimestamp = [httpResponse.allHeaderFields[@"Date"] isEqualToString:httpCacheResponse.allHeaderFields[@"Date"]];
                        if(sameTimestamp){
                            responseCacheStatus = FhtResponseCacheStatus_FreshFromCache;
                        }
                        else {
                            [[NSURLCache sharedURLCache] removeCachedResponseForRequest:request];

                            NSCachedURLResponse *newCacheResponse = [[NSCachedURLResponse alloc] initWithResponse:httpResponse data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed];
                            [[NSURLCache sharedURLCache] storeCachedResponse:newCacheResponse forRequest:request];
                            
                            responseCacheStatus = FhtResponseCacheStatus_NotModifiedFromServer;
                        }
                    }
                    else {
                        responseCacheStatus = FhtResponseCacheStatus_NewFromServer;
                    }
                }
                FHTLogDebug(@"Possible cached response returned");
            }
            completionBlock(responseObject, nil, responseCacheStatus);
            return;
        }
        NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
        if (responseObject) {
            // Add in the (parsed) response body.
            userInfo[FhtResponseObjectErrorKey] = responseObject;
        }
        NSError *augmentedError = [error initWithDomain:error.domain code:error.code userInfo:userInfo];
        
        if(httpCacheResponse)
        {
            BOOL isReachable = [AFNetworkReachabilityManager sharedManager].reachable;
            BOOL isTimedout = ((NSError *)(error.userInfo[@"NSUnderlyingError"])).code == kCFURLErrorTimedOut;
            
            BOOL hasServerSideError = NO;
            if([response isKindOfClass:[NSHTTPURLResponse class]]){
                NSHTTPURLResponse *httpResponse =(NSHTTPURLResponse *)response;
                //if the server is temporarily busy or taken offline, aka, returning 5xx or timed out
                hasServerSideError = httpResponse && httpResponse.statusCode  >= 500;
            }
            
            BOOL useStaleCache = !isReachable || hasServerSideError || isTimedout;
            
            if(useStaleCache){
                //Will use stale cache and also provided the original error, and also the hint that it is stale from cache
                NSAssert(task, @"task is nil");
                [[NSURLCache sharedURLCache] getCachedResponseForDataTask:task completionHandler:^(NSCachedURLResponse *cachedResponse) {
                    FHTLogDebug(@"cached Response: %@", cachedResponse);
                    //TODO Simply mark the response as FhtResponseCacheStatus_StaleFromCache, EVEN if it may still be fresh. May check in future. NSCachedURLResponse does not provide an easy way to do this.
                    completionBlock(cachedResponse.data, augmentedError, FhtResponseCacheStatus_StaleFromCache);
                }];
                return;
            }
        }
        
        completionBlock(nil, augmentedError, FhtResponseCacheStatus_NewFromServer);
    }];

    return task;
}


- (NSURLSessionDataTask*)downloadTaskWithCompletionBlock:(NSURLRequest *)request 
                                         completionBlock:(void(^)(id data, NSError *error))completionBlock {

    __block NSString * tempFolderPath = [self.configuration.tempFolderPath copy];

    NSURLSessionDataTask* task = [self dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        {{classPrefix}}DebugLogResponse(response, responseObject,request,error);

        if(error) {
            NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
            if (responseObject) {
                userInfo[{{classPrefix}}ResponseObjectErrorKey] = responseObject;
            }
            NSError *augmentedError = [error initWithDomain:error.domain code:error.code userInfo:userInfo];
            completionBlock(nil, augmentedError);
            return;
        }

        NSString *directory = tempFolderPath ?: NSTemporaryDirectory();
        NSString *filename = {{classPrefix}}__fileNameForResponse(response);

        NSString *filepath = [directory stringByAppendingPathComponent:filename];
        NSURL *file = [NSURL fileURLWithPath:filepath];

        [responseObject writeToURL:file atomically:YES];

        completionBlock(file, nil);
    }];

    return task;
}

#pragma mark - Perform Request Methods

- (void)requestWithPath:(NSString *)path
                 method:(NSString *)method
             pathParams:(NSDictionary *)pathParams
            queryParams:(NSDictionary *)queryParams
             formParams:(NSDictionary *)formParams
                  files:(NSDictionary *)files
                   body:(id)body
           headerParams:(NSDictionary *)headerParams
           authSettings:(NSArray *)authSettings
     requestContentType:(NSString *)requestContentType
    responseContentType:(NSString *)responseContentType
           responseType:(NSString *)responseType
        completionBlock:(void(^)(id data, NSError *error, FhtResponseCacheStatus responseCacheStatus))completionBlock {

    AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer = [self requestSerializerForRequestContentType:requestContentType];

    __weak id<FhtSanitizer> sanitizer = self.sanitizer;
    
    const NSString *longitude = @"longitude";
    const NSString *latitude = @"latitude";

    if(queryParams[longitude] && queryParams[latitude]){
        NSMutableDictionary *qp = [queryParams mutableCopy];
        NSMutableDictionary *hp = [headerParams mutableCopy];
        NSString *longitudeString = ((NSNumber *)qp[longitude]).stringValue;
        [hp setObject:longitudeString forKey: [NSString stringWithFormat:@"X-FHT-%@", longitude.uppercaseString]];
        [hp setObject:((NSNumber *)qp[latitude]).stringValue forKey: [NSString stringWithFormat:@"X-FHT-%@", latitude.uppercaseString]];
        [qp removeObjectsForKeys:@[longitude, latitude]];
        queryParams = qp;
        headerParams = hp;
    }
    
    // sanitize parameters
    pathParams = [sanitizer sanitizeForSerialization:pathParams];
    queryParams = [sanitizer sanitizeForSerialization:queryParams];
    headerParams = [sanitizer sanitizeForSerialization:headerParams];
    formParams = [sanitizer sanitizeForSerialization:formParams];
    if(![body isKindOfClass:[NSData class]]) {
        body = [sanitizer sanitizeForSerialization:body];
    }

    // auth setting
    [self updateHeaderParams:&headerParams queryParams:&queryParams withAuthSettings:authSettings];

    NSMutableString *resourcePath = [NSMutableString stringWithString:path];
    [pathParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        NSString * safeString = ([obj isKindOfClass:[NSString class]]) ? obj : [NSString stringWithFormat:@"%@", obj];
        safeString = FhtPercentEscapedStringFromString(safeString);
        [resourcePath replaceCharactersInRange:[resourcePath rangeOfString:[NSString stringWithFormat:@"{%@}", key]] withString:safeString];
    }];

    NSString* pathWithQueryParams = [self pathWithQueryParamsToString:resourcePath queryParams:queryParams];
    if ([pathWithQueryParams hasPrefix:@"/"]) {
        pathWithQueryParams = [pathWithQueryParams substringFromIndex:1];
    }

    NSString* urlString = [[NSURL URLWithString:pathWithQueryParams relativeToURL:self.baseURL] absoluteString];

    NSError *requestCreateError = nil;
    NSMutableURLRequest * request = nil;
    if (files.count > 0) {
        request = [requestSerializer multipartFormRequestWithMethod:@"POST" URLString:urlString parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
                                                   [formParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                                                       NSString *objString = [sanitizer parameterToString:obj];
                                                       NSData *data = [objString dataUsingEncoding:NSUTF8StringEncoding];
                                                       [formData appendPartWithFormData:data name:key];
                                                   }];
                                                   [files enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                                                       NSURL *filePath = (NSURL *)obj;
                                                       [formData appendPartWithFileURL:filePath name:key error:nil];
                                                   }];
                        } error:&requestCreateError];
    } else {
        if (formParams) {
            request = [requestSerializer requestWithMethod:method URLString:urlString parameters:formParams error:&requestCreateError];
        }

        if (body) {
            request = [requestSerializer requestWithMethod:method URLString:urlString parameters:body error:&requestCreateError];
        }
    }

    if(!request) {
        completionBlock(nil, requestCreateError, FhtResponseCacheStatus_NewFromServer);
        return;
    }

    if ([headerParams count] > 0){
        for(NSString * key in [headerParams keyEnumerator]){
            [request setValue:[headerParams valueForKey:key] forHTTPHeaderField:key];
        }
    }
    [requestSerializer setValue:responseContentType forHTTPHeaderField:@"Accept"];

    [self postProcessRequest:request];


    NSURLSessionTask *task = nil;

    if ([self.downloadTaskResponseTypes containsObject:responseType]) {
        task = [self downloadTaskWithCompletionBlock:request completionBlock:^(id data, NSError *error) {
            completionBlock(data, error, FhtResponseCacheStatus_NewFromServer);
        }];
    } else {
        __weak typeof(self) weakSelf = self;
        task = [self taskWithCompletionBlock:request completionBlock:^(id data, NSError *error, FhtResponseCacheStatus responseCacheStatus) {
            NSError * serializationError;
            id response = [weakSelf.responseDeserializer deserialize:data class:responseType error:&serializationError];

            if(!response && !error){
                error = serializationError;
            }
            [weakSelf postProcessResponse:response error:error responseCacheStatus:responseCacheStatus originalCompletionBlock:completionBlock];
        }];
    }

    [task resume];
}

- (AFHTTPRequestSerializer<AFURLRequestSerialization> *)requestSerializerForRequestContentType:(NSString *)requestContentType {
    AFHTTPRequestSerializer <AFURLRequestSerialization> * serializer = self.requestSerializerForContentType[requestContentType];
    if(!serializer) {
        NSAssert(NO, @"Unsupported request content type %@", requestContentType);
        serializer = [AFHTTPRequestSerializer serializer];
    }
    serializer.timeoutInterval = self.timeoutInterval;
    return serializer;
}

//Added for easier override to modify request
- (void)postProcessRequest:(NSMutableURLRequest *)request {

}

//Added by yuanjian at www.fht360.com for override by subclass
- (void)postProcessResponse:(id)response 
                      error:(NSError *)error 
        responseCacheStatus:(FhtResponseCacheStatus)responseCacheStatus 
    originalCompletionBlock:(void(^)(id data, NSError *error, FhtResponseCacheStatus responseCacheStatus))completionBlock {
    completionBlock(response, error, responseCacheStatus);
}

#pragma mark -

- (NSString *)pathWithQueryParamsToString:(NSString*)path 
                             queryParams:(NSDictionary*)queryParams {
    if(queryParams.count == 0) {
        return path;
    }
    NSString * separator = nil;
    NSUInteger counter = 0;

    NSMutableString * requestUrl = [NSMutableString stringWithFormat:@"%@", path];

    NSDictionary *separatorStyles = @{@"csv" : @",",
            @"tsv" : @"\t",
            @"pipes": @"|"
    };
    for(NSString * key in [queryParams keyEnumerator]){
        if (counter == 0) {
            separator = @"?";
        } else {
            separator = @"&";
        }
        id queryParam = [queryParams valueForKey:key];
        if(!queryParam) {
            continue;
        }
        NSString *safeKey = {{classPrefix}}PercentEscapedStringFromString(key);
        if ([queryParam isKindOfClass:[NSString class]]){
            [requestUrl appendString:[NSString stringWithFormat:@"%@%@=%@", separator, safeKey, {{classPrefix}}PercentEscapedStringFromString(queryParam)]];

        } else if ([queryParam isKindOfClass:[{{classPrefix}}QueryParamCollection class]]){
            {{classPrefix}}QueryParamCollection * coll = ({{classPrefix}}QueryParamCollection*) queryParam;
            NSArray* values = [coll values];
            NSString* format = [coll format];

            if([format isEqualToString:@"multi"]) {
                for(id obj in values) {
                    if (counter > 0) {
                        separator = @"&";
                    }
                    NSString * safeValue = {{classPrefix}}PercentEscapedStringFromString([NSString stringWithFormat:@"%@",obj]);
                    [requestUrl appendString:[NSString stringWithFormat:@"%@%@=%@", separator, safeKey, safeValue]];
                    counter += 1;
                }
                continue;
            }
            NSString * separatorStyle = separatorStyles[format];
            NSString * safeValue = {{classPrefix}}PercentEscapedStringFromString([values componentsJoinedByString:separatorStyle]);
            [requestUrl appendString:[NSString stringWithFormat:@"%@%@=%@", separator, safeKey, safeValue]];
        } else {
            NSString * safeValue = {{classPrefix}}PercentEscapedStringFromString([NSString stringWithFormat:@"%@",queryParam]);
            [requestUrl appendString:[NSString stringWithFormat:@"%@%@=%@", separator, safeKey, safeValue]];
        }
        counter += 1;
    }
    return requestUrl;
}

/**
 * Update header and query params based on authentication settings
 */
- (void)updateHeaderParams:(NSDictionary **)headers 
               queryParams:(NSDictionary **)querys
          withAuthSettings:(NSArray *)authSettings {

    if ([authSettings count] == 0) {
        return;
    }

    NSMutableDictionary *headersWithAuth = [NSMutableDictionary dictionaryWithDictionary:*headers];
    NSMutableDictionary *querysWithAuth = [NSMutableDictionary dictionaryWithDictionary:*querys];

    id<{{classPrefix}}Configuration> config = self.configuration;
    for (NSString *auth in authSettings) {
        NSDictionary *authSetting = config.authSettings[auth];

        if(!authSetting) { // auth setting is set only if the key is non-empty
            continue;
        }
        NSString *type = authSetting[@"in"];
        NSString *key = authSetting[@"key"];
        NSString *value = authSetting[@"value"];
        if ([type isEqualToString:@"header"] && [key length] > 0 ) {
            headersWithAuth[key] = value;
        } else if ([type isEqualToString:@"query"] && [key length] != 0) {
            querysWithAuth[key] = value;
        }
    }

    *headers = [NSDictionary dictionaryWithDictionary:headersWithAuth];
    *querys = [NSDictionary dictionaryWithDictionary:querysWithAuth];
}

- (AFSecurityPolicy *)createSecurityPolicy {
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];

    id<{{classPrefix}}Configuration> config = self.configuration;

    if (config.sslCaCert) {
        NSData *certData = [NSData dataWithContentsOfFile:config.sslCaCert];
        [securityPolicy setPinnedCertificates:[NSSet setWithObject:certData]];
    }

    if (config.verifySSL) {
        [securityPolicy setAllowInvalidCertificates:NO];
    } else {
        [securityPolicy setAllowInvalidCertificates:YES];
        [securityPolicy setValidatesDomainName:NO];
    }

    return securityPolicy;
}

- (NSDictionary<NSString *, AFHTTPRequestSerializer<AFURLRequestSerialization> *> *)requestSerializerForContentType
{
    AFHTTPRequestSerializer* afhttpRequestSerializer = [AFHTTPRequestSerializer serializer];
    FhtJSONRequestSerializer * swgjsonRequestSerializer = [FhtJSONRequestSerializer serializer];
    return @{kFhtApplicationJSONType : swgjsonRequestSerializer,
             @"application/x-www-form-urlencoded": afhttpRequestSerializer,
             @"multipart/form-data": afhttpRequestSerializer};
}

@end
