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

import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.clustering.Member;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.protocol.HTTP;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.SynapseException;
import org.apache.synapse.aspects.ComponentType;
import org.apache.synapse.aspects.flow.statistics.collectors.CloseEventCollector;
import org.apache.synapse.aspects.flow.statistics.collectors.OpenEventCollector;
import org.apache.synapse.aspects.flow.statistics.collectors.RuntimeStatisticCollector;
import org.apache.synapse.core.LoadBalanceMembershipHandler;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.apache.synapse.core.axis2.Axis2SynapseEnvironment;
import org.apache.synapse.endpoints.algorithms.AlgorithmContext;
import org.apache.synapse.endpoints.dispatch.Dispatcher;
import org.apache.synapse.endpoints.dispatch.HttpSessionDispatcher;
import org.apache.synapse.endpoints.dispatch.SALSessions;
import org.apache.synapse.endpoints.dispatch.SessionInformation;
import org.apache.synapse.transport.nhttp.NhttpConstants;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

/**
 * Represents a dynamic load balance endpoint. The application membership is not static,
 * but discovered through some mechanism such as using a GCF
 */
public class DynamicLoadbalanceEndpoint extends LoadbalanceEndpoint {

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

    private static final String PORT_MAPPING_PREFIX = "port.mapping.";

    /**
     *  Flag to enable session affinity based load balancing.
     */
    protected boolean sessionAffinity = false;

    /**
     * Dispatcher used for session affinity.
     */
    protected Dispatcher dispatcher = null;

    /* Sessions time out interval*/
    protected long sessionTimeout = -1;

    /**
     * The algorithm context , place holder for keep any runtime states related to the load balance
     * algorithm
     */
    private AlgorithmContext algorithmContext;

    @Override
    public void init(SynapseEnvironment synapseEnvironment) {
        ConfigurationContext cc =
                ((Axis2SynapseEnvironment) synapseEnvironment).getAxis2ConfigurationContext();
        if (!initialized) {
            super.init(synapseEnvironment);
            if (algorithmContext == null) {
                algorithmContext = new AlgorithmContext(isClusteringEnabled, cc, getName());
            }

            // Initialize the SAL Sessions if already has not been initialized.
            SALSessions salSessions = SALSessions.getInstance();
            if (!salSessions.isInitialized()) {
                salSessions.initialize(isClusteringEnabled, cc);
            }
        }
        log.info("Dynamic load balance endpoint initialized");
    }

    private LoadBalanceMembershipHandler lbMembershipHandler;

    public DynamicLoadbalanceEndpoint() {
    }

    public void setLoadBalanceMembershipHandler(LoadBalanceMembershipHandler lbMembershipHandler) {
        this.lbMembershipHandler = lbMembershipHandler;
    }

    public LoadBalanceMembershipHandler getLbMembershipHandler() {
        return lbMembershipHandler;
    }

    public void send(MessageContext synCtx) {
        if (RuntimeStatisticCollector.isStatisticsEnabled()) {
            Integer currentIndex = null;
            boolean retry = (synCtx.getProperty(SynapseConstants.LAST_ENDPOINT) != null);
            if ((getDefinition() != null) && !retry) {
                currentIndex = OpenEventCollector.reportChildEntryEvent(synCtx, getReportingName(),
                        ComponentType.ENDPOINT, getDefinition().getAspectConfiguration(), true);
            }
            try {
                sendMessage(synCtx);
            } finally {
                if (currentIndex != null) {
                    CloseEventCollector.closeEntryEvent(synCtx, getReportingName(),
                            ComponentType.MEDIATOR, currentIndex, false);
                }
            }
        } else {
            sendMessage(synCtx);
        }
    }

