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

import java.util.Arrays;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.script.access.AccessControlConfig;
import org.apache.synapse.script.access.AccessControlConstants;
import org.apache.synapse.script.access.AccessControlListType;
import org.apache.synapse.script.access.ScriptAccessControl;
import org.apache.synapse.aspects.flow.statistics.collectors.RuntimeStatisticCollector;
import org.apache.synapse.aspects.flow.statistics.tracing.opentelemetry.OpenTelemetryManagerHolder;
import org.apache.synapse.commons.jmx.MBeanRegistrar;
import org.apache.synapse.config.SynapsePropertiesLoader;
import org.apache.synapse.data.ConfigDataHolder;
import org.apache.synapse.mediators.eip.EIPUtils;
import org.wso2.securevault.PasswordManager;
import org.wso2.securevault.SecurityConstants;

import java.util.Date;
import javax.management.NotCompliantMBeanException;
import static org.apache.synapse.script.access.AccessControlConstants.ENABLE;

/**
 * This is the core class that starts up a Synapse instance.
 * <p/>
 * From the command line scripts synapse.sh and synapse-daemon.sh (though the wrapper.conf)
 * the SynapseServer is invoked which in turn calls on this to start the instance
 * <p/>
 * When the WAR deployment is used, the SynapseStartUpServlet servlet calls on this class to
 * initialize Synapse.
 * <p/>
 * This is the entry point for starting an Synapse instance. All the synapse related management
 * operations are exposed through this class.
 */
@SuppressWarnings({"UnusedDeclaration"})
public class ServerManager {

    private static final Log log = LogFactory.getLog(ServerManager.class);

    /**
     * The controller for synapse create and Destroy synapse artifacts in a particular environment
     * Only for internal usage - DON"T PUT GETTER ,SETTER
     */
    private SynapseController synapseController;

    /* Server Configuration  */
    private ServerConfigurationInformation serverConfigurationInformation;

    /* Server context */
    private ServerContextInformation serverContextInformation;

    /**
     * Only represents whether server manager has been initialized by given required
     * configuration information - not server state or internal usage - DON"T PUT SETTER
     */
    private boolean initialized = false;

    /**
     * Save the TCCL of the initial thread that starts the ESB for future use. When JMX calls are
     * received via RMI connections, re-start etc may otherwise fail due to class loading issues.
     */
    private ClassLoader classLoader;

    /**
     * Construct a server manager.
     */
    public ServerManager() {
    }

    /**
     * Initializes the server, if we need to create a new axis2 instance, calling this will create
     * the new axis2 environment, but this won't start the transport listeners
     *
     * @param serverConfigurationInformation ServerConfigurationInformation instance
     * @param serverContextInformation       ServerContextInformation instance
     * @return ServerState - State of the server which is
     * {@link org.apache.synapse.ServerState#INITIALIZED}, if successful
     */
    public synchronized ServerState init(
            ServerConfigurationInformation serverConfigurationInformation,
            ServerContextInformation serverContextInformation) {

        classLoader = Thread.currentThread().getContextClassLoader();

        // sets the initializations parameters
        this.serverConfigurationInformation = serverConfigurationInformation;

        if (serverContextInformation == null) {
            this.serverContextInformation =
                    new ServerContextInformation(serverConfigurationInformation);
        } else {
            this.serverContextInformation = serverContextInformation;
        }
        synapseController = SynapseControllerFactory
                .createSynapseController(serverConfigurationInformation);

        // set GsonJsonProvider as the default configuration for Jayway JsonPath
        EIPUtils.setJsonPathConfiguration();

        // does the initialization of the controller
        doInit();
        initialized = true;
        RuntimeStatisticCollector.init();

        return this.serverContextInformation.getServerState();
    }

    /**
     * Shuts down the Server instance. If the Server is stopped this will shutdown the
     * ServerManager, and if it is running (i.e. in the STARTED state) this will first stop the
     * ServerManager and shutdown it in turn.
     *
     * @return the state after the shutdown, {@link org.apache.synapse.ServerState#UNDETERMINED}
     */
    public synchronized ServerState shutdown() {

        ServerState serverState = ServerStateDetectionStrategy.currentState(
                serverContextInformation, serverConfigurationInformation);

        switch (serverState) {
            // if the current state is INITIALIZED, then just destroy
            case INITIALIZED: {
                doShutdown();
                break;
            }
            // if the current state is STOPPED, then again just destroy
            case STOPPED: {
                doShutdown();
                break;
            }
            // if the current state is STARTED, then stop and destroy
            case STARTED: {
                stop();
                doShutdown();
                break;
            }
            // if the current state is MAINTENANCE, then stop and destroy
            case MAINTENANCE: {
                stop();
                doShutdown();
                break;
            }
        }

        // clear the instance parameters
        this.synapseController = null;
        this.serverContextInformation = null;
        this.serverConfigurationInformation = null;
        this.initialized = false;
        return ServerState.UNDETERMINED;
    }

