package com.eisgroup.csaa.metadata.impl;
import com.eisgroup.csaa.metadata.api.ApiMetadataExtensions;
import com.eisgroup.csaa.metadata.api.ApiMetadataProvider;
import com.eisgroup.csaa.metadata.api.ApiMetadataRequest;
import com.eisgroup.csaa.metadata.api.ApiRuleMetadataProvider;
import com.eisgroup.csaa.metadata.api.ApiRuleMetadataRequest;
import com.eisgroup.csaa.metadata.dto.OperationRule;
import com.fasterxml.jackson.core.type.TypeReference;
import core.dataproviders.dto.InternalDTO;
import core.dataproviders.impl.ContextPreservingCompletionStageFactory;
import core.exceptions.InternalGatewayException;
import core.exceptions.ValidationException;
import core.exceptions.dto.ValidationExceptionDTO;
import core.utils.JsonUtils;
import dataproviders.productfactory.dto.MapDTO;
import dataproviders.productfactory.dto.ProductFactoryOperationDTO;
import io.swagger.converter.ModelConverters;
import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.Swagger;
import io.swagger.models.properties.Property;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
public class ApiRuleMetadataProviderImpl implements ApiRuleMetadataProvider {
    @Inject
    private ApiMetadataProvider apiMetadataProvider;
    @Inject
    private JsonUtils jsonUtils;
    public void runRules(ApiRuleMetadataRequest apiRuleMetadataRequest) {
        List<OperationRule> changesForValidations = findChangesForRules(apiRuleMetadataRequest);
        if(CollectionUtils.isNotEmpty(changesForValidations)) {
            ContextPreservingCompletionStageFactory.completedFuture(generateMetadataRequest(apiRuleMetadataRequest, changesForValidations))
                    .thenCompose(apiMetadataRequest -> apiMetadataProvider.get(apiMetadataRequest))
                    .thenApply(metadata -> findAndApplyMetatdata(changesForValidations, metadata, apiRuleMetadataRequest))
                    .thenCompose(this::executeApplicability)
                    .thenCompose(this::executeDefault)
                    .thenCompose(this::executeRequired)
                    .thenCompose(this::executeValueRange)
                    .thenCompose(this::executeValidators)
                    .whenComplete(this::throwExceptionIfNeeded)
                    .toCompletableFuture().join();
        }
    }
    private void throwExceptionIfNeeded(Stream<OperationRule> operationRule, Throwable throwable) {
        Map<String, HashSet<String>> exceptions = new HashMap<>();
        operationRule.filter(rule -> rule.getValidationResult() != null)
                .forEach(result -> result.getValidationResult().forEach((errorMessage, isPassed) -> {
                    if (!isPassed) {
                        exceptions.computeIfAbsent(result.getAttributeName(), key -> new HashSet<>()).add(errorMessage);
                    }
                }));
        if (MapUtils.isNotEmpty(exceptions)) {
            throw new ValidationException("FIELD VALIDATIONS", null, exceptions.entrySet().stream().map(exception ->
                            new ValidationExceptionDTO(":)", StringUtils.joinWith(" ", exception.getValue()), exception.getKey()))
                    .collect(Collectors.toList()));
        }
    }
    private CompletableFuture<Stream<OperationRule>> executeValueRange(Stream<OperationRule> operationRules) {
        return CompletableFuture.completedFuture(operationRules.peek(rule -> {
            if (!rule.hasFailure() && ruleHasValueRange(rule)) {
                handleValueRangeRule(rule);
                rule.setFailure(true);
            }
        }));
    }
    private CompletableFuture<Stream<OperationRule>> executeRequired(Stream<OperationRule> operationRules) {
        return CompletableFuture.completedFuture(operationRules.peek(rule -> {
            if (!rule.hasFailure() && CollectionUtils.isNotEmpty(rule.getValidators()) && ruleHasRequiredValidator(rule)) {
                handleRequiredRule(rule);
            }
        }));
    }
    private CompletableFuture<Stream<OperationRule>> executeValidators(Stream<OperationRule> operationRules) {
        return CompletableFuture.completedFuture(operationRules.peek(rule -> {
            if(!rule.hasFailure() && CollectionUtils.isNotEmpty(rule.getValidators())) {
                handleValidationRules(rule);
            }
        }));
    }
    private CompletableFuture<Stream<OperationRule>> executeApplicability(Stream<OperationRule> operationRules) {
        return CompletableFuture.completedFuture(
                operationRules
                        .peek(rule -> {
                            if(!rule.isApplicable() && !ObjectUtils.allNotNull(rule.getAttributeValue())) {
                                rule.setFailure(true);
                                return;
                            }
                            if(!rule.isApplicable()) {
                                handleApplicabilityRule(rule);
                                rule.setFailure(true);
                            }
                        })
        );
    }
    private CompletableFuture<Stream<OperationRule>> executeDefault(Stream<OperationRule> operationRules) {
        return CompletableFuture.completedFuture(
                operationRules.peek(rule -> {
                    if(StringUtils.isNotBlank(rule.getDefaultValue()) && rule.getAttributeValue() == null
                            || (rule.getAttributeValue() instanceof String && StringUtils.isBlank((String) rule.getAttributeValue()))) {
                        //TODO Add type resolver for defaulting...
                        rule.setAttributeValue(rule.getDefaultValue());
                        if (rule.getOperation().requestData instanceof MapDTO) {
                            ((MapDTO) rule.getOperation().requestData).getItems().put(rule.getAttributeName(), rule.getDefaultValue());
                            return;
                        }
                        InternalDTO internalDTO = rule.getOperation().requestData;
                        try {
                            Field field = internalDTO.getClass().getField(rule.getAttributeName());
                            field.set(internalDTO, rule.getDefaultValue());
                        } catch (NoSuchFieldException | IllegalAccessException e) {
                            throw new InternalGatewayException(String.format("Cannot find attribute in class by this name '%s'", rule.getAttributeName()), e);
                        }
                    }
                }));
    }
    private Stream<OperationRule> findAndApplyMetatdata(List<OperationRule> changesForValidations, Swagger metadata, ApiRuleMetadataRequest apiRuleMetadataRequest) {
        var scopeValidators = new ArrayList<String>();
        if (StringUtils.isNotBlank(apiRuleMetadataRequest.getScope())) {
            scopeValidators.addAll(metadata.getDefinitions().entrySet().stream()
                    .flatMap(definitions -> definitions.getValue().getProperties().entrySet().stream())
                    .flatMap(properties -> properties.getValue().getVendorExtensions().entrySet().stream())
                    .filter(extension -> extension.getKey().equals(ApiMetadataExtensions.OpenLValidator.NAME))
                    .flatMap(extension ->
                            Stream.of((String[]) extension.getValue())
                                    .filter(validatorName -> validatorName.contains(apiRuleMetadataRequest.getScope())))
                    .distinct()
                    .collect(Collectors.toList()));
        }
        return changesForValidations.stream()
                .filter(rule -> filterChangeByVendorExtensions(rule, metadata))
                .map(rule -> setMetadata(rule, metadata, scopeValidators, apiRuleMetadataRequest.getScope()));
    }
    private boolean ruleHasRequiredValidator(OperationRule rule) {
        return rule.getValidators().stream()
                .anyMatch(validatorName -> validatorName.equals("required"));
    }
    private void handleRequiredRule(OperationRule rule) {
        boolean isEmpty = Validators.VALIDATORS.get("required")
                .apply(rule.getAttributeValue())
                .entrySet()
                .stream()
                .anyMatch(validationResult -> !validationResult.getValue());
        if (isEmpty) {
            rule.getValidationResult().computeIfAbsent("Field is required", key -> false);
            rule.setFailure(true);
        }
    }
    private boolean ruleHasValueRange(OperationRule rule) {
        return CollectionUtils.isNotEmpty(rule.getValueRange())
                && !rule.getValueRange().contains(rule.getAttributeValue());
    }
    private void handleApplicabilityRule(OperationRule rule) {
        rule.getValidationResult().computeIfAbsent(String.format("Field is not applicable",
                rule.getAttributeValue(), rule.getValueRange()), key -> false);
    }
    private void handleValueRangeRule(OperationRule rule) {
        rule.getValidationResult().computeIfAbsent(String.format("Field value is not in range. Value: %s - Range: %s",
                rule.getAttributeValue(), rule.getValueRange()), key -> false);
    }
    private void handleValidationRules(OperationRule rule) {
        rule.getValidationResult().putAll(executeValidationRules(rule));
    }
    private Map<String, Boolean> executeValidationRules(OperationRule rule) {
        return rule.getValidators().stream()
                .filter(Validators.VALIDATORS::containsKey)
                .flatMap(validatorName -> {
                    var validator = Validators.VALIDATORS.get(validatorName);
                    return validator.apply(rule.getAttributeValue()).entrySet().stream();
                })
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }
    private ApiMetadataRequest generateMetadataRequest(ApiRuleMetadataRequest apiRuleMetadataRequest, List<OperationRule> chg) {
        Map<String, Set<String>> map = new HashMap<>();
        String lowerCaseLob = StringUtils.lowerCase(apiRuleMetadataRequest.getLob());
        chg.forEach(changes -> {
            map.computeIfAbsent("attributes", key -> new HashSet<>()).add(changes.getAttributeName());
            map.computeIfAbsent("models", key -> new HashSet<>()).add(((ModelImpl) changes.getModel()).getName());
        });
        return ApiMetadataRequest.buildFor(apiRuleMetadataRequest.getApiDtoClass())
                .addRequestParam("openLServiceName", "aaa-" + lowerCaseLob + "-policy-rules")
                .addRequestParam("productCd", apiRuleMetadataRequest.getProductCd())
                .addRequestParam("riskStateCd", apiRuleMetadataRequest.getRiskState())
                .addRequestParam("effectiveDate", apiRuleMetadataRequest.getEffectiveDate())
                .filterByModels(map.get("models"))
                .filterByProperties(map.get("attributes"))
                .withSmartModels(apiRuleMetadataRequest.getLob());
    }
    private List<OperationRule> findChangesForRules(ApiRuleMetadataRequest apiRuleMetadataRequest) {
        var modelMap = ModelConverters.getInstance().readAll(apiRuleMetadataRequest.getApiDtoClass()).values();
        return apiRuleMetadataRequest.getRequest().getOperations().stream()
                .flatMap(operation -> {
                    if (operation.requestData instanceof MapDTO) {
                        return createOperationRuleFromMap(((MapDTO) operation.requestData).getItems(), operation);
                    }
                    //TODO need to add skip for non component operations
                    if(operation.requestData != null) {
                        return createOperationRuleFromIternalDto(operation);
                    }
                    return Stream.empty();
                })
                .filter(changes -> changes.getOperation().referenceName != null)
                .filter(changes -> isMatchingModel(changes, modelMap, apiRuleMetadataRequest))
                .collect(Collectors.toList());
    }
    private Stream<OperationRule> createOperationRuleFromMap(Map<String, Object> requestData, ProductFactoryOperationDTO operation) {
        return requestData.entrySet().stream()
                .map(entry -> new OperationRule(operation, entry.getKey(), entry.getValue()));
    }
    private Stream<OperationRule> createOperationRuleFromIternalDto(ProductFactoryOperationDTO operation) {
        var changes = jsonUtils.getDefaultObjectMapper().convertValue(operation.requestData, new TypeReference<Map<String, Object>>() {});
        return createOperationRuleFromMap(changes, operation);
    }
    private boolean isMatchingModel(OperationRule changes, Collection<Model> modelMap, ApiRuleMetadataRequest apiRuleMetadataRequest) {
        return modelMap.stream().anyMatch(model -> {
            Map<String, Property> properties = model.getProperties();
            if (!properties.containsKey(changes.getAttributeName())) {
                return false;
            }
            Property property = properties.get(changes.getAttributeName());
            Map<String, Object> vendor = property.getVendorExtensions();
            if (vendor.containsKey(ApiMetadataExtensions.Pf.NAME)) {
                var extension = (Map<String, String>) vendor.get(ApiMetadataExtensions.Pf.NAME);
                String referenceName = extension.get(ApiMetadataExtensions.Pf.REFERENCE_NAME);
                String attributeName = extension.get(ApiMetadataExtensions.Pf.ATTRIBUTE_NAME);
                if (changes.getOperation().referenceName.equals(referenceName)
                        && changes.getAttributeName().equals(attributeName)) {
                    changes.setModel(model);
                    return true;
                }
            }
            String modelName = getModelName(changes, apiRuleMetadataRequest);
            if (modelName.equals(((ModelImpl) model).getName())) {
                changes.setModel(model);
                return true;
            }
            return false;
        });
    }
    private boolean filterChangeByVendorExtensions(OperationRule change, Swagger metadata) {
        Model model = getModelFromSwagger(change, metadata);
        if (model != null) {
            Property property = model.getProperties().get(change.getAttributeName());
            Map<String, Object> vendorExtensions = property.getVendorExtensions();
            return vendorExtensions.containsKey(ApiMetadataExtensions.OpenLValidator.NAME)
                    || vendorExtensions.containsKey(ApiMetadataExtensions.OpenLDefaultValue.NAME)
                    || vendorExtensions.containsKey(ApiMetadataExtensions.OpenLValueRange.NAME)
                    || vendorExtensions.containsKey(ApiMetadataExtensions.OpenLApplicable.NAME);
        }
        return false;
    }
    private OperationRule setMetadata(OperationRule rule, Swagger metadata, List<String> scopeValidators, String scope) {
        Model model = getModelFromSwagger(rule, metadata);
        if (model != null) {
            Property property = model.getProperties().get(rule.getAttributeName());
            Map<String, Object> vendorExtensions = property.getVendorExtensions();
            rule.setModel(model);
            if (vendorExtensions.containsKey(ApiMetadataExtensions.OpenLValidator.NAME)) {
                var validatorsArray = (String[]) vendorExtensions.get(ApiMetadataExtensions.OpenLValidator.NAME);
                var validators = new ArrayList<>(Arrays.asList(validatorsArray));
                if(StringUtils.isNotBlank(scope)) {
                    scopeValidators.forEach(scopeValidator -> {
                        if (validators.stream().anyMatch(scopeValidator::equals)) {
                            validators.remove(scopeValidator);
                            return;
                        }
                        validators.remove(scopeValidator.replace("For" + scope, ""));
                    });
                }
                rule.setValidators(validators);
            }
            if(vendorExtensions.containsKey(ApiMetadataExtensions.OpenLDefaultValue.NAME)) {
                var defaultValue = (String) vendorExtensions.get(ApiMetadataExtensions.OpenLDefaultValue.NAME);
                rule.setDefaultValue(defaultValue);
            }
            if(vendorExtensions.containsKey(ApiMetadataExtensions.OpenLValueRange.NAME)) {
                var valueRange = (LinkedHashMap<String, String>) vendorExtensions.get(ApiMetadataExtensions.OpenLValueRange.NAME);
                valueRange.remove(StringUtils.EMPTY);
                if(CollectionUtils.isNotEmpty(valueRange.keySet()) && ObjectUtils.allNotNull(rule.getAttributeValue())) {
                    rule.setValueRange(valueRange.keySet());
                }
            }
            if(vendorExtensions.containsKey(ApiMetadataExtensions.OpenLApplicable.NAME)) {
                rule.setApplicable((Boolean) vendorExtensions.get(ApiMetadataExtensions.OpenLApplicable.NAME));
            }
        }
        return rule;
    }
    private Model getModelFromSwagger(OperationRule change, Swagger metadata) {
        String modelName = ((ModelImpl) change.getModel()).getName();
        return metadata.getDefinitions().get(modelName);
    }
    private String getModelName(OperationRule change, ApiRuleMetadataRequest apiRuleMetadataRequest) {
        return apiRuleMetadataRequest.getLob() + change.getOperation().referenceName;
    }
    
}