/*-
 * ~~~~~~licensing~~~~~~
 * schema-component
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.esb.platform.runtime.core.infrastructure.schema.component;

import org.apache.camel.*;
import org.apache.camel.converter.jaxp.XmlConverter;
import org.apache.camel.support.AsyncProcessorHelper;
import org.apache.camel.support.processor.validation.DefaultValidationErrorHandler;
import org.apache.camel.support.processor.validation.SchemaValidationException;
import org.apache.camel.support.processor.validation.ValidatorErrorHandler;
import org.apache.camel.util.IOHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import ru.entaxy.esb.platform.runtime.core.infrastructure.schema.component.exception.SchemaNotFoundException;
import ru.entaxy.esb.platform.runtime.core.infrastructure.schema.component.util.SchemaReaderFromDB;
import ru.entaxy.platform.base.support.xml.CommonXMLUtils;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stax.StAXSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
 * A processor which validates the XML version of the inbound message body
 * against some schema either in XSD or RelaxNG
 */
public class ValidatingProcessor implements AsyncProcessor {
    private static final Logger LOG = LoggerFactory.getLogger(ValidatingProcessor.class);
    private final SchemaReaderFromDB schemaReaderFromDB;
    private ValidatorErrorHandler errorHandler = new DefaultValidationErrorHandler();
    private final XmlConverter converter = new XmlConverter();
    private boolean useDom;
    private boolean useSharedSchema = true;
    private boolean failOnNullBody = true;
    private boolean failOnNullHeader = true;
    private String headerName;
    private String uri;

    private final Map<String, Schema> schemaMap = new HashMap<>();

    public ValidatingProcessor() {
        schemaReaderFromDB = new SchemaReaderFromDB();
    }

    public ValidatingProcessor(SchemaReaderFromDB schemaReaderFromDB, String uri) {
        // schema reader can be a singelton per schema, therefore make reuse, see ValidatorEndpoint and ValidatorProducer
        this.schemaReaderFromDB = schemaReaderFromDB;
        this.uri = uri;
    }

    public void process(Exchange exchange) throws Exception {
        AsyncProcessorHelper.process(this, exchange);
    }

    public boolean process(Exchange exchange, AsyncCallback callback) {
        try {
            doProcess(exchange);
        } catch (Exception e) {
            exchange.setException(e);
        }
        callback.done(true);
        return true;
    }

    @Override
    public CompletableFuture<Exchange> processAsync(Exchange exchange) {
        return null;
    }

    protected void doProcess(Exchange exchange) throws Exception {
        Schema schema = null;
        Validator validator = null;

        if (uri != null && !uri.isEmpty()) {
            schema = getOrCreateSchema(schemaReaderFromDB.getSchemaResourceUri());
            validator = getValidator(schema);
        }

        // the underlying input stream, which we need to close to avoid locking files or other resources
        Source source = null;
        InputStream is = null;
        try {
            Result result = null;
            // only convert to input stream if really needed
            if (isInputStreamNeeded(exchange)) {
                is = getContentToValidate(exchange, InputStream.class);
                if (is != null) {
                    source = getSource(exchange, is);
                }
            } else {
                Object content = getContentToValidate(exchange);
                if (content != null) {
                    source = getSource(exchange, content);
                }
            }

            //CAMEL-7036 We don't need to set the result if the source is an instance of StreamSource
            if (source instanceof DOMSource) {
                result = new DOMResult();
            } else if (source instanceof SAXSource) {
                result = new SAXResult();
            } else if (source instanceof StAXSource || source instanceof StreamSource) {
                result = null;
            }

            if (source != null) {

                if (validator == null) {
                    String namespace = "namespace:" + getNamespace(source);
                    schemaReaderFromDB.setSchemaResourceUri(namespace);

                    schema = getOrCreateSchema(namespace);
                    validator = getValidator(schema);
                }

                if (validator == null)
                    throw new SchemaNotFoundException("Schema not found for {" + schemaReaderFromDB.getSchemaResourceUri() + "}");

                // create a new errorHandler and set it on the validator
                // must be a local instance to avoid problems with concurrency (to be
                // thread safe)
                ValidatorErrorHandler handler = errorHandler.getClass().newInstance();
                validator.setErrorHandler(handler);
                try {
                    LOG.trace("Validating {}", source);
                    validator.validate(source, result);
                    handler.handleErrors(exchange, schema, result);
                } catch (SAXParseException e) {
                    // can be thrown for non well formed XML
                    throw new SchemaValidationException(exchange, schema, Collections.singletonList(e),
                            Collections.emptyList(),
                            Collections.emptyList());
                }
            }
        } finally {
            IOHelper.close(is);
        }
    }

