/**
 * Copyright (c) 2007-2012 EBM WebSourcing, 2012-2024 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.binding.soap;

import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jbi.messaging.MessagingException;
import javax.xml.namespace.QName;

import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.ow2.petals.binding.soap.exception.ServiceClientPoolExhaustedException;
import org.ow2.petals.binding.soap.listener.incoming.PetalsReceiver;
import org.ow2.petals.binding.soap.listener.incoming.SoapServerConfig;
import org.ow2.petals.binding.soap.listener.incoming.jetty.IncomingProbes;
import org.ow2.petals.binding.soap.listener.outgoing.OutgoingProbes;
import org.ow2.petals.binding.soap.listener.outgoing.PetalsServiceClient;
import org.ow2.petals.binding.soap.listener.outgoing.ServiceClientConfiguration;
import org.ow2.petals.binding.soap.listener.outgoing.ServiceClientKey;
import org.ow2.petals.binding.soap.listener.outgoing.ServiceClientPoolObjectFactory;
import org.ow2.petals.binding.soap.monitoring.Monitoring;
import org.ow2.petals.component.framework.AbstractComponent;
import org.ow2.petals.component.framework.api.configuration.ConfigurationExtensions;
import org.ow2.petals.component.framework.jbidescriptor.generated.Component;
import org.ow2.petals.component.framework.jbidescriptor.generated.Consumes;
import org.ow2.petals.component.framework.jbidescriptor.generated.Provides;
import org.ow2.petals.component.framework.listener.AbstractListener;
import org.ow2.petals.probes.api.exceptions.ProbeNotStartedException;

/**
 * The SOAP component context.
 * <p>
 * The context is filled by the SU listener (adding modules, service descriptions...) and used by the listeners/workers.
 * </p>
 * 
 * @author Christophe HAMERLING - EBM WebSourcing
 */
public class SoapComponentContext {

    public static class ServiceManager<E> {
        private final Map<E, ServiceContext<E>> contexts;

        private final SoapComponentContext componentContext;

        public ServiceManager(final SoapComponentContext componentContext) {
            this.componentContext = componentContext;
            this.contexts = new HashMap<E, ServiceContext<E>>();
        }

        /**
         * 
         * @param e
         * @return
         */
        public ServiceContext<E> createServiceContext(final E e) {
            final ServiceContext<E> context = new ServiceContext<E>(e, componentContext);
            this.contexts.put(e, context);
            return context;
        }

        /**
         * Delete the service context
         * 
         * @param e
         */
        public ServiceContext<E> deleteServiceContext(final E e) {
            return this.contexts.remove(e);
        }

        /**
         * @param e
         * @return
         */
        public ServiceContext<E> getServiceContext(final E e) {
            return this.contexts.get(e);
        }
    }

    private final ServiceManager<Consumes> consumersManager;

    private final ServiceManager<Provides> providersManager;

    private final ConfigurationContext axis2ConfigurationContext;

    /**
     * The pools of service clients used to call external web services.
     * 
     * It could be a normal hash map because all accesses to it are actually enclosed in synchronized blocks.
     * 
     * But since we are returning it with {@link #getServiceClientPools()}, we make it concurrent to be sure no error
     * arise when it is traversed by other classes and that data in it is up to date.
     */
    private final ConcurrentMap<ServiceClientKey, GenericObjectPool<ServiceClient>> serviceClientPools;

    /**
     * A map used to link the provides instance to the pools which use it, it has the same locking lifecycle as
     * serviceClientPools.
     */
    private final Map<Provides, Set<ServiceClientKey>> providesServiceClientPools;

    /**
     * This is used to ensure when returning that we can safely access the current pools without being worried that
     * someone remove pools from {@link #serviceClientPools} when they are closed by
     * {@link #deleteServiceClientPools(Provides)}.
     */
    private final ReadWriteLock poolsLock = new ReentrantReadWriteLock();

    /**
     * The component configuration information at CDK level.
     */
    private final Component cdkComponentConfiguration;

    /**
     * The component configuration extensions at BC SOAP level.
     */
    private final ConfigurationExtensions componentConfigurationExtensions;

