/*
 * #%L
 * Alfresco Search Services
 * %%
 * Copyright (C) 2005 - 2020 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco 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 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

package org.alfresco.solr.lifecycle;

import static java.util.Arrays.asList;
import static java.util.Optional.ofNullable;

import static org.alfresco.solr.SolrInformationServer.CASCADE_TRACKER_ENABLED;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Predicate;

import org.alfresco.opencmis.dictionary.CMISStrictDictionaryService;
import org.alfresco.solr.AlfrescoCoreAdminHandler;
import org.alfresco.solr.AlfrescoSolrDataModel;
import org.alfresco.solr.SolrInformationServer;
import org.alfresco.solr.SolrKeyResourceLoader;
import org.alfresco.solr.client.SOLRAPIClient;
import org.alfresco.solr.client.SOLRAPIClientFactory;
import org.alfresco.solr.tracker.AclTracker;
import org.alfresco.solr.tracker.CascadeTracker;
import org.alfresco.solr.tracker.CommitTracker;
import org.alfresco.solr.tracker.ContentTracker;
import org.alfresco.solr.tracker.MetadataTracker;
import org.alfresco.solr.tracker.ModelTracker;
import org.alfresco.solr.tracker.NodeStatePublisher;
import org.alfresco.solr.tracker.SolrTrackerScheduler;
import org.alfresco.solr.tracker.Tracker;
import org.alfresco.solr.tracker.TrackerRegistry;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.AbstractSolrEventListener;
import org.apache.solr.core.CloseHook;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.CoreDescriptorDecorator;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.handler.ReplicationHandler;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.search.SolrIndexSearcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Listeners for *FIRST SEARCHER* events in order to prepare and register the SolrContentStore and the Tracking Subsystem.
 *
 * @author Gethin James
 * @author Andrea Gazzarini
 */
public class SolrCoreLoadListener extends AbstractSolrEventListener
{
    private static final Logger LOGGER = LoggerFactory.getLogger(SolrCoreLoadListener.class);

    /**
     * Builds a new listener instance with the given {@link SolrCore} (event source).
     *
     * @param core the {@link SolrCore} instance representing the event source of this listener.
     */
    public SolrCoreLoadListener(SolrCore core)
    {
        super(core);
    }

