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

import java.io.StringReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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 org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import net.javacrumbs.jsonunit.assertj.JsonAssert;

/**
 * Represents a received message from a messaging system (Kafka or IBM MQ).
 * Provides unified API for extracting data regardless of the source system.
 */
public class ReceivedMessage {

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

    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 ? new HashMap<>(builder.headers) : new HashMap<>();
        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 value using JSON path (dot/bracket notation).
     * For XML content, automatically converts to XPath evaluation.
     *
     * @param path JSON path (e.g., "items[0].sku") or XPath for XML
     * @return JsonNode representing the extracted value
     */
    public JsonNode extractJson(String path) {
        if (body == null) {
            return null;
        }

        if (contentType == MessageContentType.XML) {
            // For XML, try XPath first, then fall back to JSON path
            try {
                return extractAsXml(path);
            } catch (Exception e) {
                // Fall through to JSON parsing
            }
        }

        try {
            JsonNode rootNode = JSON_MAPPER.readTree(body);
            return extractNode(path, rootNode);
        } catch (Exception e) {
            throw new RuntimeException("Failed to extract JSON path '" + path + "' from body: " + e.getMessage(), e);
        }
    }

    /**
     * Extracts value using XPath expression (for XML messages).
     *
     * @param xpath XPath expression (e.g., "/response/balance")
     * @return String value of the extracted node
     */
    public String extractXml(String xpath) {
        if (body == null) {
            return null;
        }

        try {
            Document doc = XML_MAPPER.readTree(body).isMissingNode() ? null : XML_MAPPER.readTree(body).isObject()
                    ? parseXmlToDocument(body)
                    : parseXmlToDocument(body);

            if (doc == null) {
                doc = parseXmlToDocument(body);
            }

            XPath xPath = XPathFactory.newInstance().newXPath();
            NodeList nodes = (NodeList) xPath.evaluate(xpath, doc, XPathConstants.NODESET);

            if (nodes.getLength() == 0) {
                return null;
            }

            return nodes.item(0).getTextContent();
        } catch (Exception e) {
            throw new RuntimeException("Failed to evaluate XPath '" + xpath + "' on body: " + e.getMessage(), e);
        }
    }

    /**
     * Universal extract method - auto-detects content type and evaluates expression accordingly.
     *
     * @param expression JSON path for JSON content, XPath for XML content
     * @return String value of the extracted node
     */
    public String extract(String expression) {
        if (body == null) {
            return null;
        }

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

    /**
     * Deserializes the message body to a Java object.
     *
     * @param type the target type
     * @param <T>  the target type
     * @return deserialized object
     */
    public <T> T mapTo(Class<T> type) {
        if (body == null) {
            return null;
        }

        try {
            if (contentType == MessageContentType.JSON) {
                return JSON_MAPPER.readValue(body, type);
            } else if (contentType == MessageContentType.XML) {
                return XML_MAPPER.readValue(body, type);
            } else {
                throw new IllegalStateException("Cannot deserialize RAW_TEXT to " + type.getName());
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to deserialize body to " + type.getName() + ": " + e.getMessage(), e);
        }
    }

    /**
     * Deserializes the message body to a list of objects.
     *
     * @param type the element type
     * @param <T>  the element type
     * @return list of deserialized objects
     */
    public <T> List<T> mapToList(Class<T> type) {
        if (body == null) {
            return Collections.emptyList();
        }

        try {
            if (contentType == MessageContentType.JSON) {
                return JSON_MAPPER.readValue(body, new TypeReference<List<T>>() {});
            } else {
                throw new IllegalStateException("Cannot deserialize to list for content type " + contentType);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to deserialize body to List<" + type.getName() + ">: " + e.getMessage(), e);
        }
    }

    /**
     * Gets the message body as string.
     */
    public String getBody() {
        return body;
    }

    /**
     * Gets the message key (Kafka) or null (IBM MQ).
     */
    public String getKey() {
        return key;
    }

    /**
     * Gets header value by name (Kafka header or JMS property).
     */
    public String getHeader(String name) {
        return headers.get(name);
    }

    /**
     * Gets all headers.
     */
    public Map<String, String> getHeaders() {
        return Collections.unmodifiableMap(headers);
    }

    /**
     * Gets the message timestamp.
     */
    public long getTimestamp() {
        return timestamp;
    }

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

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

    /**
     * Creates JsonAssert for AssertJ-style assertions on the message body.
     */
    public JsonAssert.ConfigurableJsonAssert andAssertWithAssertJ() {
        if (body == null) {
            throw new IllegalStateException("Cannot assert on null body");
        }
        return net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson(body);
    }

    private JsonNode extractNode(String path, JsonNode rootNode) {
        if (StringUtils.isBlank(path)) {
            return 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.valueOf(matcher.group(2)));
                            } else {
                                return r.path(p);
                            }
                        },
                        (j1, j2) -> j1);
    }

    private JsonNode extractAsXml(String path) throws Exception {
        // Try XPath first
        try {
            String value = extractXml(path);
            if (value != null) {
                return JSON_MAPPER.valueToTree(value);
            }
        } catch (Exception e) {
            // Fall back to JSON path on XML body
        }
        return extractNode(path, XML_MAPPER.readTree(body));
    }

    private Document parseXmlToDocument(String xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(new InputSource(new StringReader(xml)));
    }

    /**
     * Builder for ReceivedMessage.
     */
    public static class Builder {
        private String body;
        private MessageContentType contentType = MessageContentType.JSON;
        private Map<String, String> headers = new HashMap<>();
        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);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ReceivedMessage that = (ReceivedMessage) o;
        return timestamp == that.timestamp &&
                Objects.equals(body, that.body) &&
                contentType == that.contentType &&
                Objects.equals(headers, that.headers) &&
                Objects.equals(source, that.source) &&
                Objects.equals(key, that.key);
    }

    @Override
    public int hashCode() {
        return Objects.hash(body, contentType, headers, timestamp, source, key);
    }

    @Override
    public String toString() {
        return "ReceivedMessage{" +
                "body='" + body + '\'' +
                ", contentType=" + contentType +
                ", headers=" + headers +
                ", timestamp=" + timestamp +
                ", source='" + source + '\'' +
                ", key='" + key + '\'' +
                '}';
    }
}
