package ru.yandex.direct.api.v5.ws.validation;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.xml.namespace.QName;

import one.util.streamex.StreamEx;
import org.apache.commons.lang3.StringUtils;

import ru.yandex.direct.api.v5.ApiFaultTranslations;
import ru.yandex.direct.api.v5.ws.json.JsonMessage;

import static ru.yandex.direct.api.v5.ws.validation.NullValueApiException.missedFieldInArray;
import static ru.yandex.direct.api.v5.ws.validation.NullValueApiException.missedParameter;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.COMPLEX_CONTENT;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.COMPLEX_TYPE;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.ELEMENT;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.EXTENSION;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.SEQUENCE;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.XMLSCHEMA_NAMESPACE;

/**
 * Валидатор по WSDL, может валидировать все запросы одного какого-то сервиса (например, ads, adgroups) по WSDL этого
 * сервиса.
 */
@ParametersAreNonnullByDefault
public class WsdlValidator {
    private static final Set<String> SIMPLE_NUMERIC_TYPES = Set.of("long", "int");
    private static final Set<String> SIMPLE_TYPES =
            StreamEx.of(SIMPLE_NUMERIC_TYPES).append("string").toImmutableSet();

    private final Map<String, WsdlElement> requests;
    private final Map<QName, WsdlElement> types;
    private final Map<String, String> namespaces;
    private final String xsdPrefix;

    WsdlValidator(Map<String, WsdlElement> requests, Map<QName, WsdlElement> types, Map<String, String> namespaces) {
        this.requests = requests;
        this.types = types;
        this.namespaces = namespaces;
        this.xsdPrefix = namespaces.entrySet().stream()
                .filter(e -> e.getValue().equals(XMLSCHEMA_NAMESPACE))
                .findFirst()
                .map(Map.Entry::getKey)
                .orElseThrow(() -> new RuntimeException("xsd namespace not found"));
    }

    /**
     * Валидирует один JSON-запрос к сервису по WSDL этого сервиса. Проверяет наличие обязательных элементов,
     * соответствие nillable с null, minOccurs/maxOccurs с количеством элементов.
     */
    public void validateJsonRequest(JsonMessage request) {
        if (!(request.getPayloadSource().getObject() instanceof Map)) {
            throw new IncorrectRequestApiException();
        }
        @SuppressWarnings("unchecked")
        Map<String, Object> payload = (Map<String, Object>) request.getPayloadSource().getObject();
        String operation = request.getOperation();
        validateElementContents(requests.get(operation), payload, "", false);
    }

    private void validateElementContents(WsdlElement elementType,
                                         Map<String, Object> elementData,
                                         String path,
                                         boolean isInArray) {
        StreamEx<WsdlElement> contents = getContentsStream(elementType, path);

        for (WsdlElement childType : contents) {
            String name = childType.getName();
            boolean nillable = childType.isNillable();
            Object childData = elementData.get(name);

            if (!nillable && elementData.containsKey(name) && childData == null) {
                throw new IncorrectRequestApiException(ApiFaultTranslations.INSTANCE.detailedMustNotBeNull(name));
            }

            validateElement(childType, childData, path, isInArray);
        }
    }

    private void validateElement(WsdlElement elementType,
                                 @Nullable Object elementData,
                                 String path, boolean isInArray) {
        String name = elementType.getName();
        String currentPath = path;
        if (!path.isEmpty()) {
            currentPath += ".";
        }
        currentPath += name;

        String type = elementType.getType();
        int minOccurs = elementType.getMinOccurs();
        int maxOccurs = elementType.getMaxOccurs();

        if (elementData == null) {   // Zero items
            if (minOccurs > 0 && !elementType.isNillable()) {
                if (isInArray) {
                    throw missedFieldInArray(path, name);
                }
                throw missedParameter(name);
            }
        } else if (elementData instanceof Collection) {  // Many items
            var collection = (Collection) elementData;

            if (maxOccurs <= 1) {
                throw new IncorrectRequestApiException(
                        ApiFaultTranslations.INSTANCE.detailedIncorrectValueFieldShouldNotBeArray(currentPath));
            }
            if (minOccurs > collection.size()) {
                throw new IncorrectRequestApiException(
                        ApiFaultTranslations.INSTANCE.detailedInvalidMinSize(currentPath, minOccurs));
            }
            for (var item : collection) {
                validateSingleItem(type, item, currentPath, true);
            }
        } else {    // One item
            if (maxOccurs > 1) {
                throw new IncorrectRequestApiException(
                        ApiFaultTranslations.INSTANCE.detailedIncorrectValueFieldShouldBeArray(currentPath));
            }
            validateSingleItem(type, elementData, currentPath, false);
        }
    }

