/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.admin.jmx.internal;

import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.JMRuntimeException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.MBeanServerNotification;
import javax.management.MalformedObjectNameException;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.timer.TimerMBean;

import org.apache.commons.modeler.ManagedBean;
import org.apache.commons.modeler.Registry;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;

import org.apache.geode.SystemFailure;
import org.apache.geode.admin.RuntimeAdminException;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.internal.classloader.ClassPathLoader;
import org.apache.geode.logging.internal.log4j.api.LogService;

/**
 * Common support for MBeans and {@link ManagedResource}s. Static loading of this class creates the
 * MBeanServer and Modeler Registry.
 *
 * @since GemFire 3.5
 *
 */
@Deprecated
public class MBeanUtils {

  private static final Logger logger = LogService.getLogger();

  /** The default MBeanServer domain name is "GemFire" */
  private static final String DEFAULT_DOMAIN = "GemFire";

  /** MBean Name for refreshTimer */
  private static final String REFRESH_TIMER_NAME = DEFAULT_DOMAIN + ":type=RefreshTimer";

  /* indicates whether the mbeanServer, registry & refreshTimer are started */
  @MakeNotStatic
  private static boolean isStarted;

  /** The Commons-Modeler configuration registry for our managed beans */
  @MakeNotStatic
  private static Registry registry;

  /** The <code>MBeanServer</code> for this application */
  @MakeNotStatic
  private static MBeanServer mbeanServer;

  /** MBean name of the Timer which handles refresh notifications */
  @MakeNotStatic
  private static ObjectName refreshTimerObjectName;

  /** Actual TimerMBean responsible for refresh notifications */
  @MakeNotStatic
  private static TimerMBean refreshTimer;

  /**
   * Map of ObjectNames to current timerNotificationIds
   * <p>
   * map: key=ObjectName, value=map: key=RefreshNotificationType, value=timerNotificationId
   */
  @MakeNotStatic
  private static final Map<NotificationListener, Map<RefreshNotificationType, Integer>> refreshClients =
      new HashMap<>();

  /** key=ObjectName, value=ManagedResource */
  @MakeNotStatic
  private static final Map<ObjectName, ManagedResource> managedResources =
      new HashMap<>();

  static {
    try {
      refreshTimerObjectName = ObjectName.getInstance(REFRESH_TIMER_NAME);
    } catch (Exception e) {
      logStackTrace(Level.ERROR, e);
    }
  }

  /**
   * Initializes Mbean Server, Registry, Refresh Timer & registers Server Notification Listener.
   *
   * @return reference to the mbeanServer
   */
  static MBeanServer start() {
    if (!isStarted) {
      mbeanServer = createMBeanServer();
      registry = createRegistry();

      registerServerNotificationListener();
      createRefreshTimer();
      isStarted = true;
    }

    return mbeanServer;
  }

  /**
   * Stops Registry, Refresh Timer. Releases Mbean Server after.
   */
  static void stop() {
    if (isStarted) {
      stopRefreshTimer();

      registry.stop();
      registry = null;
      releaseMBeanServer();// makes mbeanServer null
      isStarted = false;
    }
  }

  /**
   * Create and configure (if necessary) and return the <code>MBeanServer</code> with which we will
   * be registering our <code>ModelMBean</code> implementations.
   *
   * @see javax.management.MBeanServer
   */
  static synchronized MBeanServer createMBeanServer() {
    if (mbeanServer == null) {
      mbeanServer = MBeanServerFactory.createMBeanServer(DEFAULT_DOMAIN);
    }
    return mbeanServer;
  }

