/***********************************************************
Copyright (C) 2024 VeriSign, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

http://www.verisign.com/nds/naming/namestore/techdocs.html
***********************************************************/
package com.verisign.epp.interfaces;

import java.io.DataOutputStream;
import java.io.File;
import java.net.URI;
import java.net.http.HttpClient;
import java.security.KeyStore;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.verisign.epp.pool.parser.EPPSchemaCachingParserPool;
import com.verisign.epp.transport.client.EPPSSLConfig;
import com.verisign.epp.transport.client.EPPSSLContext;
import com.verisign.epp.transport.client.EPPSSLImpl;
import com.verisign.epp.util.EPPEnv;
import com.verisign.epp.util.EPPEnvException;
import com.verisign.epp.util.EPPSendReceiveLogger;
import com.verisign.epp.util.EPPXMLStream;

import tech.kwik.core.QuicClientConnection;
import tech.kwik.core.QuicConnection.QuicVersion;
import tech.kwik.core.QuicStream;
import tech.kwik.core.log.NullLogger;

/**
 * EPPQuicSession provides behavior for communicating with an EPP Server using
 * the QUIC protocol as the transport.
 */
public class EPPQuicSession extends EPPSession {

	/** Category for logging */
	private static Logger cat = LoggerFactory.getLogger(EPPQuicSession.class);

	/**
	 * Packet sent when the client starts an EoQ connection, which is a QUIC
	 * stream
	 */
	public static final String EOQ_CONNECTION_START_PACKET = "EoQ Connection Start";

	/**
	 * QUIC connection
	 */
	private QuicClientConnection quicConnection = null;

	/**
	 * QUIC stream created from QUIC connection and representing an EPP
	 * connection
	 * 
	 * @todo Look at a way to reuse a QUIC connection across multiple QUIC
	 *       streams
	 */
	private QuicStream quickStream = null;

	/**
	 * The URL with port to connect to.
	 */
	private String url = null;

	/**
	 * {@code EPPSSLContext} to see for the {@code EPPQuicSession}.
	 */
	private EPPSSLContext sslContext;

	/**
	 * Log the packet logging and receiver.
	 */
	private static EPPSendReceiveLogger sendReceiveLogger = EPPEnv.getSendReceiveLogger();

	/**
	 * Contact an {@code EPPQuicSession} instance using the URL generated from
	 * the "EPP.ServerName" and "EPP.ServerPort" configuration properties. The
	 * host name "localhost" and port number 1700, will result in the URL
	 * "eoq://localhost:1700".
	 * 
	 * @exception EPPCommandException
	 *               Error initializing the {@code EPPQuicSession}
	 */
	public EPPQuicSession() throws EPPCommandException {
		this.init();
	}

	/**
	 * Construct an {@code EPPQuicSession} instance that points to the given URL.
	 * An example is {@code https://localhost:1700/}.
	 *
	 * @param aUrl
	 *           URL to connect to
	 * @throws EPPCommandException
	 *            Error initializing the session
	 */
	public EPPQuicSession(String aUrl) throws EPPCommandException {
		this.url = aUrl;
		this.init();
	}

	/**
	 * Construct an {@code EPPQuicSession} instance that points to the given URL
	 * and an {@code EPPSSLContext}. An example is
	 * {@code eoq://test.vgrs.com:1700/}.
	 *
	 * @param aUrl
	 *           URL to connect to
	 * @param aSSLContext
	 *           Optional specific SSL context to use. Set to {@code null} if
	 *           undefined.
	 * @throws EPPCommandException
	 *            Error initializing the session
	 */
	public EPPQuicSession(String aUrl, EPPSSLContext aSSLContext) throws EPPCommandException {
		this.url = aUrl;
		if (aSSLContext != null) {
			this.sslContext = aSSLContext;
		}
		this.init();
	}

	/**
	 * Construct an {@code EPPQuicSession} instance that dynamically creates the
	 * server URL to connect to based on the host name and port number. The host
	 * name "localhost" and port number 1700, will result in the URL
	 * "eoq://localhost:1700".
	 *
	 * @param aHostName
	 *           Host name or IP address of server
	 * @param aPortNumber
	 *           Server port number
	 * @throws EPPCommandException
	 *            Error initializing the session
	 */
	public EPPQuicSession(String aHostName, int aPortNumber) throws EPPCommandException {
		this.url = "eoq://" + aHostName + ":" + aPortNumber;
		this.init();
	}

	/**
	 * Construct an {@code EPPQuicSession} instance that dynamically creates the
	 * server URL to connect to based on the host name and port number, with an
	 * {@code SSLContext}. The host name "localhost" and port number 1700, will
	 * result in the URL "eoq://localhost:1700".
	 *
	 * @param aHostName
	 *           Host name or IP address of server
	 * @param aPortNumber
	 *           Server port number
	 * @param aSSLContext
	 *           Optional specific SSL context to use. Set to {@code null} if
	 *           undefined.
	 * @throws EPPCommandException
	 *            Error initializing the session
	 */
	public EPPQuicSession(String aHostName, int aPortNumber, EPPSSLContext aSSLContext) throws EPPCommandException {
		this.url = "eoq://" + aHostName + ":" + aPortNumber;
		if (aSSLContext != null) {
			this.sslContext = aSSLContext;
		}
		this.init();
	}

