/*
* Copyright (c) 2022 Green Arrowhead LLP

* 
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
* 
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* 
*/

/**
 * This provides methods to connect to internetmessagebus.com
 * and trigger method whenever client request is received on the queue
 * on internetmessagebus.com. 
 * This in turn make request/response to the configured web/application server. 
 * Response received from web/application server is send back to internetmessagebus.com.
 * @version 2022-11-11
 */
package com.gah.imb.client;

import java.util.Base64;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.net.URI;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.jms.BytesMessage;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.JMSSecurityException;
import javax.jms.MapMessage;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.naming.InitialContext;

import java.net.HttpURLConnection;
import java.net.URL;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * IMBClientMain connects to Internet Message Bus server and pass-on requests to
 * the configured web application. Web application could be running locally or
 * on any server accessible on the network. All configuration parameters used by
 * IMBClientMain are set in the properties file mentioned in the system
 * property: config_imb.
 * 
 * Refer to CoreProperties.java to know more about configurable parameters.
 * 
 * @author Green Arrowhead LLP
 * @version 2022-11-01
 */
public class IMBClientMain {

	// IMBClient configuration data is set using properties file
	// Refer to CoreProperties.java for more details.
	//
	// Do not modify the code unless necessary customization is required.

	CoreProperties coreProperties = new CoreProperties();

	// single connection per instance
	Connection connection = null;
	InitialContext initialContext = null;
	ConnectionFactory cf = null;

	public Session resetSession(Session oldSession) {

		// wait for 10 seconds and try connecting again
		try {
			this.wait(10000);
		} catch (Exception thexception) {
			// do nothing
		}

		try {
			if (oldSession != null) {
				oldSession.close();
			}
		} catch (Exception exp) {
			exp.printStackTrace();
		}
		oldSession = createAndStartSession(connection);

		return oldSession;
	}

	public Session createAndStartSession(Connection inputConnection) {
		Session newSession = null;
		try {
			newSession = inputConnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
		} catch (Exception exp) {
			exp.printStackTrace();
		}

		return newSession;
	}

	public Connection createAndStartConnection() {
		Connection newConnection = null;

		try {
			Properties p = new Properties();

			p.put("java.naming.factory.initial", "org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory");
			p.put("connectionFactory.ConnectionFactory",
					"tcp://" + coreProperties.server + ":" + coreProperties.port + "?minLargeMessageSize=5709120");

			// Create an initial context to perform the JNDI lookup.
			initialContext = new InitialContext(p);

			// Perform a lookup on the Connection Factory
			cf = (ConnectionFactory) initialContext.lookup("ConnectionFactory");

			try {
				newConnection = cf.createConnection(coreProperties.user, coreProperties.pass);
				newConnection.setExceptionListener(new IMBClientConnectionExceptionListener());
				// start connection
				newConnection.start();
			} catch (JMSSecurityException securityExp) {
				securityExp.printStackTrace();
				// fatal error
				System.exit(-1);
			}
		} catch (Exception exp) {
			exp.printStackTrace();
		}

		return newConnection;

	}

	public void start() {

		String requestqueuname = coreProperties.requestqueue;
		try {
			connection = createAndStartConnection();
			Session session = createAndStartSession(connection);
			MessageConsumer consumer = session.createConsumer(session.createQueue(requestqueuname));
			while (true) {
				try {
					MapMessage messageReceived = (MapMessage) consumer.receive(5000);
					if (messageReceived != null) {
						Logger.log("New message received: " + messageReceived.getJMSMessageID());
						// send response
						BackendRequestResponseHandler backendhandler = new BackendRequestResponseHandler(connection,
								messageReceived, messageReceived.getJMSReplyTo());

						backendhandler.start();

					}
					else {
						//no message received, wait for messages in new session
						if(consumer != null) {
							try{
								consumer.close(); 
							}
							catch(Exception exp4) {
								exp4.printStackTrace();
							}
						}
						if(session != null) {
							try{
								session.close(); 
							}
							catch(Exception exp4) {
								exp4.printStackTrace();
							}							
						}
						session = createAndStartSession(connection);
						consumer = session.createConsumer(session.createQueue(requestqueuname));
						
					}
				} catch (Exception e) {
					// handle exceptions related to consumer.
					e.printStackTrace();
					// sleep for 5 seconds and recreate connection/session/consumer
					try {
						Thread.sleep(5000);
					} catch (Exception exp2) {
						// ignore exception
					}
					if (consumer != null) {
						try {
							consumer.close();
						} catch (Exception consumercloseexp) {
							consumercloseexp.printStackTrace();
						}
					}
					try {
						session = resetSession(session);
						consumer = session.createConsumer(session.createQueue(requestqueuname));
					} catch (Exception exp) {
						exp.printStackTrace();
						try {
							Thread.sleep(5000);
						} catch (Exception exp2) {
							// ignore exception
						}
						// reset connection
						try {
							connection = createAndStartConnection();
							session = createAndStartSession(connection);
							consumer = session.createConsumer(session.createQueue(requestqueuname));
						} catch (Exception exp3) {
							exp3.printStackTrace();
						}

					}
				}
			}

		} catch (Exception exp) {
			// do nothing
			exp.printStackTrace();
		}

	}