    private void sendMessage(MessageContext synCtx) {

        logSetter();

        SessionInformation sessionInformation = null;
        Member currentMember = null;
        //TODO Temp hack: ESB removes the session id from request in a random manner.
        setCookieHeader(synCtx);

        ConfigurationContext configCtx =
                ((Axis2MessageContext) synCtx).getAxis2MessageContext().getConfigurationContext();
        if (lbMembershipHandler.getConfigurationContext() == null) {
            lbMembershipHandler.setConfigurationContext(configCtx);
        }

        if (isSessionAffinityBasedLB()) {
            // first check if this session is associated with a session. if so, get the endpoint
            // associated for that session.
            sessionInformation =
                    (SessionInformation) synCtx.getProperty(
                            SynapseConstants.PROP_SAL_CURRENT_SESSION_INFORMATION);

            currentMember = (Member) synCtx.getProperty(
                    SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_MEMBER);

            if (sessionInformation == null && currentMember == null) {
                sessionInformation = dispatcher.getSession(synCtx);
                if (sessionInformation != null) {

                    if (log.isDebugEnabled()) {
                        log.debug("Current session id : " + sessionInformation.getId());
                    }

                    currentMember = sessionInformation.getMember();
                    synCtx.setProperty(
                            SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_MEMBER, currentMember);
                    // This is for reliably recovery any session information if while response is getting ,
                    // session information has been removed by cleaner.
                    // This will not be a cost as  session information a not heavy data structure
                    synCtx.setProperty(
                            SynapseConstants.PROP_SAL_CURRENT_SESSION_INFORMATION, sessionInformation);
                }
            }

        }


        DynamicLoadbalanceFaultHandlerImpl faultHandler = new DynamicLoadbalanceFaultHandlerImpl();
        if (sessionInformation != null && currentMember != null) {
            //send message on current session
            sessionInformation.updateExpiryTime();
            sendToApplicationMember(synCtx, currentMember, faultHandler, false);
        } else {
            // prepare for a new session
            currentMember = lbMembershipHandler.getNextApplicationMember(algorithmContext);
            if (currentMember == null) {
                String msg = "No application members available";
                log.error(msg);
                throw new SynapseException(msg);
            }
            sendToApplicationMember(synCtx, currentMember, faultHandler, true);
        }
    }

    protected void setCookieHeader(MessageContext synCtx) {
        String cookieHeader = extractSessionID(synCtx, "Cookie");
        if (cookieHeader != null) {
            synCtx.setProperty("LB_COOKIE_HEADER", cookieHeader);
        }
    }