  /**
   * Create and configure (if necessary) and return the Commons-Modeler registry of managed object
   * descriptions.
   *
   * @see org.apache.commons.modeler.Registry
   */
  static synchronized Registry createRegistry() {
    if (registry == null) {
      try {
        registry = Registry.getRegistry(null, null);
        if (mbeanServer == null) {
          throw new IllegalStateException(
              "MBean Server is not initialized yet.");
        }
        registry.setMBeanServer(mbeanServer);

        String mbeansResource = getOSPath("/org/apache/geode/admin/jmx/mbeans-descriptors.xml");

        URL url = ClassPathLoader.getLatest().getResource(MBeanUtils.class, mbeansResource);
        raiseOnFailure(url != null, String.format("Failed to find %s",
            mbeansResource));
        registry.loadMetadata(url);

        // simple test to make sure the xml was actually loaded and is valid...
        String[] test = registry.findManagedBeans();
        raiseOnFailure(test != null && test.length > 0,
            String.format("Failed to load metadata from %s",
                mbeansResource));
      } catch (Exception e) {
        logStackTrace(Level.WARN, e);
        throw new RuntimeAdminException(
            "Failed to get MBean Registry", e);
      }
    }
    return registry;
  }

  /**
   * Creates and registers a <code>ModelMBean</code> for the specified <code>ManagedResource</code>.
   * State changing callbacks into the <code>ManagedResource</code> will also be made.
   *
   * @param resource the ManagedResource to create a managing MBean for
   *
   * @return The object name of the newly-created MBean
   *
   * @see ManagedResource#setModelMBean
   */
  static ObjectName createMBean(ManagedResource resource) {
    return createMBean(resource, lookupManagedBean(resource));
  }

  /**
   * Creates and registers a <code>ModelMBean</code> for the specified <code>ManagedResource</code>.
   * State changing callbacks into the <code>ManagedResource</code> will also be made.
   *
   * @param resource the ManagedResource to create a managing MBean for
   * @param managed the ManagedBean definition to create the MBean with
   * @see ManagedResource#setModelMBean
   */
  static ObjectName createMBean(ManagedResource resource, ManagedBean managed) {

    try {
      DynamicManagedBean mb = new DynamicManagedBean(managed);
      resource.setModelMBean(mb.createMBean(resource));

      // create the ObjectName and register the MBean...
      final ObjectName objName;
      try {
        objName = ObjectName.getInstance(resource.getMBeanName());
      } catch (MalformedObjectNameException e) {
        throw new MalformedObjectNameException(String.format("%s in '%s'",
            e.getMessage(), resource.getMBeanName()));
      }

      synchronized (MBeanUtils.class) {
        // Only register a bean once. Otherwise, you risk race
        // conditions with things like the RMI connector accessing it.

        if (mbeanServer != null && !mbeanServer.isRegistered(objName)) {
          mbeanServer.registerMBean(resource.getModelMBean(), objName);
          synchronized (managedResources) {
            managedResources.put(objName, resource);
          }
        }
      }
      return objName;
    } catch (java.lang.Exception e) {
      throw new RuntimeAdminException(
          String.format("Failed to create MBean representation for resource %s.",
              resource.getMBeanName()),
          e);
    }
  }

  /**
   * Ensures that an MBean is registered for the specified <code>ManagedResource</code>. If an MBean
   * cannot be found in the <code>MBeanServer</code>, then this creates and registers a
   * <code>ModelMBean</code>. State changing callbacks into the <code>ManagedResource</code> will
   * also be made.
   *
   * @param resource the ManagedResource to create a managing MBean for
   *
   * @return The object name of the MBean that manages the ManagedResource
   *
   * @see ManagedResource#setModelMBean
   */
  static ObjectName ensureMBeanIsRegistered(ManagedResource resource) {
    try {
      ObjectName objName = ObjectName.getInstance(resource.getMBeanName());
      synchronized (MBeanUtils.class) {
        if (mbeanServer != null && !mbeanServer.isRegistered(objName)) {
          return createMBean(resource);
        }
      }
      raiseOnFailure(mbeanServer.isRegistered(objName),
          String.format("Could not find a MBean registered with ObjectName: %s.",
              objName.toString()));
      return objName;
    } catch (java.lang.Exception e) {
      throw new RuntimeAdminException(e);
    }
  }