    /**
     * Starts the system, if the system is initialized, and if not a Runtime exception of type
     * {@link org.apache.synapse.SynapseException} will be thrown
     *
     * @return the state of the server after starting, for a successful start
     * {@link org.apache.synapse.ServerState#STARTED}
     */
    public synchronized ServerState start() {

        // if the system is not initialized we are not happy
        assertInitialized();

        // starts the system
        ServerState serverState = ServerStateDetectionStrategy.currentState(
                serverContextInformation, serverConfigurationInformation);

        if (serverState == ServerState.INITIALIZED || serverState == ServerState.STOPPED) {

            // creates the Synapse Configuration using the SynapseController
            serverContextInformation.setSynapseConfiguration(
                    synapseController.createSynapseConfiguration());

            ConfigDataHolder.getInstance().setRegistry(serverContextInformation.
                    getSynapseConfiguration().getRegistry());

            // creates the Synapse Environment using the SynapseController
            serverContextInformation.setSynapseEnvironment(
                    synapseController.createSynapseEnvironment());

            // starts the SynapseController
            synapseController.start();

            changeState(ServerState.STARTED);
            log.info("Server ready for processing...");
        } else if (serverState == ServerState.STARTED) {
            String message = "The server has already been started.";
            handleException(message);
        } else if (serverState == ServerState.MAINTENANCE) {
            endMaintenance();
        } else {
            // if the server cannot be started just set the current state as the server state
            changeState(serverState);
        }

        return this.serverContextInformation.getServerState();
    }

    /**
     * Put transport listeners and senders into maintenance mode.
     *
     * @return the state of the server after maintenance request, for a successful execution
     * {@link org.apache.synapse.ServerState#MAINTENANCE}
     */
    public synchronized ServerState startMaintenance() {

        assertInitialized();

        ServerState serverState = ServerStateDetectionStrategy.currentState(serverContextInformation,
                serverConfigurationInformation);

        // if the system is started we can enter the maintenance mode
        if (serverState == ServerState.STARTED) {
            synapseController.startMaintenance();
            changeState(ServerState.MAINTENANCE);
        } else if (serverState == ServerState.MAINTENANCE) {
            String message = "The server is already in maintenance mode.";
            handleException(message);
        } else {
            String message = "Couldn't enter maintenance mode, the server has not been started.";
            handleException(message);
        }

        return serverContextInformation.getServerState();
    }

    /**
     * Ends server maintenance resuming transport listeners, senders and tasks.
     *
     * @return the state of the server after maintenance request, for a successful execution
     * {@link org.apache.synapse.ServerState#MAINTENANCE}
     */
    public synchronized ServerState endMaintenance() {

        assertInitialized();

        ServerState serverState = ServerStateDetectionStrategy.currentState(
                serverContextInformation, serverConfigurationInformation);

        // if the system is started we can enter the maintenance mode
        if (serverState == ServerState.MAINTENANCE) {
            synapseController.endMaintenance();
            changeState(ServerState.STARTED);
        } else {
            String message = "Couldn't leave maintenance mode."
                    + " The server has not been in maintenance.";
            handleException(message);
        }

        return serverContextInformation.getServerState();
    }

    /**
     * Stops the system, if it is started and if not a Runtime exception of type
     * {@link org.apache.synapse.SynapseException} will be thrown
     *
     * @return the state of the system after stopping, which is
     * {@link org.apache.synapse.ServerState#STOPPED} for a successful stopping
     */
    public synchronized ServerState stop() {

        assertInitialized();

        ServerState serverState = ServerStateDetectionStrategy.currentState(
                serverContextInformation, serverConfigurationInformation);

        // if the system is started then stop if not we are not happy
        if (serverState == ServerState.STARTED || serverState == ServerState.MAINTENANCE) {

            // destroy the created Synapse Environment
            synapseController.destroySynapseEnvironment();
            serverContextInformation.setSynapseEnvironment(null);

            // destroy the created Synapse Configuration
            synapseController.destroySynapseConfiguration(true);
            serverContextInformation.setSynapseConfiguration(null);

            // stop the SynapseController
            synapseController.stop();

            changeState(ServerState.STOPPED);
        } else {
            // if the server cannot be stopped just set the current state as the server state
            changeState(serverState);
            String message = "Couldn't stop the ServerManager, it has not been started yet";
            handleException(message);
        }

        return this.serverContextInformation.getServerState();
    }

