/***********************************************************
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.transport.server;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

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

import com.verisign.epp.codec.gen.EPPDcp;
import com.verisign.epp.codec.gen.EPPGreeting;
import com.verisign.epp.codec.gen.EPPPurpose;
import com.verisign.epp.codec.gen.EPPRecipient;
import com.verisign.epp.codec.gen.EPPResponse;
import com.verisign.epp.codec.gen.EPPResult;
import com.verisign.epp.codec.gen.EPPStatement;
import com.verisign.epp.codec.gen.EPPTransId;
import com.verisign.epp.framework.EPPAssemblerException;
import com.verisign.epp.framework.EPPDispatcher;
import com.verisign.epp.framework.EPPEventException;
import com.verisign.epp.serverstub.SessionData;
import com.verisign.epp.transport.ServerEventHandler;
import com.verisign.epp.util.EPPEnv;

/**
 * The {@code EPPEPPQuicClientConnectionHandler} class manages a single client
 * EoQ connection. An EoQ connection is logically started when the
 * {@code handleConnection()} method is invoked when a QUIC stream is accepted.
 */
public class EPPQuicClientConnectionHandler implements ServerEventHandler, Cloneable {
	
	/**
	 * Packet sent when the client starts an EoQ connection
	 */
	public static final String EOQ_CONNECTION_START_PACKET = "EoQ Connection Start";
	
	/** Category for logging */
	private static Logger cat = LoggerFactory.getLogger(EPPQuicClientConnectionHandler.class);

	/** The idle timeout. */
	private static final int TIMEOUT_MINUTES = 3;

	/**
	 * The state this connection is in. If running is true then the session
	 * should still be accepting commands
	 */
	private boolean running;

	/** The number of commands that have been completed during the session */
	private int SessionCommandCount;

	/**
	 * The Session data for this session. Session data is sent to the event
	 * handlers in the server
	 */
	private SessionData sessionData;

	/** Connection idle timeout */
	private GregorianCalendar idleTimeOutTime;

	/**
	 * Creates a new EPPQuicClientConnectionHandler instance.
	 */
	public EPPQuicClientConnectionHandler() {
		this.running = true;
		this.SessionCommandCount = 0;

		this.sessionData = new SessionData();
		this.idleTimeOutTime = new GregorianCalendar();
		this.idleTimeOutTime.add(Calendar.MINUTE, TIMEOUT_MINUTES);
	}

	/**
	 * Makes a bitwise copy of this {@code EPPQuicClientConnectionHandler}
	 *
	 * @return Clone of the {@code EPPQuicClientConnectionHandler}
	 *
	 * @throws CloneNotSupportedException
	 *            Error with cloning
	 */
	@Override
	public Object clone() throws CloneNotSupportedException {
		EPPQuicClientConnectionHandler theCopy = (EPPQuicClientConnectionHandler) super.clone();
		theCopy.sessionData = (SessionData) this.sessionData.clone();
		theCopy.idleTimeOutTime = (GregorianCalendar) this.idleTimeOutTime.clone();

		return theCopy;
	}