    /**
     * The JNDI initial factory used by the JMS transport layer.
     */
    private String jmsJndiInitialFactory;

    /**
     * The JNDI provider URL used by the JMS transport layer.
     */
    private String jmsJndiProviderUrl;

    /**
     * The connection factory JNDI name used by the JMS transport layer.
     */
    private String jmsConnectionFactoryName;

    /**
     * The logger
     */
    private final Logger logger;

    /**
     * Technical monitoring probes about outgoing requests
     */
    private final OutgoingProbes outgoingProbes;

    /**
     * Technical monitoring probes about incoming requests
     */
    private final IncomingProbes incomingProbes;

    private final PetalsReceiver petalsReceiver;

    private final AbstractComponent component;

    private final SoapServerConfig soapServerConfig;

    /**
     * Creates a new instance of SoapComponentContext
     * 
     * @param cdkComponentConfiguration
     *            The component configuration information at CDK level
     * @param logger
     */
    public SoapComponentContext(final ConfigurationContext axisContext, final AbstractComponent component,
            final Component cdkComponentConfiguration, final ConfigurationExtensions extensions,
            final Monitoring monitoringBean, final Logger logger) {
        assert cdkComponentConfiguration != null;
        assert cdkComponentConfiguration.getProcessorMaxPoolSize() != null;
        assert cdkComponentConfiguration.getAcceptorPoolSize() != null;
        assert axisContext != null;
        assert component != null;
        assert monitoringBean != null;

        this.outgoingProbes = monitoringBean.getOutgoingProbes();
        this.incomingProbes = monitoringBean.getIncomingProbes();

        assert outgoingProbes.probeWsClientPoolClientsInUse != null;
        assert outgoingProbes.probeWsClientPoolExhaustions != null;
        assert outgoingProbes.probeWsRequestsInvocationsCount != null;
        assert outgoingProbes.probeWsClientInvocationsResponseTime != null;

        this.logger = logger;
        this.petalsReceiver = new PetalsReceiver(this);
        this.component = component;
        this.cdkComponentConfiguration = cdkComponentConfiguration;
        this.componentConfigurationExtensions = extensions;
        this.axis2ConfigurationContext = axisContext;
        this.soapServerConfig = new SoapServerConfig(logger, extensions);

        // managers
        this.consumersManager = new ServiceManager<Consumes>(this);
        this.providersManager = new ServiceManager<Provides>(this);

        // Service client pools creation
        this.serviceClientPools = new ConcurrentHashMap<ServiceClientKey, GenericObjectPool<ServiceClient>>();
        this.providesServiceClientPools = new HashMap<Provides, Set<ServiceClientKey>>();
    }

