/*
 * 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.SipApplicationSessionImpl;
import com.ericsson.ssa.sip.SipServletMessageImpl;
import com.ericsson.ssa.sip.SipServletRequestImpl;
import com.ericsson.ssa.sip.SipServletResponseImpl;
import com.ericsson.ssa.sip.SipSessionBase;
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.util.LogUtil;

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

import javax.servlet.sip.ServletParseException;
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 localSipTcpSocket; // Socket for traffic between F-E to B-E and responses

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

    /**
     * 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.getBeKey() == null) {
            // The SipLoadBalancerIncomingHandler was not present (this is a
            // pure backend). Extract BEKey that was extracted by front-end
            String beKey = req.getHeader(StickyHashKeyExtractor.PROXY_BEKEY_HEADER);

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

            if (beKey != null) {
                req.setBeKey(beKey);
            }
        }

        // Unconditional remove of Proxy-Bekey header
        req.removeHeader(StickyHashKeyExtractor.PROXY_BEKEY_HEADER);
    }

    /**
     * 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 = getHashKey(request);

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

            SipSessionBase ss = request.getSessionImpl();

            if (ss != null) {
                SipApplicationSessionImpl sas = request.getApplicationSessionImpl();

                if (sas.isValid()) {
                    sas.setBeKey(hashkey);
                }
            }

            request.setBeKey(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(getLocalSipTcpSocket()));

        // Check if the application has set Contact
        if (request.isContactIndicated()) {
            // Check if this was an initial request issued by a local
            // application
            if (request.isInitial()) {
                // 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 if (request.isRecordRouteIndicated()) {
            // This request was proxied, save ID of incoming connection.
            String encodedConnection = getEncondedConnection(request);

            if (encodedConnection != null) {
                via.setParameter(SipLoadBalancerIncomingHandler.CONNID_PARAM,
                    getEncondedConnection(request));
            }

            Header rrHeader = request.getRawHeader(Header.RECORD_ROUTE);
            AddressImpl rrAddr;

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

            // The R-R was added by the application, thus we shall set 'bekey'
            // 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);
            // 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;
        } else {
            // This request was proxied but not Record-Routed, save ID of incoming 
            // connection so that response can travel back.
            String encodedConnection = getEncondedConnection(request);

            if (encodedConnection != null) {
                via.setParameter(SipLoadBalancerIncomingHandler.CONNID_PARAM,
                    encodedConnection);
            }

            replaceTopVia(request, via);

            return null;
        }
    }

    private String getEncondedConnection(SipServletRequestImpl request) {
        Connection c = (Connection) request.getAttribute(SipLoadBalancerIncomingHandler.CONNID_ATTR);

        if (c != null) {
            return c.getEncodedValue();
        }

        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.getSessionImpl() != null) &&
                response.getApplicationSessionImpl().isValid()) {
            response.getApplicationSessionImpl().setBeKey(hashkey);
        }

        if ((hashkey != null) && response.isContactIndicated()) {
            // This is a UAS.
            Header contactHeader = response.getRawHeader(Header.CONTACT);

            if (contactHeader != null) {
                AddressImpl contactAddress;

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

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

                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);
            }
        }

        // Get the connection that was extracted from the Via in the (possible) incoming response
        Connection connection = (Connection) response.getAttribute(SipLoadBalancerIncomingHandler.CONNID_ATTR);

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

        return connection;
    }

    private String getHashKey(SipServletMessageImpl msg) {
        String hashkey = getBeKeyFromSession(msg);

        if (hashkey == null) {
            // Could not get it from session, try the message
            hashkey = msg.getBeKey();
        }

        return hashkey;
    }

    private String getBeKeyFromSession(SipServletMessageImpl msg) {
        String hashkey = null;
        SipSessionBase ss = msg.getSessionImpl();

        if (ss != null) {
            SipApplicationSessionImpl sas = ss.getApplicationSessionImpl();
            hashkey = sas.getBeKey();
        }

        return hashkey;
    }

    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 HashKeyExtractor getHashKeyExtractor() {
        return hashKeyExtractor;
    }

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

        return routeString;
    }

    static Socket decodeBeRoute(String beRouteParameter)
        throws SipRoutingException {
        beRouteParameter = beRouteParameter.substring(1,
                beRouteParameter.length() - 1);

        StringTokenizer st = new StringTokenizer(beRouteParameter, ":", false);
        String[] addrArr = new String[2];
        int i = 0;

        while (st.hasMoreTokens() && (i < addrArr.length)) {
            addrArr[i++] = st.nextToken();
        }

        // Now addrArr[0]=IP address and
        // addrArr[1]=port
        if (i == addrArr.length) {
            try {
                return new Socket(addrArr[0], Integer.parseInt(addrArr[1]));
            } catch (NumberFormatException e) {
                throw new SipRoutingException("Malformed 'beroute' parameter", e);
            }
        }

        throw new SipRoutingException("Could not decode 'beroute' parameter");
    }

    private Socket getLocalSipTcpSocket() {
        return localSipTcpSocket;
    }
}