  /**
   * Retrieves the <code>ManagedBean</code> configuration from the Registry for the specified
   * <code>ManagedResource</code>
   *
   * @param resource the ManagedResource to find the configuration for
   */
  static ManagedBean lookupManagedBean(ManagedResource resource) {
    // find the registry defn for our MBean...
    ManagedBean managed = null;
    if (registry != null) {
      managed = registry.findManagedBean(resource.getManagedResourceType().getClassTypeName());
    } else {
      throw new IllegalArgumentException(
          "ManagedBean is null");
    }

    if (managed == null) {
      throw new IllegalArgumentException(
          "ManagedBean is null");
    }

    // customize the defn...
    managed.setClassName("org.apache.geode.admin.jmx.internal.MX4JModelMBean");

    return managed;
  }

  /**
   * Registers a refresh notification for the specified client MBean. Specifying zero for the
   * refreshInterval disables notification for the refresh client. Note: this does not currently
   * support remote connections.
   *
   * @param client client to listen for refresh notifications
   * @param userData userData to register with the Notification
   * @param type refresh notification type the client will use
   * @param refreshInterval the seconds between refreshes
   */
  static void registerRefreshNotification(NotificationListener client, Object userData,
      RefreshNotificationType type, long refreshInterval) {
    if (client == null) {
      throw new IllegalArgumentException(
          "NotificationListener is required");
    }
    if (type == null) {
      throw new IllegalArgumentException(
          "RefreshNotificationType is required");
    }
    if (refreshTimerObjectName == null || refreshTimer == null) {
      throw new IllegalStateException(
          "RefreshTimer has not been properly initialized.");
    }

    try {
      // get the notifications for the specified client...
      Map<RefreshNotificationType, Integer> notifications = null;
      synchronized (refreshClients) {
        notifications = refreshClients.get(client);
      }

      if (notifications == null) {
        // If refreshInterval is being set to zero and notifications is removed return
        if (refreshInterval <= 0) {
          return;
        }

        // never registered before, so add client...
        notifications = new HashMap<>();
        synchronized (refreshClients) {
          refreshClients.put(client, notifications);
        }
        validateRefreshTimer();
        try {
          // register client as a listener with MBeanServer...
          mbeanServer.addNotificationListener(refreshTimerObjectName, // timer to listen to
              client, // the NotificationListener object
              null, // optional NotificationFilter TODO: convert to using
              new Object() // not used but null throws IllegalArgumentException
          );
        } catch (InstanceNotFoundException e) {
          // should not happen since we already checked refreshTimerObjectName
          logStackTrace(Level.WARN, e,
              "Could not find registered RefreshTimer instance.");
        }
      }

      // TODO: change to manipulating timer indirectly thru mserver...

      // check for pre-existing refresh notification entry...
      Integer timerNotificationId = notifications.get(type);
      if (timerNotificationId != null) {
        try {
          // found one, so let's remove it...
          refreshTimer.removeNotification(timerNotificationId);
        } catch (InstanceNotFoundException e) {
          // that's ok cause we just wanted to remove it anyway
        } finally {
          // null out the map entry for that notification type...
          notifications.put(type, null);
        }
      }

      if (refreshInterval > 0) {
        // add notification to the refresh timer...
        timerNotificationId = refreshTimer.addNotification(type.getType(), // type
            type.getMessage(), // message = "refresh"
            userData, // userData
            new Date(System.currentTimeMillis() + refreshInterval * 1000L), // first occurrence
            refreshInterval * 1000L); // period to repeat

        // put an entry into the map for the listener...
        notifications.put(type, timerNotificationId);
      } else {
        // do nothing! refreshInterval must be over 0 to do anything...
      }
    } catch (java.lang.RuntimeException e) {
      logStackTrace(Level.WARN, e);
      throw e;
    } catch (VirtualMachineError err) {
      SystemFailure.initiateFailure(err);
      // If this ever returns, rethrow the error. We're poisoned
      // now, so don't let this thread continue.
      throw err;
    } catch (java.lang.Error e) {
      // Whenever you catch Error or Throwable, you must also
      // catch VirtualMachineError (see above). However, there is
      // _still_ a possibility that you are dealing with a cascading
      // error condition, so you also need to check to see if the JVM
      // is still usable:
      SystemFailure.checkFailure();
      logStackTrace(Level.ERROR, e);
      throw e;
    }
  }