    private void validateSingleItem(String type, @Nullable Object itemData, String currentPath, boolean isInArray) {
        if (itemData == null && isInArray) {
            throw new IncorrectRequestApiException(
                    ApiFaultTranslations.INSTANCE.detailedArrayContainsNull(currentPath));
        }
        if (isInArray && isSimpleNumericType(type) &&
                itemData instanceof String && StringUtils.isBlank((String) itemData)) {
            throw new IncorrectRequestApiException(
                    ApiFaultTranslations.INSTANCE.detailedInvalidFormatExpectedIntegerInArray(currentPath));
        }

        if (itemData instanceof Map && !isSimpleType(type)) {
            @SuppressWarnings("unchecked")
            Map<String, Object> itemDataMap = (Map<String, Object>) itemData;
            WsdlElement elementType = types.get(qname(type));
            if (elementType == null) {
                throw new IllegalStateException("WSDL type not found.");
            }
            validateElementContents(elementType, itemDataMap, currentPath, isInArray);
        }
    }

    private boolean isSimpleType(String type) {
        return isInTargetTypes(type, SIMPLE_TYPES);
    }

    private boolean isSimpleNumericType(String type) {
        return isInTargetTypes(type, SIMPLE_NUMERIC_TYPES);
    }

    private boolean isInTargetTypes(String type, Set<String> targetTypes) {
        QName name = qname(type);
        return name.getNamespaceURI().equals(XMLSCHEMA_NAMESPACE) && targetTypes.contains(name.getLocalPart());
    }

    private StreamEx<WsdlElement> getContentsStream(WsdlElement element, String path) {
        StreamEx<WsdlElement> complexType = typedChildren(element, xsd(COMPLEX_TYPE), path,
                (ct, pinner) -> sequence(ct, pinner).append(complexContent(ct, pinner)));
        StreamEx<WsdlElement> sequence = sequence(element, path);
        StreamEx<WsdlElement> complexContent = complexContent(element, path);
        return complexType.append(sequence).append(complexContent);
    }

    private StreamEx<WsdlElement> typedChildren(WsdlElement element, String tag, String path,
                                                BiFunction<WsdlElement, String, StreamEx<WsdlElement>> collector) {
        StreamEx<WsdlElement> result = StreamEx.of();
        List<WsdlElement> children = element.getChildren();
        for (WsdlElement child : children) {
            if (child.getTag().equals(tag)) {
                StreamEx<WsdlElement> converted = collector.apply(child, path + "." + tag);
                result = result.append(converted);
            }
        }
        return result;
    }

    private StreamEx<WsdlElement> sequence(WsdlElement element, String path) {
        return typedChildren(element, xsd(SEQUENCE), path, (seq, pinner) ->
                typedChildren(seq, xsd(ELEMENT), pinner, (item, pdeep) -> StreamEx.of(item)));
    }

    private StreamEx<WsdlElement> complexContent(WsdlElement element, String path) {
        return typedChildren(element, xsd(COMPLEX_CONTENT), path, (cc, pinner) ->
                typedChildren(cc, xsd(EXTENSION), pinner, this::extensionContents));
    }

    private StreamEx<WsdlElement> extensionContents(WsdlElement element, String path) {
        String base = element.getBase();
        return getContentsStream(types.get(qname(base)), path)
                .append(sequence(element, path));
    }

    private String xsd(String tagName) {
        return xsdPrefix + ":" + tagName;
    }

    private QName qname(String prefixedTagName) {
        return StreamEx.split(prefixedTagName, ':').toListAndThen(p ->
                new QName(namespaces.get(p.get(0)), p.get(1)));
    }
}

