package at.procon.eventhub.tachographfilesession.service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

@Component
public class TachographXmlParser {

    private static final String JAXP_MAX_OCCUR_LIMIT = "http://www.oracle.com/xml/jaxp/properties/maxOccurLimit";

    private final Schema schema;

    public TachographXmlParser() {
        this.schema = loadSchema();
    }

    public ParsedTachographXml parse(String xmlContent) {
        try {
            String normalizedXmlContent = normalizeXmlContent(xmlContent);
            validate(normalizedXmlContent);
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(false);
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
            factory.setXIncludeAware(false);
            factory.setExpandEntityReferences(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(new InputSource(new StringReader(normalizedXmlContent)));
            String rootName = document.getDocumentElement() == null ? null : document.getDocumentElement().getNodeName();
            if (!"DriverCard".equals(rootName) && !"VehicleUnit".equals(rootName)) {
                throw new UnsupportedTachographFileTypeException("Only DriverCard and VehicleUnit XML documents are supported.");
            }
            return new ParsedTachographXml(document, rootName);
        } catch (ParserConfigurationException | IOException | SAXException e) {
            throw new TachographXmlValidationException("Failed to parse tachograph XML.", e);
        }
    }

    private void validate(String xmlContent) {
        try {
            schema.newValidator().validate(new StreamSource(new StringReader(xmlContent)));
        } catch (IOException | SAXException e) {
            throw new TachographXmlValidationException("Tachograph XML failed XSD validation.", e);
        }
    }

    private String normalizeXmlContent(String xmlContent) {
        if (xmlContent == null || xmlContent.isEmpty()) {
            return xmlContent;
        }
        return xmlContent.charAt(0) == '\uFEFF' ? xmlContent.substring(1) : xmlContent;
    }

    private Schema loadSchema() {
        try {
            SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
            factory.setProperty(JAXP_MAX_OCCUR_LIMIT, 100_000);
            byte[] content = new ClassPathResource("xsd/Tachograph.xsd").getContentAsByteArray();
            return factory.newSchema(new StreamSource(new ByteArrayInputStream(content)));
        } catch (IOException | SAXException e) {
            throw new IllegalStateException("Failed to load Tachograph XSD schema.", e);
        }
    }

    public record ParsedTachographXml(
            Document document,
            String rootElementName
    ) {
    }
}
