/*
 * Copyright (C) 2020 - 2025 ScalAgent Distributed Technologies
 *
 * 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 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.
 *
 * Initial developer(s): ScalAgent Distributed Technologies
 * Contributor(s): 
 */
package org.ow2.joram.tools.jmscheck;

import java.text.DateFormat;
import java.util.Date;

import jakarta.jms.Connection;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageProducer;
import jakarta.jms.Session;
import jakarta.jms.TextMessage;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.ow2.joram.jakarta.jms.Queue;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

import fr.dyade.aaa.common.Debug;

/**
 * Component allowing to check a JMS connector.
 */
public final class JMSConnectorCheck {
  private static final Logger logger = Debug.getLogger(JMSConnectorCheck.class.getName());
  private static final boolean DEBUG = logger.isLoggable(BasicLevel.DEBUG);
  
  public static final int RUNNING = 0;
  public static final int UNREACHABLE = 1;
  static final String[] info = { "RUNNING", "UNREACHABLE" };

  /** retryStatusCount == 0 => RUNNING,  retryStatusCount > 0 => UNREACHABLE */ 
  private int retryStatusCount = UNREACHABLE;

  /**
   * Returns the status of the connector: 0 => RUNNING, >0 => UNREACHABLE.
   * 
   * @return the number of unreachable connection
   */
  public int getStatus() {
    return retryStatusCount;
  }

  public String getStatusInfo() {
    if (retryStatusCount > 0)
      return info[1] + '(' + retryStatusCount + ')';
    return info[0];
  }
  
  private void setStatus(int retry) {
    this.retryStatusCount = retry;
  }

  private int nbtry = 0;
  
  /**
   * Returns the total number of try since starting.
   * @return the total number of try since starting.
   */
  public int getNbTry() {
    return nbtry;
  }
  
  private transient int failures = 0;
  
  /**
   * Returns the total number of failures since starting.
   * @return the total number of failures since starting.
   */
  public int getNbFailures() {
    return failures;
  }

  private String errorMsg;

  /**
   * Returns the error message of the last try.
   * Returns null if the test is successful.
   * @return
   */
  public String getErrorMsg() {
    return errorMsg;
  }

  private String lastConnectTime = "-";

  /**
   * Returns the date of last successful connection.
   * @return the date of last successful connection.
   */
  public String getLastConnectTime() {
    return lastConnectTime;
  }
  
  private long latencyConnect = -1;

  /**
   * Returns the latency of the connection during the last try.
   * Returns -1 if the test failed.
   * @return the latency of the connection during the last try.
   */
  public long getLatencyConnect() {
    return latencyConnect;
  }

  private void setLatencyConnect(long latencyConnect) {
    this.latencyConnect = latencyConnect;
  }

  private long latencyPubSub = -1;

  /**
   * Returns the latency of the send/receive during the last try.
   * Returns -1 if the test failed.
   * @return the latency of the send/receive during the last try.
   */
  public long getLatencyPubSub() {
    return latencyPubSub;
  }

  private void setLatencyPubSub(long latencyPubSub) {
    this.latencyPubSub = latencyPubSub;
  }
  
  /** The maximum time waiting for connection. */
  private int timeout = 10; // in seconds

  /**
   * Returns the maximum time waiting for connection.
   * @return the maximum time waiting for connection.
   */
  public int getTimeOut() {
    return timeout;
  }

  /**
   * Sets the maximum time waiting for connection.
   * @param timeOut the maximum time waiting for connection.
   */
  public void setTimeOut(int timeOut) {
    this.timeout = timeOut;
  }
  
  /** JNDI Name of the ConnectionFactory to use. */
  private String cfname;

  public String getCFName() {
    return cfname;
  }
  
  /* User name and password for authentication */ 
  private String user;
  private String pass;

  /* Internal name of the queue, be careful this name is not a JNDI name. */
  private String qname;

  private InitialContext ictx;
  
  private ConnectionFactory cf;
  private Queue queue;

  /**
   * Creates a new component allowing to check a JMS connector.
   * 
   * @param cfname  JNDI name of the ConnectionFactory to use.
   * @param ictx    JNDI context allowing to retrieve ConnectionFactory.
   * @param user    User name for authentication, if no defined uses the ConnectionFactory default.
   * @param pass    Password for authentication, if no defined uses the ConnectionFactory default.
   * @param qname   Internal name of JMS destination.
   * @param timeout Maximum amount of time to wait connecting and receiving messages, by default 10s.
   */
  public JMSConnectorCheck(String cfname,
                           InitialContext ictx,
                           String user, String pass,
                           String qname,
                           int timeOut) {
    if (cfname == null)
      throw new IllegalArgumentException("Shall define ConnectionFactory name");
    this.cfname = cfname;
    
    if (ictx == null)
      throw new IllegalArgumentException("Shall define JNDI initial context");
    this.ictx = ictx;
    
    this.user = user;
    this.pass = pass;
    
    if (qname == null)
      throw new IllegalArgumentException("Shall define Queue name");
    this.qname = qname;
    
    this.timeout = timeOut;
  }
  