    @Override
    public void newSearcher(SolrIndexSearcher newSearcher, SolrIndexSearcher currentSearcher)
    {
        if (getCore().isReloaded())
        {
            LOGGER.info("Solr Core {}, instance {} has been reloaded. " +
                    "The previous tracking subsystem will be stopped and another set of trackers will be registered on this new instance.",
                    getCore().getName(),
                    getCore().hashCode());
        }
        else
        {
            LOGGER.info("Solr Core {}, instance {}, has been registered for the first time.",
                    getCore().getName(),
                    getCore().hashCode());
        }

        CoreContainer coreContainer = getCore().getCoreContainer();
        AlfrescoCoreAdminHandler admin = (AlfrescoCoreAdminHandler) coreContainer.getMultiCoreHandler();
        SolrCore core = getCore();

        TrackerRegistry trackerRegistry = admin.getTrackerRegistry();
        Properties coreProperties = new CoreDescriptorDecorator(core.getCoreDescriptor()).getProperties();

        SolrResourceLoader loader = core.getLatestSchema().getResourceLoader();
        SolrKeyResourceLoader keyResourceLoader = new SolrKeyResourceLoader(loader);
        SOLRAPIClientFactory clientFactory = new SOLRAPIClientFactory();
        SOLRAPIClient repositoryClient =
                clientFactory.getSOLRAPIClient(coreProperties, keyResourceLoader,
                    AlfrescoSolrDataModel.getInstance().getDictionaryService(CMISStrictDictionaryService.DEFAULT),
                    AlfrescoSolrDataModel.getInstance().getNamespaceDAO());

        SolrInformationServer informationServer = new SolrInformationServer(admin, core, repositoryClient);
        coreProperties.putAll(informationServer.getProps());
        admin.getInformationServers().put(core.getName(), informationServer);

        final SolrTrackerScheduler scheduler = admin.getScheduler();

        // Prevents other threads from registering the ModelTracker at the same time
        // Create model tracker and load all the persisted models
        synchronized(SolrCoreLoadListener.class)
        {
            createModelTracker(core.getName(),
                    trackerRegistry,
                    coreProperties,
                    coreContainer.getSolrHome(),
                    repositoryClient,
                    informationServer,
                    scheduler);
        }

        /*
         * The shutdown hook needs to be registered regardless we are slave or masters.
         * This because if we are master all trackers will be scheduled, if we are slave the node state publisher
         * will be scheduled.
         *
         * As consequence of that, regardless the node role, we will always have something to shutdown in the tracker
         * registry.
         */
        final List<Tracker> trackers = new ArrayList<>();
        core.addCloseHook(new CloseHook()
        {
            @Override
            public void preClose(SolrCore core)
            {
                LOGGER.info("Solr Core instance {} with name {} is going to be closed. Tracking Subsystem shutdown callback procedure has been started.", core.hashCode(), core.getName());

                // IMPORTANT: the closure needs to be created with the trackers created in this method
                shutdownTrackers(core, trackers, scheduler, false);
            }

            @Override
            public void postClose(SolrCore core)
            {
                LOGGER.info("Solr Core instance {} with name {} has been closed. Tracking Subsystem shutdown callback procedure has been completed.", core.hashCode(), core.getName());
            }
        });

        boolean trackersHaveBeenEnabled = Boolean.parseBoolean(coreProperties.getProperty("enable.alfresco.tracking", "true"));
        boolean owningCoreIsSlave = isSlaveModeEnabledFor(core);

        if (trackerRegistry.hasTrackersForCore(core.getName()))
        {
            LOGGER.info("Trackers (it could be only the node state publisher in case this node is a slave) for " + core.getName() + " are already registered, shutting them down.");
            Collection<Tracker> alreadyRegisteredTrackers = trackerRegistry.getTrackersForCore(core.getName());
            trackerRegistry.removeTrackersForCore(core.getName());

            shutdownTrackers(core, alreadyRegisteredTrackers, scheduler, core.isReloaded());
            admin.getInformationServers().remove(core.getName());
        }

        // Re-put the information server in the map because a Core reload (see above) could have removed the reference.
        admin.getInformationServers().put(core.getName(), informationServer);

        // Guard conditions: if trackers must be disabled then immediately return, we've done here.
        // Case #1: trackers have been explicitly disabled.
        if (!trackersHaveBeenEnabled)
        {
            LOGGER.info("SearchServices Core Trackers have been explicitly disabled on core \"{}\" through \"enable.alfresco.tracking\" configuration property.", core.getName());

            NodeStatePublisher statePublisher = new NodeStatePublisher(false, coreProperties, repositoryClient, core.getName(), informationServer);
            trackerRegistry.register(core.getName(), statePublisher);
            scheduler.schedule(statePublisher, core.getName(), coreProperties);
            trackers.add(statePublisher);

            LOGGER.info("SearchServices Slave Node Provider have been created and scheduled for core \"{}\".", core.getName());

            return;
        }

        // Case #2: we are on a slave node.
        if (owningCoreIsSlave)
        {
            LOGGER.info("SearchServices Core Trackers have been disabled on core \"{}\" because it is a slave core.", core.getName());

            NodeStatePublisher statePublisher = new NodeStatePublisher(false, coreProperties, repositoryClient, core.getName(), informationServer);
            trackerRegistry.register(core.getName(), statePublisher);
            scheduler.schedule(statePublisher, core.getName(), coreProperties);
            trackers.add(statePublisher);

            LOGGER.info("SearchServices Slave Node Provider have been created and scheduled for Core instance {} with name {}.", core.hashCode(), core.getName());

            return;
        }

        LOGGER.info("SearchServices Tracking Subsystem starts on Solr Core instance {} with name {}", core.hashCode(), core.getName());

        trackers.addAll(createAndScheduleCoreTrackers(core, trackerRegistry, coreProperties, scheduler, repositoryClient, informationServer));

        CommitTracker commitTracker = new CommitTracker(coreProperties, repositoryClient, core.getName(), informationServer, trackers);
        trackerRegistry.register(core.getName(), commitTracker);
        scheduler.schedule(commitTracker, core.getName(), coreProperties);

        LOGGER.info("Tracker {}, instance {}, belonging to Core {}, instance {} has been registered and scheduled.",
                commitTracker.getClass().getSimpleName(),
                commitTracker.hashCode(),
                core.getName(),
                core.hashCode());

        //Add the commitTracker to the list of scheduled trackers that can be shutdown
        trackers.add(commitTracker);
    }

