package com.nordstrom.kafka.connect.lambda;

import org.apache.kafka.common.Configurable;
import org.apache.kafka.common.config.AbstractConfig;
import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.connect.errors.ConnectException;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.nordstrom.kafka.connect.auth.AWSAssumeRoleCredentialsProvider;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.time.Duration;
import java.lang.reflect.InvocationTargetException;

public class InvocationClientConfig extends AbstractConfig {
    static final String CONFIG_GROUP_NAME = "Lambda";

    static final String AWS_REGION_KEY = "aws.region";
    static final String AWS_REGION_DOC = "AWS region of the Lambda function";
    static final String FUNCTION_ARN_KEY = "aws.lambda.function.arn";
    static final String FUNCTION_ARN_DOC = "Full ARN of the function to be called";
    static final String INVOCATION_MODE_KEY = "aws.lambda.invocation.mode";
    static final String INVOCATION_MODE_DOC = "Determines whether to invoke the lambda asynchronously (Event) or synchronously (RequestResponse)";
    static final String INVOCATION_TIMEOUT_KEY = "aws.lambda.invocation.timeout.ms";
    static final String INVOCATION_TIMEOUT_DOC = "Time to wait for a response after invoking a lambda. If the response times out, the connector will continue.";
    static final String FAILURE_MODE_KEY = "aws.lambda.invocation.failure.mode";
    static final String FAILURE_MODE_DOC = "Determines whether the connector should stop or drop and continue on failure (specifically, payload limit exceeded)";

    // Client configuration properties
    static final String HTTP_PROXY_HOST_KEY = "http.proxy.host";
    static final String HTTP_PROXY_HOST_DOC = "HTTP proxy host to use when invoking the Lambda API";
    static final String HTTP_PROXY_PORT_KEY = "http.proxy.port";
    static final String HTTP_PROXY_PORT_DOC = "HTTP proxy port to use when invoking the Lambda API";

    // Authentication properties
    static final String CREDENTIALS_PROVIDER_CONFIG_PREFIX = "aws.credentials.provider.";
    static final String CREDENTIALS_PROVIDER_CLASS_KEY = "aws.credentials.provider.class";
    static final String CREDENTIALS_PROVIDER_CLASS_DOC = "Implementation class which provides AWS authentication credentials";
    static final String IAM_ROLE_ARN_KEY = CREDENTIALS_PROVIDER_CONFIG_PREFIX + AWSAssumeRoleCredentialsProvider.ROLE_ARN_CONFIG;
    static final String IAM_ROLE_ARN_DOC = "Full ARN of an IAM role to assume";
    static final String IAM_SESSION_NAME_KEY = CREDENTIALS_PROVIDER_CONFIG_PREFIX + AWSAssumeRoleCredentialsProvider.SESSION_NAME_CONFIG;
    static final String IAM_SESSION_NAME_DOC = "IAM session name to use when assuming an IAM role";
    static final String IAM_EXTERNAL_ID_KEY = CREDENTIALS_PROVIDER_CONFIG_PREFIX + AWSAssumeRoleCredentialsProvider.EXTERNAL_ID_CONFIG;
    static final String IAM_EXTERNAL_ID_DOC = "External ID to use when assuming an IAM role";

    final InvocationClient.Builder clientBuilder;

    InvocationClientConfig(final Map<String, String> parsedConfig) {
        this(new InvocationClient.Builder(), parsedConfig);
    }

    InvocationClientConfig(final InvocationClient.Builder builder, final Map<String, String> parsedConfig) {
        super(configDef(), parsedConfig);

        builder
            .setFunctionArn(getString(FUNCTION_ARN_KEY))
            .setInvocationMode(InvocationMode.valueOf(getString(INVOCATION_MODE_KEY)))
            .setInvocationTimeout(Duration.ofMillis(getLong(INVOCATION_TIMEOUT_KEY)))
            .setFailureMode(InvocationFailure.valueOf(getString(FAILURE_MODE_KEY)))
            .withClientConfiguration(loadAwsClientConfiguration())
            .withCredentialsProvider(loadAwsCredentialsProvider());

        String awsRegion = getString(AWS_REGION_KEY);
        if (awsRegion != null)
            builder.setRegion(awsRegion);

        this.clientBuilder = builder;
    }

    public InvocationClient getInvocationClient() {
        return this.clientBuilder.build();
    }

    ClientConfiguration loadAwsClientConfiguration() {
        ClientConfiguration clientConfiguration = new ClientConfiguration();

        String httpProxyHost = this.getString(HTTP_PROXY_HOST_KEY);
        if (httpProxyHost != null && !httpProxyHost.isEmpty()) {
            clientConfiguration.setProxyHost(httpProxyHost);

            Integer httpProxyPort = this.getInt(HTTP_PROXY_PORT_KEY);
            if (httpProxyPort > 0)
                clientConfiguration.setProxyPort(httpProxyPort);
        }

        return clientConfiguration;
    }

