/*
 * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * Licensed 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.wso2.carbon.event.output.adapter.soap;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.util.AXIOMUtil;
import org.apache.axis2.AxisFault;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.addressing.RelatesTo;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.ConfigurationContextFactory;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyEngine;
import org.apache.rampart.RampartMessageData;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.event.output.adapter.core.EventAdapterUtil;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapter;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterConfiguration;
import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterException;
import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterRuntimeException;
import org.wso2.carbon.event.output.adapter.core.exception.TestConnectionNotSupportedException;
import org.wso2.carbon.event.output.adapter.soap.internal.util.SoapEventAdapterConstants;
import org.wso2.carbon.utils.ServerConstants;

import javax.xml.stream.XMLStreamException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;

public class SoapEventAdapter implements OutputEventAdapter {

    private static final Log log = LogFactory.getLog(SoapEventAdapter.class);
    private OutputEventAdapterConfiguration eventAdapterConfiguration;
    private Map<String, String> globalProperties;
    private ExecutorService executorService;
    private ConfigurationContext configContext;
    private int tenantId;

    public SoapEventAdapter(OutputEventAdapterConfiguration eventAdapterConfiguration,
                            Map<String, String> globalProperties) {
        this.eventAdapterConfiguration = eventAdapterConfiguration;
        this.globalProperties = globalProperties;
    }

    @Override
    public void init() throws OutputEventAdapterException {

        tenantId= PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();

        //executorService will be assigned  if it is null
        if (executorService == null) {
            int minThread;
            int maxThread;
            long defaultKeepAliveTime;
            int jobQueueSize;

            //If global properties are available those will be assigned else constant values will be assigned
            if (globalProperties.get(SoapEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME) != null) {
                minThread = Integer.parseInt(globalProperties.get(
                        SoapEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME));
            } else {
                minThread = SoapEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE;
            }

            if (globalProperties.get(SoapEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME) != null) {
                maxThread = Integer.parseInt(globalProperties.get(
                        SoapEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME));
            } else {
                maxThread = SoapEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE;
            }

            if (globalProperties.get(SoapEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME) != null) {
                defaultKeepAliveTime = Integer.parseInt(globalProperties.get(
                        SoapEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME));
            } else {
                defaultKeepAliveTime = SoapEventAdapterConstants.DEFAULT_KEEP_ALIVE_TIME_IN_MILLIS;
            }

            if (globalProperties.get(SoapEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME) != null) {
                jobQueueSize = Integer.parseInt(globalProperties.get(
                        SoapEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME));
            } else {
                jobQueueSize = SoapEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE;
            }

            executorService = new ThreadPoolExecutor(minThread, maxThread, defaultKeepAliveTime, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>(jobQueueSize),new ThreadFactoryBuilder().
                    setNameFormat("Thread pool- component - SoapEventAdapter.executorService;adapterName - " +
                            this.eventAdapterConfiguration.getName()).build());
        }

    }

    @Override
    public void testConnect() throws TestConnectionNotSupportedException {
        throw new TestConnectionNotSupportedException("Test connection is not available");
    }

    @Override
    public void connect() {
        try {
            configContext = ConfigurationContextFactory.createConfigurationContextFromFileSystem(
                    System.getProperty(ServerConstants.CARBON_HOME)
                            + SoapEventAdapterConstants.SERVER_CLIENT_DEPLOYMENT_DIR,
                    System.getProperty(ServerConstants.CARBON_CONFIG_DIR_PATH)
                            + SoapEventAdapterConstants.AXIS2_CLIENT_CONF_FILE);

            int axis2ClientTimeOutInMillis = SoapEventAdapterConstants.DEFAULT_AXIS2_CLIENT_CONNECTION_TIMEOUT;
            boolean isReuseHTTPClient = SoapEventAdapterConstants.IS_DEFAULT_AXIS2_REUSE_HTTP_CLIENT;
            boolean isAutoReleaseConnection = SoapEventAdapterConstants.IS_DEFAULT_AXIS2_AUTO_RELEASE_CONNECTION;
            int maxConnectionPerHostValue = SoapEventAdapterConstants.DEFAULT_AXIS2_MAX_CONNECTION_PER_HOST;

            String axi2ClientTimeOut = globalProperties.get(SoapEventAdapterConstants.AXIS2_CLIENT_CONNECTION_TIMEOUT);
            try {
                if (axi2ClientTimeOut != null) {
                    axis2ClientTimeOutInMillis = Integer.parseInt(axi2ClientTimeOut);
                }
            } catch (NumberFormatException e) {
                log.error("Invalid axis2 client timeout value " + axi2ClientTimeOut + " ignoring the configuration and using default value " + axis2ClientTimeOutInMillis);
            }

            String reuseHTTPClient = globalProperties.get(SoapEventAdapterConstants.AXIS2_REUSE_HTTP_CLIENT);
            try {
                if (reuseHTTPClient != null) {
                    isReuseHTTPClient = Boolean.parseBoolean(reuseHTTPClient);
                }
            } catch (NumberFormatException e) {
                log.error("Invalid Reuse HTTP Client value " + reuseHTTPClient + " ignoring the configuration and using default value " + isReuseHTTPClient);
            }

            String autoReleaseConnection = globalProperties.get(SoapEventAdapterConstants.AXIS2_AUTO_RELEASE_CONNECTION);
            try {
                if (autoReleaseConnection != null) {
                    isAutoReleaseConnection = Boolean.parseBoolean(autoReleaseConnection);
                }
            } catch (NumberFormatException e) {
                log.error("Invalid Auto release connection value " + autoReleaseConnection + " ignoring the configuration and using default value " + isAutoReleaseConnection);
            }

            String maxConnectionPerHost = globalProperties.get(SoapEventAdapterConstants.AXIS2_MAX_CONNECTION_PER_HOST);
            try {
                if (maxConnectionPerHost != null) {
                    maxConnectionPerHostValue = Integer.parseInt(maxConnectionPerHost);
                }
            } catch (NumberFormatException e) {
                log.error("Invalid Max connection per host value " + maxConnectionPerHost + " ignoring the configuration and using default value " + maxConnectionPerHostValue);
            }

            configContext.setProperty(HTTPConstants.REUSE_HTTP_CLIENT, isReuseHTTPClient);
            configContext.setProperty(HTTPConstants.CACHED_HTTP_CLIENT, createMultiThreadedHttpConnectionManager(axis2ClientTimeOutInMillis, maxConnectionPerHostValue));
            configContext.setProperty(HTTPConstants.AUTO_RELEASE_CONNECTION, isAutoReleaseConnection);

        } catch (AxisFault axisFault) {
            throw new OutputEventAdapterRuntimeException("Error while creating configuration context from filesystem ", axisFault);
        }
    }

    @Override
    public void publish(Object message, Map<String, String> dynamicProperties) {

        String url = dynamicProperties.get(SoapEventAdapterConstants.ADAPTER_CONF_SOAP_URL);
        String userName = dynamicProperties.get(SoapEventAdapterConstants.ADAPTER_CONF_SOAP_USERNAME);
        String password = dynamicProperties.get(SoapEventAdapterConstants.ADAPTER_CONF_SOAP_PASSWORD);
        Map<String, String> soapHeaders = this.extractHeaders(dynamicProperties.get(
                SoapEventAdapterConstants.ADAPTER_CONF_SOAP_HEADERS));
        Map<String, String> httpHeaders = this.extractHeaders(dynamicProperties.get(
                SoapEventAdapterConstants.ADAPTER_CONF_HTTP_HEADERS));

        try {
            this.executorService.submit(new SoapSender(url, message, userName, password, soapHeaders, httpHeaders));
        } catch (RejectedExecutionException e) {
            EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), message, "Job queue is full", e, log, tenantId);
        }
    }

    @Override
    public void disconnect() {
        //not required
    }

    @Override
    public void destroy() {
        //not required
    }

    @Override
    public boolean isPolled() {
        return false;
    }

    private Map<String, String> extractHeaders(String headers) {
        if (headers == null || headers.trim().length() == 0) {
            return null;
        }
        String[] entries = headers.split(SoapEventAdapterConstants.HEADER_SEPARATOR);
        String[] keyValue;
        Map<String, String> result = new HashMap<String, String>();
        for (String header : entries) {
            try {
                keyValue = header.split(SoapEventAdapterConstants.ENTRY_SEPARATOR, 2);
                result.put(keyValue[0].trim(), keyValue[1].trim());
            } catch (Throwable e) {
                log.error("Header property \"" + header + "\" is not defined in the correct format.", e);
            }
        }
        return result;

    }

    public class SoapSender implements Runnable {

        private String url;
        private Object payload;
        private String username;
        private String password;
        private Map<String, String> soapHeaders;
        private Map<String, String> httpHeaders;

        public SoapSender(String url, Object payload, String username, String password,
                          Map<String, String> soapHeaders, Map<String, String> httpHeaders) {
            this.url = url;
            this.payload = payload;
            this.username = username;
            this.password = password;
            this.soapHeaders = soapHeaders;
            this.httpHeaders = httpHeaders;
        }

        @Override
        public void run() {

            ServiceClient serviceClient = null;
            try {
                serviceClient = new ServiceClient(configContext, null);
                Options options = new Options();
                options.setTo(new EndpointReference(url));

                if (soapHeaders != null) {
                    serviceClient.engageModule("addressing");
                    setSoapHeaders(soapHeaders, options);
                }

                if (httpHeaders != null) {
                    setHttpHeaders(httpHeaders, options);
                }

                if (username != null || password != null) {
                    options.setUserName(username);
                    options.setPassword(password);
                    serviceClient.engageModule("rampart");
                    options.setProperty(RampartMessageData.KEY_RAMPART_POLICY, loadPolicy());
                }

                serviceClient.setOptions(options);
                serviceClient.fireAndForget(AXIOMUtil.stringToOM(payload.toString()));

            } catch (AxisFault e) {
                EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), payload, "Cannot send to endpoint '" + url + "'", e, log, tenantId);
            } catch (XMLStreamException e) {
                EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), payload, "Cannot convert event to XML", e, log, tenantId);
            } catch (Exception e) {
                EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), payload, null, e, log, tenantId);
            } finally {
                if (serviceClient != null) {
                    try {
                        serviceClient.cleanup();
                    } catch (AxisFault axisFault) {
                        log.error("Error while cleaning-up service client resources of Output SOAP Event Adapter '" + eventAdapterConfiguration.getName() + "' : " + axisFault.getMessage(), axisFault);
                    }
                }
            }

        }

        private Policy loadPolicy() throws XMLStreamException {
            OMElement omElement = AXIOMUtil.stringToOM(
                    "<wsp:Policy xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy\"\n" +
                            "            xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"\n" +
                            "            wsu:Id=\"UTOverTransport\">\n" +
                            "    <wsp:ExactlyOne>\n" +
                            "        <wsp:All>\n" +
                            "            <sp:TransportBinding xmlns:sp=\"http://schemas.xmlsoap.org/ws/2005/07/securitypolicy\">\n" +
                            "                <wsp:Policy>\n" +
                            "                    <sp:TransportToken>\n" +
                            "                        <wsp:Policy>\n" +
                            "                            <sp:HttpsToken RequireClientCertificate=\"false\"></sp:HttpsToken>\n" +
                            "                        </wsp:Policy>\n" +
                            "                    </sp:TransportToken>\n" +
                            "                    <sp:AlgorithmSuite>\n" +
                            "                        <wsp:Policy>\n" +
                            "                            <sp:Basic256></sp:Basic256>\n" +
                            "                        </wsp:Policy>\n" +
                            "                    </sp:AlgorithmSuite>\n" +
                            "                    <sp:Layout>\n" +
                            "                        <wsp:Policy>\n" +
                            "                            <sp:Lax></sp:Lax>\n" +
                            "                        </wsp:Policy>\n" +
                            "                    </sp:Layout>\n" +
                            "                    <sp:IncludeTimestamp></sp:IncludeTimestamp>\n" +
                            "                </wsp:Policy>\n" +
                            "            </sp:TransportBinding>\n" +
                            "            <sp:SignedSupportingTokens\n" +
                            "                    xmlns:sp=\"http://schemas.xmlsoap.org/ws/2005/07/securitypolicy\">\n" +
                            "                <wsp:Policy>\n" +
                            "                    <sp:UsernameToken\n" +
                            "                            sp:IncludeToken=\"http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/AlwaysToRecipient\"></sp:UsernameToken>\n" +
                            "                </wsp:Policy>\n" +
                            "            </sp:SignedSupportingTokens>\n" +
                            "        </wsp:All>\n" +
                            "    </wsp:ExactlyOne>\n" +
                            "</wsp:Policy>");
            return PolicyEngine.getPolicy(omElement);
        }

        private void setSoapHeaders(Map<String, String> headers, Options options) {

            for (Map.Entry<String, String> headerValue : headers.entrySet()) {
                try {
                    if (headerValue.getKey().equalsIgnoreCase("SOAPAction")) {
                        options.setAction(headerValue.getValue());
                    } else if (headerValue.getKey().equalsIgnoreCase("From")) {
                        options.setFrom(new EndpointReference(headerValue.getValue()));
                    } else if (headerValue.getKey().equalsIgnoreCase("FaultTo")) {
                        options.setFaultTo(new EndpointReference(headerValue.getValue()));
                    } else if (headerValue.getKey().equalsIgnoreCase("TransportIn")) {
                        options.setTransportIn(new TransportInDescription(headerValue.getValue()));
                    } else if (headerValue.getKey().equalsIgnoreCase("TransportInProtocol")) {
                        options.setTransportInProtocol(headerValue.getValue());
                    } else if (headerValue.getKey().equalsIgnoreCase("MessageID")) {
                        options.setMessageId(headerValue.getValue());
                    } else if (headerValue.getKey().equalsIgnoreCase("RelatesTo")) {
                        options.addRelatesTo(new RelatesTo(headerValue.getValue()));
                    } else if (headerValue.getKey().equalsIgnoreCase("ReplyTo")) {
                        options.setReplyTo(new EndpointReference(headerValue.getValue()));
                    } else if (headerValue.getKey().equalsIgnoreCase("TransportOut")) {
                        options.setTransportOut(new TransportOutDescription(headerValue.getValue()));
                    } else if (headerValue.getKey().equalsIgnoreCase("SoapVersionURI")) {
                        options.setSoapVersionURI(headerValue.getValue());
                    } else if (headerValue.getKey().equalsIgnoreCase("To")) {
                        options.setTo(new EndpointReference(headerValue.getValue()));
                    } else if (headerValue.getKey().equalsIgnoreCase("ManageSession")) {
                        options.setManageSession(Boolean.parseBoolean(headerValue.getValue()));
                    } else {
                        try {
                            int headerParameterValue = Integer.parseInt(headerValue.getValue());
                            options.setProperty(headerValue.getKey(), headerParameterValue);
                        } catch (NumberFormatException e) {
                            options.setProperty(headerValue.getKey(), headerValue.getValue());
                        }
                    }

                } catch (Throwable e) {
                    //Catching the exception because there can be several exception thrown from axis2 level and we cannot drop the message because of a header issue
                    log.warn("Invalid soap header : \"" + headerValue + "\", ignoring corresponding header..." + e.getMessage());
                }
            }
        }

        private void setHttpHeaders(Map<String, String> headers, Options options) {
            List<Header> list = new ArrayList<>();

            for (Map.Entry<String, String> headerValue : headers.entrySet()) {
                try {
                    Header header = new Header();
                    header.setName(headerValue.getKey());
                    header.setValue(headerValue.getValue());
                    list.add(header);
                } catch (Throwable e) {
                    //Catching the exception because there can be several exception thrown from axis2 level and we cannot drop the message because of a header issue
                    log.warn("Invalid HTTP header : \"" + headerValue + "\", ignoring corresponding header..." + e.getMessage());
                }
            }
            options.setProperty(org.apache.axis2.transport.http.HTTPConstants.HTTP_HEADERS, list);
        }

    }

    private HttpClient createMultiThreadedHttpConnectionManager(int connectionTimeOut, int maxConnectionPerHost) {

        HttpConnectionManagerParams params = new HttpConnectionManagerParams();
        params.setDefaultMaxConnectionsPerHost(maxConnectionPerHost);
        params.setConnectionTimeout(connectionTimeOut);
        MultiThreadedHttpConnectionManager httpConnectionManager = new MultiThreadedHttpConnectionManager();
        httpConnectionManager.setParams(params);
        return new HttpClient(httpConnectionManager);
    }
}


