package com.atlassian.stash.internal.mail;

import com.atlassian.botocss.BotocssStyles;
import com.atlassian.event.api.EventListener;
import com.atlassian.plugin.Plugin;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.elements.ResourceDescriptor;
import com.atlassian.plugin.event.events.PluginModuleDisabledEvent;
import com.atlassian.plugin.event.events.PluginModuleEnabledEvent;
import com.atlassian.plugin.util.PluginUtils;
import com.atlassian.plugin.webresource.WebResourceModuleDescriptor;
import com.atlassian.plugin.webresource.impl.Globals;
import com.atlassian.plugin.webresource.impl.RequestCache;
import com.atlassian.plugin.webresource.impl.UrlBuildingStrategy;
import com.atlassian.plugin.webresource.legacy.ModuleDescriptorStub;
import com.atlassian.plugin.webresource.legacy.ResourceDependencyResolver;
import com.atlassian.stash.internal.annotation.Profiled;
import com.atlassian.stash.internal.plugin.SimpleServletContextFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Iterators;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;

import static com.atlassian.bitbucket.util.Chainable.chain;
import static com.atlassian.bitbucket.util.MoreStreams.streamIterable;
import static java.util.stream.Collectors.toList;

@Component("botocssStylesResolver")
@Profiled
public class CachingBotocssStylesResolver implements BotocssStylesResolver {

    private static final Logger log = LoggerFactory.getLogger(BotocssStylesResolver.class);

    private final Globals webResourceGlobals;
    private final LoadingCache<String, BotocssStyles> cache;
    private final ResourceDependencyResolver resourceDependencyResolver;
    private final PluginAccessor pluginAccessor;
    private final SimpleServletContextFactory servletContextFactory;

    @Autowired
    public CachingBotocssStylesResolver(Globals webResourceGlobals,
                                        PluginAccessor pluginAccessor,
                                        ResourceDependencyResolver resourceDependencyResolver,
                                        SimpleServletContextFactory servletContextFactory) {
        // Previously this was managed by a Spring cache, but in a clustered environment
        // plugin state is node-local and BotocssStyles is not serializable so it isn't possible
        // to put it in a cluster wide cache
        this.cache = CacheBuilder.newBuilder()
                .maximumSize(20L) // Should this be configurable?
                .build(new CacheLoader<String, BotocssStyles>() {

                    @Nonnull
                    @Override
                    public BotocssStyles load(@Nonnull String moduleKey) throws Exception {
                        return loadBotocssStylesForModuleKey(moduleKey);
                    }
                });

        this.webResourceGlobals = webResourceGlobals;
        this.pluginAccessor = pluginAccessor;
        this.resourceDependencyResolver = resourceDependencyResolver;
        this.servletContextFactory = servletContextFactory;
    }

    private void clearCache() {
        // Because we don't keep track of the dependencies of a module key -> other module keys,
        //our only option is to clear out the whole cache defensively.
        cache.invalidateAll();
    }

    public BotocssStyles getBotocssStylesForModuleKey(String moduleKey) {
        return cache.getUnchecked(moduleKey);
    }

    /**
     * Clear the cache on {@link PluginModuleEnabledEvent}
     */
    @EventListener
    public void onPluginModuleEnabled(PluginModuleEnabledEvent event) {
        clearCache();
    }

    /**
     * Clear the cache on {@link PluginModuleDisabledEvent}
     */
    @EventListener
    public void onPluginModuleDisabled(PluginModuleDisabledEvent event) {
        clearCache();
    }

    /**
     * Get a BotocssStyles object for the given plugin module key
     * <p>
     * This will contain a series of styles in CSS resource descriptors, and maybe dependencies that contain more.
     *
     * @param moduleKey Complete module key to the styles required for this email
     * @return BotocssStyles object for these styles.
     */
    @Nonnull
    private BotocssStyles loadBotocssStylesForModuleKey(String moduleKey) {
        Iterable<InputStream> css = getStylesForModuleKey(moduleKey);

        // TODO Fix BotoCSS so it doesn't have this old school varargs API

        // We pre-merge the stylesheet input streams into a single String.
        // This is as BotoCSS creates a StyleSheet object per String passed, and we really don't want that overhead
        try {
            return BotocssStyles.parse(IOUtils.toString(new SequenceInputStream(Iterators.asEnumeration(css.iterator()))));
        } catch (IOException e) {
            log.debug("IOException when attempting to convert CSS to String", e);
            return BotocssStyles.parse();
        }
    }