    List<Tracker> createAndScheduleCoreTrackers(SolrCore core,
                                                TrackerRegistry trackerRegistry,
                                                Properties props,
                                                SolrTrackerScheduler scheduler,
                                                SOLRAPIClient repositoryClient,
                                                SolrInformationServer srv)
    {
        AclTracker aclTracker =
                registerAndSchedule(
                        new AclTracker(props, repositoryClient, core.getName(), srv),
                        core,
                        props,
                        trackerRegistry,
                        scheduler);

        ContentTracker contentTracker =
                registerAndSchedule(
                    new ContentTracker(props, repositoryClient, core.getName(), srv),
                        core,
                        props,
                        trackerRegistry,
                        scheduler);

        MetadataTracker metadataTracker =
                registerAndSchedule(
                    new MetadataTracker(props, repositoryClient, core.getName(), srv, true),
                        core,
                        props,
                        trackerRegistry,
                        scheduler);

        NodeStatePublisher coreStateTracker =
                registerAndSchedule(
                        new NodeStatePublisher(true, props, repositoryClient, core.getName(), srv),
                        core,
                        props,
                        trackerRegistry,
                        scheduler
                );

        List<Tracker> trackers = new ArrayList<>();

        String cascadeTrackerEnabledProp = ofNullable((String) props.get(CASCADE_TRACKER_ENABLED)).orElse("true");
        if (Boolean.valueOf(cascadeTrackerEnabledProp))
        {
            CascadeTracker cascadeTracker =
                    registerAndSchedule(
                            new CascadeTracker(props, repositoryClient, core.getName(), srv),
                            core,
                            props,
                            trackerRegistry,
                            scheduler);
            trackers.add(cascadeTracker);
        }

        //The CommitTracker will acquire these locks in order
        //The ContentTracker will likely have the longest runs so put it first to ensure the MetadataTracker is not paused while
        //waiting for the ContentTracker to release it's lock.
        //The aclTracker will likely have the shortest runs so put it last.
        trackers.addAll(asList(contentTracker, metadataTracker, aclTracker, coreStateTracker));
        return trackers;
    }

    /**
     * Accepts a {@link Tracker} instance, registers and schedules it.
     *
     * @param tracker the tracker that will be scheduled and registered.
     * @param core the owning core.
     * @param properties configuration properties.
     * @param registry the tracker registry instance.
     * @param scheduler the tracker schedule instance.
     * @param <T> the tracker instance.
     * @return the registered and scheduled tracker instance.
     */
    private <T extends Tracker> T registerAndSchedule(T tracker, SolrCore core, Properties properties, TrackerRegistry registry, SolrTrackerScheduler scheduler)
    {
        registry.register(core.getName(), tracker);
        scheduler.schedule(tracker, core.getName(), properties);

        LOGGER.info("Tracker {}, instance {}, belonging to Core {}, instance {} has been registered and scheduled.",
                tracker.getClass().getSimpleName(),
                tracker.hashCode(),
                core.getName(),
                core.hashCode());

        return tracker;
    }

    private void createModelTracker(String coreName,
                                    TrackerRegistry trackerRegistry,
                                    Properties props,
                                    String solrHome,
                                    SOLRAPIClient repositoryClient,
                                    SolrInformationServer srv,
                                    SolrTrackerScheduler scheduler)
    {
        ModelTracker mTracker = trackerRegistry.getModelTracker();
        if (mTracker == null)
        {
            LOGGER.debug("Creating a new Model Tracker instance.");
            mTracker = new ModelTracker(solrHome, props, repositoryClient, coreName, srv);
            trackerRegistry.setModelTracker(mTracker);

            LOGGER.info("Model Tracker: ensuring first model sync.");
            mTracker.ensureFirstModelSync();

            scheduler.schedule(mTracker, coreName, props);

            LOGGER.info("Model Tracker has been correctly initialised, registered and scheduled.");
        }
    }

