package com.atlassian.stash.internal.plugin;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.util.RequestLocalCache;
import com.atlassian.bitbucket.util.UrlUtils;
import com.atlassian.cache.Supplier;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.event.PluginEventManager;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.plugin.webresource.UrlMode;
import com.atlassian.plugin.webresource.WebResourceBatchingStateCounter;
import com.atlassian.plugin.webresource.WebResourceIntegration;
import com.atlassian.plugin.webresource.cdn.CDNStrategy;
import com.atlassian.stash.internal.BuildInfo;
import com.atlassian.stash.internal.i18n.LocaleHolder;
import com.atlassian.stash.internal.server.InternalApplicationPropertiesService;
import com.atlassian.util.concurrent.LazyReference;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;

@AvailableToPlugins(WebResourceIntegration.class)
@Component("webResourceIntegration")
public class WebResourceIntegrationImpl implements WebResourceIntegration {

    private static final String PROP_CDN_BASE_URL = "cdn.base.url";
    private static final String TMP_DIR_WEB_RESOURCES = "webresources";

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

    private final BuildInfo buildInfo;
    private final LazyReference<CDNStrategy> cdnStrategy;
    private final LocaleHolder localeHolder;
    private final NavBuilder navBuilder;
    private final PluginAccessor pluginAccessor;
    private final PluginEventManager pluginEventManager;
    private final InternalApplicationPropertiesService propertiesService;
    private final RequestLocalCache<String, Object> requestCache;
    private final WebResourceBatchingStateCounter stateCounter;
    private final I18nService i18nService;
    private final I18nStateHasher i18nStateHasher;

    @Autowired
    public WebResourceIntegrationImpl(BuildInfo buildInfo,
                                      I18nService i18nService,
                                      LocaleHolder localeHolder, NavBuilder navBuilder,
                                      PluginAccessor pluginAccessor, PluginEventManager pluginEventManager,
                                      InternalApplicationPropertiesService propertiesService,
                                      RequestContext requestContext, WebResourceBatchingStateCounter stateCounter,
                                      I18nStateHasher i18nStateHasher) {
        this.buildInfo = buildInfo;
        this.localeHolder = localeHolder;
        this.navBuilder = navBuilder;
        this.pluginAccessor = pluginAccessor;
        this.propertiesService = propertiesService;
        this.stateCounter = stateCounter;
        this.i18nService = i18nService;
        this.pluginEventManager = pluginEventManager;
        this.i18nStateHasher = i18nStateHasher;

        requestCache = new RequestLocalCache<>(requestContext);
        cdnStrategy = new LazyReference<CDNStrategy>() {

            @Override
            protected CDNStrategy create() {
                String baseUrl = propertiesService.getProperty(PROP_CDN_BASE_URL);
                if (baseUrl != null) {
                    try {
                        return new BaseUrlCDNStrategy(new URI(baseUrl));
                    } catch (URISyntaxException e) {
                        log.warn("Failed to parse CDN base URL. Disabling CDN support", e);
                    }
                }
                return new DisabledCDNStrategy();
            }
        };
    }

    @Override
    public Set<String> allowedCondition1Keys() {
        throw new UnsupportedOperationException("This function is not supported");
    }

    @Override
    public Set<String> allowedTransform1Keys() {
        throw new UnsupportedOperationException("This function is not supported");
    }

    @Override
    public boolean forbidCondition1AndTransformer1() {
        // Should be enabled in on-demand only.
        return false;
    }

    @Override
    public String getBaseUrl() {
        return getBaseUrl(UrlMode.AUTO);
    }

    @Override
    public String getBaseUrl(UrlMode urlMode) {
        return urlMode == UrlMode.ABSOLUTE ? navBuilder.buildAbsolute() : navBuilder.buildRelative();
    }

    @Override
    public PluginAccessor getPluginAccessor() {
        return pluginAccessor;
    }

    @Override
    public PluginEventManager getPluginEventManager() {
        return pluginEventManager;
    }

    @Override
    public Map<String, Object> getRequestCache() {
        Map<String, Object> cacheMap = requestCache.asMap();
        if (cacheMap == null) {
            return Maps.newHashMap();
        }
        return cacheMap;
    }

    @Override
    public String getSystemBuildNumber() {
        return buildInfo.getDisplayCommitHash();
    }

    @Override
    public String getHostApplicationVersion() {
        return buildInfo.getBuildVersion();
    }

    @Override
    public String getSystemCounter() {
        return Long.toString(stateCounter.getBatchingStateCounter());
    }

    @Override
    public String getSuperBatchVersion() {
        return Long.toString(stateCounter.getBatchingStateCounter());
    }

    @Override
    public String getStaticResourceLocale() {
        return getLocale().toString();
    }

    @Override
    public String getI18nStateHash() {
        return i18nStateHasher.get();
    }

    @Override
    public File getTemporaryDirectory() {
        return new File(propertiesService.getTempDir(), TMP_DIR_WEB_RESOURCES);
    }

    @Override
    public CDNStrategy getCDNStrategy() {
        return cdnStrategy.get();
    }

    @Override
    public Locale getLocale() {
        // .getLocale() can be invoked hundreds of times a request. It is much cheaper to
        // cache the locale for the remainder of the request than it is to look it up each
        // time which may involve cache deserialisation or database costs
        return (Locale) requestCache.get("locale", new Supplier<Object>() {

            @Override
            public Object get() {
                return localeHolder.getLocales().iterator().next();
            }
        });
    }

    @Override
    public Iterable<Locale> getSupportedLocales() {
        // This method is required for pre-baking, as we are not using pre-baking then we can safely
        // return an empty Iterable here
        return Collections.emptyList();
    }

    @Override
    public String getI18nRawText(Locale locale, String key) {
        return MoreObjects.firstNonNull(i18nService.getMessagePattern(locale, key), key);
    }

    @Override
    public String getI18nText(Locale locale, String key) {
        return i18nService.getText(locale, key, null);
    }

    private static class BaseUrlCDNStrategy implements CDNStrategy {

        private final URI baseUrl;

        private BaseUrlCDNStrategy(URI baseUrl) {
            this.baseUrl = UrlUtils.trimTrailingSlashesFromPath(checkNotNull(baseUrl, "baseUrl"));
        }

        @Override
        public boolean supportsCdn() {
            return true;
        }

        @Override
        public String transformRelativeUrl(String url) {
            return baseUrl.toASCIIString() + url;
        }
    }

    private static class DisabledCDNStrategy implements CDNStrategy {

        @Override
        public boolean supportsCdn() {
            return false;
        }

        @Override
        public String transformRelativeUrl(String url) {
            throw new UnsupportedOperationException();
        }
    }
}
