/*
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License).  You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the license at
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Copyright (c) Ericsson AB, 2004-2007. All rights reserved.
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 */
package org.jvnet.glassfish.comms.clb.core;

import com.sun.enterprise.admin.server.core.AdminService;
import com.sun.enterprise.config.ConfigContext;
import com.sun.enterprise.config.ConfigException;
import com.sun.enterprise.config.serverbeans.Cluster;
import com.sun.enterprise.config.serverbeans.ClusterHelper;
import com.sun.enterprise.config.serverbeans.Config;
import com.sun.enterprise.config.serverbeans.ConfigAPIHelper;
import com.sun.enterprise.ee.cms.core.CallBack;
import com.sun.enterprise.ee.cms.core.FailureNotificationActionFactory;
import com.sun.enterprise.ee.cms.core.FailureNotificationSignal;
import com.sun.enterprise.ee.cms.core.GMSConstants;
import com.sun.enterprise.ee.cms.core.GMSException;
import com.sun.enterprise.ee.cms.core.GMSFactory;
import com.sun.enterprise.ee.cms.core.GMSNotEnabledException;
import com.sun.enterprise.ee.cms.core.GroupHandle;
import com.sun.enterprise.ee.cms.core.GroupManagementService;
import com.sun.enterprise.ee.cms.core.JoinNotificationActionFactory;
import com.sun.enterprise.ee.cms.core.JoinNotificationSignal;
import com.sun.enterprise.ee.cms.core.PlannedShutdownActionFactory;
import com.sun.enterprise.ee.cms.core.PlannedShutdownSignal;
import com.sun.enterprise.ee.cms.core.ServiceProviderConfigurationKeys;
import com.sun.enterprise.ee.cms.core.Signal;
import com.sun.enterprise.ee.cms.impl.client.FailureNotificationActionFactoryImpl;
import com.sun.enterprise.ee.cms.impl.client.JoinNotificationActionFactoryImpl;
import com.sun.enterprise.ee.cms.impl.client.PlannedShutdownActionFactoryImpl;
import com.sun.enterprise.server.ApplicationServer;
import java.util.logging.Logger;
import org.jvnet.glassfish.comms.clb.admin.CLBConfigurator;

import org.jvnet.glassfish.comms.util.LogUtil;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;

import java.net.Socket;
import java.net.UnknownHostException;

import java.util.List;
import java.util.Properties;
import java.util.logging.Level;


/**
 *
 * @author kshitiz
 */
public class ClusterHealthMonitor implements EventListener, CallBack {
    private static final Logger _logger = LogUtil.CLB_LOGGER.getLogger();
    private String clusterName;
    private GroupManagementService gms;
    private boolean startedGMSModule;
    private FailureNotificationActionFactory failureNotificationActionFactory;
    private JoinNotificationActionFactory joinNotificationActionFactory;
    private PlannedShutdownActionFactory plannedShutdownActionFactory;

    /** Creates a new instance of ServerHealthMonitor */
    public ClusterHealthMonitor(String clusterName)
        throws CLBRuntimeException {
        this.clusterName = clusterName;
        gms = null;
        registerCallBackHandler();
    }

    public void processNotification(Signal signal) {
        if (signal instanceof JoinNotificationSignal) {
            onRecovery(signal.getGroupName(), signal.getMemberToken());
        } else if (signal instanceof FailureNotificationSignal) {
            onFailure(signal.getGroupName(), signal.getMemberToken());
        } else if (signal instanceof PlannedShutdownSignal) {
            onFailure(signal.getGroupName(), signal.getMemberToken());
        }
    }