    /**
     * Shuts down the trackers for a core.
     *
     * The trackers are only deleted from the scheduler if they are the exact same instance of the Tracker class
     * passed into this method.
     * For example, you could have 2 cores of the same name and have the trackers registered with the scheduler BUT
     * the scheduler only keys by core name. The Collection<Tracker>s passed into this method are only removed
     * from the scheduler if the instances are == (equal). See scheduler.deleteJobForTrackerInstance()
     *
     * Trackers are not removed from the registry because the registry only keys by core name; its possible to
     * have multiple cores of the same name running.  Left over trackers in the registry are cleaned up by the CoreContainer
     * shutdown, that happens in the the AlfrescoCoreAdminHandler.shutdown().
     *
     * The coreHasBeenReloaded flag is used just for logging out meaningful messages about the owning core instance.
     * If we are in a RELOAD scenario (coreHasBeenReloaded = true) we no longer have the reference of the closed core
     * so we print only its name. Instead in case we are here because a core has been closed, we can print out the core
     * reference in order to add meaningful information in the log.
     *
     * @param core The owning core name.
     * @param coreTrackers A collection of trackers
     * @param scheduler The scheduler
     * @param coreHasBeenReloaded a flag indicating if we are on a Core RELOAD scenario.
     */
    void shutdownTrackers(SolrCore core, Collection<Tracker> coreTrackers, SolrTrackerScheduler scheduler, boolean coreHasBeenReloaded)
    {
        coreTrackers.forEach(tracker -> shutdownTracker(core, tracker, scheduler, coreHasBeenReloaded));
    }

    /**
     * Shutdown procedure for a single tracker.
     * The coreHasBeenReloaded flag is used just for logging out meaningful messages about the owning core instance.
     * If we are in a RELOAD scenario (coreHasBeenReloaded = true) we no longer have the reference of the closed core
     * so we print only its name. Instead in case we are here because a core has been closed, we can print out the core
     * reference in order to add meaningful information in the log.
     *
     * @param core the owning {@link SolrCore}
     * @param tracker the {@link Tracker} instance we want to stop.
     * @param scheduler the scheduler.
     * @param coreHasBeenReloaded a flag indicating if we are on a Core RELOAD scenario.
     */
    private void shutdownTracker(SolrCore core, Tracker tracker, SolrTrackerScheduler scheduler, boolean coreHasBeenReloaded)
    {
        // In case of reload the input core is not the owner: the owner is instead the previous (closed) core and we don't have its reference here.
        String coreReference = core.getName() + (coreHasBeenReloaded ? "" : ", instance " + core.hashCode());

        if (tracker.isAlreadyInShutDownMode())
        {
            LOGGER.info("Tracker {}, instance {} belonging to core {}, is already in shutdown mode.",
                    tracker.getClass().getSimpleName(),
                    tracker.hashCode(),
                    coreReference);
            return;
        }

        LOGGER.info("Tracker {}, instance {} belonging to core {} shutdown procedure initiated.",
                tracker.getClass().getSimpleName(),
                tracker.hashCode(),
                coreReference);
        try
        {
            tracker.setShutdown(true);
            if (!scheduler.isShutdown())
            {
                scheduler.deleteJobForTrackerInstance(core.getName(), tracker);
            }

            tracker.shutdown();

            LOGGER.info("Tracker {}, instance {}, belonging to core {} shutdown procedure correctly terminated.",
                    tracker.getClass().getSimpleName(),
                    tracker.hashCode(),
                    coreReference);
        }
        catch (Exception exception)
        {
            LOGGER.error("Tracker {}, instance {} belonging to core {}, shutdown procedure failed. " +
                            "See the stacktrace below for further details.",
                    tracker.getClass().getSimpleName(),
                    tracker.hashCode(),
                    coreReference,
                    exception);
        }
    }

    /**
     * Checks if the configuration declares this node as a slave.
     *
     * @param core the hosting {@link SolrCore} instance.
     * @return true if the content store must be set in read only mode, false otherwise.
     */
    @SuppressWarnings("rawtypes")
    boolean isSlaveModeEnabledFor(SolrCore core)
    {
        Predicate<PluginInfo> onlyReplicationHandler =
                plugin -> "/replication".equals(plugin.name)
                        || plugin.className.endsWith(ReplicationHandler.class.getSimpleName());

        Function<NamedList, Boolean> isSlaveModeEnabled =
                params ->  ofNullable(params)
                        .map(configuration -> {
                            Object enable = configuration.get("enable");
                            return enable == null ||
                                    (enable instanceof String ? StrUtils.parseBool((String)enable) : Boolean.TRUE.equals(enable));})
                        .orElse(false);

        return core.getSolrConfig().getPluginInfos(SolrRequestHandler.class.getName())
                .stream()
                .filter(PluginInfo::isEnabled)
                .filter(onlyReplicationHandler)
                .findFirst()
                .map(plugin -> plugin.initArgs)
                .map(params -> params.get("slave"))
                .map(NamedList.class::cast)
                .map(isSlaveModeEnabled)
                .orElse(false);
    }
}