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

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

import javax.jms.BytesMessage;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.JMSProducer;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.TextMessage;

import com.ibm.mq.jms.MQConnectionFactory;
import com.ibm.msg.client.wmq.WMQConstants;

import cz.moneta.test.harness.connectors.Connector;
import cz.moneta.test.harness.exception.MessagingTimeoutException;
import cz.moneta.test.harness.messaging.model.MessageContentType;
import cz.moneta.test.harness.messaging.model.MqMessageFormat;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;

public class IbmMqConnector implements Connector {

    private static final Charset EBCDIC_870 = Charset.forName("IBM870");
    private static final Charset UTF_8 = StandardCharsets.UTF_8;
    private final MQConnectionFactory connectionFactory;
    private final String user;
    private final String password;
    private final Object contextLock = new Object();
    private volatile JMSContext jmsContext;

    public IbmMqConnector(String host,
                          int port,
                          String channel,
                          String queueManager,
                          String user,
                          String password,
                          String keystorePath,
                          String keystorePassword,
                          String cipherSuite) {
        this.user = user;
        this.password = password;
        try {
            if (keystorePath != null && !keystorePath.isBlank()) {
                System.setProperty("javax.net.ssl.keyStore", keystorePath);
        		System.setProperty("javax.net.ssl.trustStore", keystorePath);
                if (keystorePassword != null) {
                    System.setProperty("javax.net.ssl.keyStorePassword", keystorePassword);
            		System.setProperty("javax.net.ssl.trustStorePassword", keystorePassword);
                }
            }

            connectionFactory = new MQConnectionFactory();
            connectionFactory.setHostName(host);
            connectionFactory.setPort(port);
            connectionFactory.setQueueManager(queueManager);
            connectionFactory.setChannel(channel);
            connectionFactory.setTransportType(WMQConstants.WMQ_CM_CLIENT);
            if (user != null && !user.isBlank()) {
            	connectionFactory.setStringProperty(WMQConstants.USERID, user);
            }
            if (password != null && !password.isBlank()) {
            	connectionFactory.setStringProperty(WMQConstants.PASSWORD, password);
            }

            if (cipherSuite != null && !cipherSuite.isBlank()) {
            	connectionFactory.setSSLCipherSuite(cipherSuite);
            }
        } catch (Exception e) {
            throw new IllegalStateException("Failed to initialize IBM MQ connection factory", e);
        }
    }

    public void send(String queueName,
                     String payload,
                     MqMessageFormat format,
                     Map<String, String> properties) {
        switch (Objects.requireNonNull(format, "format")) {
            case JSON, XML -> sendTextMessage(queueName, payload, properties);
            case EBCDIC_870 -> sendBytesMessage(queueName, payload, EBCDIC_870, 870, properties);
            case UTF8_1208 -> sendBytesMessage(queueName, payload, UTF_8, 1208, properties);
        }
    }

    public ReceivedMessage receive(String queueName,
                                   String messageSelector,
                                   MqMessageFormat expectedFormat,
                                   Duration timeout) {
        JMSContext context = getContext();
        Queue queue = context.createQueue("queue:///" + queueName);
        try (JMSConsumer consumer = messageSelector == null || messageSelector.isBlank()
                ? context.createConsumer(queue)
                : context.createConsumer(queue, messageSelector)) {
            Message message = consumer.receive(Optional.ofNullable(timeout).orElse(Duration.ofSeconds(30)).toMillis());
            if (message == null) {
                throw new MessagingTimeoutException("Timeout waiting for IBM MQ message from queue: " + queueName);
            }
            return toReceivedMessage(message, queueName, expectedFormat);
        }
    }

