/*
 * Copyright (C) 2020 - 2022 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.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

import javax.naming.InitialContext;
import javax.naming.NamingException;

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

import fr.dyade.aaa.agent.SCServer;
import fr.dyade.aaa.common.Debug;
import fr.dyade.aaa.util.management.MXWrapper;

/**
 * Root component managing all JMS connector healthcheck components.
 */
public final class JMSStatus implements JMSStatusMBean {
  static final Logger logger = Debug.getLogger(JMSStatus.class.getName());

  @Override
  public int getStatus() {
    for (JMSConnectorStatus connector : connectors.values()) {
      if (connector.getStatus() != 0)
        return JMSConnectorCheck.UNREACHABLE;
    }
    return JMSConnectorCheck.RUNNING;
  }

  @Override
  public String getStatusInfo() {
    return JMSConnectorCheck.info[getStatus()];
  }

  static final int DFLT_PERIOD = 60;  // in seconds
  private int globalPeriod = DFLT_PERIOD;   // in seconds 

  @Override
  public int getPeriod() {
    return globalPeriod;
  }

  @Override
  public void setPeriod(int period) {
    this.globalPeriod = period;
    for (JMSConnectorStatus connector : connectors.values()) {
      connector.setPeriod(period);
    }
  }
  
  static final int DFLT_TIMEOUT = 10; // in seconds
  private int globalTimeout = DFLT_TIMEOUT; // in seconds

  @Override
  public int getTimeout() {
    return globalTimeout;
  }

  @Override
  public void setTimeout(int timeout) {
    this.globalTimeout = timeout;
  }

  static final int DFLT_THRESHOLD = 5;
  static int threshold = DFLT_THRESHOLD;

  @Override
  public int getThreshold() {
    return threshold;
  }

  @Override
  public void setThreshold(int threshold) {
    this.threshold = threshold;
  }
  
  static final int DFLT_DELAY = 600;
  static int delay = DFLT_DELAY;

  @Override
  public int getDelay() {
    return delay;
  }

  @Override
  public void setDelay(int delay) {
    this.delay = delay;
  }

  private HashMap<String, JMSConnectorStatus>  connectors;

  /**
   * Adds an healthcheck for the specified 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 period  Period between 2 checks, by default 60s.
   * @param timeout Maximum amount of time to wait connecting and receiving messages, by default 10s.
   */
  public void addConnectorStatus(String cfname, InitialContext ictx,
                                 String user, String pass, String qname,
                                 int period, int timeout) {
    JMSConnectorStatus connector = new JMSConnectorStatus(this, cfname, ictx, user, pass, qname, period, timeout);
    connectors.put(cfname, connector);
  }

  static final String DFLT_DUMP_FILE = "joram.dump";

  static String dumpFilePath = DFLT_DUMP_FILE;

  static long lastDump = 0L;
  
  static void checkDump(String name, int retry) {
    if ((dumpFilePath == null) || (threshold <= 0) || (delay <= 0))
      return;
    
    long current = System.currentTimeMillis();
    if ((retry >= threshold) && (((current - lastDump) / 1000) > delay)) {
      JMSStatus.dumpServerState(dumpFilePath + '.' + current,
                                "Fail to connect to " + name + " after " + retry + " attempts.");
      JMSStatus.lastDump = current;
    }
  }
  
  public static final String JNDI_FACTORY_DFLT = "fr.dyade.aaa.jndi2.client.NamingContextFactory";
  public static final String JNDI_HOST_DFLT = "localhost";
  public static final String JNDI_PORT_DFLT = "16400";

  static ScheduledExecutorService scheduler;
  private static final int DFLT_THREAD_POOL_SIZE = 1;

  private String name;
  
  String getName() {
    return name;
  }
  
