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

import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.jms.BytesMessage;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.JMSRuntimeException;
import javax.jms.Message;
import javax.jms.TextMessage;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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.messaging.MessageContentType;
import cz.moneta.test.harness.messaging.MqMessageFormat;
import cz.moneta.test.harness.messaging.ReceivedMessage;
import cz.moneta.test.harness.messaging.exception.MessagingConnectionException;
import cz.moneta.test.harness.messaging.exception.MessagingDestinationException;
import cz.moneta.test.harness.messaging.exception.MessagingTimeoutException;
import cz.moneta.test.harness.support.messaging.ImqRequest;

/**
 * IBM MQ connector using JMS client with Jakarta JMS API. Supports
 * multi-instance Queue Manager, SSL/TLS, and multiple message formats.
 * <p>
 * Supported formats: - JSON: JMS TextMessage with plain JSON string (default) -
 * XML: JMS TextMessage with XML string - UTF-8 (CCSID 1208): JMS BytesMessage
 * with UTF-8 encoding - EBCDIC (CCSID 870): JMS BytesMessage with EBCDIC
 * IBM-870 encoding
 */
public class IbmMqConnector implements Connector {

	private static final Logger LOG = LogManager.getLogger(IbmMqConnector.class);

	private static final Charset EBCDIC_870 = Charset.forName("IBM870");
	private static final Charset UTF_8 = StandardCharsets.UTF_8;

	private static final long DEFAULT_POLL_INTERVAL_MS = 100;
	private static final long DEFAULT_MAX_POLL_INTERVAL_MS = 1000;

	private static final String TLS_VERSION = "TLSv1.2";

	private final MQConnectionFactory connectionFactory;
	private JMSContext jmsContext;
	private final String queueManager;
	private final String user;
	private final String password;

	/**
	 * Constructor with multi-instance Queue Manager support.
	 *
	 * @param connectionNameList Connection name list in format
	 *                           "host1(port1),host2(port2)"
	 * @param channel            MQ channel name
	 * @param queueManager       Queue Manager name
	 * @param user               Username for authentication
	 * @param password           Password for authentication
	 * @param keystorePath       Path to SSL keystore (can be null for non-SSL)
	 * @param keystorePassword   Password for SSL keystore
	 * @param sslCipherSuite     SSL cipher suite to use (e.g.,
	 *                           "TLS_RSA_WITH_AES_256_CBC_SHA256")
	 */
	public IbmMqConnector(String connectionNameList, String channel, String queueManager, String user, String password,
			String keystorePath, String keystorePassword, String sslCipherSuite) {
		this.queueManager = queueManager;
		this.user = user;
		this.password = password;

		try {
			connectionFactory = new MQConnectionFactory();
			connectionFactory.setConnectionNameList(connectionNameList);
			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 (keystorePath != null && !keystorePath.isBlank() && keystorePassword != null
					&& !keystorePassword.isBlank()) {
				connectionFactory.setSSLSocketFactory(getSslSocketFactory(keystorePath, keystorePassword));
			}

			if (sslCipherSuite != null && !sslCipherSuite.isBlank()) {
				connectionFactory.setSSLCipherSuite(sslCipherSuite);
			}

			// Initialize JMS context
			connect();

		} catch (Exception e) {
			throw new MessagingConnectionException("Failed to create IBM MQ connection to " + queueManager, e);
		}
	}

	/**
	 * Connect to IBM MQ.
	 */
	private void connect() {
		try {
			this.jmsContext = connectionFactory.createContext(user, password, JMSContext.AUTO_ACKNOWLEDGE);
			this.jmsContext.start();
			LOG.info("Connected to IBM MQ: {}", queueManager);
		} catch (Exception e) {
			throw new MessagingConnectionException(
					"Failed to connect to IBM MQ: " + queueManager + " - " + e.getMessage(), e);
		}
	}