    //TODO following methods are to extract the session ID temporary hack for Stratos 1.0.0 release
    protected String extractSessionID(MessageContext synCtx, String key) {

        if (key != null) {
            Map headerMap = getTransportHeaderMap(synCtx);

            if (headerMap != null) {
                Object cookieObj = headerMap.get(key);

                if (cookieObj instanceof String) {
                    return (String) cookieObj;
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("Couldn't find the " + key + " header to find the session");
                    }
                }
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Couldn't find the TRANSPORT_HEADERS to find the session");
                }

            }
        }
        return null;
    }

    private Map getTransportHeaderMap(MessageContext synCtx) {

        org.apache.axis2.context.MessageContext axis2MessageContext =
                ((Axis2MessageContext) synCtx).getAxis2MessageContext();

        Object o = axis2MessageContext.getProperty("TRANSPORT_HEADERS");
        if (o != null && o instanceof Map) {
            return (Map) o;
        }
        return null;
    }

	/**
	 * Adding the X-Forwarded-For/X-Originating-IP headers to the outgoing message.
	 * 
	 * @param synCtx Current message context
	 */
	protected void setupTransportHeaders(MessageContext synCtx) {
		Axis2MessageContext axis2smc = (Axis2MessageContext) synCtx;
		org.apache.axis2.context.MessageContext axis2MessageCtx = axis2smc.getAxis2MessageContext();
		Object headers = axis2MessageCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
		if (headers != null && headers instanceof Map) {
			Map headersMap = (Map) headers;
			String xForwardFor = (String) headersMap.get(NhttpConstants.HEADER_X_FORWARDED_FOR);
		    String remoteHost = (String) axis2MessageCtx.getProperty(org.apache.axis2.context.MessageContext.REMOTE_ADDR);

			if (xForwardFor != null && !"".equals(xForwardFor)) {
				StringBuilder xForwardedForString = new StringBuilder();
				xForwardedForString.append(xForwardFor);
				if (remoteHost != null && !"".equals(remoteHost)) {
					xForwardedForString.append(",").append(remoteHost);
				}
				headersMap.put(NhttpConstants.HEADER_X_FORWARDED_FOR, xForwardedForString.toString());
			} else {
				headersMap.put(NhttpConstants.HEADER_X_FORWARDED_FOR, remoteHost);
			}

			//Extracting information of X-Originating-IP
			if (headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_1) != null) {
				headersMap.put(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_1, headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_1));
			} else if (headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_2) != null) {
				headersMap.put(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_2, headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_2));
			}

		}
	}

    public void setName(String name) {
        super.setName(name);
//        algorithmContext.setContextID(name);
    }

    public Dispatcher getDispatcher() {
        return dispatcher;
    }

    public void setDispatcher(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    public long getSessionTimeout() {
        return sessionTimeout;
    }

    public void setSessionTimeout(long sessionTimeout) {
        this.sessionTimeout = sessionTimeout;
    }

    public void setSessionAffinity(boolean sessionAffinity){
        this.sessionAffinity = sessionAffinity;
    }

    public boolean isSessionAffinityBasedLB(){
        return sessionAffinity;
    }

    protected void sendToApplicationMember(MessageContext synCtx,
                                           Member currentMember,
                                           DynamicLoadbalanceFaultHandler faultHandler,
                                           boolean newSession) {
        //Rewriting the URL
        org.apache.axis2.context.MessageContext axis2MsgCtx =
                ((Axis2MessageContext) synCtx).getAxis2MessageContext();

        //Removing the REST_URL_POSTFIX - this is a hack.
        //In this loadbalance endpoint we create an endpoint per request by setting the complete url as the adress.
        //If a REST message comes Axis2FlexibleMEPClient append the REST_URL_POSTFIX to the adress. Hence endpoint fails
        //do send the request. e.g.  http://localhost:8080/example/index.html/example/index.html
        axis2MsgCtx.removeProperty(NhttpConstants.REST_URL_POSTFIX);

        String transport = axis2MsgCtx.getTransportIn().getName();
        String address = synCtx.getTo().getAddress();
        int incomingPort = extractPort(synCtx, transport);
        EndpointReference to = getEndpointReferenceAfterURLRewrite(currentMember,
                transport, address, incomingPort);
        synCtx.setTo(to);

        faultHandler.setTo(to);
        faultHandler.setCurrentMember(currentMember);
        synCtx.pushFaultHandler(faultHandler);
        if (isFailover()) {
            synCtx.getEnvelope().build();
        }

        Endpoint endpoint = getEndpoint(to, currentMember, synCtx);
        faultHandler.setCurrentEp(endpoint);

        if (isSessionAffinityBasedLB()) {
            synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_DEFAULT_SESSION_TIMEOUT, getSessionTimeout());
            synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_DISPATCHER, dispatcher);
            prepareEndPointSequence(synCtx, endpoint);
            if(newSession) {
            	synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_MEMBER, currentMember);
            	// we should also indicate that this is the first message in the session. so that
            	// onFault(...) method can resend only the failed attempts for the first message.
            	synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_FIRST_MESSAGE_IN_SESSION, Boolean.TRUE);
            }
        }

        Map<String, String> memberHosts;
        if ((memberHosts = (Map<String, String>) currentMember.getProperties().get(HttpSessionDispatcher.HOSTS)) == null) {
            currentMember.getProperties().put(HttpSessionDispatcher.HOSTS,
                    memberHosts = new HashMap<String, String>());
        }
        memberHosts.put(extractHost(synCtx), "true");
        setupTransportHeaders(synCtx);
        try {
            endpoint.send(synCtx);
        } catch (Exception e) {
            if(e.getMessage().toLowerCase().contains("io reactor shutdown")){
                log.fatal("System cannot continue normal operation. Restarting", e);
                System.exit(121); // restart
            } else {
                throw new SynapseException(e);
            }
        }
    }

    /*
    * Preparing the endpoint sequence for a new session establishment request
    */
    private void prepareEndPointSequence(MessageContext synCtx, Endpoint endpoint) {

        Object o = synCtx.getProperty(SynapseConstants.PROP_SAL_ENDPOINT_ENDPOINT_LIST);
        List<Endpoint> endpointList;
        if (o instanceof List) {
            endpointList = (List<Endpoint>) o;
            endpointList.add(this);

        } else {
            // this is the first endpoint in the hierarchy. so create the queue and
            // insert this as the first element.
            endpointList = new ArrayList<Endpoint>();
            endpointList.add(this);
            synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_ENDPOINT_LIST, endpointList);
            synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_DISPATCHER, dispatcher);
        }

        // if the next endpoint is not a session affinity one, endpoint sequence ends
        // here. but we have to add the next endpoint to the list.
        if (!(endpoint instanceof DynamicLoadbalanceEndpoint)) {
            endpointList.add(endpoint);
            // Clearing out if there any any session information with current message
            if (dispatcher.isServerInitiatedSession()) {
                dispatcher.removeSessionID(synCtx);
            }
        }
    }

    private EndpointReference getEndpointReferenceAfterURLRewrite(Member currentMember,
                                                                  String transport,
                                                                  String address,
                                                                  int incomingPort) {

        if (transport.startsWith("https")) {
            transport = "https";
        } else if (transport.startsWith("http")) {
            transport = "http";
        } else {
            String msg = "Cannot load balance for non-HTTP/S transport " + transport;
            log.error(msg);
            throw new SynapseException(msg);
        }
        // URL Rewrite
        if (transport.startsWith("http") || transport.startsWith("https")) {
            if (address.startsWith("http://") || address.startsWith("https://")) {
                try {
                    String _address= address.indexOf("?")>0? address.substring(address.indexOf("?"), address.length()):"";
                    address = new URL(address).getPath()+_address;
                } catch (MalformedURLException e) {
                    String msg = "URL " + address + " is malformed";
                    log.error(msg, e);
                    throw new SynapseException(msg, e);
                }
            }

            int port;
            Properties memberProperties = currentMember.getProperties();
            String mappedPort = memberProperties.getProperty(PORT_MAPPING_PREFIX + incomingPort);
            if (mappedPort != null) {
                port = Integer.parseInt(mappedPort);
            } else if (transport.startsWith("https")) {
                port = currentMember.getHttpsPort();
            } else {
                port = currentMember.getHttpPort();
            }

            String remoteHost = memberProperties.getProperty("remoteHost");
            String hostName = (remoteHost == null) ? currentMember.getHostName() : remoteHost;
            return new EndpointReference(transport + "://" + hostName +
                                 ":" + port + address);
        } else {
            String msg = "Cannot load balance for non-HTTP/S transport " + transport;
            log.error(msg);
            throw new SynapseException(msg);
        }
    }

    /**
     *
     * @param to get an endpoint to send the information
     * @param member The member to which an EP has to be created
     * @param synCtx synapse context
     * @return the created endpoint
     */
    private Endpoint getEndpoint(EndpointReference to, Member member, MessageContext synCtx) {
        AddressEndpoint endpoint = new AddressEndpoint();
        endpoint.setEnableMBeanStats(false);
        endpoint.setName("DLB:" +  member.getHostName() +
                ":" + member.getPort() + ":" + UUID.randomUUID());
        EndpointDefinition definition = new EndpointDefinition();
        definition.setSuspendMaximumDuration(10000);
        definition.setReplicationDisabled(true);
        definition.setAddress(to.getAddress());
        endpoint.setDefinition(definition);
        endpoint.init((SynapseEnvironment)
                ((Axis2MessageContext) synCtx).getAxis2MessageContext().
                        getConfigurationContext().getAxisConfiguration().
                        getParameterValue(SynapseConstants.SYNAPSE_ENV));
        return endpoint;
    }

    private String extractHost(MessageContext synCtx) {
        org.apache.axis2.context.MessageContext msgCtx =
                ((Axis2MessageContext) synCtx).getAxis2MessageContext();

        Map headerMap = (Map) msgCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
        String hostName = null;
        if (headerMap != null) {
            Object hostObj = headerMap.get(HTTP.TARGET_HOST);
            hostName = (String) hostObj;
            if (hostName.contains(":")) {
                hostName = hostName.substring(0, hostName.indexOf(":"));
            }
        }
        return hostName;
    }

    private int extractPort(MessageContext synCtx, String transport) {
        org.apache.axis2.context.MessageContext msgCtx =
                ((Axis2MessageContext) synCtx).getAxis2MessageContext();

        Map headerMap = (Map) msgCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
        int port = -1;
		if (headerMap != null) {
			String hostHeader = (String) headerMap.get(HTTP.TARGET_HOST);
			int preIndex = hostHeader.indexOf(":");
			int postIndex = hostHeader.indexOf("/");
			if (preIndex != -1 && postIndex != -1) {
				port = Integer.parseInt(hostHeader.trim().substring(preIndex + 1, postIndex));
			} else if (preIndex != -1) {
				port = Integer.parseInt(hostHeader.trim().substring(preIndex + 1));
			} else {
				if ("http".equals(transport)) {
					port = 80;
				} else if ("https".equals(transport)) {
					port = 443;
				}
			}
		}
        return port;
    }



    /**
     * This FaultHandler will try to resend the message to another member if an error occurs
     * while sending to some member. This is a failover mechanism
     */
    private class DynamicLoadbalanceFaultHandlerImpl extends DynamicLoadbalanceFaultHandler {

        private EndpointReference to;
        private Member currentMember;
        private Endpoint currentEp;

        public void setCurrentMember(Member currentMember) {
            this.currentMember = currentMember;
        }

        public void setTo(EndpointReference to) {
            this.to = to;
        }

        private DynamicLoadbalanceFaultHandlerImpl() {
        }

        public void onFault(MessageContext synCtx) {

            logSetter();

            //cleanup endpoint if exists
            if(currentEp != null){
                currentEp.destroy();
            }
            if (currentMember == null) {
                return;
            }

            Stack faultStack = synCtx.getFaultStack();
            if (faultStack != null && !faultStack.isEmpty()) {
                faultStack.pop();  // Remove the LoadbalanceFaultHandler
            }

            currentMember = lbMembershipHandler.getNextApplicationMember(algorithmContext);
            if(currentMember == null){
                String msg = "No application members available";
                log.error(msg);
                throw new SynapseException(msg);
            }
            synCtx.setTo(to);
            if(isSessionAffinityBasedLB()){
                //We are sending the this message on a new session,
                // hence we need to remove previous session information
                Set pros = synCtx.getPropertyKeySet();
                if (pros != null) {
                    pros.remove(SynapseConstants.PROP_SAL_CURRENT_SESSION_INFORMATION);
                }
            }
            sendToApplicationMember(synCtx, currentMember, this, true);
        }

        public void setCurrentEp(Endpoint currentEp) {
            this.currentEp = currentEp;
        }
    }
}