  /**
   * Creates a JMS connectors healthcheck root component.
   * 
   * @param name          Name of component, used to define the domain of the JMX MBeans.
   * @param period        Default period of activation for checks (seconds).
   * @param timeout       Default timeout for checks (seconds).
   * @param dumpFilePath  Pathname for server dump, the current time is aggregated at the end of the file name.
   * @param threshold     Number of consecutive failures needed to trigger a dump.
   * @param delay         Minimal delay between 2 dumps (seconds).
   */
  public JMSStatus(String name, int period, int timeout, String dumpFilePath, int threshold, int delay) {
    this.name = name;
    this.globalPeriod = period;
    this.globalTimeout = timeout;
    
    this.dumpFilePath = dumpFilePath;
    this.threshold = threshold;
    this.delay = delay;

    connectors = new HashMap<String, JMSConnectorStatus>();
  }

  /**
   * Creates the JNDI context to use.
   * 
   * @param jndiFile    Path of JNDI properties file. If not defined, Joram's default are used. "fr.dyade.aaa.jndi2.client.NamingContextFactory" for
   *                    JNDI Factory, "localhost", and 16400 for host and port. These values can be overloaded by specific properties below.
   * @param jndiFactory Classname of the JNDI factory (see "java.naming.factory.initial" property).
   * @param jndiHost    Hostname ou IP address of JNDI server.
   * @param jndiPort    Listening port of JNDI server.
   * @return
   * @throws IOException
   * @throws NamingException
   */
  InitialContext getInitialContext(String jndiFile,
                                          String jndiFactory,
                                          String jndiHost,
                                          String jndiPort) throws IOException, NamingException {
    Properties jndiProps = new Properties();
    if (jndiFile != null) {
      try (FileInputStream fis = new FileInputStream(jndiFile)) {
        jndiProps.load(fis);
      }
    } else {
      jndiProps.setProperty("java.naming.factory.initial", JNDI_FACTORY_DFLT);
      jndiProps.setProperty("java.naming.factory.host", JNDI_HOST_DFLT);
      jndiProps.setProperty("java.naming.factory.port", JNDI_PORT_DFLT);
    }

    if (jndiFactory != null)
      jndiProps.setProperty("java.naming.factory.initial", jndiFactory);

    if (jndiHost != null)
      jndiProps.setProperty("java.naming.factory.host", jndiHost);

    if (jndiPort != null)
      jndiProps.setProperty("java.naming.factory.port", jndiPort);

    ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
    try {
      Thread.currentThread().setContextClassLoader(getClass().getClassLoader());

      InitialContext ictx = new InitialContext(jndiProps);
      return ictx;
    } finally {
      Thread.currentThread().setContextClassLoader(originalContextClassLoader);
    }
  }

  // --------------------------------------------------------------------------------
  // Life cycle
  // --------------------------------------------------------------------------------

  /**
   * Starts the component and all connector's healthcheck defined. 
   */
  @Override
  public void start() throws Exception {
    if (scheduler == null || scheduler.isTerminated())
      scheduler = Executors.newScheduledThreadPool(DFLT_THREAD_POOL_SIZE);

    // start task
    for (JMSConnectorStatus status : connectors.values()) {
      status.start();
    }
    registerMBean();
  }

  /**
   * Stops the component and all connector's healthcheck defined.
   */
  @Override
  public void stop() throws Exception {
    for (JMSConnectorStatus status : connectors.values()) {
      try {
        status.stop();
      } catch (Exception e) { }
    }
    scheduler.shutdown();
    unregisterMBean();
  }

  // --------------------------------------------------------------------------------
  // JMX handling
  // --------------------------------------------------------------------------------

  static String mbeanName = "type=healthcheck";

  void registerMBean() {
    try {
      MXWrapper.registerMBean(this, name, mbeanName);
    } catch (Exception e) {
      logger.log(BasicLevel.WARN, "registerMBean: " + mbeanName, e);
    }
  }

  void unregisterMBean() {
    try {
      MXWrapper.unregisterMBean(name, mbeanName);
    } catch (Exception e) {
      logger.log(BasicLevel.WARN, "unregisterMBean: " + mbeanName, e);
    }
  }

  @Override
  public void dumpServerState(String path) {
    dumpServerState(path, null);
  }

  static void dumpServerState(String path, String cause) {
    SCServer.dumpServerState(path, cause);
  }
}
