package fr.adrienbrault.idea.symfony2plugin.profiler.utils;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.impl.source.html.HtmlFileImpl;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlTagValue;
import fr.adrienbrault.idea.symfony2plugin.profiler.collector.HttpDefaultDataCollector;
import fr.adrienbrault.idea.symfony2plugin.profiler.dict.HttpProfilerRequest;
import fr.adrienbrault.idea.symfony2plugin.profiler.dict.ProfilerRequestInterface;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author Daniel Espendiller <[email protected]>
 */
public class ProfilerUtil {

    /**
     * Cache for url content
     */
    private static Cache<String, String> REQUEST_CACHE = CacheBuilder.newBuilder()
        .maximumSize(50)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();

    private static Cache<String, ProfilerRequestInterface> PROFILER_REQUEST_CACHE = CacheBuilder.newBuilder()
        .maximumSize(15)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();

    /**
     * Extract "table.search-results tbody tr td"
     * We dont have complete xpath with html support inside so reuse internal html parser
     */
    @NotNull
    public static Collection<ProfilerRequestInterface> createRequestsFromIndexHtml(@NotNull Project project, @NotNull String html, @NotNull String baseUrl) {
        HtmlFileImpl htmlFile = (HtmlFileImpl) PsiFileFactory.getInstance(project).createFileFromText(HTMLLanguage.INSTANCE, html);

        final XmlTag[] result = new XmlTag[1];

        // find table
        PsiTreeUtil.processElements(htmlFile, psiElement -> {
            if(psiElement instanceof XmlTag && "table".equals(((XmlTag) psiElement).getName()) && "search-results".equals(((XmlTag) psiElement).getAttributeValue("id"))) {
                result[0] = (XmlTag) psiElement;
                return false;
            }
            return true;
        });

        if(result[0] == null) {
            return Collections.emptyList();
        }

        // find table to be our keys for Map
        XmlTag thead = result[0].findFirstSubTag("thead");
        if(thead == null) {
            return Collections.emptyList();
        }

        XmlTag tr1 = thead.findFirstSubTag("tr");
        if(tr1 == null) {
            return Collections.emptyList();
        }

        List<String> header = new ArrayList<>();
        for (XmlTag th : tr1.findSubTags("th")) {
            header.add(StringUtils.trim(stripHtmlTags(th.getValue().getText())).toLowerCase());
        }

        // we need at least this fields
        if(!header.containsAll(Arrays.asList("token", "url"))) {
            return Collections.emptyList();
        }

        XmlTag tbody = result[0].findFirstSubTag("tbody");
        if(tbody == null) {
            return Collections.emptyList();
        }

        List<ProfilerRequestInterface> requests = new ArrayList<>();
        for (XmlTag tr : tbody.findSubTags("tr")) {

            // secure limit
            if(requests.size() >= 10) {
                break;
            }

            // "td" elements dont match header "th"
            XmlTag[] findSubTags = tr.findSubTags("td");
            if(findSubTags.length < header.size()) {
                continue;
            }

            // build row map with header keys
            Map<String, Pair<XmlTag, String>> row = new HashMap<>();
            for (int i = 0; i < findSubTags.length; i++) {
                row.put(header.get(i), Pair.create(
                    findSubTags[i],
                    StringUtils.trim(stripHtmlTags(findSubTags[i].getText().replaceAll("\\n", " "))).replace("\\s+", " ")
                ));
            }

            // extract token link to be our linked profiler url
            String profilerUrl = null;
            XmlTag tokenLink = row.get("token").getFirst().findFirstSubTag("a");
            if(tokenLink != null) {
                String href = tokenLink.getAttributeValue("href");
                if(StringUtils.isNotBlank(href)) {
                    profilerUrl = StringUtils.stripEnd(baseUrl, "/") + href;
                }
            }

            // extract status code
            int statusCode = 0;
            if(row.containsKey("status")) {
                try {
                    statusCode = Integer.valueOf(row.get("status").getSecond());
                } catch (NumberFormatException ignored) {
                }
            }

            requests.add(new HttpProfilerRequest(
                statusCode,
                row.get("token").getSecond(),
                profilerUrl,
                row.containsKey("method") ? row.get("method").getSecond() : "n/a",
                row.get("url").getSecond()
            ));
        }

        return requests;
    }

    @NotNull
    public static Collection<ProfilerRequestInterface> collectHttpDataForRequest(@NotNull Project project, @NotNull Collection<ProfilerRequestInterface> requests) {
        Collection<Callable<ProfilerRequestInterface>> callable = requests.stream().map(
            request -> new MyProfilerRequestDecoratedCollectorCallable(project, request)).collect(Collectors.toCollection(ArrayList::new)
        );

        return getProfilerRequestCollectorDecorated(callable, 10);
    }

