/*
 * Copyright (c) eHealth
 */
package be.fgov.ehealth.technicalconnector.tests.server;


import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import be.ehealth.technicalconnector.utils.ConnectorIOUtils;
import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.NanoHTTPD.Response.Status;

public class HttpServerStub extends NanoHTTPD {

    public Map<String, Queue<String>> contentMap = new HashMap<String, Queue<String>>();
    public Map<String, HttpAsserter> asserterMap = new HashMap<String, HttpAsserter>();

    public HttpServerStub(int port) throws Exception {
        super(port);
        start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
    }

    @Override
    public Response serve(IHTTPSession session) {
        String path = session.getUri();
        if (StringUtils.startsWith(path, "/echo")) {
            return echo(session);
        } else if (StringUtils.startsWith(path, "/error")) {
            return error(path);
        } else if (StringUtils.startsWith(path, "/timeout")) {
            return timeout(path);
        } else if (StringUtils.startsWith(path, "/full/")) {
            return full(path, session.getParms());
        } else if (contentMap.containsKey(path)) {
            return content(path);
        } else if (StringUtils.startsWith(path, "/assert/") && asserterMap.containsKey(path)) {
            try {
                return asserterMap.get(path).assertSession(session);
            } catch (Exception e) {
                return internalError(e.getMessage(), e);
            }
        }
        return internalError("unsupported path [" + path + "]");
    }

    private Response full(String path, Map<String, String> params) {
        try {
            String location = contentMap.get(path).poll();
            Properties props = new Properties();
            props.loadFromXML(ConnectorIOUtils.getResourceAsStream(location));
            if (props.containsKey("txt")) {
                String[] searchList = ArrayUtils.EMPTY_STRING_ARRAY;
                String[] replacementList = ArrayUtils.EMPTY_STRING_ARRAY;
                for (Entry<String, String> param : params.entrySet()) {
                    searchList = (String[]) ArrayUtils.add(searchList, "${" + param.getKey() + "}");
                    replacementList = (String[]) ArrayUtils.add(replacementList, param.getValue());
                }
                String body = StringUtils.replaceEach(props.getProperty("txt"), searchList, replacementList);
                return NanoHTTPD.newFixedLengthResponse(Status.valueOf(props.getProperty("status")), props.getProperty("mimeType"), body);
            } else {
                return NanoHTTPD.newChunkedResponse(Status.valueOf(props.getProperty("status")), props.getProperty("mimeType"), ConnectorIOUtils.getResourceAsStream(props.getProperty("location")));
            }
        } catch (Exception e) {
            return internalError(e.getMessage(), e);
        }
    }

    private Response content(String path) {
        try {
            String body = contentMap.get(path).poll();
            if (!StringUtils.startsWith(body, "<")) {
                body = ConnectorIOUtils.getResourceAsString(body);
            }
            return newFixedLengthResponse(Status.OK, "text/xml", body);
        } catch (Exception e) {
            return internalError(e.getMessage(), e);
        }
    }

    private Response error(String path) {
        Integer errorCode = Integer.valueOf(StringUtils.substringAfterLast(path, "/"));
        for (Status status : Status.values()) {
            if (status.getRequestStatus() == errorCode) {
                return newFixedLengthResponse(status, "text/html", status.getDescription());
            }
        }
        return internalError("unkown error [" + errorCode + "]");
    }

    private Response timeout(String path) {
        try {
            Integer timeperiod = Integer.valueOf(StringUtils.substringAfterLast(path, "/"));
            TimeUnit.SECONDS.sleep(timeperiod);
        } catch (InterruptedException e) {
            return internalError("unkown error [" + e.getMessage() + "]");
        }
        return newFixedLengthResponse(Status.OK, "text/xml", "Waiting page");

    }

    public static Response internalError(String content, Exception... excepts) {

        StringWriter sb = new StringWriter();
        sb.append("<SOAP-ENV:Fault>");
        sb.append("<faultcode>SOAP-ENV:Server</faultcode>");
        sb.append("<faultstring>");
        sb.append("<![CDATA[").append(content).append("]]>");
        sb.append("</faultstring>");
        sb.append("<detail>");
        sb.append("<![CDATA[");
        for (Exception e : excepts) {
            e.printStackTrace(new PrintWriter(sb));
        }
        sb.append("]]>");
        sb.append("</detail>");
        sb.append("</SOAP-ENV:Fault>");
        return wrapSOAPEnvelope(sb.toString(), Status.INTERNAL_ERROR);

    }

    public static Response wrapSOAPEnvelope(String content) {
        return wrapSOAPEnvelope(content, Status.OK);
    }

    public static Response wrapSOAPEnvelope(String content, Status status) {
        StringWriter sb = new StringWriter();
        sb.append("<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body>");
        sb.append(content);
        sb.append("</SOAP-ENV:Body></SOAP-ENV:Envelope>");
        return newFixedLengthResponse(status, "text/xml", sb.toString());
    }


    private Response echo(IHTTPSession session) {
        try {

            Map<String, String> files = new HashMap<String, String>();
            session.parseBody(files);
            StringBuffer sb = new StringBuffer();
            sb.append("<EchoResponse xmlns=\"urn:be:fgov:ehealth:technicalconnector:tests:server\">");
            sb.append("<Inbound>");
            appendEntries(session.getHeaders().entrySet(), sb, "Header");
            appendEntries(files.entrySet(), sb, "File");
            sb.append("</Inbound>");
            sb.append("</EchoResponse>");
            return wrapSOAPEnvelope(sb.toString());
        } catch (Exception e) {
            return internalError("Unable to echo message", e);
        }
    }

    private static void appendEntries(Set<Entry<String, String>> entries, StringBuffer sb, String tag) {
        for (Entry<String, String> entry : entries) {
            sb.append("<" + tag + " id=\"").append(entry.getKey()).append("\">");
            sb.append("<![CDATA[").append(stripNonValidXMLCharacters(entry.getValue())).append("]]>");
            sb.append("</" + tag + ">");
        }
    }

    public void add(String path, String content) {
        if (!contentMap.containsKey(path)) {
            contentMap.put(path, new ArrayDeque<String>());
        }
        contentMap.get(path).add(content);
    }

    public void add(String path, HttpAsserter asserter) {
        asserterMap.put(path, asserter);
    }

    public interface HttpAsserter {
        Response assertSession(IHTTPSession session) throws Exception;
    }

    public static String stripNonValidXMLCharacters(String in) {
        StringBuffer out = new StringBuffer(); // Used to hold the output.
        char current; // Used to reference the current character.

        if (in == null || ("".equals(in))) return ""; // vacancy test.
        for (int i = 0; i < in.length(); i++) {
            current = in.charAt(i); // NOTE: No IndexOutOfBoundsException caught here; it should not happen.
            if ((current == 0x9) ||
                    (current == 0xA) ||
                    (current == 0xD) ||
                    ((current >= 0x20) && (current <= 0xD7FF)) ||
                    ((current >= 0xE000) && (current <= 0xFFFD)) ||
                    ((current >= 0x10000) && (current <= 0x10FFFF)))
                out.append(current);
        }
        return out.toString();
    }
}
