/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.atlas.repository.audit;

import org.apache.atlas.ApplicationProperties;
import org.apache.atlas.AtlasConfiguration;
import org.apache.atlas.AtlasException;
import org.apache.atlas.discovery.AtlasDiscoveryService;
import org.apache.atlas.model.audit.AuditReductionCriteria;
import org.apache.atlas.model.audit.EntityAuditEventV2;
import org.apache.atlas.model.tasks.AtlasTask;
import org.apache.atlas.repository.Constants.AtlasAuditAgingType;
import org.apache.atlas.repository.graphdb.AtlasGraph;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.IntervalTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.stereotype.Component;

import javax.inject.Inject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.atlas.repository.Constants.AUDIT_AGING_ACTION_TYPES_KEY;
import static org.apache.atlas.repository.Constants.AUDIT_AGING_COUNT_KEY;
import static org.apache.atlas.repository.Constants.AUDIT_AGING_ENTITY_TYPES_KEY;
import static org.apache.atlas.repository.Constants.AUDIT_AGING_SUBTYPES_INCLUDED_KEY;
import static org.apache.atlas.repository.Constants.AUDIT_AGING_TTL_KEY;
import static org.apache.atlas.repository.Constants.AUDIT_AGING_TYPE_KEY;
import static org.apache.atlas.repository.Constants.CREATE_EVENTS_AGEOUT_ALLOWED_KEY;
import static org.apache.atlas.repository.store.graph.v2.tasks.AuditReductionTaskFactory.ATLAS_AUDIT_REDUCTION_ENTITY_RETRIEVAL;

@Component
public class AtlasAuditReductionService implements SchedulingConfigurer {
    private static final Logger LOG = LoggerFactory.getLogger(AtlasAuditReductionService.class);

    private static final String VALUE_DELIMITER                           = ",";
    private static final String ATLAS_AUDIT_SWEEP_OUT_ENTITY_TYPES        = "atlas.audit.sweep.out.entity.types";
    private static final String ATLAS_AUDIT_SWEEP_OUT_ACTION_TYPES        = "atlas.audit.sweep.out.action.types";
    private static final String ATLAS_AUDIT_CUSTOM_AGEOUT_ENTITY_TYPES    = "atlas.audit.custom.ageout.entity.types";
    private static final String ATLAS_AUDIT_CUSTOM_AGEOUT_ACTION_TYPES    = "atlas.audit.custom.ageout.action.types";
    private static final String ATLAS_AUDIT_AGING_SCHEDULER_INITIAL_DELAY = "atlas.audit.aging.scheduler.initial.delay.in.min";
    private static final int    MIN_TTL_TO_MAINTAIN                       = AtlasConfiguration.MIN_TTL_TO_MAINTAIN.getInt();
    private static final int    MIN_AUDIT_COUNT_TO_MAINTAIN               = AtlasConfiguration.MIN_AUDIT_COUNT_TO_MAINTAIN.getInt();

    private final AtlasGraph            graph;
    private final AtlasDiscoveryService discoveryService;
    private final AtlasTypeRegistry     typeRegistry;
    private final Configuration         atlasConfiguration;

    private AuditReductionCriteria    ageoutCriteriaByConfig;
    private List<Map<String, Object>> ageoutTypeCriteriaMap;

    @Inject
    public AtlasAuditReductionService(Configuration config, AtlasGraph graph, AtlasDiscoveryService discoveryService, AtlasTypeRegistry typeRegistry) {
        this.atlasConfiguration = config;
        this.graph              = graph;
        this.discoveryService   = discoveryService;
        this.typeRegistry       = typeRegistry;
    }

    public List<AtlasTask> startAuditAgingByConfig() {
        List<AtlasTask> tasks = null;

        try {
            if (ageoutCriteriaByConfig == null) {
                ageoutCriteriaByConfig = convertConfigToAuditReductionCriteria();

                LOG.info("Audit aging is enabled by configuration");
            }

            LOG.info("Audit aging is triggered with configuration: {}", ageoutCriteriaByConfig.toString());

            if (ageoutTypeCriteriaMap == null) {
                ageoutTypeCriteriaMap = buildAgeoutCriteriaForAllAgingTypes(ageoutCriteriaByConfig);
            }

            tasks = startAuditAgingByCriteria(ageoutTypeCriteriaMap);
        } catch (Exception e) {
            LOG.error("Error while aging out audits by configuration: ", e);
        }

        return tasks;
    }

