package ru.nbch.credit_tracker.service.xml.jaxb;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import ru.nbch.credit_tracker.utils.xml.EscapeHandlerUtil;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.validation.Schema;
import java.io.*;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public abstract class XmlProcessorImplBase implements IXmlProcessor {
    private String charsetName;
    private XMLInputFactory xmlInputFactory;
    private Map<Class<?>, Schema> schemasMap = new HashMap<>();


    public XmlProcessorImplBase(String charsetName) {
        this.xmlInputFactory = XMLInputFactory.newInstance();
        this.xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        this.xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);

        this.charsetName = charsetName;
    }

    @Override
    public String asString(Object xmlObj, boolean useStandalone, String encoding) {
        try {
            StringWriter strWriter = new StringWriter();
            if (useStandalone) {
                strWriter.write("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>\n".formatted(encoding));
            }
            genMarshaller(xmlObj, useStandalone, encoding).marshal(xmlObj, strWriter);
            return strWriter.toString();
        } catch (Throwable e) {
            log.error("Error serializing object to XML string: {}", e.getMessage());
            throw new IllegalStateException("cannot serialize object to XML");
        }
    }

    @Override
    public String asString(Object xmlObj, boolean useStandalone) {
        return asString(xmlObj, useStandalone, charsetName);
    }

    @Override
    public void write(String path, Object xmlObj, boolean useStandalone) {
        try (OutputStream os = new FileOutputStream(path)) {
            if (useStandalone) {
                os.write("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>\n".formatted(charsetName).getBytes(charsetName));
            }
            genMarshaller(xmlObj, useStandalone, charsetName).marshal(xmlObj, os);
        } catch (Throwable e) {
            log.error("Error serializing object to XML string: {}", e.getMessage(), e);
            throw new IllegalStateException("cannot serialize object to XML");
        }
    }

    @Override
    public <Z> Z read(InputStream inputStream, Class<Z> clazz) {
        try {
            XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(inputStream);
            return genUnmarshaller(clazz).unmarshal(xmlStreamReader, clazz).getValue();
        } catch (Exception e) {
            log.error(ExceptionUtils.getStackTrace(e));
            return null;
        }
    }

    @Override
    public <Z> Z read(Path xml, Class<Z> clazz) {
        try {
            XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(new FileReader(xml.toFile()));
            return genUnmarshaller(clazz).unmarshal(xmlStreamReader, clazz).getValue();
        } catch (Exception e) {
            log.error(ExceptionUtils.getStackTrace(e));
            return null;
        }
    }

    @Override
    public <Z> Z read(String xml, Class<Z> clazz) {
        try {
            XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(new StringReader(xml));
            return genUnmarshaller(clazz).unmarshal(xmlStreamReader, clazz).getValue();
        } catch (Exception e) {
            log.error(ExceptionUtils.getStackTrace(e));
            return null;
        }
    }

    public Marshaller genMarshaller(Object obj, boolean useStandalone, String encoding) throws JAXBException {
        Marshaller marshaller = getContextByObject(obj).createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding);
        if (useStandalone) {
            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
        } else {
            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.FALSE);
        }
        marshaller.setProperty(MarshallerProperties.CHARACTER_ESCAPE_HANDLER, EscapeHandlerUtil.createEscapeHandler(marshaller));
        marshaller.setProperty(MarshallerProperties.INDENT_STRING, "    ");
        return marshaller;
    }

    private <G> Unmarshaller genUnmarshaller(Class<G> clazz) throws JAXBException {
        Unmarshaller unmarshaller = getContextByClass(clazz).createUnmarshaller();
        Schema schema;
        if (schemasMap != null && (schema = schemasMap.get(clazz)) != null) {
            unmarshaller.setSchema(schema);
        }
        return unmarshaller;
    }

    protected JAXBContext getContextByObject(Object obj) throws JAXBException {
        Class<?> aClass = obj.getClass();
        return getContextByClass(aClass);
    }

    protected abstract JAXBContext getContextByClass(Class<?> clazz) throws JAXBException;
}

