package cz.moneta.test.harness.messaging;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Represents a received message from a messaging system.
 * Body is always normalized to a String regardless of source and wire format.
 * <p>
 * For Kafka: Avro GenericRecord is automatically converted to JSON.
 * For IBM MQ (JSON): JSON string from JMS TextMessage.
 * For IBM MQ (XML): XML string from JMS TextMessage.
 * For IBM MQ (EBCDIC): byte[] from JMS BytesMessage decoded from IBM-870.
 */
public class ReceivedMessage {

    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
    private static final Pattern JSON_PATH_PATTERN = Pattern.compile("^([\\w.]+)\\[([\\d]+)\\](.*)$");

    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;

    public ReceivedMessage(String body, MessageContentType contentType, Map<String, String> headers,
                           long timestamp, String source, String key) {
        this.body = body;
        this.contentType = contentType;
        this.headers = headers != null ? Collections.unmodifiableMap(new HashMap<>(headers)) : new HashMap<>();
        this.timestamp = timestamp;
        this.source = source;
        this.key = key;
    }

    /**
     * Extract a JSON value using JSON path (dot/bracket notation).
     * Supports paths like "items[0].sku" or "nested.field".
     *
     * @param path JSON path
     * @return JsonNode for the extracted value
     */
    public JsonNode extractJson(String path) {
        if (body == null || StringUtils.isEmpty(path)) {
            return null;
        }

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

    /**
     * Extract a value using XPath (for XML messages).
     *
     * @param xpathExpression XPath expression
     * @return extracted value as string
     */
    public String extractXml(String xpathExpression) {
        if (body == null || StringUtils.isEmpty(xpathExpression)) {
            return null;
        }

        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);
            DocumentBuilder builder = factory.newDocumentBuilder();
            javax.xml.parsers.DocumentBuilder finalBuilder = builder;

            var document = finalBuilder.parse(new java.io.ByteArrayInputStream(body.getBytes()));
            document.getDocumentElement().normalize();

            XPath xpath = XPathFactory.newInstance().newXPath();
            Object result = xpath.evaluate(xpathExpression, document, XPathConstants.NODE);

            if (result instanceof org.w3c.dom.Node domNode) {
                return domNode.getTextContent();
            }

            return null;
        } catch (Exception e) {
            throw new RuntimeException("Failed to evaluate XPath: " + xpathExpression, e);
        }
    }

    /**
     * Universal extract method - auto-detects content type and uses appropriate extraction.
     *
     * @param expression JSON path or XPath expression
     * @return 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;
        };
    }

    /**
     * Evaluate JSON path on a JSON node.
     * Supports dot notation and bracket notation for arrays.
     */
    private JsonNode evaluateJsonPath(JsonNode node, String path) {
        if (StringUtils.isEmpty(path)) {
            return node;
        }

        String[] parts = tokenizePath(path);
        JsonNode current = node;

        for (String part : parts) {
            if (current == null) {
                return null;
            }

            if (part.isEmpty()) {
                continue;
            }

            if (part.contains("[")) {
                // Array access
                Matcher matcher = JSON_PATH_PATTERN.matcher(part);
                if (matcher.matches()) {
                    String arrayName = matcher.group(1);
                    int index = Integer.parseInt(matcher.group(2));
                    String remaining = matcher.group(3);

                    if (current.isArray()) {
                        current = index < current.size() ? current.get(index) : null;
                    } else if (current.isObject()) {
                        JsonNode arrayNode = current.get(arrayName);
                        if (arrayNode != null && arrayNode.isArray()) {
                            current = index < arrayNode.size() ? arrayNode.get(index) : null;
                        } else {
                            current = null;
                        }
                    }

                    // Continue with remaining path
                    if (StringUtils.isNotBlank(remaining)) {
                        current = evaluateJsonPath(current, remaining);
                    }
                } else {
                    current = current.get(part);
                }
            } else if (part.contains(".")) {
                // Navigate through object properties
                String[] segments = part.split("\\.");
                for (String segment : segments) {
                    if (StringUtils.isEmpty(segment)) {
                        continue;
                    }
                    current = current.get(segment);
                }
            } else {
                current = current.get(part);
            }
        }

        return current;
    }

    /**
     * Tokenize JSON path into segments.
     */
    private String[] tokenizePath(String path) {
        if (StringUtils.isEmpty(path)) {
            return new String[0];
        }

        java.util.List<String> tokens = new java.util.ArrayList<>();
        StringBuilder current = new StringBuilder();
        boolean inBracket = false;

        for (int i = 0; i < path.length(); i++) {
            char c = path.charAt(i);
            if (c == '[') {
                inBracket = true;
                current.append(c);
            } else if (c == ']') {
                inBracket = false;
                current.append(c);
            } else if (c == '.' && !inBracket) {
                if (current.length() > 0) {
                    tokens.add(current.toString());
                    current.setLength(0);
                }
            } else {
                current.append(c);
            }
        }

        if (current.length() > 0) {
            tokens.add(current.toString());
        }

        return tokens.toArray(new String[0]);
    }

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

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

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

    /**
     * Get the message body.
     */
    public String getBody() {
        return body;
    }

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

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

    /**
     * Get the source (topic or queue name).
     */
    public String getSource() {
        return source;
    }

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

        try {
            if (contentType == MessageContentType.XML) {
                // XML deserialization using JAXB
                return mapXmlTo(type);
            } else {
                // JSON deserialization using Jackson
                return JSON_MAPPER.readValue(body, type);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to deserialize message to " + type.getName(), e);
        }
    }

    /**
     * Deserialize the message body to a Java object for XML.
     * Uses JAXB-like parsing - simplified for basic XML structures.
     */
    private <T> T mapXmlTo(Class<T> type) {
        try {
            // For XML, parse to a simple map structure
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            javax.xml.parsers.DocumentBuilder finalBuilder = builder;

            var document = finalBuilder.parse(new java.io.ByteArrayInputStream(body.getBytes()));
            document.getDocumentElement().normalize();

            Map<String, Object> xmlMap = xmlToMap(document.getDocumentElement());
            return JSON_MAPPER.convertValue(xmlMap, type);
        } catch (Exception e) {
            throw new RuntimeException("Failed to deserialize XML message", e);
        }
    }

    /**
     * Convert XML element to Map.
     */
    private Map<String, Object> xmlToMap(org.w3c.dom.Element element) {
        Map<String, Object> result = new HashMap<>();

        for (int i = 0; i < element.getAttributes().getLength(); i++) {
            org.w3c.dom.NamedNodeMap attributes = element.getAttributes();
            org.w3c.dom.Node attr = attributes.item(i);
            result.put("@" + attr.getNodeName(), attr.getNodeValue());
        }

        // Add children
        for (int i = 0; i < element.getChildNodes().getLength(); i++) {
            org.w3c.dom.Node node = element.getChildNodes().item(i);
            if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
                org.w3c.dom.Element childElement = (org.w3c.dom.Element) node;
                String tagName = childElement.getTagName();

                if (childElement.getChildNodes().getLength() == 0) {
                    // Leaf element
                    result.put(tagName, childElement.getTextContent());
                } else {
                    // Check if all children are elements (complex) or text (simple)
                    boolean hasElement = false;
                    for (int j = 0; j < childElement.getChildNodes().getLength(); j++) {
                        org.w3c.dom.Node childNode = childElement.getChildNodes().item(j);
                        if (childNode.getNodeType() == org.w3c.dom.Node.TEXT_NODE &&
                                StringUtils.isNotBlank(childNode.getTextContent())) {
                        } else if (childNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
                            hasElement = true;
                        }
                    }

                    if (hasElement) {
                        Map<String, Object> childMap = xmlToMap(childElement);
                        if (result.containsKey(tagName)) {
                            // Convert to list if multiple elements with same name
                            java.util.List<Object> list = new java.util.ArrayList<>();
                            if (result.get(tagName) instanceof Map) {
                                list.add(result.get(tagName));
                            }
                            list.add(childMap);
                            result.put(tagName, list);
                        } else {
                            result.put(tagName, childMap);
                        }
                    } else {
                        result.put(tagName, childElement.getTextContent());
                    }
                }
            }
        }

        // If element has only text content and no attributes or children, return text
        if (element.getChildNodes().getLength() == 0) {
            Map<String, Object> textMap = new HashMap<>();
            textMap.put("#text", element.getTextContent());
            return textMap;
        }

        return result;
    }

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