    public List<ReceivedMessage> browse(String queueName,
                                        Predicate<ReceivedMessage> filter,
                                        MqMessageFormat expectedFormat,
                                        Duration timeout) {
        long timeoutMillis = Optional.ofNullable(timeout).orElse(Duration.ofSeconds(30)).toMillis();
        long deadline = System.currentTimeMillis() + timeoutMillis;
        long backoff = 100;
        JMSContext context = getContext();
        Queue queue = context.createQueue("queue:///" + queueName);

        while (System.currentTimeMillis() < deadline) {
            List<ReceivedMessage> matched = new ArrayList<>();
            try (QueueBrowser browser = context.createBrowser(queue)) {
                Enumeration<?> messages = browser.getEnumeration();
                while (messages.hasMoreElements()) {
                    Message message = (Message) messages.nextElement();
                    ReceivedMessage receivedMessage = toReceivedMessage(message, queueName, expectedFormat);
                    if (filter == null || filter.test(receivedMessage)) {
                        matched.add(receivedMessage);
                    }
                }
            } catch (JMSException e) {
                throw new IllegalStateException("Failed to browse IBM MQ queue: " + queueName, e);
            }

            if (!matched.isEmpty()) {
                return matched;
            }

            try {
                TimeUnit.MILLISECONDS.sleep(backoff);
            } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
                break;
            }
            backoff = Math.min(backoff * 2, 1000);
        }
        throw new MessagingTimeoutException("Timeout waiting for IBM MQ message from queue: " + queueName);
    }

    @Override
    public void close() {
        JMSContext context = jmsContext;
        if (context != null) {
            context.close();
        }
    }

    private void sendTextMessage(String queueName, String payload, Map<String, String> properties) {
        JMSContext context = getContext();
        JMSProducer producer = context.createProducer();
        TextMessage message = context.createTextMessage(payload);
        applyProperties(message, properties);
        producer.send(context.createQueue("queue:///" + queueName), message);
    }

    private void sendBytesMessage(String queueName,
                                  String payload,
                                  Charset charset,
                                  int ccsid,
                                  Map<String, String> properties) {
        try {
            JMSContext context = getContext();
            JMSProducer producer = context.createProducer();
            BytesMessage message = context.createBytesMessage();
            message.writeBytes(Optional.ofNullable(payload).orElse("").getBytes(charset));
            message.setIntProperty(WMQConstants.JMS_IBM_CHARACTER_SET, ccsid);
            applyProperties(message, properties);
            producer.send(context.createQueue("queue:///" + queueName), message);
        } catch (JMSException e) {
            throw new IllegalStateException("Failed to send bytes message to IBM MQ queue: " + queueName, e);
        }
    }

    private void applyProperties(Message message, Map<String, String> properties) {
        Optional.ofNullable(properties).orElseGet(Collections::emptyMap)
                .forEach((key, value) -> {
                    try {
                        message.setStringProperty(key, String.valueOf(value));
                    } catch (JMSException e) {
                        throw new IllegalStateException("Failed to set JMS property: " + key, e);
                    }
                });
    }

    private ReceivedMessage toReceivedMessage(Message message, String queueName, MqMessageFormat format) {
        try {
            Map<String, String> headers = new LinkedHashMap<>();
            Enumeration<?> names = message.getPropertyNames();
            while (names.hasMoreElements()) {
                String name = String.valueOf(names.nextElement());
                headers.put(name, String.valueOf(message.getObjectProperty(name)));
            }

            String body = decodeMessage(message, format);
            MessageContentType contentType = resolveContentType(message, format);
            return new ReceivedMessage(body, contentType, headers, message.getJMSTimestamp(), queueName);
        } catch (JMSException e) {
            throw new IllegalStateException("Failed to decode IBM MQ message", e);
        }
    }

    private MessageContentType resolveContentType(Message message, MqMessageFormat expectedFormat) {
        if (message instanceof TextMessage) {
            return expectedFormat == MqMessageFormat.XML ? MessageContentType.XML : MessageContentType.JSON;
        }
        if (expectedFormat == MqMessageFormat.XML) {
            return MessageContentType.XML;
        }
        if (expectedFormat == MqMessageFormat.JSON) {
            return MessageContentType.JSON;
        }
        return MessageContentType.RAW_TEXT;
    }

    private String decodeMessage(Message jmsMessage, MqMessageFormat format) {
        try {
            if (jmsMessage instanceof TextMessage textMessage) {
                return textMessage.getText();
            }

            if (jmsMessage instanceof BytesMessage bytesMessage) {
                byte[] data = new byte[(int) bytesMessage.getBodyLength()];
                bytesMessage.readBytes(data);
                Charset charset = switch (format) {
                    case EBCDIC_870 -> EBCDIC_870;
                    case UTF8_1208, JSON, XML -> UTF_8;
                };
                return new String(data, charset);
            }
            return "";
        } catch (Exception e) {
            throw new IllegalStateException("Failed to decode JMS message", e);
        }
    }

    private JMSContext getContext() {
        JMSContext current = jmsContext;
        if (current == null) {
            synchronized (contextLock) {
                current = jmsContext;
                if (current == null) {
                    jmsContext = current = connectionFactory.createContext(user, password, JMSContext.AUTO_ACKNOWLEDGE);
                }
            }
        }
        return current;
    }
}
