//
//  MBIMHTTPConnection.m
//  CocoaHTTPServer
//
//  Created by James Magahern on 11/16/18.
//  Copyright © 2018 James Magahern. All rights reserved.
//

#import "MBIMHTTPConnection.h"

#import "MBIMBridge.h"
#import "MBIMBridge_Private.h"
#import "MBIMBridgeOperation.h"
#import "MBIMAuthToken.h"
#import "MBIMUpdateQueue.h"
#import "MBIMURLUtilities.h"

#import <Security/Security.h>
#import "HTTPMessage.h"
#import "GCDAsyncSocket.h"

@interface HTTPConnection (/* INTERNAL */)
- (BOOL)isAuthenticated;
@end

@implementation MBIMHTTPConnection {
    NSMutableData *_bodyData;
    MBIMBridgeOperation *_currentOperation;
}

- (BOOL)isSecureServer
{
    return [[MBIMBridge sharedInstance] usesSSL];
}

- (NSArray *)sslIdentityAndCertificates
{
    return [[MBIMBridge sharedInstance] sslCertificateAndIdentity];
}

- (BOOL)isPasswordProtected:(NSString *)path
{
    if ([[MBIMBridge sharedInstance] usesAccessControl]) {
        NSURL *url = [NSURL URLWithString:path];
        NSString *endpointName = [url lastPathComponent];
        
        Class operationClass = [MBIMBridgeOperation operationClassForEndpointName:endpointName];
        return [operationClass requiresAuthentication];
    }
    
    return NO;
}

- (NSString *)passwordForUser:(NSString *)username
{
    MBIMBridge *bridge = [MBIMBridge sharedInstance];
    if ([username isEqualToString:bridge.authUsername]) {
        return bridge.authPassword;
    }
    
    // nil means "user not in system"
    return nil;
}

- (BOOL)isAuthenticated
{
    NSString *authInfo = [request headerField:@"Authorization"];
    if ([authInfo hasPrefix:@"Bearer"]) {
        NSArray *bearerAuthTuple = [authInfo componentsSeparatedByString:@" "];
        if ([bearerAuthTuple count] == 2) {
            NSString *jwtToken = [bearerAuthTuple objectAtIndex:1];
            MBIMAuthToken *authToken = [[MBIMAuthToken alloc] initWithTokenString:jwtToken];
            return [authToken isValid];
        }
    }
    
    return [super isAuthenticated];
}

- (BOOL)useDigestAccessAuthentication
{
    // TODO: should use digest all the time, but it's a bit more complicated. Allow this to be NO if
    // SSL is on, because then at least basic auth is encrypted
    return ![[MBIMBridge sharedInstance] usesSSL];
}

- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
{
    if ([method isEqualToString:@"GET"] || [method isEqualToString:@"POST"]) {
        return YES;
    }
    
    return NO;
}

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    __block NSObject<HTTPResponse> *response = nil;
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    MBIMBridgeOperationCompletionBlock completion = ^(NSObject<HTTPResponse> *incomingResponse) {
        response = incomingResponse;
        dispatch_semaphore_signal(sema);
    };
    
    NSURL *url = [NSURL URLWithString:path];
    NSString *endpointName = [url lastPathComponent];
    
    BOOL requestTimedOut = NO;
    Class operationClass = [MBIMBridgeOperation operationClassForEndpointName:endpointName];
    if (operationClass != nil) {
        _currentOperation = [[operationClass alloc] initWithRequestURL:url completion:completion];
        _currentOperation.requestBodyData = _bodyData;
        _currentOperation.request = self->request;
        
        [[[MBIMBridge sharedInstance] operationQueue] addOperation:_currentOperation];
        long status = dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(60.0 * NSEC_PER_SEC)));
        if (status != 0) {
            requestTimedOut = YES;
        }
    } else {
        response = [[HTTPErrorResponse alloc] initWithErrorCode:404];
    }
    
    if (requestTimedOut) {
        response = [_currentOperation cancelAndReturnTimeoutResponse];
    }
    
    return response;
}

- (WebSocket *)webSocketForURI:(NSString *)path
{
    NSURL *url = [NSURL URLWithString:path];
    NSString *endpointName = [url lastPathComponent];
    NSString *authTokenString = [url valueForQueryItemWithName:@"token"];
    MBIMAuthToken *queryAuthToken = [[MBIMAuthToken alloc] initWithTokenString:authTokenString];
    
    NSLog(@"Websocket for URI: %@ | authenticated request: %@", path, [self isAuthenticated] ? @"YES" : @"NO");
    if ([endpointName isEqualToString:@"updates"]) {
        if (![self isAuthenticated] && ![queryAuthToken isValid]) {
            NSLog(@"Websocket: auth invalid, rejecting.");
            NSLog(@"Query Token: %@, raw: %@", queryAuthToken, authTokenString);


            // Respond with 401 unauthorized
            HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:401 description:nil version:HTTPVersion1_1];
            [response setHeaderField:@"Content-Length" value:@"0"];
            
            NSData *responseData = [self preprocessErrorResponse:response];
            [asyncSocket writeData:responseData withTimeout:30 tag:90];
            return nil;
        }
        
        NSLog(@"Vending websocket for consumer");
        return [[MBIMUpdateQueue sharedInstance] vendUpdateWebSocketConsumerForRequest:request socket:asyncSocket];
    }
    
    return [super webSocketForURI:path];
}

- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path
{
    if ([method isEqualToString:@"POST"]) {
        return YES;
    }
    
    return NO;
}

- (void)prepareForBodyWithSize:(UInt64)contentLength
{
    _bodyData = [[NSMutableData alloc] initWithCapacity:contentLength];
}

- (void)processBodyData:(NSData *)postDataChunk
{
    [_bodyData appendData:postDataChunk];
}

- (void)die
{
    [_currentOperation cancel];
    [super die];
}

@end