	/**
	 * Helper method called by constructor to perform any initialization required
	 * for the {@code EPPQuicSession} class.
	 *
	 * @throws EPPCommandException
	 *            if something goes wrong
	 */
	@Override
	public void init() throws EPPCommandException {
		try {
			if (this.url == null) {
				this.url = EPPEnv.getServerName();
			}
		}
		catch (EPPEnvException e) {
			cat.error("Error when trying to get EPP Server Name (URL): ", e);
			throw new EPPCommandException("Error when trying to get EPP Server Name (URL): " + e.getMessage());
		}

		// Initialize SSL
		try {

			if (this.sslContext != null && this.sslContext.getSslConfig() != null) {
				cat.debug("EPPQuicSession: Initializing SSL with provided EPPSSLContext");
				this.sslContext = EPPSSLImpl.initialize(this.sslContext.getSslConfig());
			}
			else {
				if (!EPPSSLImpl.isInitialized()) {
					cat.debug("EPPQuicSession: Initializing SSL with default EPPSSLImpl");
					EPPSSLImpl.initialize();
				}
				this.sslContext = EPPSSLImpl.getEPPSSLContext();
			}
		}
		catch (Exception ex) {
			cat.error("Exception initializing SSL: " + ex);
			throw new EPPCommandException("Exception initializing SSL", ex);
		}

		EPPSSLConfig theSslConfig = this.sslContext.getSslConfig();

		try {
			// Initialize the QUIC connection / stream
			QuicClientConnection.Builder theBuilder = QuicClientConnection.newBuilder();
			theBuilder.uri(new URI(this.url));
			theBuilder.applicationProtocol("eoq");

			// Truststore
			char[] theTrustStorePassPhrase = null;
			if (theSslConfig.getTrustStorePassPhrase() != null) {
				theTrustStorePassPhrase = theSslConfig.getTrustStorePassPhrase().toCharArray();
			}
			theBuilder.customTrustStore(
			      KeyStore.getInstance(new File(theSslConfig.getTrustStoreFileName()), theTrustStorePassPhrase));

			// Identity Keystore
			char[] theIdentityPassPhrase = null;
			if (theSslConfig.getIdentityPassPhrase() != null) {
				theIdentityPassPhrase = theSslConfig.getIdentityPassPhrase().toCharArray();
			}
			theBuilder.clientKeyManager(
			      KeyStore.getInstance(new File(theSslConfig.getIdentityFileName()), theIdentityPassPhrase));
			theBuilder.clientKey(theSslConfig.getIdentityPassPhrase());
			
			// Disable Kwik looger, since there is not very useful information
			theBuilder.logger(new NullLogger());
						
			if (EPPEnv.getSSLDisableHostnameVerification()) {
				/**
				 * @todo - Replace with more specific API when Kwik supports it,
				 *       which currently disables all server certificate validation.
				 */
				theBuilder.noServerCertificateCheck();
			}

			theBuilder.socketFactory(null);

			this.quicConnection = theBuilder.build();

			this.quicConnection.connect();

			cat.debug("Created connection to URL: " + this.url);

			this.quickStream = this.quicConnection.createStream(true);

			cat.debug(
			      "Created bidirectional stream to URL: " + this.url + ", with id = " + this.quickStream.getStreamId());

			// Get input/output streams from QUIC stream
			super.inputStream = this.quickStream.getInputStream();
			super.outputStream = this.quickStream.getOutputStream();

			// Write EoQ connection start packet to trigger creatino of the stream
			// in the server
			DataOutputStream theStream = new DataOutputStream(super.outputStream);
			theStream.writeInt(EOQ_CONNECTION_START_PACKET.length() + 4);
			theStream.write(EOQ_CONNECTION_START_PACKET.getBytes());
			theStream.flush();
		}
		catch (Exception ex) {
			cat.error("Exception creating QUIC connection/stream: " + ex);
			throw new EPPCommandException("Exception creating QUIC connection/stream", ex);
		}

		// Initialize the XML stream
		super.xmlStream = new EPPXMLStream(EPPSchemaCachingParserPool.getInstance().getPool());

		// Initialize the login adapter
		super.initLoginAdapter();

		cat.debug("Successfully initialized with URL: " + this.url);
	}
	
	/**
	 * Gets the server URL to connect to.
	 *
	 * @return Gets the server URL to connection to if set; {@code null}
	 *         otherwise.
	 */
	public String getUrl() {
		return this.url;
	}

	/**
	 * Sets the server URL to connect to.
	 *
	 * @param aUrl
	 *           Server URL to
	 */
	public void setUrl(String aUrl) {
		this.url = aUrl;
	}
}