    /**
     * <p>
     * Get a service client associated to an axis service set with the good operation. It is taken from a pool object.
     * </p>
     * <p>
     * <b>This service client must be returned to the pool after usage using API:
     * <code>{@link #returnServiceClient(ServiceClient)}</code>.</b>
     * </p>
     * 
     * @param address
     *            the address of the service, mainly used as key to retrieve the associated SU.
     * @param operation
     *            the target operation QName. Non null
     * @param soapAction
     * @param mep
     *            the message exchange pattern used. Non null
     * @param context
     *            the provider context of the endpoint which is creating the external WS call
     * @return a ServiceClient. Not null. Must be returned to the pool after usage using API: <code>
     *         {@link #returnServiceClient(ServiceClient)}</code>
     * @throws ServiceClientPoolExhaustedException
     *             when none service client is available (the pool of web-service clients is exhausted)
     * @throws MessagingException
     *             when an other error occurs getting or creatinf a service client
     */
    public ServiceClient borrowServiceClient(final String address, final QName operation,
            final String soapAction, final URI mep, final ServiceContext<Provides> context)
                    throws ServiceClientPoolExhaustedException, MessagingException {

        // we need to acquire this lock to be sure the pool is not going to be closed after I get it
        // but once I got the service client, I don't care what happens, returnServiceClient will handle it
        // it also prevent pools from being removed from serviceClientPools before I check if I need to create one
        this.poolsLock.readLock().lock();
        try {
            final ServiceClientKey key = getServiceClientKey(address, operation, mep, soapAction);

            GenericObjectPool<ServiceClient> pool;
            // we synchronize on it to prevent that two different pools are added at the same time.
            // Note: if ConcurrentMap had a putIfAbsent that didn't need to instantiate the object to be added even
            // when it is not needed, we would use that instead (as in Java 8's computeIfAbsent).
            synchronized (this.serviceClientPools) {
                pool = this.serviceClientPools.get(key);
                if (pool == null) {

                    final Provides provides = context.getConfig();

                    final long timeout = AbstractListener.getTimeout(provides, this.component.getPlaceHolders(),
                            this.logger);

                    final GenericObjectPoolConfig<ServiceClient> gopc = new GenericObjectPoolConfig<>();
                    // max number of borrowed object sized to the max number of JBI message processors
                    gopc.setMaxTotal(this.cdkComponentConfiguration.getProcessorMaxPoolSize().getValue());
                    // getting an object blocks until a new or idle object is available
                    gopc.setBlockWhenExhausted(true);
                    // if getting an object is blocked for at most this delay, a NoSuchElementException will be thrown.
                    // The delay is sized to the value of the SU's parameter "timeout" or the default timeout for send.
                    gopc.setMaxWait(Duration.ofMillis(timeout));
                    // max number of idle object in the pool. Sized to the number of JBI acceptors.
                    gopc.setMaxIdle(this.cdkComponentConfiguration.getAcceptorPoolSize().getValue());
                    // min number of idle object in the pool. Sized to 0 (ie when no activity no object in pool)
                    gopc.setMinIdle(GenericObjectPoolConfig.DEFAULT_MIN_IDLE);
                    // no validation test of the borrowed object
                    gopc.setTestOnBorrow(false);
                    // no validation test of the returned object
                    gopc.setTestOnReturn(false);
                    // how long the eviction thread should sleep before "runs" of examining idle objects. Sized to 5min.
                    gopc.setTimeBetweenEvictionRuns(Duration.ofMillis(300000l));
                    // the number of objects examined in each run of the idle object evictor. Size to the default value
                    // (ie. 3)
                    gopc.setNumTestsPerEvictionRun(GenericObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN);
                    // the minimum amount of time that an object may sit idle in the pool before it is eligible for
                    // eviction due to idle time. Sized to 30min
                    gopc.setMinEvictableIdleTime(GenericObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION);
                    // no validation test of the idle object
                    gopc.setTestWhileIdle(false);
                    // the minimum amount of time an object may sit idle in the pool before it is eligible for eviction
                    // by the idle object evictor (if any), with the extra condition that at least "minIdle" amount of
                    // object remain in the pool.
                    gopc.setSoftMinEvictableIdleTime(GenericObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION);
                    // the pool returns idle objects in last-in-first-out order
                    gopc.setLifo(true);

                    pool = new GenericObjectPool<ServiceClient>(
                            // object factory
                            new ServiceClientPoolObjectFactory(new ServiceClientConfiguration(address, operation, mep, soapAction, timeout, this.getConnMaxSize()),
                                    context, this.axis2ConfigurationContext),
                            gopc);

                    this.serviceClientPools.put(key, pool);

                    Set<ServiceClientKey> serviceClientKeys = this.providesServiceClientPools.get(provides);
                    if (serviceClientKeys == null) {
                        serviceClientKeys = new HashSet<ServiceClientKey>();
                        this.providesServiceClientPools.put(provides, serviceClientKeys);
                    }
                    serviceClientKeys.add(key);
                }
            }

            try {
                final ServiceClient serviceClient = pool.borrowObject();
                ((PetalsServiceClient) serviceClient).setPool(pool);
                try {
                    // TODO shouldn't we do that also in returnServiceClient??
                    // and what about the information from closed pools still referenced by service clients?
                    this.outgoingProbes.probeWsClientPoolClientsInUse.pick(key);
                } catch (final ProbeNotStartedException e2) {
                    this.logger.warning("The WS Client Pool probe does not seem to be started");
                }
                return serviceClient;
            } catch (final NoSuchElementException e) {
                // The pool is exhausted
                this.logger.warning("Service client pool is exhausted (Key: '" + key + "')");
                try {
                    this.outgoingProbes.probeWsClientPoolExhaustions.inc(key);
                } catch (final ProbeNotStartedException e2) {
                    this.logger.warning("The WS Client Pool Exhaustion probe does not seem to be started");
                }
                throw new ServiceClientPoolExhaustedException(key.getAddress(), key.getOperation(), key.getMep());
            }

        } catch (final ServiceClientPoolExhaustedException e) {
            // This exception must be returned to the caller
            throw e;
        } catch (final Exception e) {
            throw new MessagingException("Cannot create or get an Axis service client from the pool", e);
        } finally {
            this.poolsLock.readLock().unlock();
        }
    }