    public List<AtlasTask> startAuditAgingByCriteria(List<Map<String, Object>> ageoutTypeCriteriaMap) {
        if (CollectionUtils.isEmpty(ageoutTypeCriteriaMap)) {
            return null;
        }

        List<AtlasTask> tasks = new ArrayList<>();

        try {
            for (Map<String, Object> eachCriteria : ageoutTypeCriteriaMap) {
                AtlasTask auditAgingTask = discoveryService.createAndQueueAuditReductionTask(eachCriteria, ATLAS_AUDIT_REDUCTION_ENTITY_RETRIEVAL);

                if (auditAgingTask != null) {
                    tasks.add(auditAgingTask);
                }
            }
        } catch (Exception e) {
            LOG.error("Error while aging out audits by criteria: ", e);
        }

        return tasks;
    }

    public List<Map<String, Object>> buildAgeoutCriteriaForAllAgingTypes(AuditReductionCriteria auditReductionCriteria) {
        if (auditReductionCriteria == null || !auditReductionCriteria.isAuditAgingEnabled()) {
            return null;
        }

        List<Map<String, Object>> auditAgeoutCriteriaByType = new ArrayList<>();
        Set<String>               defaultAgeoutActionTypes = Arrays.stream(EntityAuditEventV2.EntityAuditActionV2.values()).map(Enum::toString).collect(Collectors.toSet());

        boolean createEventsAgeoutAllowed = auditReductionCriteria.isCreateEventsAgeoutAllowed();
        boolean subTypesIncluded          = auditReductionCriteria.isSubTypesIncluded();
        boolean ignoreDefaultAgeoutTTL    = auditReductionCriteria.ignoreDefaultAgeoutTTL();

        boolean     defaultAgeoutEnabled    = auditReductionCriteria.isDefaultAgeoutEnabled();
        int         defaultAgeoutTTL        = ignoreDefaultAgeoutTTL ? 0 : getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_TTL, auditReductionCriteria.getDefaultAgeoutTTLInDays(), MIN_TTL_TO_MAINTAIN);
        int         defaultAgeoutAuditCount = auditReductionCriteria.getDefaultAgeoutAuditCount() <= 0 ? auditReductionCriteria.getDefaultAgeoutAuditCount() : getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_COUNT, auditReductionCriteria.getDefaultAgeoutAuditCount(), MIN_AUDIT_COUNT_TO_MAINTAIN);
        int         customAgeoutTTL         = auditReductionCriteria.getCustomAgeoutTTLInDays() <= 0 ? auditReductionCriteria.getCustomAgeoutTTLInDays() : getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_CUSTOM_AGEOUT_TTL, auditReductionCriteria.getCustomAgeoutTTLInDays(), MIN_TTL_TO_MAINTAIN);
        int         customAgeoutAuditCount  = auditReductionCriteria.getCustomAgeoutAuditCount() <= 0 ? auditReductionCriteria.getCustomAgeoutAuditCount() : getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_CUSTOM_AGEOUT_COUNT, auditReductionCriteria.getCustomAgeoutAuditCount(), MIN_AUDIT_COUNT_TO_MAINTAIN);
        Set<String> customAgeoutEntityTypes = getUniqueListOf(auditReductionCriteria.getCustomAgeoutEntityTypes());
        Set<String> customAgeoutActionTypes = getValidActionTypes(AtlasAuditAgingType.CUSTOM, getUniqueListOf(auditReductionCriteria.getCustomAgeoutActionTypes()));

        Set<String> defaultAgeoutEntityTypesToExclude = new HashSet<>(customAgeoutEntityTypes);

        if (CollectionUtils.isEmpty(customAgeoutEntityTypes)) {
            defaultAgeoutActionTypes.removeAll(customAgeoutActionTypes);
        }

        boolean isSweepOutEnabled = auditReductionCriteria.isAuditSweepoutEnabled();

        if (isSweepOutEnabled) {
            Set<String> sweepOutEntityTypes = getUniqueListOf(auditReductionCriteria.getSweepoutEntityTypes());
            Set<String> sweepOutActionTypes = getValidActionTypes(AtlasAuditAgingType.SWEEP, getUniqueListOf(auditReductionCriteria.getSweepoutActionTypes()));

            if (CollectionUtils.isNotEmpty(sweepOutEntityTypes) || CollectionUtils.isNotEmpty(sweepOutActionTypes)) {
                Map<String, Object> sweepAgeoutCriteria = getAgeoutCriteriaMap(AtlasAuditAgingType.SWEEP, 0, 0, sweepOutEntityTypes, sweepOutActionTypes, createEventsAgeoutAllowed, subTypesIncluded);

                auditAgeoutCriteriaByType.add(sweepAgeoutCriteria);
            } else {
                LOG.error("Sweepout of audits is skipped.At least one of two properties: entity types/action types should be configured.");
            }

            defaultAgeoutEntityTypesToExclude.addAll(sweepOutEntityTypes);
            customAgeoutEntityTypes.removeAll(sweepOutEntityTypes);

            if (CollectionUtils.isEmpty(sweepOutEntityTypes)) {
                defaultAgeoutActionTypes.removeAll(sweepOutActionTypes);
                customAgeoutActionTypes.removeAll(sweepOutActionTypes);
            }
        }

        if ((customAgeoutTTL > 0 || customAgeoutAuditCount > 0) && (CollectionUtils.isNotEmpty(customAgeoutEntityTypes) || CollectionUtils.isNotEmpty(customAgeoutActionTypes))) {
            Map<String, Object> customAgeoutCriteria = getAgeoutCriteriaMap(AtlasAuditAgingType.CUSTOM, customAgeoutTTL, customAgeoutAuditCount, customAgeoutEntityTypes, customAgeoutActionTypes, createEventsAgeoutAllowed, subTypesIncluded);

            auditAgeoutCriteriaByType.add(customAgeoutCriteria);
        } else if (customAgeoutTTL <= 0 && customAgeoutAuditCount <= 0 && CollectionUtils.isEmpty(customAgeoutEntityTypes) && CollectionUtils.isEmpty(customAgeoutActionTypes)) {
            //Do Nothing
        } else if (customAgeoutTTL <= 0 && customAgeoutAuditCount <= 0) {
            LOG.error("Custom Audit aging is skipped.At least one of two properties: TTL/Audit Count should be configured.");
        } else {
            LOG.error("Custom Audit aging is skipped.At least one of two properties: entity types/action types should be configured.");
        }

        if (defaultAgeoutEnabled) {
            if (ignoreDefaultAgeoutTTL) {
                LOG.info("'{}' config property or 'ignoreDefaultAgeoutTTL' property in API configured as: {}, Default audit aging will be done by audit count only", AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_IGNORE_TTL.getPropertyName(), ignoreDefaultAgeoutTTL);
            }

            /**In case of default ageout with all available audit actions, query to ATLAS_ENTITY_AUDIT_EVENTS table
             * without any action type provides data for all audit actions and is more performant than
             * multiple queries to ATLAS_ENTITY_AUDIT_EVENTS with each action type
             */
            if (defaultAgeoutActionTypes.size() == EntityAuditEventV2.EntityAuditActionV2.values().length) {
                defaultAgeoutActionTypes.clear();
            }

            if (!ignoreDefaultAgeoutTTL || defaultAgeoutAuditCount > 0) {
                Map<String, Object> defaultAgeoutCriteria = getAgeoutCriteriaMap(AtlasAuditAgingType.DEFAULT, defaultAgeoutTTL, defaultAgeoutAuditCount, defaultAgeoutEntityTypesToExclude, defaultAgeoutActionTypes, createEventsAgeoutAllowed, subTypesIncluded);

                auditAgeoutCriteriaByType.add(defaultAgeoutCriteria);
            } else {
                LOG.error("Default Audit aging is skipped. Valid audit count should be configured when TTL criteria is ignored.");
            }
        }

        return auditAgeoutCriteriaByType;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        if (!AtlasConfiguration.ATLAS_AUDIT_AGING_ENABLED.getBoolean()) {
            LOG.warn("Audit aging is not enabled");

            return;
        }

        IntervalTask task = new IntervalTask(this::startAuditAgingByConfig, getAuditAgingFrequencyInMillis(), getAuditAgingInitialDelayInMillis());

        taskRegistrar.addFixedRateTask(task);
    }

    private AuditReductionCriteria convertConfigToAuditReductionCriteria() {
        boolean auditAgingEnabled         = AtlasConfiguration.ATLAS_AUDIT_AGING_ENABLED.getBoolean();
        boolean createAuditsAgeoutAllowed = AtlasConfiguration.ATLAS_AUDIT_CREATE_EVENTS_AGEOUT_ALLOWED.getBoolean();
        boolean subTypesIncluded          = AtlasConfiguration.ATLAS_AUDIT_AGING_SUBTYPES_INCLUDED.getBoolean();
        boolean ignoreDefaultAgeoutTTL    = AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_IGNORE_TTL.getBoolean();

        int defaultAgeoutTTLConfigured        = AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_TTL.getInt();
        int defaultAgeoutAuditCountConfigured = AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_COUNT.getInt();

        int customAgeoutTTLConfigured        = AtlasConfiguration.ATLAS_AUDIT_CUSTOM_AGEOUT_TTL.getInt();
        int customAgeoutAuditCountConfigured = AtlasConfiguration.ATLAS_AUDIT_CUSTOM_AGEOUT_COUNT.getInt();

        boolean defaultAgeoutEnabled    = AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_ENABLED.getBoolean();
        int     defaultAgeoutTTL        = getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_TTL, defaultAgeoutTTLConfigured, MIN_TTL_TO_MAINTAIN);
        int     defaultAgeoutAuditCount = defaultAgeoutAuditCountConfigured <= 0 ? defaultAgeoutAuditCountConfigured : getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_COUNT, defaultAgeoutAuditCountConfigured, MIN_AUDIT_COUNT_TO_MAINTAIN);
        int     customAgeoutTTL         = customAgeoutTTLConfigured <= 0 ? customAgeoutTTLConfigured : getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_CUSTOM_AGEOUT_TTL, customAgeoutTTLConfigured, MIN_TTL_TO_MAINTAIN);
        int     customAgeoutAuditCount  = customAgeoutAuditCountConfigured <= 0 ? customAgeoutAuditCountConfigured : getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_CUSTOM_AGEOUT_COUNT, customAgeoutAuditCountConfigured, MIN_AUDIT_COUNT_TO_MAINTAIN);

        String customAgeoutEntityTypes = getStringOf(ATLAS_AUDIT_CUSTOM_AGEOUT_ENTITY_TYPES);
        String customAgeoutActionTypes = getStringOf(ATLAS_AUDIT_CUSTOM_AGEOUT_ACTION_TYPES);

        AuditReductionCriteria auditReductionCriteria = new AuditReductionCriteria();

        auditReductionCriteria.setAuditAgingEnabled(auditAgingEnabled);
        auditReductionCriteria.setCreateEventsAgeoutAllowed(createAuditsAgeoutAllowed);
        auditReductionCriteria.setSubTypesIncluded(subTypesIncluded);
        auditReductionCriteria.setIgnoreDefaultAgeoutTTL(ignoreDefaultAgeoutTTL);

        auditReductionCriteria.setDefaultAgeoutEnabled(defaultAgeoutEnabled);
        auditReductionCriteria.setDefaultAgeoutTTLInDays(defaultAgeoutTTL);
        auditReductionCriteria.setDefaultAgeoutAuditCount(defaultAgeoutAuditCount);

        auditReductionCriteria.setCustomAgeoutTTLInDays(customAgeoutTTL);
        auditReductionCriteria.setCustomAgeoutAuditCount(customAgeoutAuditCount);
        auditReductionCriteria.setCustomAgeoutEntityTypes(customAgeoutEntityTypes);
        auditReductionCriteria.setCustomAgeoutActionTypes(customAgeoutActionTypes);

        boolean isSweepOutEnabled = AtlasConfiguration.ATLAS_AUDIT_SWEEP_OUT.getBoolean();

        auditReductionCriteria.setAuditSweepoutEnabled(isSweepOutEnabled);

        if (isSweepOutEnabled) {
            String sweepoutEntityTypes = getStringOf(ATLAS_AUDIT_SWEEP_OUT_ENTITY_TYPES);
            String sweepoutActionTypes = getStringOf(ATLAS_AUDIT_SWEEP_OUT_ACTION_TYPES);

            auditReductionCriteria.setSweepoutEntityTypes(sweepoutEntityTypes);
            auditReductionCriteria.setSweepoutActionTypes(sweepoutActionTypes);
        }

        return auditReductionCriteria;
    }

    private Map<String, Object> getAgeoutCriteriaMap(AtlasAuditAgingType agingOption, int ttl, int minCount, Set<String> entityTypes, Set<String> actionTypes, boolean createEventsAgeoutAllowed, boolean subTypesIncluded) {
        Map<String, Object> auditAgingOptions = new HashMap<>();

        auditAgingOptions.put(AUDIT_AGING_TYPE_KEY, agingOption);
        auditAgingOptions.put(AUDIT_AGING_TTL_KEY, ttl);
        auditAgingOptions.put(AUDIT_AGING_COUNT_KEY, minCount);
        auditAgingOptions.put(AUDIT_AGING_ENTITY_TYPES_KEY, entityTypes);
        auditAgingOptions.put(AUDIT_AGING_ACTION_TYPES_KEY, actionTypes);
        auditAgingOptions.put(CREATE_EVENTS_AGEOUT_ALLOWED_KEY, createEventsAgeoutAllowed);
        auditAgingOptions.put(AUDIT_AGING_SUBTYPES_INCLUDED_KEY, subTypesIncluded);

        return auditAgingOptions;
    }

    private int getGuaranteedMinValueOf(AtlasConfiguration configuration, int configuredValue, int minValueToMaintain) {
        if (configuredValue < minValueToMaintain) {
            LOG.info("Minimum value for '{}' should be {}", configuration.getPropertyName(), minValueToMaintain);
        }

        return configuredValue < minValueToMaintain ? minValueToMaintain : configuredValue;
    }

    private String getStringOf(String configProperty) {
        String configuredValue = null;

        if (StringUtils.isNotEmpty(configProperty)) {
            configuredValue = String.join(VALUE_DELIMITER, (List) atlasConfiguration.getList(configProperty));
        }

        return configuredValue;
    }

    private Set<String> getUniqueListOf(String value) {
        Set<String> configuredValues = null;

        if (StringUtils.isNotEmpty(value)) {
            configuredValues = Stream.of(value.split(VALUE_DELIMITER)).map(String::trim).collect(Collectors.toSet());
        }

        return configuredValues == null ? new HashSet<>() : configuredValues;
    }

    private Set<String> getValidActionTypes(AtlasAuditAgingType auditAgingType, Set<String> actionTypes) {
        if (CollectionUtils.isEmpty(actionTypes)) {
            return Collections.emptySet();
        }

        Set<String> allActionTypes     = Arrays.stream(EntityAuditEventV2.EntityAuditActionV2.values()).map(Enum::toString).collect(Collectors.toSet());
        Set<String> entityAuditActions = new HashSet<>();
        Set<String> invalidActionTypes = new HashSet<>();

        for (String actionType : actionTypes) {
            Set<String>  matchedActionTypes;
            final String actionTypeToMatch = actionType.contains("*") ? actionType.replace("*", "") : actionType;

            if (actionTypeToMatch.startsWith("*")) {
                matchedActionTypes = allActionTypes.stream().filter(x -> x.contains(actionTypeToMatch)).collect(Collectors.toSet());
            } else {
                matchedActionTypes = allActionTypes.stream().filter(x -> x.startsWith(actionTypeToMatch)).collect(Collectors.toSet());
            }

            if (CollectionUtils.isEmpty(matchedActionTypes)) {
                invalidActionTypes.add(actionType);
            } else {
                entityAuditActions.addAll(matchedActionTypes);
            }
        }

        if (CollectionUtils.isNotEmpty(actionTypes) && CollectionUtils.isEmpty(entityAuditActions)) {
            throw new IllegalArgumentException("No enum constant " + EntityAuditEventV2.EntityAuditActionV2.class.getCanonicalName() + "." + String.join(VALUE_DELIMITER, invalidActionTypes));
        } else {
            LOG.info("Action type name(s) {} provided for aging type-{}", String.join(VALUE_DELIMITER, entityAuditActions), auditAgingType);
        }

        if (CollectionUtils.isNotEmpty(invalidActionTypes)) {
            LOG.warn("Invalid action type name(s) {} provided for aging type-{}", String.join(VALUE_DELIMITER, invalidActionTypes), auditAgingType);
        }

        return entityAuditActions;
    }

    private long getAuditAgingFrequencyInMillis() {
        int frequencyInDays = getGuaranteedMinValueOf(AtlasConfiguration.ATLAS_AUDIT_AGING_SCHEDULER_FREQUENCY, AtlasConfiguration.ATLAS_AUDIT_AGING_SCHEDULER_FREQUENCY.getInt(), 1);

        return frequencyInDays * DateUtils.MILLIS_PER_DAY;
    }

    private long getAuditAgingInitialDelayInMillis() {
        int initialDelayInMins = 1;

        try {
            initialDelayInMins = ApplicationProperties.get().getInt(ATLAS_AUDIT_AGING_SCHEDULER_INITIAL_DELAY, 1);
        } catch (AtlasException ex) {
            LOG.error("Error while fetching application properties", ex);
        }

        return (initialDelayInMins < 1 ? 1 : initialDelayInMins) * DateUtils.MILLIS_PER_MINUTE;
    }
}
