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

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import javax.annotation.ParametersAreNonnullByDefault;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import one.util.streamex.StreamEx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import ru.yandex.direct.utils.io.RuntimeIoException;
import ru.yandex.misc.io.file.FileNotFoundIoException;

import static java.util.Collections.emptyMap;
import static java.util.Map.entry;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.ELEMENT;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.INPUT;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.MESSAGE;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.NAME;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.OPERATION;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.PART;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.PORT_TYPE;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.SCHEMA;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.TARGET_NAMESPACE;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.WSDL_NAMESPACE;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.XMLNS_PREFIX;
import static ru.yandex.direct.api.v5.ws.validation.WsdlConstants.XMLSCHEMA_NAMESPACE;

@ParametersAreNonnullByDefault
@Component
public class WsdlValidatorFactory {

    private static final Map<String, String> WSDL_PATHS = Map.ofEntries(
            entry("adextensions", "/wsdl/AdExtensions.wsdl"),
            entry("adgroups", "/wsdl/AdGroups.wsdl"),
            entry("ads", "/wsdl/Ads.wsdl"),
            entry("agencyclients", "/wsdl/AgencyClients.wsdl"),
            entry("audiencetargets", "/wsdl/AudienceTargets.wsdl"),
            entry("bidmodifiers", "/wsdl/BidModifiers.wsdl"),
            entry("bids", "/wsdl/Bids.wsdl"),
            entry("businesses", "/wsdl/Businesses.wsdl"),
            entry("campaigns", "/wsdl/Campaigns.wsdl"),
            entry("campaignsext", "/wsdl/CampaignsExt.wsdl"),
            entry("changes", "/wsdl/Changes.wsdl"),
            entry("clients", "/wsdl/Clients.wsdl"),
            entry("creatives", "/wsdl/Creatives.wsdl"),
            entry("dictionaries", "/wsdl/Dictionaries.wsdl"),
            entry("dynamictextadtargets", "/wsdl/DynamicTextAdTargets.wsdl"),
            entry("dynamicfeedadtargets", "/wsdl/DynamicFeedAdTargets.wsdl"),
            entry("features", "/wsdl/Features.wsdl"),
            entry("keywordbids", "/wsdl/KeywordBids.wsdl"),
            entry("keywords", "/wsdl/Keywords.wsdl"),
            entry("keywordsresearch", "/wsdl/KeywordsResearch.wsdl"),
            entry("leads", "/wsdl/Leads.wsdl"),
            entry("negativekeywordsharedsets", "/wsdl/NegativeKeywordSharedSets.wsdl"),
            entry("promotedcontent", "/wsdl/PromotedContent.wsdl"),
            entry("feeds", "/wsdl/Feeds.wsdl"),
            entry("retargetinglists", "/wsdl/RetargetingLists.wsdl"),
            entry("sitelinks", "/wsdl/Sitelinks.wsdl"),
            entry("smartadtargets", "/wsdl/SmartAdTargets.wsdl"),
            entry("turbopages", "/wsdl/TurboPages.wsdl"),
            entry("vcards", "/wsdl/VCards.wsdl"),
            entry("advideos", "/wsdl/AdVideos.wsdl"));

    private static final Logger logger = LoggerFactory.getLogger(WsdlValidatorFactory.class);

    @ParametersAreNonnullByDefault
    private static class Loader extends CacheLoader<String, WsdlValidator> {
        private static Map<QName, WsdlElement> general = emptyMap();
        private static Map<QName, WsdlElement> generalClients = emptyMap();
        private static Map<QName, WsdlElement> adExtendsionTypes = emptyMap();

        static {
            try {
                general = getXsdTypes("/wsdl/general.xsd");
                generalClients = getXsdTypes("/wsdl/generalclients.xsd");
                adExtendsionTypes = getXsdTypes("/wsdl/adextensiontypes.xsd");
            } catch (ParserConfigurationException | SAXException ex) {
                logger.error("Can't parse general xsds", ex);
            }
        }

