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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionEndpoint;
import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionQueue;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.messaging.exception.MessagingTimeoutException;
import cz.moneta.test.harness.support.util.FileReader;
import cz.moneta.test.harness.support.util.Template;
import org.apache.commons.lang3.StringUtils;

import java.time.Duration;
import java.util.*;
import java.util.function.Predicate;

/**
 * Fluent builder for IBM MQ requests.
 * <p>
 * Usage:
 * <pre>{@code
 * ImqRequest.toQueue(endpoint, queue)
 *     .asJson()
 *     .withPayload("{\"field\": \"value\"}")
 *     .send();
 *
 * ImqRequest.fromQueue(endpoint, queue)
 *     .receiveWhere(msg -> msg.extract("field").equals("value"))
 *     .withTimeout(10, TimeUnit.SECONDS)
 *     .andAssertFieldValue("result", "OK");
 * }</pre>
 * </p>
 */
@SuppressWarnings("unchecked")
public final class ImqRequest {

    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final String PROP_LOGICAL_QUEUE = "LogicalQueue";
    private static final String PROP_SELECTOR = "Selector";
    public static final String PROP_JMS_CORRELATION_ID = "JMSCorrelationID";
    public static final String PROP_JMS_MESSAGE_ID = "JMSMessageID";
    public static final String PROP_JMS_TYPE = "JMSType";

    private ImqRequest() {
    }

    /**
     * Start building a message to send to a queue.
     */
    public static QueuePhase toQueue(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue queue) {
        return new QueueBuilder(endpoint, queue, null);
    }

    /**
     * Start building a message to receive from a queue.
     */
    public static ReceivePhase fromQueue(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue queue) {
        return new QueueBuilder(endpoint, queue, null);
    }

    /**
     * Start building a message to send to a queue by physical name.
     */
    public static QueuePhase toQueue(ImqFirstVisionEndpoint endpoint, String queueName) {
        return new QueueBuilder(endpoint, null, queueName);
    }

    /**
     * Start building a message to receive from a queue by physical name.
     */
    public static ReceivePhase fromQueue(ImqFirstVisionEndpoint endpoint, String queueName) {
        return new QueueBuilder(endpoint, null, queueName);
    }

    /**
     * Phase after specifying queue - can send or receive.
     */
    public interface QueuePhase extends PayloadPhase, ReceivePhase {
    }

    /**
     * Phase for building message payload.
     */
    public interface PayloadPhase {
        /**
         * Set message format to JSON (default).
         */
        QueuePhase asJson();

        /**
         * Set message format to XML.
         */
        QueuePhase asXml();

        /**
         * Set message format to EBCDIC (IBM-870).
         */
        QueuePhase asEbcdic();

        /**
         * Set message format to UTF-8 (CCSID 1208).
         */
        QueuePhase asUtf8();

        /**
         * Set message payload as JSON string.
         */
        PayloadPhase withPayload(String payload);

        /**
         * Load payload from resource file.
         */
        PayloadPhase withPayloadFromFile(String path);

        /**
         * Render payload from template.
         */
        PayloadPhase withPayloadFromTemplate(Template template);

        /**
         * Add field to JSON payload at root level.
         */
        PayloadPhase addField(String fieldName, Object value);

        /**
         * Add field to JSON payload at specified path.
         */
        PayloadPhase addField(String path, String fieldName, Object value);

        /**
         * Append value to JSON array at specified path.
         */
        PayloadPhase appendToArray(String path, Object value);

        /**
         * Set JMS correlation ID.
         */
        PayloadPhase withCorrelationId(String correlationId);

        /**
         * Set JMS message ID.
         */
        PayloadPhase withMessageId(String messageId);

        /**
         * Set JMS type.
         */
        PayloadPhase withJmsType(String jmsType);

        /**
         * Send the message.
         */
        void send();
    }

    /**
     * Phase for receiving messages.
     */
    public interface ReceivePhase {
        /**
         * Set JMS message selector.
         */
        ReceivePhase withSelector(String selector);

        /**
         * Start receiving message matching predicate.
         */
        AwaitingPhase receiveWhere(Predicate<ReceivedMessage> filter);

        /**
         * Browse messages from queue (non-destructive).
         */
        List<ReceivedMessage> browse(int maxMessages);

        /**
         * Browse messages from queue with selector (non-destructive).
         */
        List<ReceivedMessage> browse(String selector, int maxMessages);

        /**
         * Set message format to JSON (default) when receiving.
         */
        QueuePhase asJson();

        /**
         * Set message format to XML when receiving.
         */
        QueuePhase asXml();

        /**
         * Set message format to EBCDIC (IBM-870) when receiving.
         */
        QueuePhase asEbcdic();

        /**
         * Set message format to UTF-8 (CCSID 1208) when receiving.
         */
        QueuePhase asUtf8();
    }

    /**
     * Phase after specifying filter - await message with timeout.
     */
    public interface AwaitingPhase {
        /**
         * Set timeout and get message response for assertions.
         */
        MessageResponse withTimeout(long duration, java.util.concurrent.TimeUnit unit);

