package com.atlassian.stash.internal.server;

import com.atlassian.cache.*;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.util.profiling.UtilTimerStack;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

/**
 * Default implementation that configures atlassian profiling.
 *
 * @since 1.2.2
 */
@Component("profilingService")
public class DefaultProfilingService implements ProfilingService {

    private static final String KEY_ENABLED = "enabled";
    private static final String MAP_PROFILING = "profiling";

    private final ApplicationPropertiesService applicationPropertiesService;
    private final ProfilingStateListener listener;
    private final Cache<String, Integer> profilingState; // for cluster awareness, tracks the state of UtilTimerStack

    @Autowired
    public DefaultProfilingService(final ApplicationPropertiesService applicationPropertiesService, CacheFactory cacheFactory) {
        this.applicationPropertiesService = applicationPropertiesService;

        listener = new ProfilingStateListener();
        profilingState = cacheFactory.getCache(MAP_PROFILING, null, new CacheSettingsBuilder().remote().build());
        profilingState.addListener(listener, true);
    }

    /**
     * Disable profiling.
     * Note that in a Stash cluster this is propagated to all cluster nodes.
     */
    @Override
    public void disable() {
        profilingState.remove(KEY_ENABLED); // the listener calls doDisable (on all nodes)
    }

    /**
     * Enable profiling with no threshold.
     * Note that in a Stash cluster this is propagated to all cluster nodes.
     */
    @Override
    public void enable() {
        enable(0);
    }

    /**
     * Enable profiling with a specific threshold.
     * Note that in a Stash cluster this is propagated to all cluster nodes.
     *
     * @param profilingThreshold the threshold in milliseconds
     */
    @Override
    public void enable(int profilingThreshold) {
        profilingState.put(KEY_ENABLED, profilingThreshold); // the listener calls doEnable (on all nodes)
    }

    @Override
    public boolean isEnabled() {
        return profilingState.containsKey(KEY_ENABLED);
    }

    private void doDisable() {
        UtilTimerStack.setActive(false);
    }

    private void doEnable(Integer profilingThreshold) {
        UtilTimerStack.setActive(true);
        UtilTimerStack.setMinTotalTime(profilingThreshold);
        if (System.getProperty(UtilTimerStack.MIN_TIME) == null) {
            // exclude any 0ms stack frames, unless the threshold has been set using a system property
            UtilTimerStack.setMinTime(1);
        }
    }

    /**
     * Ensures that profiling is enabled on startup if so configured.
     */
    @PostConstruct
    protected void onStartup() {
        Integer threshold = profilingState.get(KEY_ENABLED);
        if (threshold != null) {
            doEnable(threshold);
        } else if (applicationPropertiesService.isProfilingEnabled()) {
            enable(0);
        }
    }

    @PreDestroy
    private void onShutdown() {
        profilingState.removeListener(listener);
    }

    private class ProfilingStateListener extends CacheEntryAdapter<String, Integer> {
        @Override
        public void onAdd(@Nonnull CacheEntryEvent<String, Integer> event) {
            doEnable(event.getValue());
        }

        @Override
        public void onEvict(@Nonnull CacheEntryEvent<String, Integer> event) {
            doDisable();
        }

        @Override
        public void onRemove(@Nonnull CacheEntryEvent<String, Integer> event) {
            doDisable();
        }

        @Override
        public void onUpdate(@Nonnull CacheEntryEvent<String, Integer> event) {
            doEnable(event.getValue());
        }
    }
}
