/*
 * 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.
 */
package org.jvnet.glassfish.comms.clb.core.sip;

import com.ericsson.ssa.sip.AddressImpl;
import com.ericsson.ssa.sip.Header;
import com.ericsson.ssa.sip.MultiLineHeader;
import com.ericsson.ssa.sip.SipServletMessageImpl;
import com.ericsson.ssa.sip.SipServletRequestImpl;
import com.ericsson.ssa.sip.SipServletResponseImpl;
import com.ericsson.ssa.sip.SipURIImpl;
import com.ericsson.ssa.sip.UriUtil;
import com.ericsson.ssa.sip.ViaImpl;

import org.jvnet.glassfish.comms.clb.core.common.chr.HashKeyExtractor;
import org.jvnet.glassfish.comms.clb.core.common.chr.StickyHashKeyExtractor;
import org.jvnet.glassfish.comms.clb.core.util.LoadbalancerUtil;
import org.jvnet.glassfish.comms.util.LogUtil;

import java.util.ListIterator;
import java.util.logging.Level;

import javax.servlet.sip.Address;
import javax.servlet.sip.ServletParseException;
import javax.servlet.sip.SipServletMessage;
import javax.servlet.sip.SipServletRequest;
import javax.servlet.sip.SipSession;
import javax.servlet.sip.SipURI;


/**
 * This class implements the SIP load balancer routing logic. It implements both
 * the front-end logic (incoming requests and responses) and back-end logic
 * (both incoming and outgoing requests and responses)
 */
public class SipLoadBalancerBackend {
    private static LogUtil logger = new LogUtil(LogUtil.CLB_LOG_DOMAIN);
    private HashKeyExtractor hashKeyExtractor;
    private Socket localAddress;
    private Socket publicAddress;

    /**
     * Creates an instance and associates it with the specified hash key
     * extractor and server instance lookup.
     *
     * @param hashKeyExtractor the hash key extractor
     * @param localAddress the address of this instance
     * @param publicAddress the public address of the cluster
     */
    public SipLoadBalancerBackend(HashKeyExtractor hashKeyExtractor,
        Socket localAddress, Socket publicAddress) {
        this.hashKeyExtractor = hashKeyExtractor;
        this.localAddress = localAddress;
        this.publicAddress = publicAddress;
    }

    /**
     * Handle a request that as been received by this instance. It shall either
     * be served by this instance, or be proxied to another instance.
     *
     * @param req the request; the request may be modified (headers added or
     *                changed)
     */
    public void handleIncomingRequest(SipServletRequestImpl req) {
        if (logger.isLoggable(Level.FINE)) {
            logger.logMsg(Level.FINE, "Handle incoming request: " + req);
        }

        if (req.getAttribute(SipLoadBalancerIncomingHandler.BE_KEY_ATTR) == null) {
            // The SipLoadBalancerIncomingHandler was not present (this is a
            // pure backend).
            // Extract BEKey that was extracted by front-end
            String beKey = decodeBeKeyHeader(req);

            if (logger.isLoggable(Level.FINE)) {
                logger.logMsg(Level.FINE, "Extracted BE key: " + beKey);
            }

            if (beKey != null) {
                req.setAttribute(SipLoadBalancerIncomingHandler.BE_KEY_ATTR,
                    beKey);
            }
        }
    }

