//
// Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//

#import "AWSLambdaInvoker.h"
#import "AWSSynchronizedMutableDictionary.h"
#import "AWSLambdaService.h"
#import "AWSService.h"
#import "AWSClientContext.h"

NSString *const AWSLambdaInvokerErrorDomain = @"com.amazonaws.AWSLambdaInvokerErrorDomain";
NSString *const AWSLambdaInvokerFunctionErrorKey = @"com.amazonaws.AWSLambdaInvokerFunctionErrorKey";
NSString *const AWSLambdaInvokerErrorTypeKey = @"errorType";
NSString *const AWSLambdaInvokerErrorMessageKey = @"errorMessage";
NSString *const AWSLambdaInvokerErrorStackTraceKey = @"stackTrace";
NSString *const AWSLambdaInvokerUserAgent = @"invoker";

static NSString *const AWSInfoLambdaInvoker = @"LambdaInvoker";

@interface AWSLambda()

- (instancetype)initWithConfiguration:(AWSServiceConfiguration *)configuration;

@end

@interface AWSLambdaInvoker()

@property (nonatomic, strong) AWSLambda *lambda;
@property (nonatomic, strong) AWSClientContext *clientContext;

@end

@implementation AWSLambdaInvoker

static AWSSynchronizedMutableDictionary *_serviceClients = nil;

+ (instancetype)defaultLambdaInvoker {
    static AWSLambdaInvoker *_defaultLambdaInvoker = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        AWSServiceConfiguration *serviceConfiguration = nil;
        AWSServiceInfo *serviceInfo = [[AWSInfo defaultAWSInfo] defaultServiceInfo:AWSInfoLambdaInvoker];
        if (serviceInfo) {
            serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:serviceInfo.region
                                                               credentialsProvider:serviceInfo.cognitoCredentialsProvider];
        }

        if (!serviceConfiguration) {
            serviceConfiguration = [AWSServiceManager defaultServiceManager].defaultServiceConfiguration;
        }

        if (!serviceConfiguration) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                           reason:@"The service configuration is `nil`. You need to configure `Info.plist` or set `defaultServiceConfiguration` before using this method."
                                         userInfo:nil];
        }

        _defaultLambdaInvoker = [[AWSLambdaInvoker alloc] initWithConfiguration:serviceConfiguration];
    });

    return _defaultLambdaInvoker;
}

+ (void)registerLambdaInvokerWithConfiguration:(AWSServiceConfiguration *)configuration forKey:(NSString *)key {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _serviceClients = [AWSSynchronizedMutableDictionary new];
    });

    AWSLambdaInvoker *LambdaInvoker = [[AWSLambdaInvoker alloc] initWithConfiguration:configuration];
    [_serviceClients setObject:LambdaInvoker
                        forKey:key];
}

+ (instancetype)LambdaInvokerForKey:(NSString *)key {
    @synchronized(self) {
        AWSLambdaInvoker *serviceClient = [_serviceClients objectForKey:key];
        if (serviceClient) {
            return serviceClient;
        }

        AWSServiceInfo *serviceInfo = [[AWSInfo defaultAWSInfo] serviceInfo:AWSInfoLambdaInvoker
                                                                     forKey:key];
        if (serviceInfo) {
            AWSServiceConfiguration *serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:serviceInfo.region
                                                                                        credentialsProvider:serviceInfo.cognitoCredentialsProvider];
            [AWSLambdaInvoker registerLambdaInvokerWithConfiguration:serviceConfiguration
                                                              forKey:key];
        }

        return [_serviceClients objectForKey:key];
    }
}

+ (void)removeLambdaInvokerForKey:(NSString *)key {
    [_serviceClients removeObjectForKey:key];
}

- (instancetype)init {
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:@"`- init` is not a valid initializer. Use `+ defaultLambdaInvoker` or `+ LambdaInvokerForKey:` instead."
                                 userInfo:nil];
    return nil;
}

