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

import java.util.concurrent.ThreadLocalRandom;

import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.verisign.epp.interfaces.EPPCommandException;
import com.verisign.epp.interfaces.EPPSession;

public class EPPGenericSessionPoolableFactory extends EPPSessionPoolableFactory
      implements PooledObjectFactory<EPPPooledSession> {

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

	/**
	 * Default constructor. Must set the following attributes for using:<br>
	 * <br>
	 * <ul>
	 * <li>clientId
	 * <li>password
	 * <li>absoluteTimeout
	 * <li>idleTimeout
	 * </ul>
	 */
	public EPPGenericSessionPoolableFactory() {
		super();
	}

	/**
	 * Create an EPP session poolable factory with the client id, password used
	 * to authenticate the session along with the timeout settings.
	 *
	 * @param aClientId
	 *           Login id used to authenticate
	 * @param aPassword
	 *           Password used to authenticate
	 * @param aAbsoluteTimeout
	 *           Session absolute timeout
	 * @param aIdleTimeout
	 *           Session idle timeout
	 */
	public EPPGenericSessionPoolableFactory(String aClientId, String aPassword, long aAbsoluteTimeout,
	      long aIdleTimeout) {
		super();
		this.clientId = aClientId;
		this.password = aPassword;
		this.absoluteTimeout = aAbsoluteTimeout;
		this.idleTimeout = aIdleTimeout;
	}

	/**
	 * Create an EPP session poolable factory with the client id, password used
	 * to authenticate the session along with the timeout settings.
	 *
	 * @param aClientId
	 *           Login id used to authenticate
	 * @param aPassword
	 *           Password used to authenticate
	 * @param aMinAbsoluteTimeout
	 *           Session minimum absolute timeout
	 * @param aMaxAbsoluteTimeout
	 *           Session maximum absolute timeout
	 * @param aIdleTimeout
	 *           Session idle timeout
	 */
	public EPPGenericSessionPoolableFactory(String aClientId, String aPassword, long aMinAbsoluteTimeout,
	      long aMaxAbsoluteTimeout, long aIdleTimeout) {
		super();
		this.clientId = aClientId;
		this.password = aPassword;
		this.minAbsoluteTimeout = aMinAbsoluteTimeout;
		this.maxAbsoluteTimeout = aMaxAbsoluteTimeout;
		this.idleTimeout = aIdleTimeout;
	}

	/**
	 * Session being borrowed from the pool.
	 *
	 * @param aSession
	 *           Session being returned
	 */
	@Override
	public void activateObject(PooledObject<EPPPooledSession> aSession) throws Exception {
		// Nothing for now
	}

	/**
	 * Destroy object from the pool.
	 *
	 * @param aSession
	 *           Session being destroyed
	 */
	@Override
	public void destroyObject(PooledObject<EPPPooledSession> aSession) throws Exception {
		log.debug("destroyObject(): enter, EPPPooledGenericSessionession id = " + aSession.getObject());

		// Automatically end the session?
		if (this.initSessionOnMake) {
			EPPSession theSession = (EPPSession) aSession.getObject();

			// Try to end the session gracefully
			try {
				// Set the client transaction identifier?
				if (this.clientTransIdGenerator != null) {
					theSession.setTransId(this.clientTransIdGenerator.genClientTransId());
				}

				log.debug("destroyObject(): Calling end session");
				theSession.endSession();
			}
			catch (Exception ex) {
				log.debug("destroyObject(): Exception ending session: " + ex);
			}
		}
		else {
			log.debug("destroyObject(): initSessionOnMake is false, so don't end the session automatically on destroy");
		}
	}

	/**
	 * Creates a new session object.
	 */
	@Override
	public PooledObject<EPPPooledSession> makeObject() throws Exception {

		log.debug("makeObject(): enter");

		EPPPooledGenericSession theSession = this.makeSession();

		log.debug("makeObject(): Make session with id = " + theSession);

		theSession.setClientID(this.clientId);
		theSession.setPassword(this.password);

		// Set the client transaction identifier?
		if (this.clientTransIdGenerator != null) {
			theSession.setTransId(this.clientTransIdGenerator.genClientTransId());
		}

		// Initialize the session via an EPP login?
		if (this.initSessionOnMake) {

			log.debug("makeObject(): establishing session, with session class " + theSession.getClass().getName());

			// Establish authenticated session
			try {
				theSession.initSession();

				log.debug("makeObject(): established session, with session class " + theSession.getClass().getName());
			}
			catch (EPPCommandException ex) {
				log.error("makeObject(): error initializing session " + theSession.getClass().getName() + ": " + ex);

				// Ensure that the connection is closed
				try {
					theSession.endConnection();
				}
				catch (EPPCommandException ex1) {
					// Ignore
				}

				throw ex;
			}
		}

		log.debug("makeObject(): exit");
		return new DefaultPooledObject<>(theSession);
	}

	/**
	 * Session is being returned to the pool.
	 *
	 * @param aSession
	 *           Session being returned
	 */
	@Override
	public void passivateObject(PooledObject<EPPPooledSession> aSession) throws Exception {
		// Nothing for now
	}

	/**
	 * Validates a session by sending a keep alive. If an exception occurs from
	 * the keep alive, than the session is not valid.
	 *
	 * @param aSession
	 *           Session to validate
	 *
	 * @return {@code true} if the session is valid; {@code false} otherwise.
	 */
	@Override
	public boolean validateObject(PooledObject<EPPPooledSession> aSession) {
		log.debug("validateObject(): enter, session id = " + aSession);
		EPPPooledSession theSession = aSession.getObject();
		long currentTime = System.currentTimeMillis();
		boolean isValid;

		try {
			// Is session past absolute timeout?
			if (currentTime - theSession.getCreatedTime() > theSession.getAbsoluteTimeout()) {
				log.debug("validateObject(): session id = " + aSession + " is past absolute timeout");
				isValid = false;
			} // Idle timeout?
			else if (System.currentTimeMillis() - theSession.getLastTouched() > this.getIdleTimeout()) {
				log.debug("validateObject(): session id = " + aSession + " is past idle timeout, sending hello");
				theSession.hello();
				theSession.touch();
				isValid = true;
			}
			else {
				log.debug("validateObject(): session id = " + aSession + " is valid");
				isValid = true;
			}

		}
		catch (Exception ex) {
			log.debug("validateObject(): session id = " + aSession + " caused Exception: " + ex);
			isValid = false;
		}

		log.debug("validateObject(): exit, isValid = " + isValid);
		return isValid;
	}

	/**
	 * Make an EPP session instance for pool. This can be overridden by a derived
	 * class to create a custom EPP session instance (i.e. HTTP).
	 *
	 * @return {@code EPPSession} instance
	 *
	 * @throws Exception
	 *            Configuration error or error creating pooled session.
	 */
	protected EPPPooledGenericSession makeSession() throws Exception {
		log.debug("makeSession(): enter");

		EPPPooledGenericSession session = null;

		if (this.serverName == null || this.serverPort == null) {
			throw new EPPSessionPoolException("makeSession(): serverName or serverPort is null");
		}

		if (this.clientHost == null) {
			session = new EPPPooledGenericSession(this.serverName, this.serverPort.intValue(), this.sslContext);
		}
		else {
			session = new EPPPooledGenericSession(this.serverName, this.serverPort.intValue(), this.clientHost,
			      this.sslContext);
		}

		// Randomize the absolute timeout?
		if (this.isRandomAbsoluteTimeout()) {
			// Randomize the session absolute timeout
			long theAbsoluteTimeout = ThreadLocalRandom.current().nextLong(this.minAbsoluteTimeout,
			      this.maxAbsoluteTimeout + 1);
			log.debug("makeSession(): setting random absolute timeout (ms) = " + theAbsoluteTimeout);

			session.setAbsoluteTimeout(theAbsoluteTimeout);
		}
		else {
			log.debug("makeSession(): setting absolute timeout (ms) = " + this.absoluteTimeout);

			session.setAbsoluteTimeout(this.absoluteTimeout);
		}

		return session;
	}

}
