package com.atlassian.bitbucket.internal.integration.jira.web.markup;

import com.atlassian.bitbucket.integration.jira.JiraIssue;
import com.atlassian.bitbucket.internal.integration.jira.InternalJiraIssueService;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.util.MarkupUtils;
import com.atlassian.integration.jira.JiraKeyScanner;
import com.atlassian.integration.jira.JiraService;
import com.atlassian.markup.renderer.MarkupRendererComponent;
import com.atlassian.markup.renderer.RenderContext;
import com.atlassian.markup.renderer.RenderTransform;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Map;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;

import static java.util.Collections.singletonList;

public class JiraMarkupRenderer implements MarkupRendererComponent {

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

    // Exclude keys found in links (urls or sub-strings of the form [foo](bar))
    private static final Pattern EXCLUDE_PATTERN = MarkupUtils.LINKS;
    private static final String JIRA_MARKUP_CLASS = "markup-issues-trigger";
    private static final String LINK_TEMPLATE = "jiraIntegration.templates.trigger.issuesTrigger";
    private static final String LINK_TEMPLATE_RESOURCE = "com.atlassian.integration.jira.jira-integration-plugin:server-side-templates";
    private static final String LINKED = "__linked";

    private final JiraService jiraService;
    private final InternalJiraIssueService jiraIssueService;
    private final JiraKeyScanner jiraKeyScanner;
    private final ThreadLocal<Map<String, String>> requestCache;
    private final RequestContext requestContext;
    private final SoyTemplateRenderer soyTemplateRenderer;

    public JiraMarkupRenderer(JiraService jiraService, InternalJiraIssueService jiraIssueService,
                              JiraKeyScanner jiraKeyScanner, final RequestContext requestContext,
                              SoyTemplateRenderer soyTemplateRenderer) {
        this.jiraService = jiraService;
        this.jiraIssueService = jiraIssueService;
        this.jiraKeyScanner = jiraKeyScanner;
        this.requestContext = requestContext;
        this.soyTemplateRenderer = soyTemplateRenderer;

        this.requestCache = new ThreadLocal<Map<String, String>>() {
            @Override
            protected Map<String, String> initialValue() {
                // register a cleanup listener for the current request
                if (requestContext.isActive()) {
                    requestContext.addCleanupCallback(requestCache::remove);
                }

                return Maps.newHashMap();
            }
        };
    }

    @Nonnull
    @Override
    public RenderTransform process(CharSequence charSequence, @Nonnull RenderContext renderContext) {
        RenderTransform.TransformBuilder builder = RenderTransform.builder();

        try {
            if (isLinked()) {
                Map<String, Collection<MatchResult>> matches = jiraKeyScanner.findMatches(charSequence, EXCLUDE_PATTERN).asMap();
                Map<String, JiraIssue> issuesByKey = Maps.uniqueIndex(jiraIssueService.findIssuesByKey(matches.keySet()), JiraIssue::getKey);
                matches.keySet().retainAll(issuesByKey.keySet());

                matches.forEach((issueKey, results) -> {
                    String link = getOrRenderLink(issuesByKey.get(issueKey));
                    results.forEach(result -> builder.add(result.start(), result.end(), link));
                });
            }

            return builder.build();
        } finally {
            if (!requestContext.isActive()) {
                // not in the context of a request, clear the cache now
                requestCache.remove();
            }
        }
    }

    private String getOrRenderLink(JiraIssue issue) {
        // only render a specific link for issue once per request
        Map<String, String> cache = requestCache.get();
        String key = issue.getKey();
        String link = cache.get(key);
        if (link == null) {
            link = renderLink(issue);
            cache.put(key, link);
        }
        return link;
    }

    private boolean isLinked() {
        // resolving whether there are JIRA application links is not free and a _lot_ of fragments may be rendered in a
        // single request. Check only once per request and store the outcome in the requestCache
        Map<String, String> cache = requestCache.get();
        if (cache.containsKey(LINKED)) {
            return cache.get(LINKED) != null;
        }

        boolean linked = jiraService.isLinked();
        cache.put(LINKED, linked ? "t" : null);
        return linked;
    }

    private String renderLink(JiraIssue issue) {
        try {
            return soyTemplateRenderer.render(LINK_TEMPLATE_RESOURCE, LINK_TEMPLATE,
                    ImmutableMap.of(
                            // This template takes a list of issues to show a sidebar for navigation. In this case,
                            // (i.e. Not from the Commit List JIRA Issue Column), we don't want to show the sidebar,
                            // only the actual linked issue. So we pass it through as a singleton list.
                            "issues", singletonList(issue),
                            "displayedIssue", issue,
                            "className", JIRA_MARKUP_CLASS));
        } catch (SoyException e) {
            log.error("Exception while rendering JIRA issue links", e);
            return issue.getKey();
        }
    }
}