package de.avatic.lcc.service.access;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.model.db.properties.CountryPropertyMappingId;
import de.avatic.lcc.model.db.properties.PackagingPropertyMappingId;
import de.avatic.lcc.model.db.properties.PropertyDataType;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.repositories.country.CountryPropertyRepository;
import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.util.exception.badrequest.PropertyValidationException;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class PropertyValidationService {


    private final ObjectMapper objectMapper;

    private final CountryPropertyRepository countryPropertyRepository;
    private final PropertyRepository propertyRepository;
    private final PackagingPropertiesRepository packagingPropertiesRepository;

    public PropertyValidationService(ObjectMapper objectMapper, CountryPropertyRepository countryPropertyRepository, PropertyRepository propertyRepository, PackagingPropertiesRepository packagingPropertiesRepository) {
        this.objectMapper = objectMapper;
        this.countryPropertyRepository = countryPropertyRepository;
        this.propertyRepository = propertyRepository;
        this.packagingPropertiesRepository = packagingPropertiesRepository;
    }

    public void validateProperty(SystemPropertyMappingId mappingId, String value) {

        var property = propertyRepository.getPropertyByMappingId(mappingId);
        validate(mappingId.name(), PropertyDataType.valueOf(property.orElseThrow().getDataType()), property.orElseThrow().getValidationRule(), value);
    }

    public void validatePackagingProperty(Integer packagingId, PackagingPropertyMappingId mappingId, String value) {

        var property = packagingPropertiesRepository.getByPackagingIdAndType(packagingId, mappingId.name());
        validate(mappingId.name(), property.orElseThrow().getType().getDataType(), property.orElseThrow().getType().getValidationRule(), value);

    }

    public void validateCountryProperty(Integer countryId, CountryPropertyMappingId mappingId, String value) {

        var property = countryPropertyRepository.getByMappingIdAndCountryId(mappingId, countryId);
        validate(mappingId.name(), PropertyDataType.valueOf(property.orElseThrow().getDataType()), property.orElseThrow().getValidationRule(), value);

    }

    private void validate(String propertyId, PropertyDataType propertyDataType, String validationRule, String value) {

        if (validationRule == null) {
            if (propertyDataType == PropertyDataType.CURRENCY || propertyDataType == PropertyDataType.PERCENTAGE || propertyDataType == PropertyDataType.INT) {
                checkNumeric(propertyId, value);
            }

            if (propertyDataType == PropertyDataType.ENUMERATION) {
                throw new InternalErrorException("A validation rule has to be specified for ENUMERATION properties. Property ID: " + propertyId);
            }

            return;
        }

        try {
            var ruleList = parseRuleList(propertyId, validationRule);

            if (propertyDataType == PropertyDataType.BOOLEAN && value != null && (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")))
                return;

            if (propertyDataType == PropertyDataType.TEXT)
                return;

            ruleList.evaluate(value);
        } catch (JsonProcessingException e) {
            throw new InternalErrorException("Faulty JSON Format in validation rule. Property ID: " + propertyId, e);
        }

    }

    private void checkNumeric(String propertyId, String value) {
        try {
            Double.parseDouble(value);
        } catch (NumberFormatException e) {
            throw new PropertyValidationException(propertyId, value, e);
        }
    }

    private RuleList parseRuleList(String propertyId, String string) throws JsonProcessingException {

        RuleList ruleList = new RuleList();
        JsonNode jsonNode = objectMapper.readTree(string);

        jsonNode.fields().forEachRemaining(entry -> {
            ruleList.put(new Rule(propertyId, CompareOperator.valueOf(entry.getKey()), convertValue(CompareOperator.valueOf(entry.getKey()), entry.getValue())));
        });

        return ruleList;
    }

    private Object convertValue(CompareOperator compareOperator, JsonNode value) {
        if (compareOperator == CompareOperator.ENUM) {
            List<String> list = new ArrayList<>();
            value.elements().forEachRemaining(e -> {
                list.add(e.asText());
            });
            return (list);
        }

        return value.asDouble();
    }

    private enum CompareOperator {
        E("equal to"),
        NE("not equal to"),
        GT("greater than"),
        GTE("greater than or equal to"),
        LT("less than"),
        LTE("less than or equal to"),
        ENUM("in list");

        private final String identifier;

        CompareOperator(String identifier) {
            this.identifier = identifier;

        }

        public String getIdentifier() {
            return identifier;
        }

        public boolean evaluate(Double value, Double compareValue) {
            return switch (this) {
                case E -> value.equals(compareValue);
                case NE -> !value.equals(compareValue);
                case GT -> value > compareValue;
                case GTE -> value >= compareValue;
                case LT -> value < compareValue;
                case LTE -> value <= compareValue;
                default -> throw new IllegalStateException("Unexpected value: " + this);
            };
        }
    }

    private static class RuleList {
        private final List<Rule> rules;

        private RuleList() {
            this.rules = new ArrayList<>();

        }

        public void put(Rule rule) {
            this.rules.add(rule);
        }

        public void evaluate(String value) {
            rules.forEach(rule -> rule.evaluate(value));
        }
    }

    private record Rule(String propertyId, CompareOperator operator, Object value) {


        public void evaluate(String value) {
            if (operator == CompareOperator.ENUM) {
                if (!((List<String>) this.value).contains(value)) {
                    throw new PropertyValidationException(propertyId, operator.getIdentifier(), this.value.toString(), value);
                }
            } else {
                try {
                    if (!operator.evaluate(Double.parseDouble(value), (Double) this.value)) {
                        throw new PropertyValidationException(propertyId, operator.getIdentifier(), ((Double) this.value).toString(), value);
                    }

                } catch (NumberFormatException e) {
                    throw new PropertyValidationException(propertyId, value, e);
                }

            }


        }

    }


}