    /**
     * Perform a graceful stop of Synapse. Before the instance is stopped it will be put
     * to maintenance mode.
     *
     * @param maxWaitMillis the maximum number of ms to wait until a graceful stop is achieved,
     *                      before forcing a stop
     * @return if successful ServerState#STOPPED
     * @throws SynapseException
     */
    public synchronized ServerState stopGracefully(long maxWaitMillis) {

        final long startTime = System.currentTimeMillis();
        final long endTime = startTime + maxWaitMillis;
        final long waitIntervalMillis = 2000;
        log.info(new StringBuilder("Requesting a graceful shutdown at: ").append(new Date())
                .append(" in a maximum of ").append(maxWaitMillis / 1000)
                .append(" seconds.").toString());

        startMaintenance();

        // wait until it is safe to to stop the server or the maximum time to wait is over
        if (synapseController.waitUntilSafeToStop(waitIntervalMillis, endTime)) {
            log.info(new StringBuilder("The instance could not be gracefully stopped in: ")
                    .append(maxWaitMillis / 1000)
                    .append(" seconds. Performing an immediate stop...").toString());
        }

        stop();

        log.info(new StringBuilder("Graceful stop request completed in ")
                .append((System.currentTimeMillis() - startTime))
                .append(" milliseconds.").toString());

        return this.serverContextInformation.getServerState();
    }

    /**
     * Returns the ServerConfigurationInformation, if the system is initialized and if not a
     * Runtime exception of type {@link org.apache.synapse.SynapseException} will be thrown
     *
     * @return the configuration information of the initialized system
     */
    public ServerConfigurationInformation getServerConfigurationInformation() {
        assertInitialized();
        return serverConfigurationInformation;
    }

    /**
     * Returns the ServerContextInformation, if the system is initialized and if not a Runtime
     * Exception of type {@link org.apache.synapse.SynapseException} will be thrown
     *
     * @return the context information of the initialized system
     */
    public ServerContextInformation getServerContextInformation() {
        assertInitialized();
        return serverContextInformation;
    }

    /**
     * Returns the context class loader of the original thread.
     *
     * @return the context class loader of the original thread.
     */
    public ClassLoader getClassLoader() {
        return classLoader;
    }

    /**
     * Has server manager been initialized ?
     *
     * @return true if the server manager has been initialized by given required
     * configuration information
     */
    public boolean isInitialized() {
        return initialized;
    }

    /**
     * Retrieves the state of the server.
     *
     * @return the state of the server
     */
    public ServerState getServerState() {
        if (serverContextInformation != null) {
            return serverContextInformation.getServerState();
        }
        return ServerState.UNDETERMINED;
    }

    /**
     * Helper method for initializing the ServerManager
     */
    private void doInit() {

        ServerState serverState = ServerStateDetectionStrategy.currentState(
                serverContextInformation, serverConfigurationInformation);

        // if the server is ready for the initialization, this will make sure that we are not
        // calling the initialization on an already initialized/started system
        if (serverState == ServerState.INITIALIZABLE) {

            // register the ServerManager MBean
            registerMBean();

            // initialize global PasswordManager instance used in synapse
            Properties properties = SynapsePropertiesLoader.loadSynapseProperties();
            PasswordManager.getInstance().init(properties, SynapseConstants.SYNAPSE);

            readAndSetScriptAccessControl(properties);

            // initializes the SynapseController
            this.synapseController.init(serverConfigurationInformation, serverContextInformation);

            // mark as initialized
            changeState(ServerState.INITIALIZED);
        } else {
            // if the server cannot be initialized just set the current state as the server state
            changeState(serverState);
        }
    }