    /**
     * Handle a request that as been created or proxied by this instance.
     *
     * @param request the request; the request may be modified (headers added or
     *                changed)
     * @return a possible new connection; if null continue with the connection
     *         in the transaction stack.
     * @throws SipRoutingException thrown in case request was malformed
     */
    public Connection handleOutgoingRequest(SipServletRequestImpl request)
        throws SipRoutingException {
        if (logger.isLoggable(Level.FINE)) {
            logger.logMsg(Level.FINE, "Handle outgoing request: " + request);
        }

        // Check if hash key has been previously saved
        String hashkey = (String) getAttributeFromSession(request,
                SipLoadBalancerIncomingHandler.BE_KEY_ATTR);

        if (hashkey == null) {
            // New request, save hash key
            hashkey = getHashKeyExtractor().getHashKey(request);

            SipSession session = request.getSession();

            if ((session != null) && session.isValid()) {
                session.setAttribute(SipLoadBalancerIncomingHandler.BE_KEY_ATTR,
                    hashkey);
            }

            request.setAttribute(SipLoadBalancerIncomingHandler.BE_KEY_ATTR,
                hashkey);
        }

        if (logger.isLoggable(Level.FINE)) {
            logger.logMsg(Level.FINE, "Hashkey: " + hashkey);
        }

        // Set BERoute parameter on topmost Via
        ViaImpl via = getTopVia(request);
        via.setParameter(SipLoadBalancerIncomingHandler.BE_ROUTE_PARAM,
            encodeBeRoute(getLocalAddress()));

        // Check if applications has proxied the request
        if (!requestWasProxied(request)) {
            // Check if this was an initial request issued by a local
            // application
            if (requestIsInitial(request)) {
                // This is an initial request
                try {
                    Header contactHeader = request.getRawHeader(Header.CONTACT);

                    if (contactHeader != null) {
                        AddressImpl addressValue = (AddressImpl) contactHeader.getAddressValue();

                        if (addressValue != null) {
                            // Fetch current read-only status
                            boolean readOnly = addressValue.isReadOnly();
                            // Allow writes on Address object
                            addressValue.setReadOnly(false);
                            UriUtil.setParameter(addressValue.getURI(), StickyHashKeyExtractor.BE_KEY_PARAM,
                                hashkey);
                            // Reset read-only status
                            addressValue.setReadOnly(readOnly);
                        }
                    }
                } catch (ServletParseException e) {
                    throw new SipRoutingException("Malformed Contact", e);
                }
            }

            replaceTopVia(request, via);

            if (logger.isLoggable(Level.FINE)) {
                logger.logMsg(Level.FINE,
                    "Was initial, altered request: " + request);
            }

            return null;
        } else {
            // This request was proxied, save ID of incoming connection.
            via.setParameter(SipLoadBalancerIncomingHandler.CONNID_PARAM,
                new Connection(request.getRemote().getProtocol(),
                    new Socket(request.getLocal().getHostName(),
                        request.getLocal().getPort()),
                    new Socket(request.getRemote().getIP(),
                        request.getRemote().getPort())).getBase64());

            if (requestIsInitial(request)) {
                Header rrHeader = request.getRawHeader(Header.RECORD_ROUTE);
                boolean addRr = false;
                AddressImpl rrAddr;

                if (rrHeader != null) {
                    try {
                        rrAddr = (AddressImpl) rrHeader.getAddressValue();
                    } catch (ServletParseException e) {
                        throw new SipRoutingException("Malformed record-Route",
                            e);
                    }

                    if (!getPublicAddress().getHostName()
                                 .equals(((SipURI) rrAddr.getURI()).getHost())) {
                        addRr = true;
                    }
                } else {
                    addRr = true;
                    rrHeader = new MultiLineHeader(Header.RECORD_ROUTE, true);

                    SipURIImpl uri = new SipURIImpl();
                    uri.setHost(getPublicAddress().getHostName());
                    uri.setPort(getPublicAddress().getPort());
                    rrHeader.setValue("<" + uri.toString() + ">", true);
                    request.addHeader(rrHeader);

                    try {
                        rrAddr = (AddressImpl) rrHeader.getAddressValue();
                    } catch (ServletParseException e) {
                        throw new SipRoutingException("Malformed Record-Route",
                            e);
                    }
                }

                // Fetch current read-only status
                boolean readOnly = rrAddr.isReadOnly();
                // Allow writes on Address object
                rrAddr.setReadOnly(false);
                UriUtil.setParameter(rrAddr.getURI(),StickyHashKeyExtractor.BE_KEY_PARAM, hashkey);

                if (addRr) {
                    UriUtil.setParameter(rrAddr.getURI(), SipLoadBalancerIncomingHandler.LB_PARAM,
                        "");
                }

                // Reset read-only status
                rrAddr.setReadOnly(readOnly);
            }

            replaceTopVia(request, via);

            if (logger.isLoggable(Level.FINE)) {
                logger.logMsg(Level.FINE,
                    "Was proxied, altered request: " + request);
            }

            return null;
        }
    }

