#import "NetworkInterceptor.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <React/RCTNetworking.h>
#import "NetworkInterceptorEmitter.h"

#import <CommonCrypto/CommonDigest.h>

static NSString * const kHandledKey          = @"Handled";
static NSString * const kHTTPMethodDefault   = @"GET";
static NSString * const kHTTP                = @"http";
static NSString * const kHTTPS               = @"https";
static NSString * const kLocalhost           = @"localhost";
static NSString * const kLoopbackIP          = @"127.0.0.1";

static const NSTimeInterval kRequestTimeout  = 5.0;
static const NSTimeInterval kResourceTimeout = 30.0;

@interface NetworkURLProtocol : NSURLProtocol
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSDate *startTime;
@property (nonatomic, copy) NSString *httpMethod;
@property (nonatomic, copy) NSString *traceId;
@property (nonatomic, copy) NSString *spanId;
@end

@implementation NetworkURLProtocol

static NSURLSession *session = nil;
static NSMutableDictionary<NSNumber *, NetworkURLProtocol *> *delegateMap = nil;

+ (void)initialize {
    if (self == [NetworkURLProtocol class]) {
        delegateMap = [NSMutableDictionary new];
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        config.timeoutIntervalForRequest = kRequestTimeout;
        config.timeoutIntervalForResource = kResourceTimeout;
        session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    NSURL *url = request.URL;
    if (url) {
        NSLog(@"Coralogix SDK captured request: %@", url.absoluteString);
    }

    if ([NSURLProtocol propertyForKey:kHandledKey inRequest:request]) return NO;
    if (!url) return NO;

    NSString *host = url.host.lowercaseString;
    if (!host) return NO;

    NSArray *firebaseHosts = @[
        @"firebaseinstallations.googleapis.com",
        @"firebaseremoteconfig.googleapis.com",
        @"firebase.googleapis.com"
    ];
    if ([firebaseHosts containsObject:host]) return NO;

    if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"127.0.0.1"]) return NO;

    static NSRegularExpression *ipRegex = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSError *error = nil;
        ipRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\d{1,3}(\\.\\d{1,3}){3}$" options:0 error:&error];
    });

    if (ipRegex) {
        NSUInteger ipMatches = [ipRegex numberOfMatchesInString:host options:0 range:NSMakeRange(0, host.length)];
        if (ipMatches > 0) return NO;
    }

    NSString *scheme = url.scheme.lowercaseString;
    if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"]) return NO;

    return YES;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

- (void)startLoading {
    NSMutableURLRequest *mutableRequest = [self deepCloneRequest:self.request];

    [NSURLProtocol setProperty:@YES forKey:kHandledKey inRequest:mutableRequest];

    self.startTime = [NSDate date];
    self.httpMethod = mutableRequest.HTTPMethod ?: kHTTPMethodDefault;

    self.dataTask = [session dataTaskWithRequest:mutableRequest];

    @synchronized (delegateMap) {
        delegateMap[@(self.dataTask.taskIdentifier)] = self;
    }

    [self.dataTask resume];
}

- (void)stopLoading {
    [self.dataTask cancel];
    @synchronized (delegateMap) {
        [delegateMap removeObjectForKey:@(self.dataTask.taskIdentifier)];
    }
}

- (NSMutableURLRequest *)deepCloneRequest:(NSURLRequest *)originalRequest {
    NSMutableURLRequest *cloned = [originalRequest mutableCopy];
    [cloned setAllHTTPHeaderFields:originalRequest.allHTTPHeaderFields];

    if (originalRequest.HTTPBody) {
        cloned.HTTPBody = originalRequest.HTTPBody;
    }

    cloned.HTTPMethod = originalRequest.HTTPMethod;
    cloned.cachePolicy = originalRequest.cachePolicy;
    cloned.timeoutInterval = originalRequest.timeoutInterval;
    cloned.allowsCellularAccess = originalRequest.allowsCellularAccess;
    if (@available(iOS 13.0, *)) {
        cloned.allowsConstrainedNetworkAccess = originalRequest.allowsConstrainedNetworkAccess;
        cloned.allowsExpensiveNetworkAccess = originalRequest.allowsExpensiveNetworkAccess;
    }

    cloned.networkServiceType = originalRequest.networkServiceType;
    cloned.HTTPShouldHandleCookies = originalRequest.HTTPShouldHandleCookies;
    cloned.HTTPShouldUsePipelining = originalRequest.HTTPShouldUsePipelining;

    NSString *traceId = [self generateRandomHexWithLength:32];
    NSString *spanId = [self generateRandomHexWithLength:16];
    NSString *traceparentValue = [NSString stringWithFormat:@"00-%@-%@-01", traceId, spanId];
    [cloned setValue:traceparentValue forHTTPHeaderField:@"traceparent"];

    self.traceId = traceId;
    self.spanId = spanId;

    return cloned;
}