  /**
   * Verifies a refresh notification for the specified client MBean. If notification is not
   * registered, then returns a false
   *
   * @param client client to listen for refresh notifications
   * @param type refresh notification type the client will use
   *
   * @return isRegistered boolean indicating if a notification is registered
   */
  static boolean isRefreshNotificationRegistered(NotificationListener client,
      RefreshNotificationType type) {
    boolean isRegistered = false;

    // get the notifications for the specified client...
    Map<RefreshNotificationType, Integer> notifications = null;
    synchronized (refreshClients) {
      notifications = refreshClients.get(client);
    }

    // never registered before if null ...
    if (notifications != null) {
      // check for pre-existing refresh notification entry...
      Integer timerNotificationId = notifications.get(type);
      if (timerNotificationId != null) {
        isRegistered = true;
      }
    }

    return isRegistered;
  }

  /**
   * Validates refreshTimer has been registered without problems and attempts to re-register if
   * there is a problem.
   */
  static void validateRefreshTimer() {
    if (refreshTimerObjectName == null || refreshTimer == null) {
      createRefreshTimer();
    }

    raiseOnFailure(refreshTimer != null, "Failed to validate Refresh Timer");

    if (mbeanServer != null && !mbeanServer.isRegistered(refreshTimerObjectName)) {
      try {
        mbeanServer.registerMBean(refreshTimer, refreshTimerObjectName);
      } catch (JMException e) {
        logStackTrace(Level.WARN, e);
      } catch (JMRuntimeException e) {
        logStackTrace(Level.WARN, e);
      }
    }
  }

  /**
   * Initializes the timer for sending refresh notifications.
   */
  static void createRefreshTimer() {
    try {
      refreshTimer = new javax.management.timer.Timer();
      mbeanServer.registerMBean(refreshTimer, refreshTimerObjectName);

      refreshTimer.start();
    } catch (JMException e) {
      logStackTrace(Level.WARN, e,
          "Failed to create/register/start refresh timer.");
    } catch (JMRuntimeException e) {
      logStackTrace(Level.WARN, e,
          "Failed to create/register/start refresh timer.");
    } catch (Exception e) {
      logStackTrace(Level.WARN, e,
          "Failed to create/register/start refresh timer.");
    }
  }

  /**
   * Initializes the timer for sending refresh notifications.
   */
  static void stopRefreshTimer() {
    try {
      if (refreshTimer != null && mbeanServer != null) {
        mbeanServer.unregisterMBean(refreshTimerObjectName);

        refreshTimer.stop();
      }
    } catch (JMException e) {
      logStackTrace(Level.WARN, e);
    } catch (JMRuntimeException e) {
      logStackTrace(Level.WARN, e);
    } catch (Exception e) {
      logStackTrace(Level.DEBUG, e, "Failed to stop refresh timer for MBeanUtil");
    }
  }

  /**
   * Return a String that been modified to be compliant as a property of an ObjectName.
   * <p>
   * The property name of an ObjectName may not contain any of the following characters: <b><i>: , =
   * * ?</i></b>
   * <p>
   * This method will replace the above non-compliant characters with a dash: <b><i>-</i></b>
   * <p>
   * If value is empty, this method will return the string "nothing".
   * <p>
   * Note: this is <code>public</code> because certain tests call this from outside of the package.
   * TODO: clean this up
   *
   * @param value the potentially non-compliant ObjectName property
   * @return the value modified to be compliant as an ObjectName property
   */
  public static String makeCompliantMBeanNameProperty(String value) {
    value = value.replace(':', '-');
    value = value.replace(',', '-');
    value = value.replace('=', '-');
    value = value.replace('*', '-');
    value = value.replace('?', '-');
    if (value.length() < 1) {
      value = "nothing";
    }
    return value;
  }