    /**
     * "_controller" and "_route"
     * "/_profiler/242e61?panel=request"
     *
     * <tr>
     *  <th>_route</th>
     *  <td>foo_route</td>
     * </tr>
     */
    @NotNull
    public static Map<String, String> getRequestAttributes(@NotNull Project project, @NotNull String html) {
        HtmlFileImpl htmlFile = (HtmlFileImpl) PsiFileFactory.getInstance(project).createFileFromText(HTMLLanguage.INSTANCE, html);

        String[] keys = new String[] {"_controller", "_route"};

        Map<String, String> map = new HashMap<>();
        PsiTreeUtil.processElements(htmlFile, psiElement -> {
            if(!(psiElement instanceof XmlTag) || !"th".equals(((XmlTag) psiElement).getName())) {
                return true;
            }

            XmlTagValue keyTag = ((XmlTag) psiElement).getValue();
            String key = StringUtils.trim(keyTag.getText());
            if(!ArrayUtils.contains(keys, key)) {
                return true;
            }

            XmlTag tdTag = PsiTreeUtil.getNextSiblingOfType(psiElement, XmlTag.class);
            if(tdTag == null || !"td".equals(tdTag.getName())) {
                return true;
            }

            XmlTagValue valueTag = tdTag.getValue();
            String value = valueTag.getText();
            if(StringUtils.isBlank(value)) {
                return true;
            }

            // Symfony 3.2 profiler debug? strip html
            map.put(key, stripHtmlTags(value));

            // exit if all item found
            return map.size() != keys.length;
        });

        return map;
    }

    /**
     * ["foo/foo.html.twig": 1]
     *
     * <tr>
     *  <td>@Twig/Exception/traces_text.html.twig</td>
     *  <td class="font-normal">1</td>
     * </tr>
     */
    public static Map<String, Integer> getRenderedElementTwigTemplates(@NotNull Project project, @NotNull String html) {
        HtmlFileImpl htmlFile = (HtmlFileImpl) PsiFileFactory.getInstance(project).createFileFromText(HTMLLanguage.INSTANCE, html);

        final XmlTag[] xmlTag = new XmlTag[1];
        PsiTreeUtil.processElements(htmlFile, psiElement -> {
            if(!(psiElement instanceof XmlTag) || !"h2".equals(((XmlTag) psiElement).getName())) {
                return true;
            }

            XmlTagValue keyTag = ((XmlTag) psiElement).getValue();
            String contents = StringUtils.trim(keyTag.getText());
            if(!"Rendered Templates".equalsIgnoreCase(contents)) {
                return true;
            }

            xmlTag[0] = (XmlTag) psiElement;

            return true;
        });

        if(xmlTag[0] == null) {
            return Collections.emptyMap();
        }

        XmlTag tableTag = PsiTreeUtil.getNextSiblingOfType(xmlTag[0], XmlTag.class);
        if(tableTag == null || !"table".equals(tableTag.getName())) {
            return Collections.emptyMap();
        }

        XmlTag tbody = tableTag.findFirstSubTag("tbody");
        if(tbody == null) {
            return Collections.emptyMap();
        }

        Map<String, Integer> templates = new HashMap<>();

        for (XmlTag tag : PsiTreeUtil.getChildrenOfTypeAsList(tbody, XmlTag.class)) {
            if(!"tr".equals(tag.getName())) {
                continue;
            }

            XmlTag[] tds = tag.findSubTags("td");
            if(tds.length < 2) {
                continue;
            }

            String template = stripHtmlTags(StringUtils.trim(tds[0].getValue().getText()));
            if(StringUtils.isBlank(template)) {
                continue;
            }

            Integer count;
            try {
                count = Integer.valueOf(stripHtmlTags(StringUtils.trim(tds[1].getValue().getText())));
            } catch (NumberFormatException e) {
                count = 0;
            }

            templates.put(template, count);
        }

        return templates;
    }

    @NotNull
    private static String stripHtmlTags(@NotNull String text)
    {
        return text.replaceAll("<[^>]*>", "");
    }