    /**
     * Handle a response that has been created or proxied by this instance.
     *
     * @param response the response; the response may be modified (headers added
     *                or changed)
     * @return a possible new connection; if null continue with the connection
     *         in the transaction stack.
     * @throws SipRoutingException thrown if message was malformed
     */
    public Connection handleOutgoingResponse(SipServletResponseImpl response)
        throws SipRoutingException {
        if (logger.isLoggable(Level.FINE)) {
            logger.logMsg(Level.FINE, "Handle outgoing response: " + response);
        }

        String hashkey = getHashKey(response);

        if ((hashkey != null) && (response.getSession() != null) &&
                response.getSession().isValid()) {
            response.getSession()
                    .setAttribute(SipLoadBalancerIncomingHandler.BE_KEY_ATTR,
                hashkey);
        }

        Header contactHeader = response.getRawHeader(Header.CONTACT);

        if ((contactHeader != null) && (hashkey != null)) {
            AddressImpl contactAddress;

            try {
                contactAddress = (AddressImpl) (contactHeader.getAddressValue());
            } catch (ServletParseException e) {
                throw new SipRoutingException("Malformed Contact", e);
            }

            SipURI contactUri = ((SipURI) contactAddress.getURI());

            if (getPublicAddress().getHostName().equals(contactUri.getHost())) {
                contactAddress = ((AddressImpl) contactAddress.clone());
                contactUri = ((SipURI) contactUri.clone());
                contactUri.setParameter(StickyHashKeyExtractor.BE_KEY_PARAM,
                    hashkey);
                // Allow writes on cloned Address object
                contactAddress.setReadOnly(false);
                contactAddress.setURI(contactUri);
                contactHeader.setReadOnly(false);
                contactHeader.removeValues();
                contactHeader.setValue(contactAddress.toString(), true);
                contactHeader.setReadOnly(true);
            }
        }

        Connection connection = (Connection) getAttributeFromSession(response,
                SipLoadBalancerIncomingHandler.CONNID_ATTR);

        if (logger.isLoggable(Level.FINE)) {
            logger.logMsg(Level.FINE, "Altered response: " + response);
        }

        return connection;
    }

    private String getHashKey(SipServletResponseImpl response) {
        String hashkey;
        hashkey = (String) getAttributeFromSession(response,
                SipLoadBalancerIncomingHandler.BE_KEY_ATTR);

        if (hashkey == null) {
            // Could not get it from session, try the request
            SipServletRequest request = response.getRequest();

            if (request != null) {
                hashkey = (String) request.getAttribute(SipLoadBalancerIncomingHandler.BE_KEY_ATTR);
            }
        }

        return hashkey;
    }

    // ---------------- Internal methods --------------------
    private Object getAttributeFromSession(SipServletMessage message,
        String attributeName) {
        SipSession session = message.getSession();

        if ((session != null) && session.isValid()) {
            return session.getAttribute(attributeName);
        } else {
            return null;
        }
    }

    private boolean requestIsInitial(SipServletMessageImpl request)
        throws SipRoutingException {
        Header toHeader = request.getRawHeader(Header.TO);
        boolean isInitial;

        try {
            isInitial = toHeader.getAddressValue().getParameter("tag") == null;
        } catch (ServletParseException e) {
            throw new SipRoutingException("Malformed To", e);
        }

        return isInitial;
    }

    private boolean requestWasProxied(SipServletMessageImpl request) {
        // Check if applications has proxied the request, check if there exist
        // more than one Via
        ListIterator<String> iter = request.getRawHeader(Header.VIA).getValues();
        int n = 0;

        for (; iter.hasNext(); iter.next()) {
            n++;
        }

        boolean wasProxied = n > 1;

        return wasProxied;
    }

    private ViaImpl getTopVia(SipServletMessageImpl msg) {
        return new ViaImpl(msg.getRawHeader(Header.VIA).getValue());
    }

    private void replaceTopVia(SipServletMessageImpl msg, ViaImpl newTopVia) {
        Header viaHeader = msg.getRawHeader(Header.VIA);
        viaHeader.setReadOnly(false);

        ListIterator<String> viaIter = viaHeader.getValues();

        while (viaIter.hasNext()) {
            viaIter.next();
            viaIter.remove();

            break;
        }

        viaHeader.setValue(newTopVia.toString(), true);
        viaHeader.setReadOnly(true);
    }

    private Socket getPublicAddress() {
        return publicAddress;
    }

    private Socket getLocalAddress() {
        return localAddress;
    }

    private HashKeyExtractor getHashKeyExtractor() {
        return hashKeyExtractor;
    }

    private String encodeBeRoute(Socket address) {
        String routeString = address.getHostName() + ":" + address.getPort();

        return LoadbalancerUtil.encodeParameter(routeString);
    }

    private String decodeBeKeyHeader(SipServletMessageImpl msg) {
        String beKey = msg.getHeader(StickyHashKeyExtractor.PROXY_BEKEY_HEADER);
        msg.removeHeader(StickyHashKeyExtractor.PROXY_BEKEY_HEADER);

        return beKey;
    }
}
