/**
 * Copyright 2005-2018 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.ksb.messaging.config;

import org.kuali.rice.core.api.config.CoreConfigHelper;
import org.kuali.rice.core.api.config.module.RunMode;
import org.kuali.rice.core.api.config.property.Config;
import org.kuali.rice.core.api.config.property.ConfigContext;
import org.kuali.rice.core.api.exception.RiceRuntimeException;
import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
import org.kuali.rice.core.api.lifecycle.Lifecycle;
import org.kuali.rice.core.api.resourceloader.ResourceLoader;
import org.kuali.rice.core.api.util.ClassLoaderUtils;
import org.kuali.rice.core.api.util.RiceConstants;
import org.kuali.rice.core.framework.config.module.ModuleConfigurer;
import org.kuali.rice.core.framework.config.module.WebModuleConfiguration;
import org.kuali.rice.core.framework.lifecycle.ServiceDelegatingLifecycle;
import org.kuali.rice.ksb.api.KsbApiConstants;
import org.kuali.rice.ksb.api.KsbApiServiceLocator;
import org.kuali.rice.ksb.api.bus.ServiceDefinition;
import org.kuali.rice.ksb.messaging.AlternateEndpoint;
import org.kuali.rice.ksb.messaging.AlternateEndpointLocation;
import org.kuali.rice.ksb.messaging.MessageFetcher;
import org.kuali.rice.ksb.messaging.resourceloader.KSBResourceLoaderFactory;
import org.kuali.rice.ksb.service.KSBServiceLocator;
import org.kuali.rice.ksb.util.KSBConstants;
import org.quartz.Scheduler;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import javax.xml.ws.WebServiceException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * Used to configure the embedded workflow. This could be used to configure
 * embedded workflow programmatically but mostly this is a base class by which
 * to hang specific configuration behavior off of through subclassing
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class KSBConfigurer extends ModuleConfigurer implements SmartApplicationListener {
	
    private static final String SERVICE_BUS_CLIENT_SPRING = "classpath:org/kuali/rice/ksb/config/KsbServiceBusClientSpringBeans.xml";
    private static final String MESSAGE_CLIENT_SPRING = "classpath:org/kuali/rice/ksb/config/KsbMessageClientSpringBeans.xml";
    private static final String JPA_MESSAGE_CLIENT_SPRING = "classpath:org/kuali/rice/ksb/config/KsbJpaMessageClientSpringBeans.xml";
    private static final String BAM_SPRING = "classpath:org/kuali/rice/ksb/config/KsbBamSpringBeans.xml";
    private static final String REGISTRY_SERVER_SPRING = "classpath:org/kuali/rice/ksb/config/KsbRegistryServerSpringBeans.xml";
    private static final String JPA_REGISTRY_SPRING = "classpath:org/kuali/rice/ksb/config/KsbJpaRegistrySpringBeans.xml";
    private static final String WEB_SPRING = "classpath:org/kuali/rice/ksb/config/KsbWebSpringBeans.xml";

    private List<ServiceDefinition> services = new ArrayList<ServiceDefinition>();

    private List<AlternateEndpointLocation> alternateEndpointLocations = new ArrayList<AlternateEndpointLocation>();

    private List<AlternateEndpoint> alternateEndpoints = new ArrayList<AlternateEndpoint>();

    private DataSource registryDataSource;

    private DataSource messageDataSource;

    private DataSource nonTransactionalMessageDataSource;

    private DataSource bamDataSource;

    private Scheduler exceptionMessagingScheduler;

    private PlatformTransactionManager platformTransactionManager;

    private List<Lifecycle> internalLifecycles;

    public KSBConfigurer() {
        super(KsbApiConstants.KSB_MODULE_NAME);
        setValidRunModes(Arrays.asList(RunMode.THIN, RunMode.REMOTE, RunMode.LOCAL));
        this.internalLifecycles = new ArrayList<Lifecycle>();
    }

    @Override
    public void addAdditonalToConfig() {
        configureDataSource();
        configureScheduler();
        configurePlatformTransactionManager();
        configureAlternateEndpoints();
    }

    @Override
    public List<String> getPrimarySpringFiles() {
        LOG.info("KSBConfigurer:getPrimarySpringFiles: getRunMode => " + getRunMode());
        final List<String> springFileLocations = new ArrayList<String>();

        springFileLocations.add(SERVICE_BUS_CLIENT_SPRING);

        if (getRunMode() != RunMode.THIN) {
            // Loading these beans unconditionally now, see:
            // KULRICE-6574: Some KSB beans not defined unless message persistence turned on
            //
            // if (isMessagePersistenceEnabled()) {
            springFileLocations.add(MESSAGE_CLIENT_SPRING);
            springFileLocations.add(JPA_MESSAGE_CLIENT_SPRING);
            // }

            if (isBamEnabled()) {
                springFileLocations.add(BAM_SPRING);
            }
        }

        if (getRunMode().equals(RunMode.LOCAL)) {
            springFileLocations.add(JPA_REGISTRY_SPRING);
            springFileLocations.add(REGISTRY_SERVER_SPRING);
        }

        return springFileLocations;
    }

    @Override
    public boolean hasWebInterface() {
        return true;
    }

    // See KULRICE-7093: KSB Module UI is not available on client applications
    @Override
    public boolean shouldRenderWebInterface() {
        if (ConfigContext.getCurrentContextConfig().getBooleanProperty(KSBConstants.Config.WEB_FORCE_ENABLE)) {
            return true;
        }
        return super.shouldRenderWebInterface();
    }

    @Override
    protected WebModuleConfiguration loadWebModule() {
        WebModuleConfiguration configuration = super.loadWebModule();
        configuration.getWebSpringFiles().add(WEB_SPRING);
        return configuration;
    }

    @Override
    public Collection<ResourceLoader> getResourceLoadersToRegister() throws Exception {
        ResourceLoader ksbRemoteResourceLoader = KSBResourceLoaderFactory.createRootKSBRemoteResourceLoader();
        ksbRemoteResourceLoader.start();
        return Collections.singletonList(ksbRemoteResourceLoader);
    }

    @Override
    public List<Lifecycle> loadLifecycles() throws Exception {
        List<Lifecycle> lifecycles = new LinkedList<Lifecycle>();

        // this validation of our service list needs to happen after we've
        // loaded our configs so it's a lifecycle
        lifecycles.add(new BaseLifecycle() {
            @Override
            public void start() throws Exception {
                super.start();
            }
        });

        return lifecycles;
    }

    protected void validateServices(List<ServiceDefinition> services) {
        for (final ServiceDefinition serviceDef : this.services) {
            serviceDef.validate();
        }
    }

    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        if (applicationEvent instanceof ContextRefreshedEvent) {
            doAdditionalContextStartedLogic();
        } else if (applicationEvent instanceof ContextStoppedEvent) {
            doAdditionalContextStoppedLogic();
        }
    }

    protected void doAdditionalContextStartedLogic() {
        validateServices(getServices());
        ServicePublisher servicePublisher = new ServicePublisher(getServices());
        Lifecycle serviceBus = new ServiceDelegatingLifecycle(KsbApiServiceLocator.SERVICE_BUS);
        Lifecycle threadPool = new ServiceDelegatingLifecycle(KSBConstants.ServiceNames.THREAD_POOL_SERVICE);
        Lifecycle scheduledThreadPool = new ServiceDelegatingLifecycle(KSBConstants.ServiceNames.SCHEDULED_THREAD_POOL_SERVICE);

        try {
            servicePublisher.start();
            internalLifecycles.add(servicePublisher);
            serviceBus.start();
            internalLifecycles.add(serviceBus);
            threadPool.start();
            internalLifecycles.add(threadPool);
            scheduledThreadPool.start();
            internalLifecycles.add(scheduledThreadPool);
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }
            throw new RiceRuntimeException("Failed to initialize KSB on context startup");
        }

        // Don't requeue messages if we are in thin client mode
        if (getRunMode() != RunMode.THIN) {
            requeueMessages();
        }
    }

    protected void doAdditionalContextStoppedLogic() {
        cleanUpConfiguration();
    }

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        return true;
    }

    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        return true;
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    protected void doAdditionalModuleStartLogic() throws Exception {
        // this allows us to become aware of remote services, in case the application needs to use any of them during startup
        /* Wrapped this call in a try/catch block to handle when the web
		 * service call fails so the KSB module will continue starting even
		 * if the remote service registry is not available.
		 */
        try {
            // this allows us to become aware of remote services, in case the application needs to use any of them during startup
            LOG.info("Synchronizing remote services with service bus after KSB startup...");
            long startTime = System.currentTimeMillis();
            KsbApiServiceLocator.getServiceBus().synchronizeRemoteServices();
            long endTime = System.currentTimeMillis();
            LOG.info("...total time to synchronize remote services with service bus after KSB startup: " + (endTime - startTime));
        } catch (WebServiceException e) {
            LOG.error("Caught web service exception synchronizing service bus while configuring KSB", e);
        }
    }

    @Override
    protected void doAdditionalModuleStopLogic() throws Exception {
        for (int index = internalLifecycles.size() - 1; index >= 0; index--) {
            try {
                internalLifecycles.get(index).stop();
            } catch (Exception e) {
                LOG.error("Failed to properly execute shutdown logic.", e);
            }
        }
    }

    /**
     * Used to refresh the service registry after the Application Context is initialized.  This way any services that
     * were exported on startup
     * will be available in the service registry once startup is complete.
     */
    private void requeueMessages() {
        LOG.info("Refreshing Service Registry to export services to the bus.");
        try {
            KsbApiServiceLocator.getServiceBus().synchronizeLocalServices();
        }catch (WebServiceException e) {
            LOG.error("Caught web service exception while starting the KSB Configurer", e);
        }

        //automatically requeue documents sitting with status of 'R'
        MessageFetcher messageFetcher = new MessageFetcher((Integer) null);
        KSBServiceLocator.getThreadPool().execute(messageFetcher);
    }

    protected boolean isMessagePersistenceEnabled() {
        return ConfigContext.getCurrentContextConfig().getBooleanProperty(KSBConstants.Config.MESSAGE_PERSISTENCE, true);
    }

    protected boolean isBamEnabled() {
        return ConfigContext.getCurrentContextConfig().getBooleanProperty(Config.BAM_ENABLED, false);
    }

    protected void configureScheduler() {
        if (this.getExceptionMessagingScheduler() != null) {
            LOG.info("Configuring injected exception messaging Scheduler");
            ConfigContext.getCurrentContextConfig().putObject(KSBConstants.Config.INJECTED_EXCEPTION_MESSAGE_SCHEDULER_KEY, this.getExceptionMessagingScheduler());
        }
    }

    protected void configureDataSource() {
        if (isMessagePersistenceEnabled()) {
            if (getMessageDataSource() != null) {
                ConfigContext.getCurrentContextConfig().putObject(KSBConstants.Config.KSB_MESSAGE_DATASOURCE, getMessageDataSource());
            }
            if (getNonTransactionalMessageDataSource() != null) {
                ConfigContext.getCurrentContextConfig().putObject(KSBConstants.Config.KSB_MESSAGE_NON_TRANSACTIONAL_DATASOURCE, getNonTransactionalMessageDataSource());
            }
        }
        if (getRunMode().equals(RunMode.LOCAL)) {
            if (getRegistryDataSource() != null) {
                ConfigContext.getCurrentContextConfig().putObject(KSBConstants.Config.KSB_REGISTRY_DATASOURCE, getRegistryDataSource());
            }
        }
        if (isBamEnabled()) {
            if (getBamDataSource() != null) {
                ConfigContext.getCurrentContextConfig().putObject(KSBConstants.Config.KSB_BAM_DATASOURCE, getBamDataSource());
            }
        }
    }

    protected void configurePlatformTransactionManager() {
        if (getPlatformTransactionManager() == null) {
            return;
        }
        ConfigContext.getCurrentContextConfig().putObject(RiceConstants.SPRING_TRANSACTION_MANAGER, getPlatformTransactionManager());
    }

    protected void configureAlternateEndpoints() {
        ConfigContext.getCurrentContextConfig().putObject(KSBConstants.Config.KSB_ALTERNATE_ENDPOINT_LOCATIONS, getAlternateEndpointLocations());
        ConfigContext.getCurrentContextConfig().putObject(KSBConstants.Config.KSB_ALTERNATE_ENDPOINTS, getAlternateEndpoints());
    }

    /**
     * Because our configuration is global, shutting down Rice does not get rid of objects stored there.  For that
     * reason we need to manually clean these up.  This is most important in the case of the service bus because the
     * configuration is used to store services to be exported.  If we don't clean this up then a shutdown/startup
     * within the same class loading context causes the service list to be doubled and results in "multiple endpoint"
     * error messages.
     */
    protected void cleanUpConfiguration() {
        ConfigContext.getCurrentContextConfig().removeObject(KSBConstants.Config.KSB_ALTERNATE_ENDPOINTS);
    }

    public List<ServiceDefinition> getServices() {
        return this.services;
    }

    public void setServices(List<ServiceDefinition> javaServices) {
        this.services = javaServices;
    }

    public DataSource getMessageDataSource() {
        return this.messageDataSource;
    }

    public void setMessageDataSource(DataSource messageDataSource) {
        this.messageDataSource = messageDataSource;
    }

    public DataSource getNonTransactionalMessageDataSource() {
        return this.nonTransactionalMessageDataSource;
    }

    public void setNonTransactionalMessageDataSource(DataSource nonTransactionalMessageDataSource) {
        this.nonTransactionalMessageDataSource = nonTransactionalMessageDataSource;
    }

    public DataSource getRegistryDataSource() {
        return this.registryDataSource;
    }

    public void setRegistryDataSource(DataSource registryDataSource) {
        this.registryDataSource = registryDataSource;
    }

    public DataSource getBamDataSource() {
        return this.bamDataSource;
    }

    public void setBamDataSource(DataSource bamDataSource) {
        this.bamDataSource = bamDataSource;
    }

    public Scheduler getExceptionMessagingScheduler() {
        return this.exceptionMessagingScheduler;
    }

    public void setExceptionMessagingScheduler(Scheduler exceptionMessagingScheduler) {
        this.exceptionMessagingScheduler = exceptionMessagingScheduler;
    }

    public PlatformTransactionManager getPlatformTransactionManager() {
        return platformTransactionManager;
    }

    public void setPlatformTransactionManager(PlatformTransactionManager springTransactionManager) {
        this.platformTransactionManager = springTransactionManager;
    }

    public List<AlternateEndpointLocation> getAlternateEndpointLocations() {
        return this.alternateEndpointLocations;
    }

    public void setAlternateEndpointLocations(List<AlternateEndpointLocation> alternateEndpointLocations) {
        this.alternateEndpointLocations = alternateEndpointLocations;
    }

    public List<AlternateEndpoint> getAlternateEndpoints() {
        return this.alternateEndpoints;
    }

    public void setAlternateEndpoints(List<AlternateEndpoint> alternateEndpoints) {
        this.alternateEndpoints = alternateEndpoints;
    }

    private final class ServicePublisher extends BaseLifecycle {

        private final List<ServiceDefinition> serviceDefinitions;

        ServicePublisher(List<ServiceDefinition> serviceDefinitions) {
            this.serviceDefinitions = serviceDefinitions;
        }

        @Override
        public void start() throws Exception {
            if (serviceDefinitions != null && !serviceDefinitions.isEmpty()) {
                LOG.debug("Configuring " + serviceDefinitions.size() + " services for application id " + CoreConfigHelper.getApplicationId() + " using config for classloader " + ClassLoaderUtils.getDefaultClassLoader());
                KsbApiServiceLocator.getServiceBus().publishServices(serviceDefinitions, true);
                super.start();
            }
        }

    }

}