  /**
   * Unregisters all GemFire MBeans and then releases the MBeanServer for garbage collection.
   */
  static void releaseMBeanServer() {
    try {
      // unregister all GemFire mbeans...
      for (final ObjectName name : mbeanServer.queryNames(null, null)) {
        if (name.getDomain().startsWith(DEFAULT_DOMAIN)) {
          unregisterMBean(name);
        }
      }

      // last, release the mbean server...
      MBeanServerFactory.releaseMBeanServer(mbeanServer);
      mbeanServer = null;
    } catch (JMRuntimeException e) {
      logStackTrace(Level.WARN, e);
    }
    /*
     * See #42391. Cleaning up the static maps which might be still holding references to
     * ManagedResources
     */
    synchronized (MBeanUtils.managedResources) {
      MBeanUtils.managedResources.clear();
    }
    synchronized (refreshClients) {
      refreshClients.clear();
    }
    /*
     * See #42391. Cleaning up the static maps which might be still holding references to
     * ManagedResources
     */
    synchronized (MBeanUtils.managedResources) {
      MBeanUtils.managedResources.clear();
    }
    synchronized (refreshClients) {
      refreshClients.clear();
    }
  }

  /**
   * Returns true if a MBean with given ObjectName is registered.
   *
   * @param objectName ObjectName to use for checking if MBean is registered
   * @return true if MBeanServer is not null & MBean with given ObjectName is registered with the
   *         MBeanServer
   */
  static boolean isRegistered(ObjectName objectName) {
    return mbeanServer != null && mbeanServer.isRegistered(objectName);
  }

  /**
   * Unregisters the identified MBean if it's registered.
   */
  static void unregisterMBean(ObjectName objectName) {
    try {
      if (mbeanServer != null && mbeanServer.isRegistered(objectName)) {
        mbeanServer.unregisterMBean(objectName);
      }
    } catch (MBeanRegistrationException e) {
      logStackTrace(Level.WARN, null,
          String.format("Failed while unregistering MBean with ObjectName : %s",
              objectName));
    } catch (InstanceNotFoundException e) {
      logStackTrace(Level.WARN, null,
          String.format("While unregistering, could not find MBean with ObjectName : %s",
              objectName));
    } catch (JMRuntimeException e) {
      logStackTrace(Level.WARN, null,
          String.format("Could not un-register MBean with ObjectName : %s",
              objectName));
    }
  }

  static void unregisterMBean(ManagedResource resource) {
    if (resource != null) {
      unregisterMBean(resource.getObjectName());

      // call cleanup on managedResource here and not rely on listener
      // since it is possible that notification listener not deliver
      // all notifications of un-registration. If resource is
      // cleaned here, another call from the listener should be as good as a no-op
      cleanupResource(resource);
    }
  }

