package be.iminds.ilabt.jfed.call_log_output;

import be.iminds.ilabt.jfed.lowlevel.stitching.StitchingData;
import be.iminds.ilabt.jfed.util.DataConversionUtils;
import be.iminds.ilabt.jfed.util.IOUtils;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import be.iminds.ilabt.jfed.util.XmlUtil;
import org.rendersnake.HtmlCanvas;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

import static org.rendersnake.HtmlAttributesFactory.*;

/**
 * ApiTestResult: list of test method results
 */
public abstract class HtmlLogOutput extends LogOutput {
    private final String reportTitle;

    protected HtmlLogOutput(String reportTitle) {
        this.reportTitle = reportTitle;
    }

    public String getReportTitle() {
        return reportTitle;
    }

    public String getEmbeddedImageCss(String cssClassName, String resourcename, int x, int y) throws IOException {
        InputStream is = HtmlLogOutput.class.getResourceAsStream(resourcename);
        assert is != null : "Failed to get InputStream for resource \""+resourcename+"\"";
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = is.read(buffer)) != -1) {
            bout.write(buffer, 0, len);
        }
        bout.close();
        String base64 = DataConversionUtils.encodeBase64(bout.toByteArray());
        return "div.image-"+cssClassName+" {\n" +
                "  width:"+x+"px;\n" +
                "  height:"+y+"px;\n" +
                "  background-image:url(data:image/png;base64,"+base64+"); \n" +
                "}";
    }

    public void writeImage(HtmlCanvas html, TestResultState state) throws IOException {
        String stateCssClassName = stateClass(state);

        //TODO: support non embedded images
        //  html.img(src(stateIconImage(state)));

        html.div(class_("image-"+stateCssClassName));
        html._div();
    }


    public String stateClass(TestResultState state) {
        switch (state) {
            case SKIPPED:
                return "skip";
            case SUCCESS:
                return "success";
            case WARN:
                return "warn";
            case FAILED:
                return "fail";
            default:
                return "fail";
        }
    }

    public void addKeyValue(HtmlCanvas html, String clazz, String key, String value) throws IOException {
        html.div();
        html.span().content(key + ": ", true);
        html.span(class_(clazz)).content(value, true);
        html._div();
    }

    public void addCollapsable(HtmlCanvas html, String clazz, String shortText, String fullContent) throws IOException {
        if (fullContent == null) {
            addKeyValue(html, clazz, shortText, "");
        } else {
            int newLineCount = 0;
            for (int i = 0; i < fullContent.length(); i++)
                if (fullContent.charAt(i) == '\n')
                    newLineCount++;

            if (fullContent.length() > 150 || newLineCount > 1) {
                String id = clazz + Math.random();

                html.div();
                html.span().content(shortText + ": ", true);
                html.a(class_("reveal").onClick("switchVisibility('" + id + "')").title("Hide/Show " + shortText)).content("Hide/Show", true);
                html._div();
                html.div(style("display:none;").class_(clazz + "-big").id(id)).content(fullContent, true);
            } else {
                addKeyValue(html, clazz, shortText, fullContent);
            }
        }
    }

    public void addClassVisibleToggle(HtmlCanvas html, String className, String text) throws IOException {
        html.div();
        html.span().content(text + ": ", true);
        html.a(class_("reveal").onClick("switchClassVisibility('" + className + "')").title("Hide/Show " + text)).content("Hide/Show", true);
        html._div();
    }

    public void toHtmlFile(File file) {
        toHtmlFile(file, false);
    }
    public abstract void htmlContent(HtmlCanvas html) throws IOException;

    public String toHtmlString(boolean showSecurityRisks) {
        StringWriter writer = null;
        try {
            writer = new StringWriter();
            toHtml(writer, showSecurityRisks);
            return writer.getBuffer().toString();
        } catch (IOException e) {
            LOG.error("IOException writing to string", e);
            return null;
        } finally {
            if (writer != null)
                try {
                    writer.close();
                } catch (IOException e) {
                    LOG.error("IOException trying to close StringWriter", e);
                }
        }
    }
    public void toHtmlFile(File file, boolean showSecurityRisks) {
        FileWriter writer = null;
        try {
            writer = new FileWriter(file);
            toHtml(writer, showSecurityRisks);
        } catch (IOException e) {
            LOG.error("IOException writing to file \""+file.getPath()+"\"", e);
        } finally {
            if (writer != null)
                try {
                    writer.close();
                } catch (IOException e) {
                    LOG.error("IOException trying to close file \"" + file.getPath() + "\"", e);
                }
        }
    }
    public void toHtml(Writer writer, boolean showSecurityRisks) throws IOException {
        disableRemoveSecurityRisks = showSecurityRisks; //a bit of a hack now, would be nice to do this cleaner

        String failCss = "";
        String okCss = "";
        String skipCss = "";
        String warnCss = "";

        try {

            failCss = getEmbeddedImageCss("fail", "error15x15.png", 15, 15);
            okCss = getEmbeddedImageCss("success", "ok15x15.png", 15, 15);
            skipCss = getEmbeddedImageCss("skip", "skip15x15.png", 15, 15);
            warnCss = getEmbeddedImageCss("warn", "warning15x15.png", 15, 15);
        } catch (IOException e) {
            LOG.error("Error while loading generated html css or png. This will be ignored.", e);
        }

        HtmlCanvas html = new HtmlCanvas(writer);

        html
                .html()
                .head()
                .title().content(reportTitle)
                .meta(name("description").add("content", "description", false))
                .macros().stylesheet("result.css");

//            if (embedCss) {
        InputStream cssis = HtmlLogOutput.class.getResourceAsStream("result.css");
        String cssContent = IOUtils.streamToString(cssis, "UTF-8");
//                if (embedPng) {
        cssContent += failCss+"\n";
        cssContent += okCss+"\n";
        cssContent += skipCss+"\n";
        cssContent += warnCss+"\n";
//                }
        html.style(media("screen").type("text/css")).content(cssContent, false);
//            }

        //javascript
        InputStream jsis = HtmlLogOutput.class.getResourceAsStream("result.js");
        String jsContent = IOUtils.streamToString(jsis, "UTF-8");
        html.script(language("JavaScript1.3")).content(jsContent, false);


        html._head().body();

        htmlContent(html);

        html._body()._html();

        disableRemoveSecurityRisks = false;
    }

    public void writeOverviewItem(HtmlCanvas html, int index, TestResultState state, String name) throws IOException {
        html.div(class_("methodoverview-full"));
        writeImage(html, state);
        html.span(class_("methodoverview-methodname " + stateClass(state) + "header"))
                .span().a(href("#test" + index)).content(name, true)._span()
                ._span();
        html._div();
    }

    /*
        writeGroup(html, index++, res.state, res.methodName, res.description,
                   res.timeMs, new Date(res.startTimeMs), new Date(res.stopTimeMs)
                   res.exception, res.apiCallDetails, res.getLogLines())
    */

    public void writeGroup(HtmlCanvas html, int index,
                           TestResultState state, String name, String description,
                           long durationMs, Date start, Date stop,
                           Throwable exception,
                           List<SerializableApiCallDetails> calls,
                           List<LogEntry> logEntries) throws IOException {
        String exceptionStackTrace;
        if (exception != null) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            exception.printStackTrace(pw);
            pw.close();
            exceptionStackTrace = removeSecurityRisks(sw.getBuffer().toString());
        } else {
            exceptionStackTrace = null;
        }

        writeGroup(html, index, state, name, description, durationMs, start, stop, exceptionStackTrace, calls, logEntries);
    }
    public void writeGroup(HtmlCanvas html, int index,
                           TestResultState state, String name, String description,
                           long durationMs, Date start, Date stop,
                           String exceptionStackTrace,
                           List<SerializableApiCallDetails> calls,
                           List<LogEntry> logEntries) throws IOException {
        html.span();
        html.div(class_("methoddetail-header " + stateClass(state) + "header"));//.content(res.methodName + "-> " +res.state, true);
        writeImage(html, state);
        html.span().a(id("test" + index)).content(name)._span();
        html._div();
        html.div(class_("methoddetail " + stateClass(state) + "body")); //.content(res.methodName + "-> " +res.state, true);
        html.div(class_("state")).content(state.toString());
        if (description != null)
            html.div(class_("description")).content(description);
        html.div(class_("times"));
        if (durationMs != 0) {
            html.span(class_("timeheader")).content("duration ");
            html.span(class_("value-nondata")).content((durationMs / 1000.0) + "s");
        }
        if (start != null) {
            html.span(class_("timeheader")).content(" from ");
            html.span(class_("value-nondata")).content(dateToString(start));
        }
        if (stop != null) {
            html.span(class_("timeheader")).content(" to ");
            html.span(class_("value-nondata")).content(dateToString(stop));
        }
        html._div();
        if (exceptionStackTrace != null) {
            addCollapsable(html, "value-data", "Exception Stacktrace", exceptionStackTrace);
        }
        int apiCallCount = 1;
        if (calls != null)
            for (SerializableApiCallDetails details : calls) {
                String id = "api-call-" + index + "-" + apiCallCount;

                html.div();

                html.span().content("Api Call " + apiCallCount + ": ", true);
                html.a(class_("reveal").onClick("switchVisibility('" + id + "')").title("Hide/Show Api Call")).content("Hide/Show", true);
                html.span().content(" (" + details.getGeniMethodName() + " @ " + details.getAuthorityUrn() + ")");
                html._div();

                apiCallCount++;

                html.div(style("display:none;").class_("api-call").id(id));
                addKeyValue(html, "value-nondata", "API Name", details.getApiName());
//                    html.div(class_("api")).content(details.getApiName(), true);
                if (details.getAuthorityName() != null || details.getAuthorityUrn() != null) {
                    addKeyValue(html, "value-nondata", "Authority HRN", details.getAuthorityName());
                    addKeyValue(html, "value-data", "Authority URN", details.getAuthorityUrn());
//                        html.div(class_("authority-hrn")).content(details.getServer().getDefaultComponentManagerAsGeniUrn(), true);
//                        html.div(class_("authority-urn")).content(details.getServer().getName(), true);
                }
                if (details.getStartTime() != null)
                    addKeyValue(html, "value-data", "Start Time", dateToString(details.getStartTime()));
                if (details.getStopTime() != null)
                    addKeyValue(html, "value-data", "Stop Time", dateToString(details.getStopTime()));
                addKeyValue(html, "value-data", "API Method Name", details.getGeniMethodName());
                addKeyValue(html, "value-data", "Java Method Name", details.getGeniMethodName());
                addCollapsable(html, "value-data", "Parameters", removeSecurityRisks(details.getMethodParameters() + ""));
                addKeyValue(html, "value-data", "Server URL", details.getCallServerUrl());
                if (!Objects.equals(details.getBaseServerUrl(), details.getCallServerUrl()))
                    addKeyValue(html, "value-data", "Base Server URL", details.getBaseServerUrl());
                if (details.getConnectionSslAuthUserUrn() != null)
                    addKeyValue(html, "value-data", "Connection User URN", details.getConnectionSslAuthUserUrn());
                if (details.getProxyInfo() != null)
                    addKeyValue(html, "value-data", "Proxy", details.getProxyInfo().toString());
                if (details.getConnectionSslAuthUserCertificates() != null)
                    addCollapsable(html, "value-data", "Connection User certificates", removeSecurityRisks(details.getConnectionSslAuthUserCertificates()));
                if (details.getProxyInfo() != null)
                    addCollapsable(html, "value-data", "Connection Proxy Settings", removeSecurityRisks(details.getProxyInfo().toString()));
//                    html.div(class_("api-method-name")).content(details.getGeniMethodName(), true);
//                    html.div(class_("api-method-name-java")).content(details.getGeniMethodName(), true);
//                    html.div(class_("api-method-parameters")).content(details.getMethodParameters()+"", true);
//                    html.div(class_("api-method-server-url")).content(details.getServerUrl(), true);
//                    html.div(class_("xmlrpc-request")).content(details.getXmlRpcRequest()+"", true);
//                    html.div(class_("xmlrpc-reply")).content(details.getXmlRpcResponse()+"", true);
//                    html.div(class_("http-request")).content(details.getHttpRequest(), true);
//                    html.div(class_("http-reply")).content(details.getHttpResponse(), true);
                addCollapsable(html, "value-data", "XMLRPC Request",
                        removeSecurityRisks(
                                details.getXmlRpcRequestJsonString()
                        )
                );
                addCollapsable(html, "value-data", "XMLRPC Reply",
                        removeSecurityRisks(
                                details.getXmlRpcResponseJsonString()
                        )
                );
                addCollapsable(html, "value-data", "HTTP Request", removeSecurityRisks(details.getHttpRequest()));
                addCollapsable(html, "value-data", "HTTP Reply", removeSecurityRisks(details.getHttpResponse()));
                addCollapsable(html, "value-data", "HTTP Reply (formatted)", removeSecurityRisks(XmlUtil.formatXmlFromString_alwaysSafe(details.getHttpResponse())));
                //ApiCallReply reply = details.getReply();
                //if (reply != null) {
                if (details.getGeniResponseCode() != null)
                    addKeyValue(html, "value-data", "Geni Response Code", details.getGeniResponseCode() + " " + details.getGeniResponseCodeDescription());
//                        html.div(class_("geni-response-code")).content(reply.getGeniResponseCode().getCode()+" "+reply.getGeniResponseCode().getDescription(), true);
//                        html.div(class_("geni-value")).content(reply.getValue()+"", true);
//                        html.div(class_("geni-output")).content(reply.getOutput(), true);
                if ((details.getRawValue() + "").startsWith("<?xml") || (details.getRawValue() + "").startsWith("<rspec"))
                    addCollapsable(html, "value-data", "Geni Reply Value", removeSecurityRisks(XmlUtil.formatXmlFromString_alwaysSafe(details.getRawValue() + "")));
                else
                    addCollapsable(html, "value-data", "Geni Reply Value", removeSecurityRisks(details.getRawValue() + ""));
                addCollapsable(html, "value-data", "Geni Reply Output Reply", removeSecurityRisks(details.getGeniResponseOutput() + ""));
//                        html.div(class_("raw")).content(reply.getRawResult()+"", true);
                //}

                String stitchingOverview = makeStitchingOverview(details);
                if (stitchingOverview != null)
                    addCollapsable(html, "value-data", "Stitching Overview", stitchingOverview);
                html._div();
            }
        int logLineCount = 1;
        if (logEntries != null)
            for (LogEntry logline : logEntries) {
                String type = logline.type.name();

                //minor hack to change output meant for machines to debug level in this output meant for humans.
                if (logline.text.startsWith(MACHINE_LOGLINE_PREFIX)) {
                    type = LogLineType.DEBUG.name();
                }

                String logText = removeSecurityRisks(logline.text);

                String id = "logline-" + index + "-" + logLineCount;
                String className = "logline-" + type;
                String headerClassName = className + "-header";

                if (logline.getExceptionStackTrace() == null) {
                    html = addParagraphs(html.div(
                            logline.isFormatAsCode() ?
                                    class_(className+" format-as-code").id(id) :
                                    class_(className).id(id)
                    ).span(class_(headerClassName)).content(type + ": ", true), logText)._div();
                } else {
                    html = addParagraphs(html.div(
                            logline.isFormatAsCode() ?
                                    class_(className+" format-as-code").id(id) :
                                    class_(className).id(id)
                    ).span(class_(headerClassName)).content(type + ": ", true).div(), logText)._div();
                    String stacktrace = removeSecurityRisks(logline.getExceptionStackTrace());
                    html.div(class_("logline-exception"));
                    addCollapsable(html, "value-data", "Exception Stacktrace", stacktrace);
                    html._div()._div();
                }

                logLineCount++;
            }
        html._div();
        html._span();

    }
    public static final String MACHINE_LOGLINE_PREFIX = "RAW_INFO";

    public static HtmlCanvas addParagraphs(HtmlCanvas html, String lines) {
        HtmlCanvas res = html;
        try {
            res = res.span();
            BufferedReader br = new BufferedReader(new StringReader(lines));
            String line = br.readLine();
            if (line != null) {
                res = res.write(line);
                line = br.readLine();
                while (line != null) {
                    res = res.div().content(line, true);
                    line = br.readLine();
                }
            }
            res = res._span();
        } catch (IOException e) {
            LOG.error("Did not expect an exception here", e);
        }
        return res;
    }


    public static String dateToString(long ms) {
        return dateToString(new Date(ms));
    }
    public static String dateToString(Date d) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS X");
        String iso8601LocalTZ = sdf.format(d);
        String rfc3339ZTZ = RFC3339Util.dateToRFC3339String(d, true/*forceZuluHack*/, true/* discardSubSeconds*/, true/*useZinsteadOfZero*/);
        return  iso8601LocalTZ + "(" +rfc3339ZTZ+ ")";
    }





    public static String makeStitchingOverview(String rspec) {

        if (rspec == null) {
//            return "Null RSpec";
            return null;
        }
        Element stitchingElement = StitchingData.extractStitchingElementFromRspec(rspec);
        if (stitchingElement == null) {
//            return("No stitching info in RSpec\n");
            return null;
        }

        List<Element> pathEls = quickDomChildrenHelper(stitchingElement, "path");
        if (pathEls == null || pathEls.isEmpty()) {
//            return("No paths in RSpec stitching info\n");
            return null;
        }
        String res = "";
        for (Element pathEl : pathEls) {
            String pathId = pathEl.getAttribute("id");
            assert pathId != null : "<stitching> has <path> element without id attribute: " + pathEl;

            res += ("  - path \"");
            res += (pathId);
            res += ("\"\n");

            NodeList hopList = pathEl.getElementsByTagName("hop");
            for (int i = 0; i < hopList.getLength(); i++) {
                org.w3c.dom.Node hopNode = hopList.item(i);
                assert hopNode != null;
                assert hopNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE;
                Element hopElement = (Element) hopNode;

                String hopId = hopElement.getAttribute("id");
                res += "    - hop id=\""+hopId+"\"\n";

                List<Element> linkEls = quickDomChildrenHelper(hopElement, "link");
                for (Element linkEl : linkEls) {
                    String linkIdUrn = linkEl.getAttribute("id");
                    res += ("      - link \"");
                    res += (linkIdUrn);
                    res += ("\"\n");

                    Element scDescriptorEl = quickDomChildHelper(linkEl, "switchingCapabilityDescriptor");
                    Element scSpecificInfoEl = quickDomChildHelper(scDescriptorEl, "switchingCapabilitySpecificInfo");
                    Element scSpecificInfo_L2scEl =
                            quickDomChildHelper(scSpecificInfoEl, "switchingCapabilitySpecificInfo_L2sc");

                    Element vlanRangeAvailabilityEl =
                            quickDomChildHelper(scSpecificInfo_L2scEl, "vlanRangeAvailability");
                    Element suggestedVLANRangeEl = quickDomChildHelper(scSpecificInfo_L2scEl, "suggestedVLANRange");

                    String vlanRangeAvailability = vlanRangeAvailabilityEl.getTextContent();
                    String suggestedVLANRange = suggestedVLANRangeEl.getTextContent();

                    res += ("        vlanRangeAvailability=\"");
                    res += (vlanRangeAvailability);
                    res += ("\"\n");

                    res += ("        suggestedVLANRange   =\"");
                    res += (suggestedVLANRange);
                    res += ("\"\n");
                }
            }
        }
        return res;
    }

    private static List<Element> quickDomChildrenHelper(Element e, String tagName) {
        NodeList resList = e.getElementsByTagName(tagName);
        List<Element> resElements = new ArrayList<>();
        for (int i = 0; i < resList.getLength(); i++) {
            org.w3c.dom.Node resNode = resList.item(i);
            assert resNode != null;
            assert resNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE;
            Element resElement = (Element) resNode;
            resElements.add(resElement);
        }
        return resElements;
    }

    private static Element quickDomChildHelper(Element e, String tagName) {
        NodeList resList = e.getElementsByTagName(tagName);
        assert resList.getLength() == 1;
        org.w3c.dom.Node resNode = resList.item(0);
        assert resNode != null;
        assert resNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE;
        return (Element) resNode;
    }


    private static boolean isRspec(String text) { //TODO: better test
        if (text == null) return false;
        String head = text;
        if (text.length() > 100)
            head = text.substring(0, 100);
        return head.contains("<rspec");
    }

    private static List<String> extractRspecsFromJson(Object jsonRootObject) {
        if (jsonRootObject == null) return Collections.emptyList();

        List<String> res = new ArrayList<>();
        Deque<Object> objectsToProcess = new LinkedList<>();
        objectsToProcess.add(jsonRootObject);
        while (!objectsToProcess.isEmpty()) {
            Object jsonObject = objectsToProcess.pollFirst();
            if (jsonObject == null)
                continue;

            if (jsonObject instanceof String) {
                String s = (String) jsonObject;
                if (isRspec(s))
                    res.add(s);
            }
            if (jsonObject instanceof List) {
                List v = (List) jsonObject;
                objectsToProcess.addAll(v);
            }
            if (jsonObject instanceof Map) {
                Map ht = (Map) jsonObject;
                objectsToProcess.addAll(ht.values());
            }
        }

        return res;
    }

    public static String makeStitchingOverview(SerializableApiCallDetails callDetails) {
        if (callDetails != null) {
            String stitchingOverviewText = "";
            int infoCount = 0;
            try {
                List<String> rspecsInRequest = extractRspecsFromJson(callDetails.getXmlRpcRequestJsonObject());

                if (!rspecsInRequest.isEmpty()) {
                    String requestRspecText = "Stitching info in request RSpec:\n";
                    int count = 0;
                    for (String rspec : rspecsInRequest) {
                        if (count > 0)
                            requestRspecText += "\n!!! There is an additional Rspec in the request:\n";
                        String rspecOverview = makeStitchingOverview(rspec);
                        if (rspecOverview != null) {
                            requestRspecText += rspecOverview;
                            count++;
                            infoCount++;
                        }
                    }
                    if (count > 0) {
                        stitchingOverviewText += requestRspecText+"\n\n";
                    }
                }
//                else
//                    stitchingOverviewText += "No stitching info parsed in request RSpec.\n";
                List<String> rspecsInReply = extractRspecsFromJson(callDetails.getXmlRpcResponseJsonObject());
                if (!rspecsInReply.isEmpty()) {
                    String replyRspecText = "Stitching info in reply RSpec:\n";
                    int count = 0;
                    for (String rspec : rspecsInReply) {
                        if (count > 0) replyRspecText += "\n!!! There is an additional Rspec in the reply:\n";
                        String rspecOverview = makeStitchingOverview(rspec);
                        if (rspecOverview != null) {
                            replyRspecText += rspecOverview;
                            count++;
                            infoCount++;
                        }
                    }
                    if (count > 0) {
                        stitchingOverviewText += replyRspecText+"\n\n";
                    }
                }
//                else
//                    stitchingOverviewText += ("No stitching info parsed in reply RSpec.\n");

                if (callDetails.getGeniResponseCodeIsSuccess() == Boolean.FALSE) { //note that this can be null
                    if (callDetails.getGeniResponseOutput().toLowerCase().contains("vlan") || callDetails.getGeniResponseOutput().toLowerCase().contains("any")) {
                        stitchingOverviewText += "Error reply with output \""+callDetails.getGeniResponseOutput()+"\""+"\n\n";
                        infoCount++;
                    }
                }
            } catch (AssertionError t) {
                LOG.error("AssertionError while processing stitching data: " + t.getMessage(), t);
                stitchingOverviewText += "\n\nException while processing stitching data: ";
                stitchingOverviewText += t.getMessage();
                infoCount++;
            } catch (Exception t) {
                LOG.error("Exception while processing stitching data: " + t.getMessage(), t);
                stitchingOverviewText += "\n\nException while processing stitching data: ";
                stitchingOverviewText += t.getMessage();
                infoCount++;
            }

            if (infoCount > 0)
                return stitchingOverviewText;
            else
                return null;
        } else
            return null;
    }
}