  /**
   * Creates a new component allowing to check a JMS connector.
   * 
   * @param cf      ConnectionFactory to use.
   * @param user    User name for authentication, if no defined uses the ConnectionFactory default.
   * @param pass    Password for authentication, if no defined uses the ConnectionFactory default.
   * @param qname   Internal name of JMS destination.
   * @param timeout Maximum amount of time to wait connecting and receiving messages, by default 10s.
   */
  public JMSConnectorCheck(ConnectionFactory cf,
                           String user, String pass,
                           String qname,
                           int timeOut) {
    if (cf == null)
      throw new IllegalArgumentException("Shall define ConnectionFactory");
    this.cf = cf;
    
    this.user = user;
    this.pass = pass;
    
    if (qname == null)
      throw new IllegalArgumentException("Shall define Queue name");
    
    this.timeout = timeOut;
  }
  

  private void getConnectionFactory() throws NamingException {
    if (cf != null) return;
    
    ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
    try {
      Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
      cf = (ConnectionFactory) ictx.lookup(cfname);
      try {
        ((org.ow2.joram.jakarta.jms.ConnectionFactory) cf).getParameters().connectingTimer = timeout;
      } catch (ClassCastException exc) {
        logger.log(BasicLevel.INFO, "JMSConnectorStatus(" + cfname + "): Cannot set connectingTimer.");
      }
    } catch (NamingException exc) {
      if (DEBUG)
        logger.log(BasicLevel.WARN, "JMSConnectorStatus(" + cfname + "): Cannot get ConnectionFactory.", exc);
      else
        logger.log(BasicLevel.WARN, "JMSConnectorStatus(" + cfname + "): Cannot get ConnectionFactory.");
      throw exc;
    } finally {
      Thread.currentThread().setContextClassLoader(originalContextClassLoader);
    }
  }

  /**
   * Checks the related JMS connector, creates a connection, then sends and receives a message.
   * After the call the test results are updated.
   * 
   * @return true if the check is successful, false otherwise.
   */
  public boolean check() {
    if (DEBUG)
      logger.log(BasicLevel.DEBUG, "JMSConnectorStatus(" + ((cfname == null)?"":cfname) + ").check");
    
    nbtry += 1;
    try {
      getConnectionFactory();
    } catch (NamingException exc) {
      failures += 1;
      setStatus(getStatus()+1);
      setLatencyConnect(-1L);
      setLatencyPubSub(-1L);
      errorMsg = "Cannot get ConnectionFactory";
      if (DEBUG)
        logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + ")<: Cannot get ConnectionFactory.", exc);
      else
        logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + "): Cannot get ConnectionFactory.");
      return false;
    }

    Connection cnx = null;
    try {
      long start = System.nanoTime();
      if ((user == null) || (pass == null))
        cnx = cf.createConnection();
      else
        cnx = cf.createConnection(user, pass);
      long dt = System.nanoTime() - start;
      setLatencyConnect(dt / 1000000L);
      lastConnectTime = DateFormat.getDateTimeInstance().format(new Date());
    } catch (JMSException exc) {
      failures += 1;
      setStatus(getStatus()+1);
      setLatencyConnect(-1L);
      setLatencyPubSub(-1L);
      errorMsg = "Cannot connect";
      if (DEBUG)
        logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + "): Cannot connect.", exc);
      else
        logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + "): Cannot connect.");
      return false;
    }
    
    try {
      Session sess = cnx.createSession(false, Session.AUTO_ACKNOWLEDGE);
      if (queue == null)
        queue = (Queue) sess.createQueue(qname);
      MessageProducer producer = sess.createProducer(queue);
      MessageConsumer cons = sess.createConsumer(queue);
      cnx.start();

      TextMessage sent = sess.createTextMessage("Test number " + nbtry);
      long start = System.nanoTime();
      producer.send(sent, Message.DEFAULT_DELIVERY_MODE, Message.DEFAULT_PRIORITY, timeout *1000L);

      Message received = cons.receive(timeout *1000L);
      long dt = System.nanoTime() - start;
      if (received == null) {
        failures += 1;
        setStatus(getStatus()+1);
        setLatencyPubSub(-1L);
        errorMsg = "Message not received";
        return false;
      } else {
        retryStatusCount = 0;
        setLatencyPubSub(dt / 1000000L);
        errorMsg = null;
        return true;
      }
    } catch (JMSException exc) {
      failures += 1;
      setStatus(getStatus()+1);
      setLatencyPubSub(-1L);
      errorMsg = "Error during message send/receive.";
      if (DEBUG)
        logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + "): Error during message send/receive.", exc);
      else
        logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + "): Error during message send/receive.");
      return false;
    } finally {
      try {
        // Closing the connection, closes all associated resources.
        cnx.close();
      } catch (JMSException exc) {
        if (DEBUG)
          logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + "): Error closing connection.", exc);
        else
          logger.log(BasicLevel.WARN, "JMSConnectorStatus.check(" + ((cfname == null)?"":cfname) + "): Error closing connection.");
      }
    }
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("JMSConnectorCheck [")
           .append("cfname=").append(cfname)
           .append(", user=").append(user)
           .append(", pass=****")
           .append(", qname=").append(qname)
           .append(", timeout=").append(timeout)
           .append("] -> [")
           .append("retryStatusCount=").append(retryStatusCount)
           .append(", nbtry=").append(nbtry)
           .append(", errorMsg=").append(errorMsg)
           .append(", lastConnectTime=").append(lastConnectTime)
           .append(", latencyConnect=").append(latencyConnect)
           .append(", latencyPubSub=").append(latencyPubSub)
           .append("]");
    return builder.toString();
  }
}
