package cz.moneta.test.harness.messaging.model;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.apache.commons.lang3.StringUtils;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;

public class ReceivedMessage {

    private static final Pattern ARRAY_NODE_PATTERN = Pattern.compile("(.*?)\\[([0-9]*?)\\]");
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
    private static final XmlMapper XML_MAPPER = new XmlMapper();

    private final String body;
    private final MessageContentType contentType;
    private final Map<String, String> headers;
    private final long timestamp;
    private final String source;

    public ReceivedMessage(String body,
                           MessageContentType contentType,
                           Map<String, String> headers,
                           long timestamp,
                           String source) {
        this.body = Optional.ofNullable(body).orElse("");
        this.contentType = Objects.requireNonNull(contentType, "contentType");
        this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(Optional.ofNullable(headers).orElseGet(Collections::emptyMap)));
        this.timestamp = timestamp;
        this.source = Optional.ofNullable(source).orElse("");
    }

    public JsonNode extractJson(String path) {
        JsonNode root = readJsonLikeNode();
        return extractNode(path, root);
    }

    public String extractXml(String xpathExpression) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            factory.setNamespaceAware(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(new InputSource(new StringReader(body)));
            XPath xpath = XPathFactory.newInstance().newXPath();
            xpath.setNamespaceContext(new EmptyNamespaceContext());
            return String.valueOf(xpath.evaluate(xpathExpression, document, XPathConstants.STRING));
        } catch (Exception e) {
            throw new IllegalStateException("Failed to extract xml value for expression: " + xpathExpression, e);
        }
    }

    public String extract(String expression) {
        return switch (contentType) {
            case JSON -> extractJson(expression).asText();
            case XML -> extractXml(expression);
            case RAW_TEXT -> body;
        };
    }

    public <T> T mapTo(Class<T> type) {
        try {
            return switch (contentType) {
                case JSON -> JSON_MAPPER.readValue(body, type);
                case XML -> XML_MAPPER.readValue(body, type);
                case RAW_TEXT -> {
                    if (String.class.equals(type)) {
                        yield type.cast(body);
                    }
                    throw new IllegalStateException("RAW_TEXT can only be mapped to String");
                }
            };
        } catch (Exception e) {
            throw new IllegalStateException("Failed to map message body", e);
        }
    }

    public String getBody() {
        return body;
    }

    public MessageContentType getContentType() {
        return contentType;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public String getSource() {
        return source;
    }

    private JsonNode readJsonLikeNode() {
        try {
            return switch (contentType) {
                case JSON -> JSON_MAPPER.readTree(body);
                case XML -> XML_MAPPER.readTree(body.getBytes());
                case RAW_TEXT -> {
                    if (StringUtils.isBlank(body)) {
                        yield MissingNode.getInstance();
                    }
                    yield JSON_MAPPER.readTree(body);
                }
            };
        } catch (Exception e) {
            throw new IllegalStateException("Unable to parse message as JSON-like content", e);
        }
    }

    private static JsonNode extractNode(String path, JsonNode rootNode) {
        return Arrays.stream(path.split("\\."))
                .filter(StringUtils::isNotEmpty)
                .reduce(rootNode,
                        (r, p) -> {
                            Matcher matcher = ARRAY_NODE_PATTERN.matcher(p);
                            if (matcher.find()) {
                                return r.path(matcher.group(1)).path(Integer.parseInt(matcher.group(2)));
                            }
                            return r.path(p);
                        },
                        (j1, j2) -> j1);
    }

    private static final class EmptyNamespaceContext implements NamespaceContext {

        @Override
        public String getNamespaceURI(String prefix) {
            return XMLConstants.NULL_NS_URI;
        }

        @Override
        public String getPrefix(String namespaceURI) {
            return "";
        }

        @Override
        public Iterator<String> getPrefixes(String namespaceURI) {
            return Collections.emptyIterator();
        }
    }
}
