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

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

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.w3c.dom.Document;

/**
 * Represents a received message from a messaging system (IBM MQ or Kafka).
 * <p>
 * Provides unified API for accessing message content regardless of source system.
 * Body is always normalized to a String, with content type detection for proper extraction.
 * </p>
 */
public class ReceivedMessage {

    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;
    private final String key;

    private ReceivedMessage(Builder builder) {
        this.body = builder.body;
        this.contentType = builder.contentType;
        this.headers = builder.headers != null ? Collections.unmodifiableMap(new HashMap<>(builder.headers)) : Collections.emptyMap();
        this.timestamp = builder.timestamp;
        this.source = builder.source;
        this.key = builder.key;
    }

    /**
     * Creates a new builder for ReceivedMessage.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Extracts a value from the message body using JSON dot-path or bracket notation.
     * <p>
     * Supports paths like:
     * <ul>
     *   <li>{@code "field"} - top-level field</li>
     *   <li>{@code "parent.child"} - nested field</li>
     *   <li>{@code "items[0]"} - array element</li>
     *   <li>{@code "items[0].name"} - nested field in array element</li>
     * </ul>
     * </p>
     *
     * @param path the JSON path expression
     * @return the extracted value as JsonNode
     */
    public JsonNode extractJson(String path) {
        if (contentType != MessageContentType.JSON) {
            throw new IllegalStateException("JSON extraction is only supported for JSON content type, got: " + contentType);
        }
        try {
            JsonNode rootNode = JSON_MAPPER.readTree(body);
            return extractNode(path, rootNode);
        } catch (IOException e) {
            throw new RuntimeException("Failed to parse JSON body: " + body, e);
        }
    }

    /**
     * Extracts a value from XML message body using XPath expression.
     *
     * @param xpath the XPath expression
     * @return the extracted value as String
     */
    public String extractXml(String xpath) {
        if (contentType != MessageContentType.XML) {
            throw new IllegalStateException("XML extraction is only supported for XML content type, got: " + contentType);
        }
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)));
            XPath xPath = XPathFactory.newInstance().newXPath();
            return xPath.evaluate(xpath, doc);
        } catch (XPathExpressionException e) {
            throw new RuntimeException("Failed to evaluate XPath: " + xpath, e);
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse XML body: " + body, e);
        }
    }

    /**
     * Extracts a value from the message body. Auto-detects content type and uses appropriate extraction method.
     *
     * @param expression the path expression (JSON path for JSON, XPath for XML)
     * @return the extracted value as String
     */
    public String extract(String expression) {
        return switch (contentType) {
            case JSON -> extractJson(expression).asText();
            case XML -> extractXml(expression);
            case RAW_TEXT -> body;
        };
    }

    /**
     * Returns the message body as a String.
     *
     * @return the body content
     */
    public String getBody() {
        return body;
    }

    /**
     * Returns the message content type.
     *
     * @return the content type
     */
    public MessageContentType getContentType() {
        return contentType;
    }

    /**
     * Returns a header value by name.
     *
     * @param name the header name
     * @return the header value, or null if not present
     */
    public String getHeader(String name) {
        return headers.get(name);
    }

    /**
     * Returns all headers.
     *
     * @return unmodifiable map of headers
     */
    public Map<String, String> getHeaders() {
        return headers;
    }

    /**
     * Returns the message timestamp.
     *
     * @return timestamp in milliseconds
     */
    public long getTimestamp() {
        return timestamp;
    }

    /**
     * Returns the source (topic name for Kafka, queue name for IBM MQ).
     *
     * @return the source name
     */
    public String getSource() {
        return source;
    }

    /**
     * Returns the message key (Kafka only, null for IBM MQ).
     *
     * @return the message key or null
     */
    public String getKey() {
        return key;
    }

    /**
     * Deserializes the message body into a Java object.
     * <p>
     * For JSON content: uses Jackson ObjectMapper.readValue
     * For XML content: uses Jackson XmlMapper
     * </p>
     *
     * @param type the target class
     * @param <T> the target type
     * @return the deserialized object
     */
    public <T> T mapTo(Class<T> type) {
        try {
            if (contentType == MessageContentType.XML) {
                return XML_MAPPER.readValue(body, type);
            } else {
                return JSON_MAPPER.readValue(body, type);
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to deserialize message body to " + type.getName(), e);
        }
    }

    /**
     * Builder for ReceivedMessage.
     */
    public static class Builder {
        private String body;
        private MessageContentType contentType = MessageContentType.JSON;
        private Map<String, String> headers;
        private long timestamp = System.currentTimeMillis();
        private String source;
        private String key;

        public Builder body(String body) {
            this.body = body;
            return this;
        }

        public Builder contentType(MessageContentType contentType) {
            this.contentType = contentType;
            return this;
        }

        public Builder headers(Map<String, String> headers) {
            this.headers = headers;
            return this;
        }

        public Builder timestamp(long timestamp) {
            this.timestamp = timestamp;
            return this;
        }

        public Builder source(String source) {
            this.source = source;
            return this;
        }

        public Builder key(String key) {
            this.key = key;
            return this;
        }

        public ReceivedMessage build() {
            return new ReceivedMessage(this);
        }
    }

    /**
     * Extracts a node from JSON using dot/bracket notation.
     */
    private static JsonNode extractNode(String path, JsonNode rootNode) {
        Pattern arrayPattern = Pattern.compile("(.*?)\\[([0-9]+)\\]");
        return Arrays.stream(path.split("\\."))
                .filter(StringUtils::isNotEmpty)
                .reduce(rootNode,
                        (node, part) -> {
                            Matcher matcher = arrayPattern.matcher(part);
                            if (matcher.find()) {
                                return node.path(matcher.group(1)).path(Integer.parseInt(matcher.group(2)));
                            } else {
                                return node.path(part);
                            }
                        },
                        (n1, n2) -> n1);
    }

    /**
     * Converts a cz.moneta.test.harness.messaging.ReceivedMessage to this class.
     *
     * @param other the message to convert
     * @return converted message
     */
    public static ReceivedMessage fromMessagingReceivedMessage(
            cz.moneta.test.harness.messaging.ReceivedMessage other) {
        if (other == null) {
            return null;
        }
        cz.moneta.test.harness.support.messaging.MessageContentType contentType =
                switch (other.getContentType()) {
                    case JSON -> cz.moneta.test.harness.support.messaging.MessageContentType.JSON;
                    case XML -> cz.moneta.test.harness.support.messaging.MessageContentType.XML;
                    case RAW_TEXT -> cz.moneta.test.harness.support.messaging.MessageContentType.RAW_TEXT;
                };
        return builder()
                .body(other.getBody())
                .contentType(contentType)
                .headers(other.getHeaders())
                .timestamp(other.getTimestamp())
                .source(other.getSource())
                .key(other.getKey())
                .build();
    }
}