- (instancetype)initWithConfiguration:(AWSServiceConfiguration *)configuration {
    if (self = [super init]) {
        AWSServiceConfiguration *_configuration = [configuration copy];
        [_configuration addUserAgentProductToken:AWSLambdaInvokerUserAgent];
        _lambda = [[AWSLambda alloc] initWithConfiguration:_configuration];
        _clientContext = [AWSClientContext new];
    }
    return self;
}

- (AWSTask<AWSLambdaInvokerInvocationResponse *> *)invoke:(AWSLambdaInvokerInvocationRequest *)request {
    AWSLambdaInvocationRequest *invocationRequest = [AWSLambdaInvocationRequest new];
    [invocationRequest aws_copyPropertiesFromObject:request];

    if (!invocationRequest.clientContext) {
        invocationRequest.clientContext = [self.clientContext base64EncodedJSONString];
    }
    if (request.payload) {
        NSError *error = nil;
        invocationRequest.payload = [NSJSONSerialization aws_dataWithJSONObject:request.payload
                                                                        options:kNilOptions
                                                                          error:&error];
        if (!invocationRequest.payload) {
            return [AWSTask taskWithError:error];
        }
    }

    return [[self.lambda invoke:invocationRequest] continueWithSuccessBlock:^id(AWSTask *task) {
        AWSLambdaInvocationResponse *invocationResponse = task.result;
        if (invocationResponse.functionError) {
            NSMutableDictionary *userInfo = nil;
            if ([invocationResponse.payload isKindOfClass:[NSDictionary class]]) {
                userInfo = [NSMutableDictionary dictionaryWithDictionary:invocationResponse.payload];
            } else {
                userInfo = [NSMutableDictionary new];
            }
            [userInfo setValue:invocationResponse.functionError
                        forKey:AWSLambdaInvokerFunctionErrorKey];

            NSError *error = [NSError errorWithDomain:AWSLambdaInvokerErrorDomain
                                                 code:AWSLambdaInvokerErrorTypeFunctionError
                                             userInfo:userInfo];
            return [AWSTask taskWithError:error];
        }

        AWSLambdaInvokerInvocationResponse *response = [AWSLambdaInvokerInvocationResponse new];
        [response aws_copyPropertiesFromObject:invocationResponse];
        return [AWSTask taskWithResult:response];
    }];
}

- (void)invoke:(AWSLambdaInvokerInvocationRequest *)request completionHandler:(void (^ _Nullable)(AWSLambdaInvokerInvocationResponse * _Nullable response, NSError * _Nullable error))completionHandler {
    [[self invoke:request] continueWithBlock:^id _Nullable(AWSTask<AWSLambdaInvokerInvocationResponse *> * _Nonnull task) {
        AWSLambdaInvokerInvocationResponse *result = task.result;
        NSError *error = task.error;

        if (completionHandler) {
            completionHandler(result, error);
        }

        return nil;
    }];
}

- (AWSTask *)invokeFunction:(NSString *)functionName
                JSONObject:(id)JSONObject {
    AWSLambdaInvokerInvocationRequest *invocationRequest = [AWSLambdaInvokerInvocationRequest new];
    invocationRequest.functionName = functionName;
    invocationRequest.invocationType = AWSLambdaInvocationTypeRequestResponse;
    invocationRequest.payload = JSONObject;

    return [[self invoke:invocationRequest] continueWithSuccessBlock:^id(AWSTask *task) {
        AWSLambdaInvokerInvocationResponse *invocationResponse = task.result;
        return [AWSTask taskWithResult:invocationResponse.payload];
    }];
}

- (void)invokeFunction:(NSString *)functionName
            JSONObject:(id)JSONObject
     completionHandler:(void (^ _Nullable)(id _Nullable response, NSError * _Nullable error))completionHandler {
    [[self invokeFunction:functionName JSONObject:JSONObject] continueWithBlock:^id _Nullable(AWSTask * _Nonnull task) {
        id result = task.result;
        NSError *error = task.error;

        if (completionHandler) {
            completionHandler(result, error);
        }

        return nil;
    }];
}

@end

@implementation AWSLambdaInvokerInvocationRequest

@end

@implementation AWSLambdaInvokerInvocationResponse

@end