/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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.
 */
package org.apache.nifi.processors.aws.cloudwatch;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.DynamicProperty;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
import org.apache.nifi.annotation.behavior.SupportsBatching;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.components.Validator;
import org.apache.nifi.expression.AttributeExpression;
import org.apache.nifi.expression.ExpressionLanguageScope;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.aws.AbstractAWSCredentialsProviderProcessor;

import com.amazonaws.AmazonClientException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient;
import com.amazonaws.services.cloudwatch.model.Dimension;
import com.amazonaws.services.cloudwatch.model.MetricDatum;
import com.amazonaws.services.cloudwatch.model.PutMetricDataRequest;
import com.amazonaws.services.cloudwatch.model.PutMetricDataResult;
import com.amazonaws.services.cloudwatch.model.StatisticSet;
import com.amazonaws.services.cloudwatch.model.StandardUnit;

@SupportsBatching
@InputRequirement(Requirement.INPUT_REQUIRED)
@CapabilityDescription("Publishes metrics to Amazon CloudWatch. Metric can be either a single value, or a StatisticSet comprised of "+
        "minimum, maximum, sum and sample count.")
@DynamicProperty(name = "Dimension Name", value = "Dimension Value",
        description = "Allows dimension name/value pairs to be added to the metric. AWS supports a maximum of 10 dimensions.",
        expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
@Tags({"amazon", "aws", "cloudwatch", "metrics", "put", "publish"})
public class PutCloudWatchMetric extends AbstractAWSCredentialsProviderProcessor<AmazonCloudWatchClient> {

    public static final Set<Relationship> relationships = Collections.unmodifiableSet(
            new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE)));

    public static final Set<String> units = Arrays.stream(StandardUnit.values())
            .map(StandardUnit::toString).collect(Collectors.toSet());

    private static final Validator UNIT_VALIDATOR = new Validator() {
        @Override
        public ValidationResult validate(String subject, String input, ValidationContext context) {
            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
                return (new ValidationResult.Builder()).subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
            } else {
                String reason = null;

                if (!units.contains(input)) {
                    reason = "not a valid Unit";
                }
                return (new ValidationResult.Builder()).subject(subject).input(input).explanation(reason).valid(reason == null).build();
            }
        }
    };

    private static final Validator DOUBLE_VALIDATOR = new Validator() {
        @Override
        public ValidationResult validate(String subject, String input, ValidationContext context) {
            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
                return (new ValidationResult.Builder()).subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
            } else {
                String reason = null;

                try {
                    Double.parseDouble(input);
                } catch (NumberFormatException e) {
                    reason = "not a valid Double";
                }

                return (new ValidationResult.Builder()).subject(subject).input(input).explanation(reason).valid(reason == null).build();
            }
        }
    };

    public static final PropertyDescriptor NAMESPACE = new PropertyDescriptor.Builder()
            .name("Namespace")
            .displayName("Namespace")
            .description("The namespace for the metric data for CloudWatch")
            .required(true)
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
            .build();

    public static final PropertyDescriptor METRIC_NAME = new PropertyDescriptor.Builder()
            .name("MetricName")
            .displayName("Metric Name")
            .description("The name of the metric")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(true)
            .addValidator(new StandardValidators.StringLengthValidator(1, 255))
            .build();

    public static final PropertyDescriptor VALUE = new PropertyDescriptor.Builder()
            .name("Value")
            .displayName("Value")
            .description("The value for the metric. Must be a double")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(false)
            .addValidator(DOUBLE_VALIDATOR)
            .build();

    public static final PropertyDescriptor TIMESTAMP = new PropertyDescriptor.Builder()
            .name("Timestamp")
            .displayName("Timestamp")
            .description("A point in time expressed as the number of milliseconds since Jan 1, 1970 00:00:00 UTC. If not specified, the default value is set to the time the metric data was received")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(false)
            .addValidator(StandardValidators.LONG_VALIDATOR)
            .build();

    public static final PropertyDescriptor UNIT = new PropertyDescriptor.Builder()
            .name("Unit")
            .displayName("Unit")
            .description("The unit of the metric. (e.g Seconds, Bytes, Megabytes, Percent, Count,  Kilobytes/Second, Terabits/Second, Count/Second) For details see http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(false)
            .addValidator(UNIT_VALIDATOR)
            .build();

    public static final PropertyDescriptor MAXIMUM = new PropertyDescriptor.Builder()
            .name("maximum")
            .displayName("Maximum")
            .description("The maximum value of the sample set. Must be a double")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(false)
            .addValidator(DOUBLE_VALIDATOR)
            .build();

    public static final PropertyDescriptor MINIMUM = new PropertyDescriptor.Builder()
            .name("minimum")
            .displayName("Minimum")
            .description("The minimum value of the sample set. Must be a double")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(false)
            .addValidator(DOUBLE_VALIDATOR)
            .build();

    public static final PropertyDescriptor SAMPLECOUNT = new PropertyDescriptor.Builder()
            .name("sampleCount")
            .displayName("Sample Count")
            .description("The number of samples used for the statistic set. Must be a double")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(false)
            .addValidator(DOUBLE_VALIDATOR)
            .build();

    public static final PropertyDescriptor SUM = new PropertyDescriptor.Builder()
            .name("sum")
            .displayName("Sum")
            .description("The sum of values for the sample set. Must be a double")
            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
            .required(false)
            .addValidator(DOUBLE_VALIDATOR)
            .build();

    public static final List<PropertyDescriptor> properties =
            Collections.unmodifiableList(
                    Arrays.asList(NAMESPACE, METRIC_NAME, VALUE, MAXIMUM, MINIMUM, SAMPLECOUNT, SUM, TIMESTAMP,
                            UNIT, REGION, ACCESS_KEY, SECRET_KEY, CREDENTIALS_FILE, AWS_CREDENTIALS_PROVIDER_SERVICE,
                            TIMEOUT, SSL_CONTEXT_SERVICE, ENDPOINT_OVERRIDE, PROXY_HOST, PROXY_HOST_PORT, PROXY_USERNAME, PROXY_PASSWORD)
            );

    private volatile Set<String> dynamicPropertyNames = new HashSet<>();

    @Override
    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
        return properties;
    }

    @Override
    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
        return new PropertyDescriptor.Builder()
                .name(propertyDescriptorName)
                .addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
                .dynamic(true)
                .build();
    }

    @Override
    public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) {
        if (descriptor.isDynamic()) {
            final Set<String> newDynamicPropertyNames = new HashSet<>(dynamicPropertyNames);
            if (newValue == null) {           // removing a property
                newDynamicPropertyNames.remove(descriptor.getName());
            } else if (oldValue == null) {    // adding a new property
                newDynamicPropertyNames.add(descriptor.getName());
            }
            this.dynamicPropertyNames = Collections.unmodifiableSet(newDynamicPropertyNames);
        }
    }

    @Override
    public Set<Relationship> getRelationships() {
        return relationships;
    }

    @Override
    protected Collection<ValidationResult> customValidate(final ValidationContext validationContext) {
        Collection<ValidationResult> problems = super.customValidate(validationContext);

        final boolean valueSet = validationContext.getProperty(VALUE).isSet();
        final boolean maxSet = validationContext.getProperty(MAXIMUM).isSet();
        final boolean minSet = validationContext.getProperty(MINIMUM).isSet();
        final boolean sampleCountSet = validationContext.getProperty(SAMPLECOUNT).isSet();
        final boolean sumSet = validationContext.getProperty(SUM).isSet();

        final boolean completeStatisticSet = (maxSet && minSet && sampleCountSet && sumSet);
        final boolean anyStatisticSetValue = (maxSet || minSet || sampleCountSet || sumSet);

        if (valueSet && anyStatisticSetValue) {
            problems.add(new ValidationResult.Builder().subject("Metric").valid(false)
                    .explanation("Cannot set both Value and StatisticSet(Maximum, Minimum, SampleCount, Sum) properties").build());
        } else if (!valueSet && !completeStatisticSet) {
            problems.add(new ValidationResult.Builder().subject("Metric").valid(false)
                    .explanation("Must set either Value or complete StatisticSet(Maximum, Minimum, SampleCount, Sum) properties").build());
        }

        if (dynamicPropertyNames.size() > 10) {
            problems.add(new ValidationResult.Builder().subject("Metric").valid(false)
                    .explanation("Cannot set more than 10 dimensions").build());
        }

        return problems;
    }

    /**
     * Create client using aws credentials provider. This is the preferred way for creating clients
     */
    @Override
    protected AmazonCloudWatchClient createClient(ProcessContext processContext, AWSCredentialsProvider awsCredentialsProvider, ClientConfiguration clientConfiguration) {
        getLogger().info("Creating client using aws credentials provider");
        return new AmazonCloudWatchClient(awsCredentialsProvider, clientConfiguration);
    }

    /**
     * Create client using AWSCredentials
     *
     * @deprecated use {@link #createClient(ProcessContext, AWSCredentialsProvider, ClientConfiguration)} instead
     */
    @Override
    @Deprecated
    protected AmazonCloudWatchClient createClient(ProcessContext processContext, AWSCredentials awsCredentials, ClientConfiguration clientConfiguration) {
        getLogger().debug("Creating client with aws credentials");
        return new AmazonCloudWatchClient(awsCredentials, clientConfiguration);
    }

    @Override
    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
        FlowFile flowFile = session.get();
        if (flowFile == null) {
            return;
        }
        MetricDatum datum = new MetricDatum();

        try {
            datum.setMetricName(context.getProperty(METRIC_NAME).evaluateAttributeExpressions(flowFile).getValue());
            final String valueString = context.getProperty(VALUE).evaluateAttributeExpressions(flowFile).getValue();
            if (valueString != null) {
                datum.setValue(Double.parseDouble(valueString));
            } else {
                StatisticSet statisticSet = new StatisticSet();
                statisticSet.setMaximum(Double.parseDouble(context.getProperty(MAXIMUM).evaluateAttributeExpressions(flowFile).getValue()));
                statisticSet.setMinimum(Double.parseDouble(context.getProperty(MINIMUM).evaluateAttributeExpressions(flowFile).getValue()));
                statisticSet.setSampleCount(Double.parseDouble(context.getProperty(SAMPLECOUNT).evaluateAttributeExpressions(flowFile).getValue()));
                statisticSet.setSum(Double.parseDouble(context.getProperty(SUM).evaluateAttributeExpressions(flowFile).getValue()));

                datum.setStatisticValues(statisticSet);
            }

            final String timestamp = context.getProperty(TIMESTAMP).evaluateAttributeExpressions(flowFile).getValue();
            if (timestamp != null) {
                datum.setTimestamp(new Date(Long.parseLong(timestamp)));
            }

            final String unit = context.getProperty(UNIT).evaluateAttributeExpressions(flowFile).getValue();
            if (unit != null) {
                datum.setUnit(unit);
            }

            // add dynamic properties as dimensions
            if (!dynamicPropertyNames.isEmpty()) {
                final List<Dimension> dimensions = new ArrayList<>(dynamicPropertyNames.size());
                for (String propertyName : dynamicPropertyNames) {
                    final String propertyValue = context.getProperty(propertyName).evaluateAttributeExpressions(flowFile).getValue();
                    if (StringUtils.isNotBlank(propertyValue)) {
                        dimensions.add(new Dimension().withName(propertyName).withValue(propertyValue));
                    }
                }
                datum.withDimensions(dimensions);
            }

            final PutMetricDataRequest metricDataRequest = new PutMetricDataRequest()
                    .withNamespace(context.getProperty(NAMESPACE).evaluateAttributeExpressions(flowFile).getValue())
                    .withMetricData(datum);

            putMetricData(context, metricDataRequest);
            session.transfer(flowFile, REL_SUCCESS);
            getLogger().info("Successfully published cloudwatch metric for {}", new Object[]{flowFile});
        } catch (final Exception e) {
            getLogger().error("Failed to publish cloudwatch metric for {} due to {}", new Object[]{flowFile, e});
            flowFile = session.penalize(flowFile);
            session.transfer(flowFile, REL_FAILURE);
        }

    }

    protected PutMetricDataResult putMetricData(ProcessContext context, PutMetricDataRequest metricDataRequest) throws AmazonClientException {
        final AmazonCloudWatchClient client = getClient(context);
        final PutMetricDataResult result = client.putMetricData(metricDataRequest);
        return result;
    }

}
