/*
 * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.server.internal.monitoring;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.Configuration;

import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.internal.util.PropertiesHelper;
import org.glassfish.jersey.server.BackgroundSchedulerLiteral;
import org.glassfish.jersey.server.ExtendedResourceContext;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.internal.LocalizationMessages;
import org.glassfish.jersey.server.internal.monitoring.MonitoringEventListener.RequestStats;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.server.monitoring.MonitoringStatisticsListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;

/**
 * Process events of application and request processing into
 * {@link org.glassfish.jersey.server.monitoring.MonitoringStatistics monitoring statistics}.
 * The {@code MonitoringStatisticsProcessor} starts a new thread which process events in regular intervals
 * and for each new monitoring statistics it calls registered
 * {@link MonitoringStatisticsListener monitoring statistics event listeners}.
 *
 * @author Miroslav Fuksa
 */
final class MonitoringStatisticsProcessor {

    private static final Logger LOGGER = Logger.getLogger(MonitoringStatisticsProcessor.class.getName());

    private static final int DEFAULT_INTERVAL = 500;
    private static final int SHUTDOWN_TIMEOUT = 10;

    private final MonitoringEventListener monitoringEventListener;
    private final MonitoringStatisticsImpl.Builder statisticsBuilder;
    private final List<MonitoringStatisticsListener> statisticsCallbackList;
    private final ScheduledExecutorService scheduler;

    private final int interval;

    /**
     * Creates a new instance of processor.
     * @param injectionManager injection manager.
     * @param monitoringEventListener Monitoring event listener.
     */
    MonitoringStatisticsProcessor(
            final InjectionManager injectionManager, final MonitoringEventListener monitoringEventListener) {
        this.monitoringEventListener = monitoringEventListener;
        final ResourceModel resourceModel = injectionManager.getInstance(ExtendedResourceContext.class).getResourceModel();
        this.statisticsBuilder = new MonitoringStatisticsImpl.Builder(resourceModel);
        this.statisticsCallbackList = injectionManager.getAllInstances(MonitoringStatisticsListener.class);
        this.scheduler =
                injectionManager.getInstance(ScheduledExecutorService.class, BackgroundSchedulerLiteral.INSTANCE);
        this.interval = PropertiesHelper.getValue(injectionManager.getInstance(Configuration.class).getProperties(),
                                                  ServerProperties.MONITORING_STATISTICS_REFRESH_INTERVAL, DEFAULT_INTERVAL,
                                                  Collections.<String, String>emptyMap());
    }

    /**
     * Start the thread that will process the events
     * into {@link org.glassfish.jersey.server.monitoring.MonitoringStatistics}.
     */
    public void startMonitoringWorker() {
        scheduler.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    processRequestItems();
                    processResponseCodeEvents();
                    processExceptionMapperEvents();
                } catch (final Throwable t) {
                    monitoringEventListener.processorFailed();
                    LOGGER.log(Level.SEVERE, LocalizationMessages.ERROR_MONITORING_STATISTICS_GENERATION(), t);
                    // rethrowing exception stops further task execution
                    throw new ProcessingException(LocalizationMessages.ERROR_MONITORING_STATISTICS_GENERATION(), t);
                }

                final MonitoringStatisticsImpl immutableStats = statisticsBuilder.build();