    /**
     * @return the axis2ConfigurationContext
     */
    public ConfigurationContext getAxis2ConfigurationContext() {
        return axis2ConfigurationContext;
    }

    /**
     * @return the consumersManager
     */
    public ServiceManager<Consumes> getConsumersManager() {
        return consumersManager;
    }

    /**
     * The connection factory JNDI name used by the JMS transport layer.
     * 
     * @return The connection factory JNDI name used by the JMS transport layer.
     */
    public String getJmsConnectionFactoryName() {
        return jmsConnectionFactoryName;
    }

    /**
     * The JNDI initial factory used by the JMS transport layer.
     * 
     * @return The JNDI initial factory used by the JMS transport layer.
     */
    public String getJmsJndiInitialFactory() {
        return jmsJndiInitialFactory;
    }

    /**
     * The JNDI provider URL used by the JMS transport layer.
     * 
     * @return The JNDI provider URL used by the JMS transport layer.
     */
    public String getJmsJndiProviderUrl() {
        return jmsJndiProviderUrl;
    }

    /**
     * @return the providersManager
     */
    public ServiceManager<Provides> getProvidersManager() {
        return providersManager;
    }

    /**
     * Delete all the service client pools which used the provides instance
     * 
     * @param provides
     */
    public void deleteServiceClientPools(final Provides provides) {
        // let's get a write lock: borrowServiceClient and returnServiceClient will be blocked during that time
        poolsLock.writeLock().lock();
        try {
            final Set<ServiceClientKey> serviceClientKeys = this.providesServiceClientPools.remove(provides);
            // if a pool has already been created for this Provides...
            if (serviceClientKeys != null) {
                for (final ServiceClientKey key : serviceClientKeys) {
                    final GenericObjectPool<ServiceClient> objectPool = this.serviceClientPools.remove(key);
                    if (objectPool != null) {
                        try {
                            objectPool.close();
                        } catch (final Exception e) {
                            // there is very few reasons for this to happen... but if it happens, it means there
                            // is a bug somewhere!
                            this.logger.log(Level.WARNING, "Error closing the service client pool (key: " + key
                                    + ")... this should not happen", e);
                        }
                    } else {
                        // this shouldn't happen... if the key is in providesServiceClientPools,
                        // then there is a pool in serviceClientPools (note that the inverse is not true)
                        this.logger.severe(
                                "Can't find the service client pool for key '" + key + "', this should never happen.");
                    }
                }
            }
        } finally {
            poolsLock.writeLock().unlock();
        }
    }

    /**
     * Release the service client to the pool
     * 
     * @param serviceClient
     * @throws MessagingException
     */
    public void returnServiceClient(final ServiceClient serviceClient) throws MessagingException {

        final GenericObjectPool<ServiceClient> pool = ((PetalsServiceClient) serviceClient).getPool();
        ((PetalsServiceClient) serviceClient).setPool(null);

        if (pool != null) {
            try {
                // if the pool has been closed, it's not important, returning is not impacted by that
                pool.returnObject(serviceClient);
            } catch (final Exception e) {
                // there is very few reasons for this to happen... but if it happens, there is a bug somewhere
                throw new MessagingException(
                        "Can't return the Axis service client to the pool: this should never happen", e);
            }
        } else {
            throw new MessagingException("Can't find the Axis service client's pool: this should never happen!");
        }
    }