	/**
	 * Invoked for a new client connection
	 *
	 * @param aInputStream
	 *           The inputStream of the new connection
	 * @param aOutputStream
	 *           The outputStream of the new connection
	 */
	@Override
	public void handleConnection(InputStream aInputStream, OutputStream aOutputStream) {
		cat.debug("Server accepted new connection. Thread id is: " + Thread.currentThread().hashCode());

		// Create EPP greeting
		EPPGreeting greeting = new EPPGreeting();

		int threadId = Thread.currentThread().hashCode();

		greeting.setServer(EPPEnv.getGreetingServerName());
		cat.debug("Server name = " + greeting.getServer());

		// Set DCP
		EPPDcp theDCP = new EPPDcp();
		theDCP.setAccess(EPPDcp.ACCESS_ALL);

		EPPStatement theStatement = new EPPStatement();

		EPPPurpose thePurpose = new EPPPurpose();
		thePurpose.setAdmin(true);
		thePurpose.setProv(true);
		theStatement.setPurpose(thePurpose);

		EPPRecipient theRecipient = new EPPRecipient();
		theRecipient.addOurs(null);
		theRecipient.setPublic(true);
		theStatement.setRecipient(theRecipient);

		theStatement.setRetention(EPPStatement.RETENTION_STATED);

		theDCP.addStatement(theStatement);

		greeting.setDcp(theDCP);

		this.sessionData.setGreeting(greeting);

		this.idleTimeOutTime = new GregorianCalendar();
		this.idleTimeOutTime.add(Calendar.MINUTE, TIMEOUT_MINUTES);

		EPPDispatcher theDispatcher = EPPDispatcher.getInstance();

		/**
		 * First, call processConnection to send the greeting.
		 */
		theDispatcher.processConnection(aInputStream, aOutputStream, this.sessionData);
		
		/**
		 * Read the EoQ connection start packet 
		 */
		try {
			DataInputStream theStream = new DataInputStream(aInputStream);
			int thePacketSize = theStream.readInt();
			byte[] thePacket = new byte[thePacketSize - 4];
			theStream.readFully(thePacket);
			
			if (!(new String(thePacket)).equalsIgnoreCase(EOQ_CONNECTION_START_PACKET)) {
				cat.error("handleConnection: Packet [" + new String(thePacket)
				      + "] != expected EoQ Connection Start Packet [" + EOQ_CONNECTION_START_PACKET + "]");
				this.running = false;			
			}					
		}
		catch (IOException ex) {
			cat.error("handleConnection: IOException while reading the EoQ connect start packet: " + ex);
			this.running = false;
		}

		/**
		 * Now loop and process each arriving Message on the Stream.
		 */
		while (this.running) {
			try {
				theDispatcher.processMessage(aInputStream, aOutputStream, this.sessionData);
				this.SessionCommandCount++;
				this.resetIdleTimeOut();

				/**
				 * If a logout has occurred we should not bRunning. Ha!
				 */
				this.running = !this.sessionData.hasLogoutOccured();
			}
			catch (EPPEventException e) {
				cat.error("EPP Event Exception", e);

				sendErrorResponse(EPPResult.COMMAND_FAILED,
				      "Internal Server Error, " + "EPP Event Exception: " + e.getMessage(), aOutputStream,
				      this.sessionData);
			}
			catch (EPPAssemblerException ex) {
				if (ex.equals(EPPAssemblerException.FATAL)) {
					cat.error("Fatal EPPAssemblerException caught " + "stopping client thread", ex);

					sendErrorResponse(EPPResult.COMMAND_FAILED,
					      "Internal Server Error," + " EPPAssemblerException: " + ex.getMessage(), aOutputStream,
					      this.sessionData);

					this.running = false;
				}

				// Command Syntax Error? 2001
				else if (ex.equals(EPPAssemblerException.XML)) {
					cat.error("EPPAssemblerException.XML, sending " + "COMMAND_SYNTAX_ERROR response to client");

					sendErrorResponse(EPPResult.COMMAND_SYNTAX_ERROR, "XML Schema Validation Error, " + ex.getMessage(),
					      aOutputStream, this.sessionData);
				}

				// Command Syntax Error? 2001
				else if (ex.equals(EPPAssemblerException.MISSINGPARAMETER)) {
					cat.error("EPPAssemblerException.MISSINGPARAMETER," + " sending MISSINGPARAMETER response to client");

					sendErrorResponse(EPPResult.MISSING_PARAMETER, "Command processing error, , " + ex.getMessage(),
					      aOutputStream, this.sessionData);
				}
				else if (ex.equals(EPPAssemblerException.COMMANDNOTFOUND)) {
					cat.error(
					      "EPPAssemblerException.COMMANDNOTFOUND," + " sending UNIMPLEMENTED_COMMAND response to client");

					sendErrorResponse(EPPResult.UNIMPLEMENTED_COMMAND, "Command not found, " + ex.getMessage(),
					      aOutputStream, this.sessionData);
				}
				else if (ex.equals(EPPAssemblerException.EXTENSIONNOTFOUND)) {
					cat.error("EPPAssemblerException.EXTENSIONNOTFOUND,"
					      + " sending UNIMPLEMENTED_EXTENSION response to client");

					sendErrorResponse(EPPResult.UNIMPLEMENTED_EXTENSION, "Extension not found, " + ex.getMessage(),
					      aOutputStream, this.sessionData);
				}
				// Client closed the connection?
				else if (ex.equals(EPPAssemblerException.CLOSECON)) {
					cat.error("EPPAssemblerException.CLOSECON caught, " + "stopping thread");
					this.running = false;
				}

				// Interrupted IO?
				else if (ex.equals(EPPAssemblerException.INTRUPTEDIO)) {
					cat.debug("EPPAssemblerException.INTRUPTEDIO caught, " + "no command received");
				}

				// Who knows what happened? Send the old Internal Server Error.
				else {
					cat.error("Unknown EPPAssemblerException type");

					sendErrorResponse(EPPResult.COMMAND_FAILED,
					      "Internal Server Error, " + "Unknown EPPAssemblerException" + ex.getMessage(), aOutputStream,
					      this.sessionData);
				}
			}
		}

		cat.debug("Server closed connection. Thread id is: " + Thread.currentThread().hashCode());
	}

	/**
	 * Send error response to the client
	 *
	 * @param aCode
	 *           EPP result code
	 * @param aDescription
	 *           Error reason description. Set to {@code null} if there is no
	 *           reason description
	 * @param out
	 *           Output stream to write to
	 * @param aSessionData
	 *           Session data of the server
	 */
	private void sendErrorResponse(int aCode, String aDescription, OutputStream out, SessionData aSessionData) {
		cat.debug("<<<<<<<<<<<<<<<<<<<<<<  Enter sendErrorResponse()" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");

		cat.debug("Sending error response to client, code = " + aCode + ", description = " + aDescription);

		// Create the result
		EPPResult theResult = new EPPResult(aCode);
		if (aDescription != null) {
			theResult.addExtValueReason(aDescription);
		}

		// @todo - Make the server transaction id randomly generated
		EPPTransId theTransId = new EPPTransId("svrError1");

		// Create the response
		EPPResponse response = new EPPResponse(theTransId, theResult);

		// Send the response
		try {
			EPPDispatcher.getInstance().send(response, out);
			out.flush();
		}
		catch (Exception e) {
			cat.debug("sendErrorResponse(): Error sending error response to client: ");
		}

		cat.debug("sendErrorResponse(): Return");
	}

	/**
	 * Makes the current session stop receiving commands
	 */
	public void close() {
		this.running = false;
	}

	/**
	 * Resets the idle Timeout
	 */
	protected void resetIdleTimeOut() {
		this.idleTimeOutTime.setTime(new Date());
		this.idleTimeOutTime.add(Calendar.MINUTE, TIMEOUT_MINUTES);
	}
}