                final Iterator<MonitoringStatisticsListener> iterator = statisticsCallbackList.iterator();
                while (iterator.hasNext() && !Thread.currentThread().isInterrupted()) {
                    final MonitoringStatisticsListener listener = iterator.next();
                    try {
                        listener.onStatistics(immutableStats);
                    } catch (final Throwable t) {
                        LOGGER.log(Level.SEVERE,
                                LocalizationMessages.ERROR_MONITORING_STATISTICS_LISTENER(listener.getClass()), t);

                        iterator.remove();
                    }
                }
            }
        }, 0, interval, TimeUnit.MILLISECONDS);
    }

    private void processExceptionMapperEvents() {
        final Queue<RequestEvent> eventQueue = monitoringEventListener.getExceptionMapperEvents();
        final FloodingLogger floodingLogger = new FloodingLogger(eventQueue);
        RequestEvent event = null;
        while ((event = eventQueue.poll()) != null) {
            floodingLogger.conditionallyLogFlooding();
            final ExceptionMapperStatisticsImpl.Builder mapperStats = statisticsBuilder.getExceptionMapperStatisticsBuilder();

            if (event.getExceptionMapper() != null) {
                mapperStats.addExceptionMapperExecution(event.getExceptionMapper().getClass(), 1);
            }

            mapperStats.addMapping(event.isResponseSuccessfullyMapped(), 1);
        }
    }

    private void processRequestItems() {
        final Queue<MonitoringEventListener.RequestStats> requestQueuedItems = monitoringEventListener.getRequestQueuedItems();
        final FloodingLogger floodingLogger = new FloodingLogger(requestQueuedItems);
        RequestStats event = null;
        while ((event = requestQueuedItems.poll()) != null) {
            floodingLogger.conditionallyLogFlooding();
            final MonitoringEventListener.TimeStats requestStats = event.getRequestStats();
            statisticsBuilder.addRequestExecution(requestStats.getStartTime(), requestStats.getDuration());

            final MonitoringEventListener.MethodStats methodStat = event.getMethodStats();
            if (methodStat != null) {
                final ResourceMethod method = methodStat.getMethod();
                statisticsBuilder.addExecution(event.getRequestUri(), method,
                        methodStat.getStartTime(), methodStat.getDuration(),
                        requestStats.getStartTime(), requestStats.getDuration());
            }
        }
    }

    private void processResponseCodeEvents() {
        final Queue<Integer> responseEvents = monitoringEventListener.getResponseStatuses();
        final FloodingLogger floodingLogger = new FloodingLogger(responseEvents);
        Integer code = null;
        while ((code = responseEvents.poll()) != null) {
            floodingLogger.conditionallyLogFlooding();
            statisticsBuilder.addResponseCode(code);
        }

    }

    /**
     * Stops processing of any further execution of this processor. The internal thread will finish
     * processing of actual events and will be not executed again. The method finishes after the
     * internal thread finish its processing loop.
     *
     * @throws InterruptedException thrown when waiting for the thread to finish the work is interrupted. In this
     * case internal listeners will be still shutdown.
     */
    void shutDown() throws InterruptedException {
        scheduler.shutdown();

        final boolean success = scheduler.awaitTermination(SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
        if (!success) {
            LOGGER.warning(LocalizationMessages.ERROR_MONITORING_SCHEDULER_DESTROY_TIMEOUT());
        }
    }

    /**
     * Upon calling of {@link #conditionallyLogFlooding()}, flooding logger conditionally checks for the size of the associated
     * collection and if its size increases a warning about flooding is logged.
     * <p/>
     * The purpose of this flooding logger facility is to warn about disability to decrease the size of given collection which
     * leads to never ending looping while trying to empty that collection in a loop.
     *
     * @author Stepan Vavra
     */
    private static class FloodingLogger {

        /** The frequency of logging a warning about the request queue being flooded. */
        private static final int FLOODING_WARNING_LOG_INTERVAL_MILLIS = 5_000;

        private final Collection<?> collection;
        private final long startTime = System.nanoTime();

        private int i = 0;
        private int lastSize;

        /**
         * Constructs Flooding Logger and associate it with given collection.
         *
         * @param collection The collection to associate this flooding logger with.
         */
        public FloodingLogger(final Collection<?> collection) {
            this.collection = collection;
            this.lastSize = collection.size();
        }

        /**
         * With a frequency of {@link #FLOODING_WARNING_LOG_INTERVAL_MILLIS}, a warning about flooding is logged if the size of
         * the associated collection is increasing.
         */
        public void conditionallyLogFlooding() {
            // this condition prevents the log warning from being logged more frequently than
            // 'FLOODING_WARNING_LOG_INTERVAL_MILLIS' - counted from the initialization of this class
            if ((System.nanoTime() - startTime) / TimeUnit.NANOSECONDS.convert(FLOODING_WARNING_LOG_INTERVAL_MILLIS,
                    TimeUnit.MILLISECONDS) <= i) {
                return;
            }

            if (collection.size() > lastSize) {
                LOGGER.warning(LocalizationMessages.ERROR_MONITORING_QUEUE_FLOODED(collection.size()));
            }
            i++;
            lastSize = collection.size();
        }
    }
}