    @SuppressWarnings("unchecked")
    AWSCredentialsProvider loadAwsCredentialsProvider() {
        try {
            AWSCredentialsProvider credentialsProvider = ((Class<? extends AWSCredentialsProvider>)
                getClass(CREDENTIALS_PROVIDER_CLASS_KEY)).getDeclaredConstructor().newInstance();

            if (credentialsProvider instanceof Configurable) {
                Map<String, Object> configs = originalsWithPrefix(
                    CREDENTIALS_PROVIDER_CONFIG_PREFIX);

                ((Configurable)credentialsProvider).configure(configs);
            }

            return credentialsProvider;

        } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
            throw new ConnectException("Unable to create " + CREDENTIALS_PROVIDER_CLASS_KEY, e);
        }
    }

    public static ConfigDef configDef() {
        return configDef(new ConfigDef());
    }

    public static ConfigDef configDef(ConfigDef base) {
        int orderInGroup = 0;

        return new ConfigDef(base)
            .define(AWS_REGION_KEY,
                ConfigDef.Type.STRING,
                null,
                ConfigDef.Importance.HIGH,
                AWS_REGION_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "AWS region")

            .define(FUNCTION_ARN_KEY,
                ConfigDef.Type.STRING,
                ConfigDef.Importance.HIGH,
                FUNCTION_ARN_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.LONG,
                "Lambda function ARN")

            .define(INVOCATION_MODE_KEY,
                ConfigDef.Type.STRING,
                InvocationClient.DEFAULT_INVOCATION_MODE.name(),
                new InvocationModeValidator(),
                ConfigDef.Importance.MEDIUM,
                INVOCATION_MODE_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "Invocation mode",
                new InvocationModeRecommender())

            .define(INVOCATION_TIMEOUT_KEY,
                ConfigDef.Type.LONG,
                (Long)InvocationClient.DEFAULT_INVOCATION_TIMEOUT_MS,
                ConfigDef.Importance.LOW,
                INVOCATION_TIMEOUT_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "Invocation timeout")

            .define(FAILURE_MODE_KEY,
                ConfigDef.Type.STRING,
                InvocationClient.DEFAULT_FAILURE_MODE.name(),
                new InvocationFailureValidator(),
                ConfigDef.Importance.LOW,
                FAILURE_MODE_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "Invocation failure mode",
                new InvocationFailureRecommender())

            .define(HTTP_PROXY_HOST_KEY,
                ConfigDef.Type.STRING,
                null,
                ConfigDef.Importance.LOW,
                HTTP_PROXY_HOST_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "HTTP proxy host")

            .define(HTTP_PROXY_PORT_KEY,
                ConfigDef.Type.STRING,
                null,
                ConfigDef.Importance.LOW,
                HTTP_PROXY_PORT_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "HTTP proxy port")

            .define(CREDENTIALS_PROVIDER_CLASS_KEY,
                ConfigDef.Type.CLASS,
                DefaultAWSCredentialsProviderChain.class,
                new AwsCredentialsProviderValidator(),
                ConfigDef.Importance.LOW,
                CREDENTIALS_PROVIDER_CLASS_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.LONG,
                "AWS credentials provider class")

            .define(IAM_ROLE_ARN_KEY,
                ConfigDef.Type.STRING,
                null,
                ConfigDef.Importance.LOW,
                IAM_ROLE_ARN_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.LONG,
                "IAM role ARN")

            .define(IAM_SESSION_NAME_KEY,
                ConfigDef.Type.STRING,
                null,
                ConfigDef.Importance.LOW,
                IAM_SESSION_NAME_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "IAM session name")

            .define(IAM_EXTERNAL_ID_KEY,
                ConfigDef.Type.STRING,
                null,
                ConfigDef.Importance.LOW,
                IAM_EXTERNAL_ID_DOC,
                CONFIG_GROUP_NAME,
                ++orderInGroup,
                ConfigDef.Width.SHORT,
                "IAM external ID");
    }

    static class InvocationModeRecommender implements ConfigDef.Recommender {
        @Override
        public List<Object> validValues(String name, Map<String, Object> connectorConfigs) {
            return Arrays.asList(InvocationMode.values());
        }

        @Override
        public boolean visible(String name, Map<String, Object> connectorConfigs) {
            return true;
        }
    }

    static class InvocationModeValidator implements ConfigDef.Validator {
        @Override
        public void ensureValid(String name, Object invocationMode) {
            try {
                InvocationMode.valueOf(((String)invocationMode).trim());
            } catch (Exception e) {
                throw new ConfigException(name, invocationMode, "Value must be one of [" +
                    Utils.join(InvocationMode.values(), ", ") + "]");
            }
        }

        @Override
        public String toString() {
            return "[" + Utils.join(InvocationMode.values(), ", ") + "]";
        }
    }

    static class InvocationFailureRecommender implements ConfigDef.Recommender {
        @Override
        public List<Object> validValues(String name, Map<String, Object> connectorConfigs) {
            return Arrays.asList(InvocationFailure.values());
        }

        @Override
        public boolean visible(String name, Map<String, Object> connectorConfigs) {
            return true;
        }
    }

    static class InvocationFailureValidator implements ConfigDef.Validator {
        @Override
        public void ensureValid(String name, Object invocationFailure) {
            try {
                InvocationFailure.valueOf(((String)invocationFailure).trim());
            } catch (Exception e) {
                throw new ConfigException(name, invocationFailure, "Value must be one of [" +
                    Utils.join(InvocationFailure.values(), ", ") + "]");
            }
        }

        @Override
        public String toString() {
            return "[" + Utils.join(InvocationFailure.values(), ", ") + "]";
        }
    }

    static class AwsCredentialsProviderValidator implements ConfigDef.Validator {
        @Override
        public void ensureValid(String name, Object provider) {
          if (provider instanceof Class && AWSCredentialsProvider.class.isAssignableFrom((Class<?>)provider)) {
              return;
          }

          throw new ConfigException(name, provider, "Class must extend: " + AWSCredentialsProvider.class);
        }

        @Override
        public String toString() {
            return "Any class implementing: " + AWSCredentialsProvider.class;
        }
    }
}