	/**
	 * Send a JSON or XML message as TextMessage.
	 */
	private void sendTextMessage(String queueName, String payload, Map<String, String> properties) {
		javax.jms.Queue queue = getQueue(queueName);

		TextMessage message = jmsContext.createTextMessage(payload);

		// Set JMS properties
		if (properties != null) {
			for (Map.Entry<String, String> entry : properties.entrySet()) {
				try {
					if (entry.getKey().equals(ImqRequest.PROP_JMS_CORRELATION_ID)) {
						message.setJMSCorrelationID(entry.getValue());
						continue;
					} else if (entry.getKey().equals(ImqRequest.PROP_JMS_TYPE)) {
						message.setJMSType(entry.getValue());
						continue;
					} else if (entry.getKey().equals(ImqRequest.PROP_JMS_MESSAGE_ID)) {
						message.setJMSMessageID(entry.getValue());
						continue;
					}
					message.setStringProperty(entry.getKey(), entry.getValue());
				} catch (JMSException e) {
					LOG.warn("Failed to set property: {}", entry.getKey(), e);
				}
			}
		}

		try {
			jmsContext.createProducer().send(queue, message);
		} catch (RuntimeException e) {
			throw new MessagingDestinationException("Failed to send message to queue: " + queueName, e);
		}
		LOG.debug("Sent JSON/XML message to queue: {}", queueName);
	}

	/**
	 * Send a message as BytesMessage with specific encoding and CCSID.
	 */
	private void sendBytesMessage(String queueName, String payload, Charset charset, int ccsid,
			Map<String, String> properties) {
		javax.jms.Queue queue = getQueue(queueName);

		BytesMessage message = jmsContext.createBytesMessage();

		// Convert payload to bytes using specified charset
		byte[] bytes = payload.getBytes(charset);
		try {
			message.writeBytes(bytes);
			message.setIntProperty("CCSID", ccsid);
		} catch (JMSException e) {
			throw new MessagingDestinationException("Failed to create bytes message", e);
		}

		// Set JMS properties
		if (properties != null) {
			for (Map.Entry<String, String> entry : properties.entrySet()) {
				try {
					if (entry.getKey().equals(ImqRequest.PROP_JMS_CORRELATION_ID)) {
						message.setJMSCorrelationID(entry.getValue());
						continue;
					} else if (entry.getKey().equals(ImqRequest.PROP_JMS_TYPE)) {
						message.setJMSType(entry.getValue());
						continue;
					} else if (entry.getKey().equals(ImqRequest.PROP_JMS_MESSAGE_ID)) {
						message.setJMSMessageID(entry.getValue());
						continue;
					}
					message.setStringProperty(entry.getKey(), entry.getValue());
				} catch (JMSException e) {
					LOG.warn("Failed to set property: {}", entry.getKey(), e);
				}
			}
		}

		try {
			jmsContext.createProducer().send(queue, message);
		} catch (RuntimeException e) {
			throw new MessagingDestinationException("Failed to send message to queue: " + queueName, e);
		}
		LOG.debug("Sent {} message to queue: {}", charset, queueName);
	}