    private String getNamespace(Source source) throws ParserConfigurationException, IOException, SAXException {
        StreamSource domSource = (StreamSource) source;
        DocumentBuilderFactory factory = CommonXMLUtils.newDocumentBuilderFactoryInstance();

        factory.setNamespaceAware(true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse(domSource.getInputStream());
        return document.getDocumentElement().getNamespaceURI();
    }

    private Schema getOrCreateSchema(String resourceUri) throws IOException, SAXException {
        if (!schemaMap.containsKey(resourceUri) && resourceUri != null && !resourceUri.isEmpty()) {
            schemaMap.put(resourceUri, createSchema());
        }
        return resourceUri == null ? null : schemaMap.get(resourceUri);
    }

    private Validator getValidator(Schema schema) {
        return schema != null ? schema.newValidator() : null;
    }

    private Object getContentToValidate(Exchange exchange) {
        if (shouldUseHeader()) {
            return exchange.getIn().getHeader(headerName);
        } else {
            return exchange.getIn().getBody();
        }
    }

    private <T> T getContentToValidate(Exchange exchange, Class<T> clazz) {
        if (shouldUseHeader()) {
            return exchange.getIn().getHeader(headerName, clazz);
        } else {
            return exchange.getIn().getBody(clazz);
        }
    }

    private boolean shouldUseHeader() {
        return headerName != null;
    }

    public void loadSchema() throws Exception {
        schemaReaderFromDB.loadSchema();
    }

    // Properties
    // -----------------------------------------------------------------------

    public Schema getSchema() throws IOException, SAXException {
        return schemaReaderFromDB.getSchema();
    }

    public void setSchema(Schema schema) {
        schemaReaderFromDB.setSchema(schema);
    }

    public String getSchemaLanguage() {
        return schemaReaderFromDB.getSchemaLanguage();
    }

    public void setSchemaLanguage(String schemaLanguage) {
        schemaReaderFromDB.setSchemaLanguage(schemaLanguage);
    }

    public Source getSchemaSource() throws IOException {
        return schemaReaderFromDB.getSchemaSource();
    }

    public void setSchemaSource(Source schemaSource) {
        schemaReaderFromDB.setSchemaSource(schemaSource);
    }

    public URL getSchemaUrl() {
        return schemaReaderFromDB.getSchemaUrl();
    }

    public void setSchemaUrl(URL schemaUrl) {
        schemaReaderFromDB.setSchemaUrl(schemaUrl);
    }

    public File getSchemaFile() {
        return schemaReaderFromDB.getSchemaFile();
    }

    public void setSchemaFile(File schemaFile) {
        schemaReaderFromDB.setSchemaFile(schemaFile);
    }

    public byte[] getSchemaAsByteArray() {
        return schemaReaderFromDB.getSchemaAsByteArray();
    }

    public void setSchemaAsByteArray(byte[] schemaAsByteArray) {
        schemaReaderFromDB.setSchemaAsByteArray(schemaAsByteArray);
    }

    public SchemaFactory getSchemaFactory() {
        return schemaReaderFromDB.getSchemaFactory();
    }

    public void setSchemaFactory(SchemaFactory schemaFactory) {
        schemaReaderFromDB.setSchemaFactory(schemaFactory);
    }

    public ValidatorErrorHandler getErrorHandler() {
        return errorHandler;
    }

    public void setErrorHandler(ValidatorErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    @Deprecated
    public boolean isUseDom() {
        return useDom;
    }

    /**
     * Sets whether DOMSource and DOMResult should be used.
     *
     * @param useDom true to use DOM otherwise
     */
    @Deprecated
    public void setUseDom(boolean useDom) {
        this.useDom = useDom;
    }

    public boolean isUseSharedSchema() {
        return useSharedSchema;
    }

    public void setUseSharedSchema(boolean useSharedSchema) {
        this.useSharedSchema = useSharedSchema;
    }

    public LSResourceResolver getResourceResolver() {
        return schemaReaderFromDB.getResourceResolver();
    }

    public void setResourceResolver(LSResourceResolver resourceResolver) {
        schemaReaderFromDB.setResourceResolver(resourceResolver);
    }

    public boolean isFailOnNullBody() {
        return failOnNullBody;
    }

    public void setFailOnNullBody(boolean failOnNullBody) {
        this.failOnNullBody = failOnNullBody;
    }

    public boolean isFailOnNullHeader() {
        return failOnNullHeader;
    }

    public void setFailOnNullHeader(boolean failOnNullHeader) {
        this.failOnNullHeader = failOnNullHeader;
    }

    public String getHeaderName() {
        return headerName;
    }

    public void setHeaderName(String headerName) {
        this.headerName = headerName;
    }

    // Implementation methods
    // -----------------------------------------------------------------------

    protected SchemaFactory createSchemaFactory() {
        return schemaReaderFromDB.createSchemaFactory();
    }

    protected Source createSchemaSource() throws IOException {
        return schemaReaderFromDB.createSchemaSource();
    }

    protected Schema createSchema() throws SAXException, IOException {
        return schemaReaderFromDB.createSchema();
    }

    /**
     * Checks whether we need an {@link InputStream} to access the message body or header.
     * <p/>
     * Depending on the content in the message body or header, we may not need to convert
     * to {@link InputStream}.
     *
     * @param exchange the current exchange
     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards.
     */
    protected boolean isInputStreamNeeded(Exchange exchange) {
        Object content = getContentToValidate(exchange);
        if (content == null) {
            return false;
        }

        if (content instanceof InputStream) {
            return true;
        } else if (content instanceof Source) {
            return false;
        } else if (content instanceof String) {
            return false;
        } else if (content instanceof byte[]) {
            return false;
        } else //there is a direct and hopefully optimized converter to Source
            if (content instanceof Node) {
            return false;
        } else return exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass()) == null;
        // yes an input stream is needed
    }

    /**
     * Converts the inbound body or header to a {@link Source}, if it is <b>not</b> already a {@link Source}.
     * <p/>
     * This implementation will prefer to source in the following order:
     * <ul>
     * <li>DOM - DOM if explicit configured to use DOM</li>
     * <li>SAX - SAX as 2nd choice</li>
     * <li>Stream - Stream as 3rd choice</li>
     * <li>DOM - DOM as 4th choice</li>
     * </ul>
     */
    protected Source getSource(Exchange exchange, Object content) {
        if (isUseDom()) {
            // force DOM
            return exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
        }

        // body or header may already be a source
        if (content instanceof Source) {
            return (Source) content;
        }
        Source source = null;
        if (content instanceof InputStream) {
            return new StreamSource((InputStream) content);
        }
        if (content != null) {
            TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass());
            if (tc != null) {
                source = tc.convertTo(Source.class, exchange, content);
            }
        }

        if (source == null) {
            // then try SAX
            source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, content);
        }
        if (source == null) {
            // then try stream
            source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, content);
        }
        if (source == null) {
            // and fallback to DOM
            source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
        }
        if (source == null) {
            if (isFailOnNullBody()) {
                throw new ExpectedBodyTypeException(exchange, Source.class);
            } else {
                try {
                    source = converter.toDOMSource(converter.createDocument());
                } catch (ParserConfigurationException e) {
                    throw new RuntimeTransformException(e);
                }
            }
        }
        return source;
    }

}