    /**
     * Get styles for module key.
     *
     * @param moduleKey Module key we are interested in
     * @return Iterator of InputStreams for the styles defined in the module key dependency tree
     */
    private Iterable<InputStream> getStylesForModuleKey(String moduleKey) {
        Iterable<ModuleDescriptorStub> dependencies = resourceDependencyResolver.getDependencies(
                new RequestCache(webResourceGlobals),
                UrlBuildingStrategy.normal(),
                moduleKey,
                true,
                false
        );
        return streamIterable(dependencies)
                .map(stub -> pluginAccessor.getEnabledPluginModule(stub.getCompleteKey()))
                .filter(maybeWebResourceModule -> maybeWebResourceModule instanceof WebResourceModuleDescriptor)
                .flatMap(module -> streamIterable(getStylesForModuleDescriptor((WebResourceModuleDescriptor) module)))
                .collect(toList());
    }

    private Iterable<InputStream> getStylesForModuleDescriptor(WebResourceModuleDescriptor moduleDescriptor) {
        Predicate<ResourceDescriptor> cssResourcePredicate = descriptor -> descriptor != null && descriptor.getLocation().endsWith(".css");

        Function<ResourceDescriptor, InputStream> inputStreamForResourceDescriptor = rd -> {
            // NB This does not use the minification strategy. This would require yet more code duplication.
            // We use this method as it gets us an input stream directly
            // As opposed to having to create a DownloadableClasspathResource and then converting OutputStream to InputStream.
            return getResourceStreamViaMinificationStrategy(moduleDescriptor.getPlugin(), rd);
        };

        return chain(moduleDescriptor.getResourceDescriptors())
                .filter(cssResourcePredicate)
                .transform(inputStreamForResourceDescriptor)
                .filter(Objects::nonNull);
    }

    // Here down is largely duplicate logic from the web resource system. It really isn't designed for consumption in this manner.

    private InputStream getResourceStreamViaMinificationStrategy(Plugin plugin, ResourceDescriptor resourceDescriptor) {
        String resourceLocation = resourceDescriptor.getLocation();

        InputStream resourceAsStream = getResourceAsStream(plugin, resourceDescriptor, minifiedLocationIfAvailable(resourceLocation));
        if (resourceAsStream != null) {
            return resourceAsStream;
        }
        return getResourceAsStream(plugin, resourceDescriptor, resourceLocation);
    }

    /**
     * Get the given resource location within a ResourceDescriptor within a Plugin, get the resource as an InputStream.
     * <p>
     * This is necessary as plugin.getResourceAsStream breaks for StaticPlugin (like the base web app)
     */
    private InputStream getResourceAsStream(Plugin plugin, ResourceDescriptor resourceDescriptor, String resourceLocation) {
        String sourceParam = resourceDescriptor.getParameter("source");
        if ("webContextStatic".equalsIgnoreCase(sourceParam)) {
            return servletContextFactory.getServletContext().getResourceAsStream(resourceLocation);
        }
        return plugin.getResourceAsStream(resourceLocation);
    }

    private String minifiedLocationIfAvailable(String resourceLocation) {
        if (Boolean.getBoolean(PluginUtils.ATLASSIAN_DEV_MODE) || resourceLocation.endsWith("-min.css") ||
                resourceLocation.endsWith(".min.css")) {
            return resourceLocation;
        }
        return getMinifiedLocation(resourceLocation);
    }

    private String getMinifiedLocation(String location) {
        int lastDot = location.lastIndexOf(".");
        // this can never but -1 since the method call is protected by a call to
        // resouceDescriptor.getLocation.endsWith(".css")
        return location.substring(0, lastDot) + "-min" + location.substring(lastDot);
    }
}