        /**
         * Set timeout duration and get message response for assertions.
         */
        MessageResponse withTimeout(Duration timeout);
    }

    /**
     * Response with fluent assertions.
     */
    public interface MessageResponse extends cz.moneta.test.harness.support.messaging.MessageResponse {
    }

    /**
     * Builder for queue operations.
     */
    private static class QueueBuilder implements QueuePhase, AwaitingPhase {

        private final ImqFirstVisionEndpoint endpoint;
        private final ImqFirstVisionQueue logicalQueue;
        private final String physicalQueue;
        private String selector;
        private MqMessageFormat format = MqMessageFormat.JSON;
        private String payload;
        private Map<String, Object> fields = new HashMap<>();
        private List<Map.Entry<String, Object>> arrayAppends = new ArrayList<>();
        private String correlationId;
        private String messageId;
        private String jmsType;
        private Predicate<ReceivedMessage> filter;
        private Duration timeout;

        public QueueBuilder(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue logicalQueue, String physicalQueue) {
            this.endpoint = endpoint;
            this.logicalQueue = logicalQueue;
            this.physicalQueue = physicalQueue;
        }

        private String getQueueName() {
            if (logicalQueue != null) {
                return endpoint.resolveQueue(logicalQueue);
            }
            return physicalQueue;
        }

        @Override
        public QueuePhase asJson() {
            this.format = MqMessageFormat.JSON;
            return this;
        }

        @Override
        public QueuePhase asXml() {
            this.format = MqMessageFormat.XML;
            return this;
        }

        @Override
        public QueuePhase asEbcdic() {
            this.format = MqMessageFormat.EBCDIC_870;
            return this;
        }

        @Override
        public QueuePhase asUtf8() {
            this.format = MqMessageFormat.UTF8_1208;
            return this;
        }

        @Override
        public PayloadPhase withPayload(String payload) {
            this.payload = payload;
            return this;
        }

        @Override
        public PayloadPhase withPayloadFromFile(String path) {
            this.payload = FileReader.readFileFromResources(path);
            return this;
        }

        @Override
        public PayloadPhase withPayloadFromTemplate(Template template) {
            this.payload = template.render();
            return this;
        }

        @Override
        public PayloadPhase addField(String fieldName, Object value) {
            return addField("", fieldName, value);
        }

        @Override
        public PayloadPhase addField(String path, String fieldName, Object value) {
            String key = StringUtils.isNotBlank(path) && StringUtils.isNotBlank(fieldName) ? path + "." + fieldName : fieldName;
            this.fields.put(key, value);
            return this;
        }

        @Override
        public PayloadPhase appendToArray(String path, Object value) {
            this.arrayAppends.add(Map.entry(path, value));
            return this;
        }

        @Override
        public PayloadPhase withCorrelationId(String correlationId) {
            this.correlationId = correlationId;
            return this;
        }

        @Override
        public PayloadPhase withMessageId(String messageId) {
            this.messageId = messageId;
            return this;
        }

        @Override
        public PayloadPhase withJmsType(String jmsType) {
            this.jmsType = jmsType;
            return this;
        }

        @Override
        public void send() {
            String finalPayload = buildPayload();
            Map<String, String> properties = new HashMap<>();

            if (logicalQueue != null) {
                properties.put(PROP_LOGICAL_QUEUE, logicalQueue.name());
            }
            if (selector != null && !selector.isBlank()) {
                properties.put(PROP_SELECTOR, selector);
            }
            if (correlationId != null) {
                properties.put(PROP_JMS_CORRELATION_ID, correlationId);
            }
            if (messageId != null) {
                properties.put(PROP_JMS_MESSAGE_ID, messageId);
            }
            if (jmsType != null) {
                properties.put(PROP_JMS_TYPE, jmsType);
            }

            endpoint.send(getQueueName(), finalPayload, format, properties);
        }

        @Override
        public ReceivePhase withSelector(String selector) {
            this.selector = selector;
            return this;
        }

        @Override
        public AwaitingPhase receiveWhere(Predicate<ReceivedMessage> filter) {
            this.filter = filter;
            return this;
        }

        @Override
        public List<ReceivedMessage> browse(int maxMessages) {
            return browse(selector, maxMessages);
        }

        @Override
        public List<ReceivedMessage> browse(String sel, int maxMessages) {
            return endpoint.browse(getQueueName(), sel, format, maxMessages);
        }

        @Override
        public MessageResponse withTimeout(long duration, java.util.concurrent.TimeUnit unit) {
            return withTimeout(Duration.of(duration, unit.toChronoUnit()));
        }

        @Override
        public MessageResponse withTimeout(Duration timeout) {
            this.timeout = timeout;

            if (filter == null) {
                throw new IllegalStateException("Must specify receiveWhere filter before withTimeout");
            }

            ReceivedMessage message = receiveMessage();
            return new ResponseImpl(message);
        }