    @Nullable
    public static String getProfilerUrlContent(@NotNull String url) {
        URLConnection conn;
        try {
            conn = new URL(url).openConnection();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }

        try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
                return reader.lines().collect(Collectors.joining("\n"));
            }
        } catch (IOException e) {
            return null;
        }
    }

    private static class MyProfilerRequestDecoratedCollectorCallable implements Callable<ProfilerRequestInterface> {
        @NotNull
        private final Project project;

        @NotNull
        private final ProfilerRequestInterface request;

        @NotNull
        private final String profilerUrl;

        MyProfilerRequestDecoratedCollectorCallable(@NotNull Project project, @NotNull ProfilerRequestInterface request) {
            this.project = project;
            this.request = request;
            this.profilerUrl = request.getProfilerUrl();
        }

        @Override
        public ProfilerRequestInterface call() throws Exception {
            ProfilerRequestInterface requestCache = PROFILER_REQUEST_CACHE.getIfPresent(profilerUrl);
            if(requestCache != null) {
                return requestCache;
            }

            ProfilerRequestInterface httpProfilerRequest = new HttpProfilerRequest(
                request,
                new HttpDefaultDataCollector(getRequestAttributes())
            );

            PROFILER_REQUEST_CACHE.put(profilerUrl, httpProfilerRequest);

            return httpProfilerRequest;
        }

        @NotNull
        private Map<String, String> getRequestAttributes() {
            Map<String, String> requestAttributes = new HashMap<>();

            String requestContent = getUrlContent(profilerUrl + "?panel=request");
            String twigContent = getUrlContent(profilerUrl + "?panel=twig");

            if(requestContent != null) {
                ApplicationManager.getApplication().runReadAction(() ->
                    requestAttributes.putAll(ProfilerUtil.getRequestAttributes(project, requestContent))
                );
            }

            if(twigContent != null) {
                ApplicationManager.getApplication().runReadAction(() -> {
                    Map<String, Integer> templates = getRenderedElementTwigTemplates(project, twigContent);
                    if(templates.size() > 0) {
                        requestAttributes.put("_template", templates.keySet().iterator().next());
                    }
                });
            }

            return requestAttributes;
        }

        private String getUrlContent(@NotNull String url) {
            String contents = REQUEST_CACHE.getIfPresent(url);

            if(contents == null) {
                contents = ProfilerUtil.getProfilerUrlContent(url);
                REQUEST_CACHE.put(url, contents);
            }

            return contents;
        }
    }

    /**
     * Decorated request model with loaded collector data
     * loads data on multiple thread to be as fast as possible
     */
    @NotNull
    public static List<ProfilerRequestInterface> getProfilerRequestCollectorDecorated(@NotNull Collection<Callable<ProfilerRequestInterface>> callable, int threads) {
        ExecutorService executor = Executors.newFixedThreadPool(threads);

        List<Future<ProfilerRequestInterface>> futures;
        try {
            futures = executor.invokeAll(callable);
        } catch (InterruptedException e) {
            return Collections.emptyList();
        }

        List<ProfilerRequestInterface> requests = new ArrayList<>();
        for (Future<ProfilerRequestInterface> future : futures) {
            try {
                requests.add(future.get());
            } catch (ExecutionException | InterruptedException ignored) {
            }
        }

        executor.shutdown();

        return requests;
    }

    /**
     * Try to find a base url profiler relative url:
     *  "/foobar" =>  "http://127.0.0.1:8000/foobar"
     *
     * In local csv context we dont know path info. There is a way
     * to try extract it from serialized string but overhead
     *
     * Think of:
     * http://127.0.0.1/
     * http://127.0.0.1:8000/
     * https://127.0.0.1:8000/
     * https://127.0.0.1:8000/app_dev.php
     */
    @Nullable
    public static String getBaseProfilerUrlFromRequest(@NotNull String requestUrl) {
        URL url;
        try {
            url = new URL(requestUrl);
        } catch (MalformedURLException e) {
            return null;
        }

        String portValue = "";
        int port = url.getPort();
        if(port != -1 && port != 80) {
            portValue = ":" + port;
        }

        String pathSuffix = "";
        String urlPath = url.getPath();
        Matcher matcher = Pattern.compile(".*(/app_[\\w]{2,6}.php)/").matcher(urlPath);
        if(matcher.find()){
            pathSuffix = StringUtils.stripEnd(urlPath.substring(0, matcher.end()), "/");
        }

        return url.getProtocol() + "://" + url.getHost() + portValue + pathSuffix;
    }

    @Nullable
    public static String formatProfilerRow(@NotNull ProfilerRequestInterface profilerRequest) {
        int statusCode = profilerRequest.getStatusCode();

        String path = profilerRequest.getUrl();

        try {
            URL url = new URL(profilerRequest.getUrl());
            path = url.getPath();

            Matcher matcher = Pattern.compile(".*(/app_[\\w]{2,6}.php)/").matcher(path);
            if(matcher.find()){
                path = "/" + path.substring(matcher.end());
            }

        } catch (MalformedURLException ignored) {
        }

        return String.format("(%s) %s", statusCode == 0 ? "n/a" : statusCode, StringUtils.abbreviate(path, 35));
    }
}