    private void registerCallBackHandler()
        throws CLBRuntimeException {
        startedGMSModule = false;
        while(gms == null){
            try {
                gms = GMSFactory.getGMSModule(clusterName);
            } catch (GMSNotEnabledException ex) {
                // It means gms module is not started for this cluster
                
                //For a self-loadbalancing cluster, this should already be started.
                //Throw an exception in this case
                if (CLBConfigurator.getInstance().isSelfLoadbalanced()) {
                    String message = _logger.getResourceBundle().
                            getString("clb.gms_module_not_started_for_self_lb_cluster") + clusterName;
                    throw new CLBRuntimeException(message);
                }
                
                if(startedGMSModule){
                    String message = _logger.getResourceBundle().
                            getString("clb.unable_to_get_gms_module") + clusterName;
                    throw new CLBRuntimeException(message);
                }
                
                // Let me start a GMS module in this case
                if(_logger.isLoggable(Level.FINE))
                    _logger.log(Level.FINE,
                            "clb.starting_gms_module", 
                            new Object[]{clusterName});
                startGMSModule();
                startedGMSModule = true;
                if(_logger.isLoggable(Level.FINE))
                    _logger.log(Level.FINE,
                            "clb.starting_gms_module", 
                            new Object[]{clusterName, gms});
            } catch (GMSException ex) {
                throw new CLBRuntimeException(ex);
            }
        }

        failureNotificationActionFactory = new FailureNotificationActionFactoryImpl(this);
        gms.addActionFactory(failureNotificationActionFactory);

        joinNotificationActionFactory = new JoinNotificationActionFactoryImpl(this);
        gms.addActionFactory(joinNotificationActionFactory);

        plannedShutdownActionFactory = new PlannedShutdownActionFactoryImpl(this);
        gms.addActionFactory(plannedShutdownActionFactory);
    }

    private GroupManagementService startGMSModule() throws CLBRuntimeException {
        String instanceName = com.sun.enterprise.server.ondemand.OnDemandServer.getServerContext()
                                                                               .getInstanceName();
        Runnable gmsModule = GMSFactory.startGMSModule(instanceName,
                clusterName, //Adding itself as spectator
            GroupManagementService.MemberType.SPECTATOR, getGMSConfigProps());
        
        ClusterGMSModuleThread gmsModuleThread = new ClusterGMSModuleThread(gmsModule);
        gmsModuleThread.start();
        
        return gms;
    }

    public List<String> getHealthyInstances() {
        GroupHandle groupHandle = gms.getGroupHandle();

        if(_logger.isLoggable(Level.FINE)){
            _logger.log(Level.FINE,
                    "clb.gms_module_core_members",
                    new Object[]{clusterName, groupHandle.getAllCurrentMembers().toString()});
            _logger.log(Level.FINE,
                    "clb.gms_module_core_current_members",
                    new Object[]{clusterName, groupHandle.getCurrentCoreMembers().toString()});
        }
        //for all core cluster members (non spectators)
        return groupHandle.getCurrentCoreMembers();
    }

    public void onRecovery(String clusterName, String instanceName) {
        if(_logger.isLoggable(Level.FINE))
            _logger.log(Level.FINE,
                    "clb.recieved_recovery_notification_for_instance",
                    new Object[]{instanceName, clusterName});

        ServerInstance httpInstance = getServerInstance(clusterName, instanceName, true);
        ServerInstance sipInstance = getServerInstance(clusterName, instanceName, false);

        PingThread pingThread = new PingThread(httpInstance, sipInstance);
        pingThread.start();
    }

    public void onFailure(String clusterName, String instanceName) {
        if(_logger.isLoggable(Level.FINE))
            _logger.log(Level.FINE,
                    "clb.recieved_failure_notification_for_instance",
                    new Object[]{instanceName, clusterName});

        ServerInstance instance = getServerInstance(clusterName, instanceName, true);
        instance.markAsUnhealthy();
        instance = getServerInstance(clusterName, instanceName, false);
        instance.markAsUnhealthy();
    }