    /**
     * Reads and sets access control configurations.
     *
     * @param properties Synapse properties.
     */
    private void readAndSetScriptAccessControl(Properties properties) {
        String limitClassAccessEnabled = properties.getProperty(
                AccessControlConstants.LIMIT_CLASS_ACCESS_PREFIX + AccessControlConstants.ENABLE);
        if (Boolean.parseBoolean(limitClassAccessEnabled)) {
            String limitClassAccessListType =
                    properties.getProperty(
                            AccessControlConstants.LIMIT_CLASS_ACCESS_PREFIX + AccessControlConstants.LIST_TYPE);
            String limitClassAccessClassPrefixes = properties.getProperty(
                    AccessControlConstants.LIMIT_CLASS_ACCESS_PREFIX + AccessControlConstants.CLASS_PREFIXES);
            ScriptAccessControl.getInstance().setClassAccessControlConfig(
                    new AccessControlConfig(true, AccessControlListType.valueOf(limitClassAccessListType),
                            Arrays.asList(limitClassAccessClassPrefixes.split(","))));
        }

        String limitNativeObjectAccessEnabled = properties.getProperty(
                AccessControlConstants.LIMIT_NATIVE_OBJECT_ACCESS_PREFIX + ENABLE);
        if (Boolean.parseBoolean(limitNativeObjectAccessEnabled)) {
            String limitNativeObjectAccessListType =
                    properties.getProperty(AccessControlConstants.LIMIT_NATIVE_OBJECT_ACCESS_PREFIX +
                            AccessControlConstants.LIST_TYPE);
            String limitNativeObjectAccessClassPrefixes =
                    properties.getProperty(AccessControlConstants.LIMIT_NATIVE_OBJECT_ACCESS_PREFIX +
                            AccessControlConstants.OBJECT_NAMES);
            ScriptAccessControl.getInstance().setNativeObjectAccessControlConfig(
                    new AccessControlConfig(true, AccessControlListType.valueOf(limitNativeObjectAccessListType),
                            Arrays.asList(limitNativeObjectAccessClassPrefixes.split(","))));
        }
        if (!Boolean.parseBoolean(limitClassAccessEnabled) && !Boolean.parseBoolean(limitNativeObjectAccessEnabled)) {
            log.warn("\n##################################  ALERT  ##################################\n" +
                    "[WARNING]: Default configuration is used for the script mediator which might expose the " +
                    "environment to potential security risks. Please ensure that the required configuration changes " +
                    "are applied in accordance with the \"Configuring Script Mediator\" section of the \"Security " +
                    "Guidelines for Production Deployment\"\n" +
                    "#############################################################################");
        }
    }


    /**
     * Helper method to shutdown the the ServerManager
     */
    private void doShutdown() {
        ServerState serverState = ServerStateDetectionStrategy.currentState(
                serverContextInformation, serverConfigurationInformation);

        if (serverState == ServerState.INITIALIZED || serverState == ServerState.STOPPED) {

            //shutdown debug manager and close TCP connection in the debug interface
            if (serverContextInformation.isServerDebugModeEnabled()) {
                serverContextInformation.getSynapseDebugManager().shutdownDebugManager();
            }

            // Shutdown global PasswordManager instance used in synapse
            PasswordManager passwordManager = PasswordManager.getInstance();
            if (passwordManager.isInitialized()) {
                PasswordManager.getInstance().shutDown();
            }

            // un-register the ServerManager MBean
            unRegisterMBean();

            // destroy the SynapseController
            synapseController.destroy();

            // mark as destroyed
            changeState(ServerState.UNDETERMINED);
            if(OpenTelemetryManagerHolder.getOpenTelemetryManager() != null) {
                OpenTelemetryManagerHolder.getOpenTelemetryManager().close();
            }
        } else {
            // if the server cannot be destroyed just set the current state as the server state
            changeState(serverState);
        }
    }

    /**
     * Changes the server state to the specified state.
     *
     * @param serverState the new server state
     */
    private void changeState(ServerState serverState) {
        this.serverContextInformation.setServerState(serverState);
    }

    private void assertInitialized() {
        if (!initialized) {
            String msg = "Server manager has not been initialized, it requires to be " +
                    "initialized, with the required configurations before starting";
            handleException(msg);
        }
    }

    private void handleException(String msg) {
        log.error(msg);
        throw new SynapseException(msg);
    }

    private void handleException(String msg, Exception e) {
        log.error(msg, e);
        throw new SynapseException(msg, e);
    }

    private void registerMBean() {
        MBeanRegistrar.getInstance().registerMBean(new ServerManagerView(this),
                SynapseConstants.SERVER_MANAGER_MBEAN, SynapseConstants.SERVER_MANAGER_MBEAN);
        try {
            MBeanRegistrar.getInstance().registerMBean(
                    new SecretManagerAdminMBeanImpl(),
                    SecurityConstants.PROP_SECURITY_ADMIN_SERVICES,
                    SecurityConstants.PROP_SECRET_MANAGER_ADMIN_MBEAN);
        } catch (NotCompliantMBeanException e) {
            handleException("Error registering SecretManagerAdminMBeanImpl", e);
        }
    }

    private void unRegisterMBean() {
        MBeanRegistrar.getInstance().unRegisterMBean(
                SynapseConstants.SERVER_MANAGER_MBEAN, SynapseConstants.SERVER_MANAGER_MBEAN);
        MBeanRegistrar.getInstance().unRegisterMBean(
                SecurityConstants.PROP_SECURITY_ADMIN_SERVICES,
                SecurityConstants.PROP_SECRET_MANAGER_ADMIN_MBEAN);
    }
}