+ (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {

    NetworkURLProtocol *selfRef = nil;
    @synchronized (delegateMap) {
        selfRef = delegateMap[@(dataTask.taskIdentifier)];
    }

    if (!selfRef) return;

    [selfRef.client URLProtocol:selfRef didLoadData:data]; 
}

+ (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {

    NetworkURLProtocol *selfRef = nil;
    @synchronized (delegateMap) {
        selfRef = delegateMap[@(dataTask.taskIdentifier)];
    }

    if (!selfRef) {
        completionHandler(NSURLSessionResponseAllow);
        return;
    }

    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:selfRef.startTime] * 1000; // ms
    NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
    NSString *contentLength = headers[@"Content-Length"] ?: @"0";

    NSDictionary *payload = @{
        @"url": response.URL.absoluteString ?: @"unknown",
        @"method": selfRef.httpMethod ?: kHTTPMethodDefault,
        @"status_code": @(((NSHTTPURLResponse *)response).statusCode),
        @"host": response.URL.host ?: @"unknown",
        @"scheme": response.URL.scheme ?: @"unknown",
        @"status_text": [NSHTTPURLResponse localizedStringForStatusCode:((NSHTTPURLResponse *)response).statusCode] ?: @"",
        @"duration": @(duration),
        @"response_content_length": contentLength,
        @"traceId": selfRef.traceId ?: @"",
        @"spanId": selfRef.spanId ?: @""
    };

    [NetworkInterceptorEmitter emitEventToJS:payload];

    [selfRef.client URLProtocol:selfRef didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    completionHandler(NSURLSessionResponseAllow);
}

+ (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {

    NetworkURLProtocol *selfRef = nil;
    @synchronized (delegateMap) {
        selfRef = delegateMap[@(task.taskIdentifier)];
        [delegateMap removeObjectForKey:@(task.taskIdentifier)];
    }

    if (!selfRef) return;

    if (error) {
        [selfRef.client URLProtocol:selfRef didFailWithError:error];
    } else {
        [selfRef.client URLProtocolDidFinishLoading:selfRef];
    }
}

#pragma mark - Helpers

- (NSString *)generateRandomHexWithLength:(NSUInteger)length {
    NSMutableString *randomHex = [NSMutableString stringWithCapacity:length];
    for (NSUInteger i = 0; i < length; i++) {
        [randomHex appendFormat:@"%x", arc4random_uniform(16)];
    }
    return randomHex;
}

@end

@implementation NetworkInterceptor

+ (void)load {
    [self startIntercepting];
}

+ (void)startIntercepting {
    [NSURLProtocol registerClass:[NetworkURLProtocol class]];

    Class configCls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: [NSURLSessionConfiguration class];
    Method originalMethod = class_getInstanceMethod(configCls, @selector(protocolClasses));

    if (originalMethod) {
        IMP originalImplementation = method_getImplementation(originalMethod);
        IMP newImplementation = imp_implementationWithBlock(^NSArray *(id self) {
            NSArray *existingProtocols = ((NSArray *(*)(id, SEL))originalImplementation)(self, @selector(protocolClasses));
            if (![existingProtocols containsObject:[NetworkURLProtocol class]]) {
                return [@[NetworkURLProtocol.class] arrayByAddingObjectsFromArray:existingProtocols ?: @[]];
            }
            return existingProtocols;
        });
        method_setImplementation(originalMethod, newImplementation);
    }
}

@end