	public static void main(String[] args) {

		try {

			// start IMP Client
			IMBClientMain imbclient = new IMBClientMain();
			imbclient.start();

		} catch (Exception exp) {
			exp.printStackTrace();
		}

	}

	//Thread to handle all backend request/response
	class BackendRequestResponseHandler extends Thread {

		MapMessage messageReceived = null;
		Destination respondestination = null;
		Connection connection = null;

		public BackendRequestResponseHandler(Connection connection, MapMessage messageReceived,
				Destination respondestination) {
			this.messageReceived = messageReceived;
			this.respondestination = respondestination;
			this.connection = connection;
		}

		public void sendresponse(MapMessage messageReceived, Destination respondestination) throws Exception {

			MessageProducer producer = null;
			// create seperate session for all responses
			Session session = createAndStartSession(connection);
			try {
				if (messageReceived != null) {

					BytesMessage bytesMessage = session.createBytesMessage();

					ByteArrayInputStream bis = new ByteArrayInputStream(messageReceived.getBytes("RequestHeaders"));
					ObjectInput in = new ObjectInputStream(bis);

					Hashtable<String, List> requestHeaderHashtable = (Hashtable) in.readObject();
					////////////////////////////////////
					bis = new ByteArrayInputStream(messageReceived.getBytes("RequestBody"));
					in = new ObjectInputStream(bis);

					Hashtable requestbodyHashtable = (Hashtable) in.readObject();

					URI uril = new URI((String) messageReceived.getObject("RequestURI"));
					String requestmethod = (String) messageReceived.getObject("RequestMethod");
					String contentType  = "";
					if(requestHeaderHashtable.get("Content-type") != null && requestHeaderHashtable.get("Content-type").size() > 0) {
						contentType = (String) requestHeaderHashtable.get("Content-type").get(0);
					}
					else {
						Logger.log("requestHeaderHashtable is eithe null or empty " + requestHeaderHashtable);
					}

					if (requestmethod.equalsIgnoreCase("get")) {
						// if request is for file, read file and send it
						String urlquery = uril.toString();
						//Logger.log("urlquery: " + urlquery.split("reqresp.php").length);
						// urlquery is in the form ../../reqresp.php/<actual requested query>
						// so need to get <actual requested query>
						if (urlquery.contains("reqresp.php")) {
							urlquery = urlquery.split("reqresp.php").length<=1?"":urlquery.split("reqresp.php")[1];
						}

						getWebResponseGet(bytesMessage, coreProperties.mainserver + "/" + urlquery, contentType);

					} else if (requestmethod.equalsIgnoreCase("post")) {
						String urlquery = uril.toString();
						if (urlquery.contains("reqresp.php")) {
							urlquery = urlquery.split("reqresp.php").length<=1?"":urlquery.split("reqresp.php")[1];
						}

						getWebResponsePost(bytesMessage, coreProperties.mainserver + "/" + urlquery,
								requestbodyHashtable, contentType);

					}

					// Step 6. Create a JMS Message Producer
					producer = session.createProducer(respondestination);
					producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

					// Step 8. Send the Message
					producer.send(bytesMessage);

				}

			} catch (Exception exp) {
				exp.printStackTrace();
			} finally {
				if (producer != null) {
					producer.close();
					session.close();
				}
			}

		}

		public void getWebResponseGet(BytesMessage bytesMessage, String mainurl, String contentType) {

			byte[] bytes = null;
			try {

				//remove '//' from the end
				mainurl = mainurl.replaceAll("/{2,}$", "/");
				URL url = new URL(mainurl);
				HttpURLConnection con = (HttpURLConnection) url.openConnection();
				Logger.log("url: " + url);
				
				con.setRequestMethod("GET");

				if (contentType.toLowerCase().contains("text")) {
					InputStream is = con.getInputStream();

					bytes = is.readAllBytes();
					//Logger.log("Response: " + new String(bytes));
					
					is.close();

				} else { // its image, encryption not done

					InputStream is = con.getInputStream();

					bytes = is.readAllBytes();

					is.close();

				}

			} catch (Exception exp) {
				if (!mainurl.contains("favicon")) {
					exp.printStackTrace();
				}
			}

			if (bytes != null) {
				try {
					bytesMessage.setStringProperty("mimetype", contentType);
					bytesMessage.writeBytes(bytes);
				} catch (Exception exp) {
					exp.printStackTrace();
				}
			}

		}