        /**
         * Делает из имени сервиса WSDL-валидатор для этого сервиса
         */
        @Override
        public synchronized WsdlValidator load(String key) {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            try {
                var builder = factory.newDocumentBuilder();
                Document doc = builder.parse(getWsdlStream(key));
                var root = doc.getDocumentElement();
                var targetNamespace = root.getAttribute(TARGET_NAMESPACE);
                Map<String, String> namespaces = getNamespaces(doc);

                var elSchema = (Element) doc.getElementsByTagNameNS(XMLSCHEMA_NAMESPACE, SCHEMA).item(0);
                Map<QName, WsdlElement> types = new HashMap<>(getTypes(elSchema, targetNamespace));
                types.putAll(general);
                types.putAll(generalClients);
                types.putAll(adExtendsionTypes);

                var portType = getElement(doc.getDocumentElement(), PORT_TYPE);
                NodeList operations = portType.getElementsByTagNameNS(WSDL_NAMESPACE, OPERATION);
                Map<String, WsdlElement> requests = new HashMap<>();
                for (int i = 0; i < operations.getLength(); i++) {
                    var operation = (Element) operations.item(i);
                    requests.put(operation.getAttribute(NAME),
                            getOperationRequest(operation, root, namespaces, types));
                }

                return new WsdlValidator(requests, types, namespaces);
            } catch (ParserConfigurationException ex) {
                throw new RuntimeIoException("Failed to configure parser", ex);
            } catch (IOException | SAXException ex) {
                throw new RuntimeIoException("Failed to parse WSDL", ex);
            }
        }

        private static InputStream getWsdlStream(String service) {
            return WsdlValidator.class.getResourceAsStream(getWsdlName(service));
        }

        private static String getWsdlName(String service) {
            String wsdl = WSDL_PATHS.get(service);
            if (wsdl == null) {
                throw new FileNotFoundIoException("WSDL for service '" + service + "' is not found");
            }
            return wsdl;
        }

        private static Map<QName, WsdlElement> getTypes(Element schema, String targetNamespace) {
            Map<QName, WsdlElement> types = new HashMap<>();
            NodeList children = schema.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                if (children.item(i) instanceof Element) {
                    var child = (Element) children.item(i);
                    var name = child.getAttribute(NAME);
                    if (name != null) {
                        types.put(new QName(targetNamespace, name), new WsdlElement(targetNamespace, child));
                    }
                }
            }
            return types;
        }

        private static Map<QName, WsdlElement> getXsdTypes(String resourceUri)
                throws ParserConfigurationException, SAXException
        {
            try {
                var factory = DocumentBuilderFactory.newInstance();
                var builder = factory.newDocumentBuilder();
                Document document = builder.parse(WsdlValidator.class.getResource(resourceUri).toString());
                var root = document.getDocumentElement();
                var namespace = root.getAttribute(TARGET_NAMESPACE);
                return getTypes(root, namespace);
            } catch (IOException ex) {
                throw new RuntimeIoException("IO error occurred reading resource " + resourceUri, ex);
            }
        }

        private static Map<String, String> getNamespaces(Document doc) {
            Map<String, String> result = new HashMap<>();
            var root = doc.getDocumentElement();
            NamedNodeMap attrs = root.getAttributes();
            for (int i = 0; i < attrs.getLength(); i++) {
                Node node = attrs.item(i);
                var name = node.getNodeName();
                if (!name.startsWith(XMLNS_PREFIX)) {
                    continue;
                }

                result.put(name.substring(XMLNS_PREFIX.length()), node.getNodeValue());
            }

            return result;
        }

        private static WsdlElement getOperationRequest(Element operation, Element root,
                Map<String, String> namespaces,
                Map<QName, WsdlElement> types)
        {
            var input = getElement(operation, INPUT);

            var messageName = qname(input.getAttribute(MESSAGE), namespaces);
            NodeList messages = root.getElementsByTagNameNS(WSDL_NAMESPACE, MESSAGE);
            Element message = null;
            for (int i = 0; i < messages.getLength(); i++) {
                var current = (Element) messages.item(i);
                if (current.getAttribute(NAME).equals(messageName.getLocalPart())) {
                    message = current;
                    break;
                }
            }

            if (message == null) {
                throw new RuntimeIoException("Invalid WSDL format - message not found.");
            }

            var part = getElement(message, PART);
            var requestName = qname(part.getAttribute(ELEMENT), namespaces);
            if (!types.containsKey(requestName)) {
                throw new RuntimeIoException("Invalid WSDL format - request element not found.");
            }
            return types.get(requestName);
        }

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

        private static Element getElement(Element parent, String localName) {
            NodeList children = parent.getElementsByTagNameNS(WSDL_NAMESPACE, localName);
            if (children.getLength() == 0) {
                throw new RuntimeIoException("Invalid WSDL format - " + localName + " not found.");
            }
            return (Element) children.item(0);
        }
    }

    private LoadingCache<String, WsdlValidator> validatorCache = CacheBuilder.newBuilder()
            .expireAfterAccess(5, TimeUnit.MINUTES)
            .build(new Loader());

    public WsdlValidator getValidator(String service) throws ExecutionException {
        return validatorCache.get(service);
    }
}
