/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2013-2015 ForgeRock AS.
 */
package org.opends.server.snmp;

import com.sun.management.comm.CommunicatorServer;

import java.io.File;

import com.sun.management.comm.SnmpV3AdaptorServer;
import com.sun.management.snmp.InetAddressAcl;
import com.sun.management.snmp.SnmpEngineParameters;
import com.sun.management.snmp.UserAcl;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.SortedSet;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.admin.std.server.SNMPConnectionHandlerCfg;
import org.opends.server.core.DirectoryServer;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.opends.server.util.StaticUtils;
import org.forgerock.util.Reject;

import static org.opends.messages.ProtocolMessages.*;

/**
 * The SNMPClassLoaderProvider.
 */
public class SNMPClassLoaderProvider {

    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

    /**
     * The current configuration state.
     */
    private SNMPConnectionHandlerCfg currentConfig;
    /**
     * MBeanServer of OpenDS.
     */
    private MBeanServer server;
    /**
     * MIB to manage.
     */
    private DIRECTORY_SERVER_MIBImpl dsMib;
    /**
     * ObjectName of the MIB2605.
     */
    private ObjectName mibObjName;
    /**
     * ObjectName of the SnmpAdaptor.
     */
    private ObjectName snmpObjName;
    /**
     * SNMP Port Number for SNMP requests.
     */
    private int snmpPort = 161;
    /**
     * Default SNMP trap port Number for SNMP Traps.
     */
    private int snmpTrapPort = 162;
    /**
     * Registration of the SNMP MBeans.
     */
    private boolean registeredSNMPMBeans = false;
    /**
     * The unique name for this connection handler.
     */
    private String connectionHandlerName;
    /**
     * ObjectName of the UsmMIB.
     */
    private ObjectName UsmObjName;
    private SnmpV3AdaptorServer snmpAdaptor;
    private String contextName;
    private boolean sentTraps = true;

    /**
     * Default constructor.
     */
    public SNMPClassLoaderProvider() {
    // No implementation required
    }

    /**
     * Initialization.
     * @param configuration The configuration
     * @throws java.lang.Exception if the SNMP connection handler
     * could not be initialized
     */
    public void initializeConnectionHandler(
            SNMPConnectionHandlerCfg configuration) throws Exception {
        // Keep the connection handler configuration
        this.currentConfig = configuration;

        // Get the Directory Server JMX MBeanServer
        this.server = DirectoryServer.getJMXMBeanServer();

        // Initialize the Connection Handler with the given configuration
        this.initializeConnectionHandler();
    }

    /**
     * Applies the configuration changes to this change listener.
     *
     * @param configuration
     *          The new configuration containing the changes.
     * @return Returns information about the result of changing the
     *         configuration.
     */
    public ConfigChangeResult applyConfigurationChange(
            SNMPConnectionHandlerCfg configuration) {
        try {
            // Register/UnRegister SNMP MBeans
            if ((this.registeredSNMPMBeans) &&
                    (!configuration.isRegisteredMbean())) {
                this.unregisterSnmpMBeans();
                this.registeredSNMPMBeans = configuration.isRegisteredMbean();
            } else if ((!this.registeredSNMPMBeans) &&
                    (configuration.isRegisteredMbean())) {
                this.unregisterSnmpMBeans();
                this.registeredSNMPMBeans = configuration.isRegisteredMbean();
            }

            // PortNumber/Version
            if ((this.snmpPort != configuration.getListenPort())) {
                this.server.unregisterMBean(this.snmpObjName);
                this.snmpAdaptor.stop();
                this.snmpPort = configuration.getListenPort();
                this.snmpAdaptor = this.getSnmpAdaptor(configuration);


                // Creates and starts the SNMP Adaptor
                this.snmpObjName = new ObjectName(
                        SNMPConnectionHandlerDefinitions.SNMP_DOMAIN +
                        "class=SnmpAdaptorServer,protocol=snmp," +
                        "port=" + snmpPort);
                this.server.registerMBean(this.snmpAdaptor, this.snmpObjName);
                this.snmpAdaptor.start();

                // Send a coldStart SNMP Trap on the new trap port if required
                if (this.snmpTrapPort != configuration.getTrapPort()) {
                    this.snmpTrapPort = configuration.getTrapPort();
                    this.snmpAdaptor.setTrapPort(snmpTrapPort);
                    this.snmpAdaptor.snmpV1Trap(0, 0, null);
                }
            }
        } catch (Exception ex) {
            logger.traceException(ex);
        }

        // If security file have changed, changeConfiguration not supported.
        return new ConfigChangeResult();
    }

    /**
     * Gets the ObjectName of the crated USM MIB MBean.
     * @return The UsmMIB ObjectName
     */
    public ObjectName getUsmMIBName() {
        return this.UsmObjName;
    }