    public void onDisable(String clusterName, String instanceName) {
        if(_logger.isLoggable(Level.FINE))
            _logger.log(Level.FINE,
                    "clb.recieved_disable_notification_for_instance",
                    new Object[]{instanceName, clusterName});
        ServerInstance instance = getServerInstance(clusterName, instanceName, true);
        instance.disableInstance();
        instance = getServerInstance(clusterName, instanceName, false);
        instance.disableInstance();
    }

    public void onEnable(String clusterName, String instanceName) {
        if(_logger.isLoggable(Level.FINE))
            _logger.log(Level.FINE,
                    "clb.recieved_enable_notification_for_instance",
                    new Object[]{instanceName, clusterName});
        ServerInstance instance = getServerInstance(clusterName, instanceName, true);
        instance.enableInstance();
        instance = getServerInstance(clusterName, instanceName, false);
        instance.enableInstance();
    }

    private ServerInstance getServerInstance(String clusterName,
        String instanceName, boolean isHttp) {
        Controller controller;
        if(isHttp)
            controller = CLBConfigurator.getInstance().
                    getHttpControllerInitializer().getController();
        else
            controller = CLBConfigurator.getInstance().
                    getSipControllerInitializer().getController();
        assert(controller != null);
        return controller.getInstance(clusterName, instanceName);
    }

    public String getClusterName() {
        return clusterName;
    }

    public void cleanup() {
        gms.removeActionFactory(failureNotificationActionFactory);
        gms.removeActionFactory(joinNotificationActionFactory);
        gms.removeActionFactory(plannedShutdownActionFactory);
        //To be confirmed how to shut down a GMS module
        if(startedGMSModule)
            gms.shutdown(GMSConstants.shutdownType.INSTANCE_SHUTDOWN);
    }

    private Properties getGMSConfigProps() throws CLBRuntimeException {
        Properties props = new Properties();
        Config config;
        Cluster cluster;
        ConfigContext configContext;

        try {
            configContext = AdminService.getAdminService().getAdminContext()
                                        .getAdminConfigContext();
            cluster = ClusterHelper.getClusterByName(configContext, clusterName);

            String configRef = cluster.getConfigRef();
            config = ConfigAPIHelper.getConfigByName(configContext, configRef);

            com.sun.enterprise.config.serverbeans.GroupManagementService gmsConfig =
                config.getGroupManagementService();
            
            if(!gmsConfig.isEnabled()){
                String message = _logger.getResourceBundle().
                            getString("clb.gms_not_enabled") + clusterName;
                    throw new CLBRuntimeException(message);
            }
                        
            props.put(ServiceProviderConfigurationKeys.FAILURE_DETECTION_RETRIES.toString(),
                gmsConfig.getFdProtocolMaxTries());
            props.put(ServiceProviderConfigurationKeys.FAILURE_DETECTION_TIMEOUT.toString(),
                gmsConfig.getFdProtocolTimeoutInMillis());
            props.put(ServiceProviderConfigurationKeys.DISCOVERY_TIMEOUT.toString(),
                gmsConfig.getPingProtocolTimeoutInMillis());
            props.put(ServiceProviderConfigurationKeys.FAILURE_VERIFICATION_TIMEOUT.toString(),
                gmsConfig.getVsProtocolTimeoutInMillis());
            props.put(ServiceProviderConfigurationKeys.MULTICASTADDRESS.toString(),
                cluster.getHeartbeatAddress());
            props.put(ServiceProviderConfigurationKeys.MULTICASTPORT.toString(),
                cluster.getHeartbeatPort());
        } catch (ConfigException e) {
            _logger.log(Level.SEVERE,
                    "clb.exception_getting_properties_for_gms_module",
                    new Object[]{clusterName, e.getMessage()});
            if(_logger.isLoggable(Level.FINE))
                _logger.log(Level.FINE, "clb.caught_an_exception", e);
        }

        return props;
    }

    class PingThread extends Thread {
        private ServerInstance httpInstance;
        private ServerInstance sipInstance;
        
        private final int maxRetryCount = 10;
        private final long sleepBetweenRetry = 5000L;