		public void getWebResponsePost(BytesMessage bytesMessage, String mainurl,
				Hashtable<String, String> requestbodyHashtable, String contentType) {
			byte[] bytesoutput = null;

			try {
				//remove '//' from the end
				mainurl = mainurl.replaceAll("/{2,}$", "/");
				URL url = new URL(mainurl);
				HttpURLConnection con = (HttpURLConnection) url.openConnection();
				con.setRequestMethod("POST");

				con.setRequestProperty("Accept", "application/xml");
				con.setRequestProperty("Content-Type", "application/xml");
				Enumeration<String> e = requestbodyHashtable.keys();
				while (e.hasMoreElements()) {

					// Getting the key of a particular entry
					String key = e.nextElement();

					if (!key.equalsIgnoreCase("requestbody")) {
						con.setRequestProperty(key, requestbodyHashtable.get(key));
					}

				}

				String requestbodydecrypt = requestbodyHashtable.get("requestbody");
				con.setDoOutput(true);
				OutputStream outStream = con.getOutputStream();
				OutputStreamWriter outStreamWriter = new OutputStreamWriter(outStream, "UTF-8");
				outStreamWriter.write(requestbodydecrypt);
				outStreamWriter.flush();
				outStreamWriter.close();
				outStream.close();

				if (contentType.toLowerCase().contains("text")) {

					InputStream is = con.getInputStream();

					bytesoutput = is.readAllBytes();

					is.close();


				} else { // its image, encryption not done

					InputStream is = con.getInputStream();

					bytesoutput = is.readAllBytes();

					is.close();
				}

			} catch (Exception exp) {
				exp.printStackTrace();
			}
			// set message
			if (bytesoutput != null) {
				try {
					bytesMessage.setStringProperty("mimetype", contentType);
					bytesMessage.writeBytes(bytesoutput);
				} catch (Exception exp) {
					exp.printStackTrace();
				}
			}
		}

		public void run() {
			try {
				sendresponse(messageReceived, respondestination);
			} catch (Exception exp) {
				exp.printStackTrace();
			}
		}
	}

	// Exception Listener
	public class IMBClientConnectionExceptionListener implements ExceptionListener {

		@Override
		public void onException(JMSException e) {
			// wait for 10 seconds and reconnect
			try {
				Thread.sleep(10000); // sleep a little before trying again
			} catch (InterruptedException exc) {
			}

			// create a test session, if session gets create that
			// would mean connection is fine and issue was temporary,
			// else create new connection
			Session testSession = null;
			try {
				testSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
			} catch (Exception exp) {
				exp.printStackTrace();
				// issue creating session, create new connection
				connection = createAndStartConnection();

			}
			// close test session
			try {
				testSession.close();
			} catch (Exception exp) {
				exp.printStackTrace();
			}
		}
	}

	//
	// Thanks @Stéphane Moreau
	//

	public static String md5(String input) throws NoSuchAlgorithmException {
		MessageDigest md = MessageDigest.getInstance("MD5");
		byte[] messageDigest = md.digest(input.getBytes());
		BigInteger number = new BigInteger(1, messageDigest);
		return String.format("%032x", number); // number.toString(16);
	}

	public String decrypt(String encryptedData) {
		String decryptedData = encryptedData;
		try {
			if(coreProperties.secretKey != null || coreProperties.secretKey.length() >= 0) {
				SecretKeySpec skeySpec = new SecretKeySpec(md5(coreProperties.secretKey).getBytes(), "AES");
				IvParameterSpec initialVector = new IvParameterSpec(coreProperties.initialVectorString.getBytes());
				Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
				cipher.init(Cipher.DECRYPT_MODE, skeySpec, initialVector);
				byte[] encryptedByteArray = (new org.apache.commons.codec.binary.Base64()).decode(encryptedData.getBytes());
				byte[] decryptedByteArray = cipher.doFinal(encryptedByteArray);
				decryptedData = new String(decryptedByteArray, "UTF8");
			}

		} catch (Exception e) {
			e.printStackTrace();
			Logger.log("Problem decrypting the data");
		}
		return decryptedData;
	}

	private Cipher initCipher(final int mode) throws NoSuchAlgorithmException, NoSuchPaddingException,
			InvalidKeyException, InvalidAlgorithmParameterException {
		if(coreProperties.secretKey == null || coreProperties.secretKey.length() == 0) {
			return null;
		}
		else {
			final SecretKeySpec skeySpec = new SecretKeySpec(md5(coreProperties.secretKey).getBytes(), "AES");
			final IvParameterSpec initialVector = new IvParameterSpec(coreProperties.initialVectorString.getBytes());
			final Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
			cipher.init(mode, skeySpec, initialVector);
			return cipher;
		}
	}

	public String encrypt(final String dataToEncrypt) {
		String encryptedData = dataToEncrypt;
		try {
			// Initialize the cipher
			final Cipher cipher = initCipher(Cipher.ENCRYPT_MODE);
			if(cipher != null) {
				// Encrypt the data
				final byte[] encryptedByteArray = cipher.doFinal(dataToEncrypt.getBytes());
				// Encode using Base64
				encryptedData = (Base64.getEncoder()).encodeToString(encryptedByteArray);				
			}

		} catch (Exception e) {
			System.err.println("Problem encrypting the data");
			e.printStackTrace();
		}
		return encryptedData;
	}

}
