package org.jpos.tcpay.connection;

import org.apache.commons.lang3.StringUtils;
import org.jpos.iso.ISOException;
import org.jpos.iso.ISOMsg;
import org.jpos.iso.ISOUtil;
import org.jpos.iso.packager.ISO87BPackager;
import org.jpos.tcpay.CustomOutputStream;
import org.jpos.tcpay.acquirer.LengthMode;
import org.jpos.tcpay.db.entity.AcquirerConnection;
import org.jpos.util.AppLogger;
import org.jpos.util.StringParsingUtil;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.*;
import java.net.Socket;
import java.net.SocketException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Objects;

public class FiservChannel implements AcquirerChannel{
    Socket acqSocket = null;

    private AppLogger logger = new AppLogger();

    @Override
    public ISOMsg transceive(ISOMsg req, AcquirerConnection acquirerConnection) {
        ISOMsg  response = new ISOMsg();
        response.setPackager(new ISO87BPackager());
        try {
            if (Objects.isNull(acqSocket) || !acqSocket.isConnected()) {
                closeSocket();
                acqSocket = getSocket(acquirerConnection);
            }


            int ret = sendReceive(req, response, acquirerConnection);
            if (ret != 0) {
                logger.log("warn", "Fiserv Transceive Failed");
                return null;
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            closeSocket();
        }

        return response;
    }

    private void closeSocket() {
        if (Objects.isNull(acqSocket)) {
            acqSocket = null;
            return;
        }

        try {
            acqSocket.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            acqSocket = null;
        }
    }




    private Socket getSocket(AcquirerConnection acquirerConnection) throws Exception {

        if (acquirerConnection.isSsl()) {
            logger.log("SSL Connection");
            KeyStore keyStore = null;
            if (!StringUtils.isEmpty(acquirerConnection.getKeyStorePath())) {
                keyStore = KeyStore.getInstance("JKS");
                keyStore.load(Files.newInputStream(Paths.get(acquirerConnection.getKeyStorePath())), acquirerConnection.getKeyStorePassword().toCharArray());
            }

            KeyStore trustStore = null;
            if (!StringUtils.isEmpty(acquirerConnection.getKeyStorePath())) {
                trustStore = KeyStore.getInstance("JKS");
                trustStore.load(Files.newInputStream(Paths.get(acquirerConnection.getTrustStorePath())), acquirerConnection.getTrustStorePassword().toCharArray());
            }

            // Create TrustManagerFactory and initialize it with the truststore
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(trustStore);

            // Create SSLContext with the default TrustManager and initialize SSLSocketFactory
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
            // Create an SSL socket and connect to the server
            SSLSocketFactory socketFactory = sslContext.getSocketFactory();
            SSLSocket socket = (SSLSocket) socketFactory.createSocket(acquirerConnection.getIpAddress(), acquirerConnection.getPortNumber());
            socket.startHandshake();
            acqSocket = socket;
        } else {
            logger.log("Plain Connection");
            acqSocket = new Socket(acquirerConnection.getIpAddress(), acquirerConnection.getPortNumber());
        }

        return acqSocket;
    }


    public byte[] getPacketToSend(ISOMsg sendIsoMsg, AcquirerConnection acquirerConnection) throws ISOException {
        byte[] tpduArray = null;


        if(!StringUtils.isEmpty(acquirerConnection.getTpdu())){
            tpduArray = ISOUtil.str2bcd(ISOUtil.padleft(acquirerConnection.getTpdu(), 10, '0'), true);
        }


        byte[] headerByteArray = new byte[10];
        int headerByteArrayLength = 0;

        byte[] tempSendArray = sendIsoMsg.pack();
        int tempSendPktLength = tempSendArray.length;
        int packetLength = Objects.isNull(tpduArray) ? tempSendPktLength : tempSendPktLength + 5;

        byte[] lengthArray = getLengthByte(packetLength, acquirerConnection.getLengthMode());
        if (Objects.nonNull(lengthArray)) {
            System.arraycopy(lengthArray, 0, headerByteArray, headerByteArrayLength, lengthArray.length);
            headerByteArrayLength += lengthArray.length;
        }

        if (Objects.nonNull(tpduArray)) {
            System.arraycopy(tpduArray, 0, headerByteArray, headerByteArrayLength, tpduArray.length);
            headerByteArrayLength += tpduArray.length;
        }

        if (headerByteArrayLength > 0) {
            byte[] sendByteArray = new byte[headerByteArrayLength+tempSendPktLength];

            System.arraycopy(headerByteArray, 0, sendByteArray, 0, headerByteArrayLength);
            System.arraycopy(tempSendArray, 0, sendByteArray, headerByteArrayLength, tempSendPktLength);

            return sendByteArray;
        } else {
            return tempSendArray;
        }

    }
    public int sendReceive( ISOMsg sendIsoMsg, ISOMsg receiveIsoMsg, AcquirerConnection acquirerConnection){
        boolean isTpdu = (!StringUtils.isEmpty(acquirerConnection.getTpdu()));
        InputStream socketInputStream = null;
        OutputStream socketOutputStream = null;
        int retVal = -1;
        try {

            byte[] sendByteArray = new byte[4096];
            byte[] initReceiveByteArray = new byte[4096];
            byte[] tempReceiveByteArray = new byte[4096];
            byte[] receiveByteArray = new byte[4096];
            int readLength = 0;

            sendByteArray = getPacketToSend(sendIsoMsg, acquirerConnection);
            socketOutputStream = acqSocket.getOutputStream();
            socketInputStream = acqSocket.getInputStream();

            dumpIsoMsg("Send To "+acquirerConnection.getAcquirer().getName(),sendIsoMsg);
            logger.log("SendDump ", StringParsingUtil.hexString(  sendByteArray));
            socketOutputStream.write(sendByteArray);

            readLength = socketInputStream.read(initReceiveByteArray);
            logger.log("ReadLength " + readLength);
            if (readLength < 2) {
                return -1;
            }

            byte[] tpduRecvArray = new byte[5];

            System.arraycopy(initReceiveByteArray, 0, tempReceiveByteArray, 0, readLength);
            System.arraycopy(initReceiveByteArray, 2, tpduRecvArray, 0, 5);
            logger.log( "Recv Dump: " +StringParsingUtil.hexString(  tempReceiveByteArray).substring(0, readLength * 2) );

            receiveByteArray = ISOUtil.hex2byte(StringParsingUtil.hexString(tempReceiveByteArray)
                    .substring(isTpdu ? 14 : 4, readLength * 2));

            receiveIsoMsg.unpack(receiveByteArray);
            receiveIsoMsg.setDirection(ISOMsg.INCOMING);
            receiveIsoMsg.setHeader(tpduRecvArray);
            dumpIsoMsg("Recv From "+acquirerConnection.getAcquirer().getName(), receiveIsoMsg);

            retVal = 0;


        } catch (SocketException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            retVal =  1;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            retVal = 2;
        } catch (ISOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            retVal = 3;
        }
        try {
            socketInputStream.close();
            socketOutputStream.close();
        } catch (Exception e) {
            // TODO: handle exception
        }

        return retVal;
    }

    private void dumpIsoMsg(String tag, ISOMsg msg) {
        try (CustomOutputStream outputStream = new CustomOutputStream(); PrintStream printStream = new PrintStream(outputStream)) {
            msg.dump(printStream, "\t");
            logger.log(tag, outputStream.toString());
        } catch (IOException e) {
            // Not Required
        }
    }

}