        PingThread(ServerInstance httpInstance, ServerInstance sipInstance) {
            this.httpInstance = httpInstance;
            this.sipInstance = sipInstance;
        }

        public void run() {
            pingHttpEndPoint();
        }

        private void pingHttpEndPoint() {
            EndPoint endPoint;
            endPoint = httpInstance.getEndPoint(CLBConstants.HTTP_PROTOCOL);

            if (endPoint == null) {
                endPoint = httpInstance.getEndPoint(CLBConstants.HTTPS_PROTOCOL);
            }

            if (endPoint == null) {
                _logger.log(Level.SEVERE,
                        "clb.no_endpoint_to_ping",
                        new Object[]{httpInstance.getName(), clusterName});
            }

            sendHttpRequest(endPoint);
        }

        private void sendHttpRequest(EndPoint endPoint) {
            String request = "HEAD / HTTP/1.1\r\n" + "Host: " +
                endPoint.getHost() + ":" + endPoint.getPort() + "\r\n" +
                "Proxy-ping: true\r\n\r\n";
            int count = 0;

            while (count < maxRetryCount) {
                count++;

                try {
                    Socket socket = new Socket();
                    socket.connect(endPoint.getSocketAddress());

                    OutputStream outputStream = socket.getOutputStream();
                    outputStream.write(request.getBytes());
                    outputStream.flush();

                    BufferedReader reader = new BufferedReader(new InputStreamReader(
                                socket.getInputStream()));
                    String line = reader.readLine();

                    if (line != null) {
                        int index = line.indexOf(" ");

                        if (index > -1) {
                            int next = line.indexOf(" ", index + 1);

                            if (next > -1) {
                                int statusCode = Integer.valueOf(line.substring(index +
                                            1, next));

                                if ((statusCode >= 100) && (statusCode <= 499)) {
                                    if(_logger.isLoggable(Level.FINE))
                                        _logger.log(Level.FINE,
                                                "clb.ping_request_successful",
                                                new Object[]{httpInstance.getName(), clusterName});
                                    httpInstance.markAsHealthy();
                                    sipInstance.markAsHealthy();
                                }
                            }
                        }
                    }

                    try {
                        socket.close();
                    } catch (IOException ex) {
                        if(_logger.isLoggable(Level.FINE))
                            _logger.log(Level.FINE,
                                    "clb.caught_an_exception",
                                    ex);
                    }

                    if (httpInstance.isHealthy()) {
                        break;
                    }

                    _logger.log(Level.SEVERE,
                            "clb.ping_request_unsuccessful",
                            new Object[]{ httpInstance.getName(), clusterName, line});
                } catch (UnknownHostException ex) {
                    _logger.log(Level.SEVERE,
                            "clb.cannot_open_connection",
                            new Object[]{httpInstance.getName(), clusterName, 
                            endPoint.getHost(), endPoint.getPort()});
                    if(_logger.isLoggable(Level.FINE))
                        _logger.log(Level.FINE,
                                "clb.caught_an_exception",
                                ex);

                    break;
                } catch (IOException ex) {
                    if(_logger.isLoggable(Level.FINE))
                        _logger.log(Level.FINE,
                                "clb.caught_an_exception",
                                ex);
                }

                try {
                    Thread.sleep(sleepBetweenRetry);
                } catch (InterruptedException ex) {
                    if(_logger.isLoggable(Level.FINE))
                        _logger.log(Level.FINE,
                                "clb.caught_an_exception",
                                ex);
                }
            }

            if (!httpInstance.isHealthy()) {
                _logger.log(Level.SEVERE,
                        "clb.cannot_mark_instance_healthy",
                        new Object[]{httpInstance.getName(), clusterName});
            }
        }
    }
    
    class ClusterGMSModuleThread extends Thread{
        
        ClusterGMSModuleThread(Runnable gmsModule){
            super(gmsModule);
        }
        
    }
}