        private ReceivedMessage receiveMessage() {
            long startTime = System.currentTimeMillis();
            long timeoutMs = timeout.toMillis();

            while (System.currentTimeMillis() - startTime < timeoutMs) {
                try {
                    ReceivedMessage message = endpoint.receive(getQueueName(), selector, format, Duration.ofSeconds(1));
                    if (filter.test(message)) {
                        return message;
                    }
                } catch (MessagingTimeoutException e) {
                    // Continue polling
                }
            }

            throw new MessagingTimeoutException(
                    "No message matching filter found on queue '" + getQueueName() +
                            "' within " + timeout.toMillis() + "ms");
        }

        private String buildPayload() {
            if (payload == null) {
                return "{}";
            }

            if (fields.isEmpty() && arrayAppends.isEmpty()) {
                return payload;
            }

            try {
                Map<String, Object> json = JSON_MAPPER.readValue(payload, Map.class);

                for (Map.Entry<String, Object> entry : fields.entrySet()) {
                    setField(json, entry.getKey(), entry.getValue());
                }

                for (Map.Entry<String, Object> entry : arrayAppends) {
                    appendToArray(json, entry.getKey(), entry.getValue());
                }

                return JSON_MAPPER.writeValueAsString(json);
            } catch (Exception e) {
                throw new HarnessException("Failed to build payload", e);
            }
        }

        private void setField(Map<String, Object> json, String path, Object value) {
            if (StringUtils.isBlank(path)) {
                json.put(path, value);
                return;
            }

            String[] parts = path.split("\\.");
            Map<String, Object> current = json;

            for (int i = 0; i < parts.length - 1; i++) {
                String part = parts[i];
                Object next = current.get(part);
                if (!(next instanceof Map)) {
                    next = new HashMap<String, Object>();
                    current.put(part, next);
                }
                current = (Map<String, Object>) next;
            }

            current.put(parts[parts.length - 1], value);
        }

        private void appendToArray(Map<String, Object> json, String path, Object value) {
            String[] parts = path.split("\\.");
            Map<String, Object> current = json;

            for (int i = 0; i < parts.length - 1; i++) {
                String part = parts[i];
                Object next = current.get(part);
                if (!(next instanceof Map)) {
                    next = new HashMap<String, Object>();
                    current.put(part, next);
                }
                current = (Map<String, Object>) next;
            }

            String arrayKey = parts[parts.length - 1];
            Object arrayValue = current.get(arrayKey);
            List<Map<String, Object>> array;
            if (arrayValue instanceof List) {
                array = (List<Map<String, Object>>) arrayValue;
            } else {
                array = new ArrayList<>();
                current.put(arrayKey, array);
            }
            array.add((Map<String, Object>) value);
        }
    }

    /**
     * Response implementation with assertions.
     */
    private static class ResponseImpl implements MessageResponse {

        private final ReceivedMessage message;

        public ResponseImpl(ReceivedMessage message) {
            this.message = message;
        }

        @Override
        public MessageResponse andAssertFieldValue(String path, String value) {
            String actual = message.extract(path);
            if (!Objects.equals(value, actual)) {
                throw new AssertionError(String.format("Expected field '%s' to be '%s' but was '%s'", path, value, actual));
            }
            return this;
        }

        @Override
        public MessageResponse andAssertPresent(String path) {
            JsonPathValue extracted = new JsonPathValue(message.extract(path));
            if (extracted.asText() == null && !isPresentInJson(path)) {
                throw new AssertionError(String.format("Expected field '%s' to be present", path));
            }
            return this;
        }

        @Override
        public MessageResponse andAssertNotPresent(String path) {
            if (isPresentInJson(path)) {
                throw new AssertionError(String.format("Expected field '%s' to be absent", path));
            }
            return this;
        }

        @Override
        public MessageResponse andAssertHeaderValue(String headerName, String value) {
            String actual = message.getHeader(headerName);
            if (!Objects.equals(value, actual)) {
                throw new AssertionError(String.format("Expected header '%s' to be '%s' but was '%s'", headerName, value, actual));
            }
            return this;
        }

        @Override
        public MessageResponse andAssertBodyContains(String substring) {
            if (!message.getBody().contains(substring)) {
                throw new AssertionError(String.format("Body does not contain '%s'", substring));
            }
            return this;
        }

        @Override
        public JsonPathValue extract(String path) {
            return new JsonPathValue(message.extract(path));
        }

        @Override
        public <T> T mapTo(Class<T> type) {
            return message.mapTo(type);
        }

        @Override
        public ReceivedMessage getMessage() {
            return message;
        }

        @Override
        public String getBody() {
            return message.getBody();
        }

        @Override
        public String getHeader(String name) {
            return message.getHeader(name);
        }

        private boolean isPresentInJson(String path) {
            try {
                String body = message.getBody();
                if (message.getContentType() != MessageContentType.JSON) {
                    return false;
                }
                JsonNode node = JSON_MAPPER.readTree(body);
                JsonNode target = extractNode(path, node);
                return target != null && !target.isMissingNode();
            } catch (Exception e) {
                return false;
            }
        }

        private JsonNode extractNode(String path, JsonNode node) {
            return Arrays.stream(path.split("\\."))
                    .filter(StringUtils::isNotEmpty)
                    .reduce(node,
                            (n, p) -> n.isContainerNode() ? n.get(p) : n,
                            (n1, n2) -> n1);
        }
    }
}