    // private methods
    private void initializeConnectionHandler() throws Exception {
        // Compute the connectionHandler name
        this.connectionHandlerName = "SNMP Connection Handler " +
                this.currentConfig.getListenPort();

        // Gets the configuration parameters
        this.snmpPort = this.currentConfig.getListenPort();
        this.snmpTrapPort = this.currentConfig.getTrapPort();
        this.registeredSNMPMBeans = this.currentConfig.isRegisteredMbean();
        this.contextName = this.currentConfig.getCommunity();

        // Creates all the required objects for SNMP MIB 2605 Support
        try {

            // Creates and starts the SNMP Adaptor
            this.snmpObjName = new ObjectName(
                    SNMPConnectionHandlerDefinitions.SNMP_DOMAIN +
                    "class=SnmpAdaptorServer,protocol=snmp,port=" + snmpPort);

            // Create the SNMP Adaptor with the appropriated parameters
            this.snmpAdaptor = this.getSnmpAdaptor(this.currentConfig);

            if (this.snmpAdaptor == null) {
              throw new Exception(
                   ERR_SNMP_CONNHANDLER_BAD_CONFIGURATION.get().toString());
            }

            // Create the Usm MIB to allow user management
            if (this.registeredSNMPMBeans) {

                this.UsmObjName = new ObjectName(
                        SNMPConnectionHandlerDefinitions.SNMP_DOMAIN +
                        "type=USM_MIB");

                try {
                    this.snmpAdaptor.registerUsmMib(server, this.UsmObjName);
                } catch (Exception ex) {
                    throw new Exception(
                      ERR_SNMP_CONNHANDLER_BAD_CONFIGURATION.get().toString());
                }
            }

            this.snmpAdaptor.start();

            // Test  the snmpAdaptor State
            while (this.snmpAdaptor.getState() == CommunicatorServer.STARTING) {
                Thread.sleep(1000);
            }

            // Check if the snmpAdaptor is online
            if (this.snmpAdaptor.getState() != CommunicatorServer.ONLINE) {
                throw new Exception(
                      ERR_SNMP_CONNHANDLER_BAD_CONFIGURATION.get().toString());
            }

            // Check the trap destinations before trying to sent traps
            this.sentTraps =
                    checkTrapsDestinations(
                    this.currentConfig.getTrapsDestination());

            if (this.sentTraps == false) {
                logger.error(ERR_SNMP_CONNHANDLER_NO_VALID_TRAP_DESTINATIONS);
            } else {
                // Send a coldStart SNMP Trap.
                this.snmpAdaptor.setTrapPort(snmpTrapPort);
                this.snmpAdaptor.snmpV1Trap(
                        null,
                        this.currentConfig.getTrapsCommunity(),
                        0,
                        0,
                        null);
            }
            // Create an instance of the customized MIB
            this.mibObjName = new ObjectName(
                    SNMPConnectionHandlerDefinitions.SNMP_DOMAIN +
                    "class=DIRECTORY_SERVER_MIB");

            this.dsMib = new DIRECTORY_SERVER_MIBImpl(
                    this.registeredSNMPMBeans, this.mibObjName);
            this.dsMib.preRegister(this.server, this.mibObjName);

            // Register the DS MIB into the defined context
            this.dsMib.setSnmpAdaptor(snmpAdaptor, this.contextName);

            this.server.registerMBean(this.snmpAdaptor, snmpObjName);

        } catch (Exception ex) {
            throw new Exception(
                    ERR_SNMP_CONNHANDLER_BAD_CONFIGURATION.get().toString());
        }
    }

    /**
     * Finalize.
     */
    public void finalizeConnectionHandler() {
        try {
            if (this.sentTraps == true) {
                // Send a trap when stop
                this.snmpAdaptor.snmpV1Trap(
                        null,
                        this.currentConfig.getTrapsCommunity(),
                        0,
                        0,
                        null);
            }
            this.snmpAdaptor.getMibs();

            // Stop the SNMP Adaptor
            this.snmpAdaptor.stop();

            this.server.unregisterMBean(this.snmpObjName);

            if (this.server.isRegistered(this.mibObjName)) {
                this.server.unregisterMBean(this.mibObjName);
            }


            this.server.unregisterMBean(new ObjectName(
                    SNMPConnectionHandlerDefinitions.SNMP_DOMAIN +
                    "type=group,name=DsMib"));

            // Unregister the created SNMP MBeans
            if (this.registeredSNMPMBeans) {
                this.unregisterSnmpMBeans();
                this.server.unregisterMBean(this.UsmObjName);
            }
        } catch (Exception ex) {
            logger.traceException(ex);
        }
    }

    private void unregisterSnmpMBeans() {
        for (ObjectName name : this.dsMib.getMib().getEntriesObjectNames()) {
            try {
                this.server.unregisterMBean(name);
            } catch (Exception ex) {
            }
        }
    }

    private SnmpV3AdaptorServer getSnmpAdaptor(
            SNMPConnectionHandlerCfg configuration) {

        Reject.ifNull(configuration);
        SnmpV3AdaptorServer adaptor = null;
        try {

            // Set the USM security file
            String usmConfigPath = configuration.getSecurityAgentFile();
            File file = StaticUtils.getFileForPath(usmConfigPath);
            if ((!file.isFile()) || (!file.exists())) {
                return null;
            }
            System.setProperty("jdmk.security.file", file.getAbsolutePath());
            // Create the Security Parameters for the engine
            SnmpEngineParameters engineParameters = new SnmpEngineParameters();

            // Set V3 Security parameters
            engineParameters.activateEncryption();

            // Create the UACL controller
            UserAcl uacls = new SNMPUserAcl(configuration);
            engineParameters.setUserAcl(uacls);

            // V1/V2 Security parameters
            InetAddressAcl acls =
                    new SNMPInetAddressAcl(configuration);

            adaptor = new SnmpV3AdaptorServer(engineParameters, null, acls,
                    configuration.getListenPort(), null);

            // Enable the community to context translation for V1/V2 to V3
            adaptor.enableCommunityStringAtContext();

            return adaptor;
        } catch (Exception ex) {
            logger.trace("Could not instanciate the SNMP Adaptor");
            return null;
        }
    }

    private boolean checkTrapsDestinations(SortedSet<String> destinations) {
        // If the traps destinations is empty, the traps have to be sent
        // to localhost
        if ((destinations == null) || (destinations.isEmpty())) {
            return true;
        }

        boolean found = false;
        for (String dest : destinations) {
            try {
                InetAddress.getByName(dest);
                found = true;
            } catch (UnknownHostException ex) {
                logger.error(ERR_SNMP_CONNHANDLER_TRAPS_DESTINATION, dest);
            }
        }
        return found;
    }
}

