/*-
 * ~~~~~~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.util;

import org.apache.camel.CamelContext;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.SAXException;
import ru.entaxy.esb.platform.runtime.core.infrastructure.schema.api.ResourceService;
import ru.entaxy.esb.platform.runtime.core.infrastructure.schema.api.entity.Resource;

import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Optional;

public class SchemaReaderFromDB {

    /**
     * Key of the global option to switch either off or on  the access to external DTDs in the XML Validator for StreamSources.
     * Only effective, if not a custom schema factory is used.
     */
    public static final String ACCESS_EXTERNAL_DTD = "CamelXmlValidatorAccessExternalDTD";

    private static final Logger LOG = LoggerFactory.getLogger(SchemaReaderFromDB.class);

    private String schemaLanguage = XMLConstants.W3C_XML_SCHEMA_NS_URI;
    // must be volatile because is accessed from different threads see ValidatorEndpoint.clearCachedSchema
    private volatile Schema schema;
    private Source schemaSource;
    // must be volatile because is accessed from different threads see ValidatorEndpoint.clearCachedSchema
    private volatile SchemaFactory schemaFactory;
    private URL schemaUrl;
    private File schemaFile;
    private byte[] schemaAsByteArray;
    private String schemaResourceUri;
    private String version;
    private LSResourceResolver resourceResolver;
    private ResourceService resourceService;

    private final CamelContext camelContext;

    public SchemaReaderFromDB() {
        this.camelContext = null;
        this.schemaResourceUri = null;
    }

    /**
     * Specify a camel context and a schema resource URI in order to read the schema via the class resolver specified in the Camel context.
     */
    public SchemaReaderFromDB(CamelContext camelContext, String schemaResourceUri, String version) {
        ObjectHelper.notNull(camelContext, "camelContext");
        ObjectHelper.notNull(schemaResourceUri, "schemaResourceUri");
        this.camelContext = camelContext;
        this.schemaResourceUri = schemaResourceUri;
        this.version = version;
    }

    public void loadSchema() throws Exception {
        // force loading of schema
        schema = createSchema();
    }

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

    public Schema getSchema() throws IOException, SAXException {
        if (schema == null) {
            synchronized (this) {
                if (schema == null) {
                    schema = createSchema();
                }
            }
        }
        return schema;
    }

    public void setSchema(Schema schema) {
        this.schema = schema;
    }

    public String getSchemaLanguage() {
        return schemaLanguage;
    }

    public void setSchemaLanguage(String schemaLanguage) {
        this.schemaLanguage = schemaLanguage;
    }

    public Source getSchemaSource() throws IOException {
        if (schemaSource == null) {
            schemaSource = createSchemaSource();
        }
        return schemaSource;
    }

    public void setSchemaSource(Source schemaSource) {
        this.schemaSource = schemaSource;
    }

    public URL getSchemaUrl() {
        return schemaUrl;
    }

    public void setSchemaUrl(URL schemaUrl) {
        this.schemaUrl = schemaUrl;
    }

    public File getSchemaFile() {
        return schemaFile;
    }

    public void setSchemaFile(File schemaFile) {
        this.schemaFile = schemaFile;
    }

    public byte[] getSchemaAsByteArray() {
        return schemaAsByteArray;
    }

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

    public SchemaFactory getSchemaFactory() {
        if (schemaFactory == null) {
            synchronized (this) {
                if (schemaFactory == null) {
                    schemaFactory = createSchemaFactory();
                }
            }
        }
        return schemaFactory;
    }

    public void setSchemaFactory(SchemaFactory schemaFactory) {
        this.schemaFactory = schemaFactory;
    }

    public void setResourceService(ResourceService resourceService) {
        this.resourceService = resourceService;
    }

    public LSResourceResolver getResourceResolver() {
        return resourceResolver;
    }

    public void setResourceResolver(LSResourceResolver resourceResolver) {
        this.resourceResolver = resourceResolver;
    }

    public void setSchemaResourceUri(String schemaResourceUri) {
        this.schemaResourceUri = schemaResourceUri;
    }

    public String getSchemaResourceUri() {
        return schemaResourceUri;
    }

    public SchemaFactory createSchemaFactory() {
        SchemaFactory factory = SchemaFactory.newInstance(schemaLanguage);

        //Prohibit the use of all protocols by external entities
        try {
            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
        } catch (SAXException e) {
            LOG.warn(e.getMessage(), e);
        }

        if (getResourceResolver() != null) {
            factory.setResourceResolver(getResourceResolver());
        }
        if (camelContext == null || !Boolean.parseBoolean(camelContext.getGlobalOptions().get(ACCESS_EXTERNAL_DTD))) {
            try {
                LOG.debug("Configuring SchemaFactory to not allow access to external DTD/Schema");
                factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            } catch (SAXException e) {
                LOG.warn(e.getMessage(), e);
            }
        }
        return factory;
    }

    public Source createSchemaSource() throws IOException {
        throw new IllegalArgumentException("You must specify either a schema, schemaFile, schemaSource, schemaUrl, or schemaUri property");
    }

    public Schema createSchema() throws SAXException, IOException {
        SchemaFactory factory = getSchemaFactory();

        Optional<Resource> resource = Optional.empty();
        if (schemaResourceUri != null && schemaResourceUri.contains("namespace:")) {
            resource = resourceService.getResourceByNamespace(schemaResourceUri.replace("namespace:", ""));
        } else if (schemaResourceUri != null && schemaResourceUri.contains("name:")) {
            resource = resourceService.getResourceByFullName(schemaResourceUri.replace("name:", ""));
        } else if (schemaResourceUri != null && schemaResourceUri.contains("uuid:")) {
            resource = resourceService.getResourceByNamespace(schemaResourceUri.replace("uuid:", ""));
        }

        if (resource.isPresent())
            return factory.newSchema(new StreamSource(new ByteArrayInputStream(resource.get().getResourceValue())));
        return null;
    }
}
