/*
 * 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 legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015-2016 ForgeRock AS.
 */
package org.forgerock.openam.audit;

import static com.sun.identity.shared.datastruct.CollectionHelper.getMapAttr;

import com.sun.identity.shared.debug.Debug;
import org.forgerock.audit.AuditException;
import org.forgerock.audit.AuditServiceBuilder;
import org.forgerock.audit.events.EventTopicsMetaData;
import org.forgerock.audit.events.handlers.AuditEventHandler;
import org.forgerock.guice.core.InjectorHolder;
import org.forgerock.json.resource.ServiceUnavailableException;
import org.forgerock.openam.audit.configuration.AMAuditServiceConfiguration;
import org.forgerock.openam.audit.configuration.AuditEventHandlerConfiguration;
import org.forgerock.openam.audit.configuration.AuditServiceConfigurationListener;
import org.forgerock.openam.audit.configuration.AuditServiceConfigurationProvider;
import org.forgerock.util.thread.listener.ShutdownListener;
import org.forgerock.util.thread.listener.ShutdownManager;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Responsible for creating the AuditService on configuration change.
 *
 * @since 13.0.0
 */
@Singleton
public class AuditServiceProviderImpl implements AuditServiceProvider {

    private final Debug debug = Debug.getInstance("amAudit");

    private final AuditServiceConfigurationProvider configProvider;
    private final AMAuditService defaultAuditService;
    private final Map<String, AMAuditService> auditServices = new ConcurrentHashMap<>();
    private final ShutdownManager shutdownManager;
    private final EventTopicsMetaData eventTopicsMetaData;

    /**
     * Create an instance of AuditServiceProviderImpl.
     *
     * @param configProvider The configProvider responsible for providing audit service configuration.
     * @param shutdownManager The shutdown manager to register the shutdown listener to.
     */
    @Inject
    public AuditServiceProviderImpl(AuditServiceConfigurationProvider configProvider, ShutdownManager shutdownManager) {
        this.configProvider = configProvider;
        this.shutdownManager = shutdownManager;
        this.eventTopicsMetaData = configProvider.getEventTopicsMetaData();
        this.defaultAuditService = createDefaultAuditService();
        registerListeners();
    }

    private DefaultAuditServiceProxy createDefaultAuditService() {
        AMAuditServiceConfiguration configuration = new AMAuditServiceConfiguration(false);
        configuration.setAvailableAuditEventHandlers(Collections.<String>emptyList());
        AuditServiceBuilder builder = AuditServiceBuilder.newAuditService()
                .withEventTopicsMetaData(eventTopicsMetaData)
                .withConfiguration(configuration);

        DefaultAuditServiceProxy auditServiceProxy =  new DefaultAuditServiceProxy(builder.build(), configuration);
        try {
            auditServiceProxy.startup();
        } catch (ServiceUnavailableException e) {
            debug.error("Default Audit Service configuration failed.", e);
        }
        return auditServiceProxy;
    }

    private void registerListeners() {

        configProvider.addConfigurationListener(new AuditServiceConfigurationListener() {
            @Override
            public void globalConfigurationChanged() {
                refreshDefaultAuditService();
            }

            @Override
            public void realmConfigurationChanged(String realm) {
                refreshRealmAuditService(realm);
            }

            @Override
            public void realmConfigurationRemoved(String realm) {
                removeRealmAuditService(realm);
            }
        });


        shutdownManager.addShutdownListener(new ShutdownListener() {
            @Override
            public void shutdown() {
                closeAuditServices();
            }
        });
    }

    @Override
    public AMAuditService getAuditService(String realm) {
        AMAuditService auditService = auditServices.get(realm.toLowerCase());
        if (auditService == null) {
            return defaultAuditService;
        } else {
            return auditService;
        }
    }

    @Override
    public AMAuditService getDefaultAuditService() {
        return defaultAuditService;
    }

    private void refreshDefaultAuditService() {
        AMAuditServiceConfiguration configuration = configProvider.getDefaultConfiguration();
        AuditServiceBuilder builder = AuditServiceBuilder.newAuditService()
                .withEventTopicsMetaData(eventTopicsMetaData)
                .withConfiguration(configuration);
        if (configuration.isAuditEnabled()) {
            configureEventHandlers(builder, configProvider.getDefaultEventHandlerConfigurations());
        }

        try {
            defaultAuditService.setDelegate(builder.build(), configuration);
        } catch (ServiceUnavailableException e) {
            debug.error("Default Audit Service configuration failed.", e);
        }
    }

    private void refreshRealmAuditService(String realm) {
        AMAuditServiceConfiguration configuration = configProvider.getRealmConfiguration(realm);
        AuditServiceBuilder builder = AuditServiceBuilder.newAuditService()
                .withEventTopicsMetaData(eventTopicsMetaData)
                .withConfiguration(configuration);
        if (configuration.isAuditEnabled()) {
            configureEventHandlers(builder, configProvider.getRealmEventHandlerConfigurations(realm));
        }

        AMAuditService auditService = auditServices.get(realm);
        try {
            if (auditService == null) {
                auditService = new RealmAuditServiceProxy(builder.build(), defaultAuditService, configuration);
                auditService.startup();
                auditServices.put(realm, auditService);
            } else {
                auditService.setDelegate(builder.build(), configuration);
            }
        } catch (ServiceUnavailableException e) {
            debug.error("New Audit Service configuration for realm {} failed.", e, realm);
            auditServices.remove(realm); // remove it so that we can fall back to the default service
        }
    }

    private void removeRealmAuditService(String realm) {
        AMAuditService auditService = auditServices.remove(realm);
        if (auditService != null) {
            auditService.shutdown();
        }
    }

    private void closeAuditServices() {
        for (final AMAuditService auditService : auditServices.values()) {
            auditService.shutdown();
        }
        defaultAuditService.shutdown();
    }

    private void configureEventHandlers(AuditServiceBuilder builder, Set<AuditEventHandlerConfiguration> configs) {
        for (AuditEventHandlerConfiguration config : configs) {
            AuditEventHandler eventHandler = createEventHandler(config);
            if (eventHandler != null) {
                try {
                    builder.withAuditEventHandler(eventHandler);
                } catch (AuditException e) {
                    debug.error("Unable to configure audit event handler called {}", config.getHandlerName(), e);
                }
            }
        }
    }

    private AuditEventHandler createEventHandler(AuditEventHandlerConfiguration config) {
        String className = getMapAttr(config.getAttributes(), "handlerFactory");
        try {
            Class<? extends AuditEventHandlerFactory> handlerFactoryClass =
                    Class.forName(className).asSubclass(AuditEventHandlerFactory.class);
            return InjectorHolder.getInstance(handlerFactoryClass).create(config);
        } catch (AuditException | ClassNotFoundException | RuntimeException e) {
            debug.error("Unable to create audit event handler called {}", config.getHandlerName(), e);
        }

        return null;
    }

}