  // cleanup resource
  private static void cleanupResource(ManagedResource resource) {
    synchronized (MBeanUtils.managedResources) {
      MBeanUtils.managedResources.remove(resource.getObjectName());
    }
    resource.cleanupResource();

    // get the notifications for the specified client...
    Map<RefreshNotificationType, Integer> notifications = null;
    synchronized (refreshClients) {
      notifications = refreshClients.remove(resource);
    }

    // never registered before if null ...
    // Also as of current, there is ever only 1 Notification type per
    // MBean, so we do need need a while loop here
    if (notifications != null) {

      // Fix for findbugs reported inefficiency with keySet().
      Set<Map.Entry<RefreshNotificationType, Integer>> entries = notifications.entrySet();

      for (Map.Entry<RefreshNotificationType, Integer> e : entries) {
        Integer timerNotificationId = e.getValue();
        if (null != timerNotificationId) {
          try {
            // found one, so let's remove it...
            refreshTimer.removeNotification(timerNotificationId);
          } catch (InstanceNotFoundException xptn) {
            // that's ok cause we just wanted to remove it anyway
            logStackTrace(Level.DEBUG, xptn);
          }
        }
      }

      try {
        if (mbeanServer != null && mbeanServer.isRegistered(refreshTimerObjectName)) {
          // remove client as a listener with MBeanServer...
          mbeanServer.removeNotificationListener(refreshTimerObjectName, // timer to listen to
              (NotificationListener) resource // the NotificationListener object
          );
        }
      } catch (ListenerNotFoundException xptn) {
        // should not happen since we already checked refreshTimerObjectName
        logStackTrace(Level.WARN, null, xptn.getMessage());
      } catch (InstanceNotFoundException xptn) {
        // should not happen since we already checked refreshTimerObjectName
        logStackTrace(Level.WARN, null,
            String.format("While unregistering, could not find MBean with ObjectName : %s",
                refreshTimerObjectName));
      }
    }
  }

  // ----- borrowed the following from admin.internal.RemoteCommand -----
  /** Translates the path between Windows and UNIX. */
  static String getOSPath(String path) {
    if (pathIsWindows(path)) {
      return path.replace('/', '\\');
    } else {
      return path.replace('\\', '/');
    }
  }

  /** Returns true if the path is on Windows. */
  static boolean pathIsWindows(String path) {
    if (path != null && path.length() > 1) {
      return (Character.isLetter(path.charAt(0)) && path.charAt(1) == ':')
          || (path.startsWith("//") || path.startsWith("\\\\"));
    }
    return false;
  }

  static void registerServerNotificationListener() {
    if (mbeanServer == null) {
      return;
    }
    try {
      // the MBeanServerDelegate name is spec'ed as the following...
      ObjectName delegate = ObjectName.getInstance("JMImplementation:type=MBeanServerDelegate");
      mbeanServer.addNotificationListener(delegate, (notification, handback) -> {
        MBeanServerNotification serverNotification = (MBeanServerNotification) notification;
        if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION
            .equals(serverNotification.getType())) {
          ObjectName objectName = serverNotification.getMBeanName();
          synchronized (MBeanUtils.managedResources) {
            Object entry = MBeanUtils.managedResources.get(objectName);
            if (entry == null) {
              return;
            }
            if (!(entry instanceof ManagedResource)) {
              throw new ClassCastException(String.format("%s is not a ManagedResource",
                  entry.getClass().getName()));
            }
            ManagedResource resource = (ManagedResource) entry;
            {
              // call cleanup on managedResource
              cleanupResource(resource);
            }
          }
        }
      }, null, null);
    } catch (JMException e) {
      logStackTrace(Level.WARN, e,
          "Failed to register ServerNotificationListener.");
    } catch (JMRuntimeException e) {
      logStackTrace(Level.WARN, e,
          "Failed to register ServerNotificationListener.");
    }
  }

  /**
   * Logs the stack trace for the given Throwable if logger is initialized else prints the stack
   * trace using System.out.
   *
   * @param level severity level to log at
   * @param throwable Throwable to log stack trace for
   */
  public static void logStackTrace(Level level, Throwable throwable) {
    logStackTrace(level, throwable, null);
  }

  /**
   * Logs the stack trace for the given Throwable if logger is initialized else prints the stack
   * trace using System.out.
   *
   * @param level severity level to log at
   * @param throwable Throwable to log stack trace for
   * @param message user friendly error message to show
   */
  public static void logStackTrace(Level level, Throwable throwable, String message) {
    logger.log(level, message, throwable);
  }

  /**
   * Raises RuntimeAdminException with given 'message' if given 'condition' is false.
   *
   * @param condition condition to evaluate
   * @param message failure message
   */
  private static void raiseOnFailure(boolean condition, String message) {
    if (!condition) {
      throw new RuntimeAdminException(message);
    }
  }
}