    private ServiceClientKey getServiceClientKey(final String address, final QName operation, final URI mep,
            final String soapAction) throws MessagingException {
        final String resolvedOp;
        if (operation != null) {
            resolvedOp = operation.toString();
        } else if (soapAction != null) {
            resolvedOp = soapAction;
        } else {
            throw new MessagingException("Unable to resolve the operation. Set it in the Jbi exchange or SoapAction.");
        }

        return new ServiceClientKey(address, resolvedOp, mep);
    }

    /**
     * Set the connection factory JNDI name used by the JMS transport layer.
     * 
     * @param jmsConnectionFactoryName
     *            The connection factory JNDI name used by the JMS transport layer.
     */
    public void setJmsConnectionFactoryName(final String jmsConnectionFactoryName) {
        this.jmsConnectionFactoryName = jmsConnectionFactoryName;
    }

    /**
     * Set the JNDI initial factory used by the JMS transport layer.
     * 
     * @param jmsJndiInitialFactory
     *            The JNDI initial factory used by the JMS transport layer.
     */
    public void setJmsJndiInitialFactory(final String jmsJndiInitialFactory) {
        this.jmsJndiInitialFactory = jmsJndiInitialFactory;
    }

    /**
     * Set the JNDI provider URL used by the JMS transport layer.
     * 
     * @param jmsJndiProviderUrl
     *            The JNDI provider URL used by the JMS transport layer.
     */
    public void setJmsJndiProviderUrl(final String jmsJndiProviderUrl) {
        this.jmsJndiProviderUrl = jmsJndiProviderUrl;
    }

    /**
     * @return The max size of a web-service client pools (ie. the max number of JBI processors)
     */
    public int getConnMaxSize() {

        final int maxMexProcessor = this.cdkComponentConfiguration.getProcessorMaxPoolSize().getValue();
        final String connMaxSizeStr = this.componentConfigurationExtensions.get(
                org.ow2.petals.binding.soap.SoapConstants.WsClients.MAX_HTTP_CONNECTIONS_PER_HOST,
                String.valueOf(maxMexProcessor));
        int connMaxSize;
        if (connMaxSizeStr.isEmpty()) {
            connMaxSize = maxMexProcessor;
        } else {
            try {
                connMaxSize = Integer.parseInt(connMaxSizeStr);
                if (connMaxSize <= 0) {
                    this.logger.warning("The value of parameter '"
                            + org.ow2.petals.binding.soap.SoapConstants.WsClients.MAX_HTTP_CONNECTIONS_PER_HOST + "' ("
                            + connMaxSize + ") MUST be strictly upper than 0. Default value used.");
                    connMaxSize = maxMexProcessor;
                }
            } catch (final NumberFormatException e) {
                this.logger.warning("Invalid value for parameter '"
                        + org.ow2.petals.binding.soap.SoapConstants.WsClients.MAX_HTTP_CONNECTIONS_PER_HOST + "' : "
                        + connMaxSizeStr + ". Default value used.");
                connMaxSize = maxMexProcessor;
            }
        }

        return connMaxSize;
    }

    /**
     * This is used by monitoring. It should not be modified by an exterior object!
     * 
     * @return
     */
    public Map<ServiceClientKey, GenericObjectPool<ServiceClient>> getServiceClientPools() {
        return Collections.unmodifiableMap(this.serviceClientPools);
    }

    /**
     * @return Technical monitoring probes about outgoing requests
     */
    public OutgoingProbes getOutgoingProbes() {
        return this.outgoingProbes;
    }

    /**
     * @return Technical monitoring probes about incoming requests
     */
    public IncomingProbes getIncomingProbes() {
        return this.incomingProbes;
    }

    public Logger getLogger() {
        return logger;
    }

    public AbstractComponent getComponent() {
        return component;
    }

    public PetalsReceiver getPetalsReceiver() {
        return petalsReceiver;
    }

    public boolean isPublicStacktracesEnabled() {
        return !"false".equalsIgnoreCase(
                componentConfigurationExtensions.get(SoapConstants.HttpServer.PUBLIC_STACKTRACES_ENABLED));
    }

    public ConfigurationExtensions getComponentConfigurationExtensions() {
        return componentConfigurationExtensions;
    }

    public SoapServerConfig getSoapServerConfig() {
        return soapServerConfig;
    }

}