	/**
	 * Send a message to a queue with specified format.
	 *
	 * @param queueName  Queue name
	 * @param payload    Message payload
	 * @param format     Message format (JSON, XML, EBCDIC_870, UTF8_1208)
	 * @param properties JMS properties to set
	 */
	public void send(String queueName, String payload, MqMessageFormat format, Map<String, String> properties) {
		switch (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);
		}
	}

	/**
	 * Receive a message from a queue with timeout.
	 *
	 * @param queueName       Queue name
	 * @param messageSelector JMS message selector (optional)
	 * @param format          Expected message format
	 * @param timeout         Timeout duration
	 * @return Received message
	 */
	public ReceivedMessage receive(String queueName, String messageSelector, MqMessageFormat format,
			java.time.Duration timeout) {
		long timeoutMs = timeout.toMillis();

		javax.jms.Queue queue = getQueue(queueName);
		JMSConsumer consumer = (messageSelector == null || messageSelector.isBlank() ? jmsContext.createConsumer(queue)
				: jmsContext.createConsumer(queue, messageSelector));

		AtomicBoolean messageFound = new AtomicBoolean(false);
		ReceivedMessage received = null;

		long pollInterval = DEFAULT_POLL_INTERVAL_MS;
		long remainingTime = timeoutMs;

		try {
			while (remainingTime > 0 && !messageFound.get()) {
				Message message = consumer.receive(remainingTime);

				if (message != null) {
					received = decodeMessage(message, queueName, format);
					messageFound.set(true);
				} else {
					// Exponential backoff
					pollInterval = Math.min(pollInterval * 2, DEFAULT_MAX_POLL_INTERVAL_MS);
					remainingTime -= pollInterval;
				}
			}

			if (received == null) {
				throw new MessagingTimeoutException("No message matching filter found on queue '" + queueName
						+ "' within " + timeout.toMillis() + "ms");
			}

			return received;

		} catch (MessagingTimeoutException e) {
			throw e;
		} catch (Exception e) {
			throw new MessagingDestinationException("Failed to receive message from queue: " + queueName, e);
		} finally {
			try {
				consumer.close();
			} catch (JMSRuntimeException e) {
				LOG.warn("Failed to close consumer", e);
			}
		}
	}

	/**
	 * Browse a queue (non-destructive read).
	 *
	 * @param queueName       Queue name
	 * @param messageSelector JMS message selector (optional)
	 * @param format          Expected message format
	 * @param maxMessages     Maximum number of messages to browse
	 * @return List of received messages
	 */
	public List<ReceivedMessage> browse(String queueName, String messageSelector, MqMessageFormat format,
			int maxMessages) {
		List<ReceivedMessage> messages = new ArrayList<>();
		javax.jms.Queue queue = getQueue(queueName);

		try (javax.jms.QueueBrowser browser = (messageSelector == null || messageSelector.isBlank())
				? jmsContext.createBrowser(queue)
				: jmsContext.createBrowser(queue, messageSelector)) {

			Enumeration<?> enumeration = browser.getEnumeration();
			int count = 0;

			while (enumeration.hasMoreElements() && count < maxMessages) {
				Message message = (Message) enumeration.nextElement();
				if (message != null) {
					ReceivedMessage received = decodeMessage(message, queueName, format);
					messages.add(received);
					count++;
				}
			}

			return messages;
		} catch (JMSException e) {
			throw new MessagingDestinationException("Failed to browse queue: " + queueName, e);
		}
	}

	/**
	 * Decode a JMS message to ReceivedMessage.
	 */
	private ReceivedMessage decodeMessage(Message jmsMessage, String queueName, MqMessageFormat format) {
		long timestamp;
		try {
			timestamp = jmsMessage.getJMSTimestamp();
		} catch (JMSException e) {
			timestamp = System.currentTimeMillis();
		}
		if (timestamp == 0) {
			timestamp = System.currentTimeMillis();
		}

		String body;
		MessageContentType contentType;
		Map<String, String> headers = new HashMap<>();

		// Extract JMS properties as headers
		extractJmsProperties(jmsMessage, headers);

		if (jmsMessage instanceof TextMessage textMessage) {
			try {
				body = textMessage.getText();
			} catch (JMSException e) {
				throw new RuntimeException("Failed to read text message body", e);
			}
			contentType = switch (format) {
			case XML -> MessageContentType.XML;
			default -> MessageContentType.JSON;
			};
		} else if (jmsMessage instanceof BytesMessage bytesMessage) {
			int ccsid;
			try {
				ccsid = bytesMessage.getIntProperty("CCSID");
			} catch (JMSException e) {
				ccsid = 1208; // default UTF-8
			}
			body = decodeBytesMessage(bytesMessage, ccsid);
			contentType = MessageContentType.RAW_TEXT;
		} else {
			try {
				throw new IllegalArgumentException("Unsupported message type: " + jmsMessage.getJMSType());
			} catch (JMSException e) {
				throw new IllegalArgumentException("Unsupported message type", e);
			}
		}

		return new ReceivedMessage(body, contentType, headers, timestamp, queueName, null);
	}

	/**
	 * Decode BytesMessage body based on CCSID.
	 */
	private String decodeBytesMessage(BytesMessage bytesMessage, int ccsid) {
		try {
			long bodyLength;
			try {
				bodyLength = bytesMessage.getBodyLength();
			} catch (JMSException e) {
				throw new RuntimeException("Failed to get message body length", e);
			}
			byte[] data = new byte[(int) bodyLength];
			bytesMessage.readBytes(data);

			Charset charset = switch (ccsid) {
			case 870 -> EBCDIC_870;
			case 1208 -> UTF_8;
			default -> UTF_8;
			};

			return new String(data, charset);
		} catch (JMSException e) {
			throw new RuntimeException("Failed to read BytesMessage body", e);
		}
	}

	/**
	 * Extract JMS properties as headers.
	 */
	@SuppressWarnings("unchecked")
	private void extractJmsProperties(Message message, Map<String, String> headers) {
		try {
			// Common JMS headers
			headers.put("JMSMessageID", message.getJMSMessageID());
			try {
				headers.put("JMSType", message.getJMSType() != null ? message.getJMSType() : "");
			} catch (JMSException e) {
				headers.put("JMSType", "");
			}
			try {
				headers.put("JMSDestination",
						message.getJMSDestination() != null ? message.getJMSDestination().toString() : "");
			} catch (JMSException e) {
				headers.put("JMSDestination", "");
			}
			try {
				headers.put("JMSDeliveryMode", String.valueOf(message.getJMSDeliveryMode()));
			} catch (JMSException e) {
				headers.put("JMSDeliveryMode", "");
			}
			try {
				headers.put("JMSPriority", String.valueOf(message.getJMSPriority()));
			} catch (JMSException e) {
				headers.put("JMSPriority", "");
			}
			try {
				headers.put("JMSTimestamp", String.valueOf(message.getJMSTimestamp()));
			} catch (JMSException e) {
				headers.put("JMSTimestamp", "");
			}

			// Extract custom properties
			Enumeration<String> propertyNames = (Enumeration<String>) message.getPropertyNames();
			while (propertyNames.hasMoreElements()) {
				String propName = propertyNames.nextElement();
				Object propValue = message.getObjectProperty(propName);
				if (propValue != null) {
					headers.put(propName, propValue.toString());
				}
			}
		} catch (JMSException e) {
			LOG.warn("Failed to extract JMS properties", e);
		}
	}

	/**
	 * Get Queue object from queue name.
	 */
	private javax.jms.Queue getQueue(String queueName) {
		return jmsContext.createQueue(queueName);
	}

	@Override
	public void close() {
		if (jmsContext != null) {
			try {
				jmsContext.close();
				LOG.info("Closed connection to IBM MQ: {}", queueManager);
			} catch (Exception e) {
				LOG.error("Failed to close IBM MQ connection", e);
			}
		}
	}

	private SSLSocketFactory getSslSocketFactory(String keystorePath, String keystorePassword) throws Exception {
		// --- keystore ---
		KeyStore keyStore = KeyStore.getInstance("JKS");
		InputStream ksStream = IbmMqConnector.class.getClassLoader().getResourceAsStream(keystorePath);

		if (ksStream == null) {
			throw new IllegalStateException("Keystore not found: " + keystorePath);
		}
		keyStore.load(ksStream, keystorePassword.toCharArray());
		
		KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
		kmf.init(keyStore, keystorePassword.toCharArray());

		// --- truststore ---
		KeyStore trustStore = KeyStore.getInstance("JKS");
		InputStream tsStream = IbmMqConnector.class.getClassLoader().getResourceAsStream(keystorePath);

		if (tsStream == null) {
			throw new IllegalStateException("Truststore not found: " + keystorePath);
		}
		trustStore.load(tsStream, keystorePassword.toCharArray());

		TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
		tmf.init(trustStore);

		// --- SSL context ---
		SSLContext sslContext = SSLContext.getInstance(TLS_VERSION);
		sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

		return sslContext.getSocketFactory();
	}
}
