package be.iminds.ilabt.jfed.lowlevel.connection;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Service;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import org.apache.http.Header;
import org.apache.http.HttpVersion;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.XmlRpcRequest;
import org.apache.xmlrpc.client.*;
import org.apache.xmlrpc.common.XmlRpcRequestProcessor;
import org.apache.xmlrpc.common.XmlRpcStreamConfig;
import org.apache.xmlrpc.parser.XmlRpcResponseParser;
import org.apache.xmlrpc.serializer.XmlRpcWriter;
import org.apache.xmlrpc.server.XmlRpcServer;
import org.apache.xmlrpc.util.SAXParsers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * GeniConnection: XmlRpc over HTTP without SSL (= unencrypted, plain HTTP)
 */
public class MockSfaConnection extends SfaConnection {
    private final static Logger LOG = LoggerFactory.getLogger(MockSfaConnection.class);

    private List<MockXmlRpcReply> mockReplies = new ArrayList<>();
    private List<OnMockXmlRpcListener> noMockListeners = new ArrayList<>();
    private List<MockXmlRpcHistoryListener> historyListeners = new ArrayList<>();

    /**
     * Create a connection with debugMode disabled and with the specified auth added as debugInfo
     *
     * @param serverUrl url to connect to (may not be null)
     * @param service   the service to add for debug information purposes (may be null)
     * @param proxyInfo the proxy to use (may be null if no proxy)
     */
    public MockSfaConnection(URL serverUrl, Service service, ProxyInfo proxyInfo) throws JFedException {
        this(serverUrl, proxyInfo, new DebugInfo(service, null));
    }

    /**
     * Create a connection by specifying all low level details needed.
     *
     * @param serverUrl url to connect to (may not be null, unless this is a fake connection used for debugging)
     * @param proxyInfo the proxy to use (may be null if no proxy)
     * @param debugInfo debug information, such as authority and user, and whether debug mode is enabled. (may be null)
     */
    public MockSfaConnection(URL serverUrl, ProxyInfo proxyInfo, DebugInfo debugInfo) throws JFedException {
        super(new ConnectionConfig(serverUrl, debugInfo, proxyInfo,
                ConnectionConfig.Authentication.SSL_CLIENT_AUTH, null, null, null, ConnectionConfig.Protocol.HTTPS, null, null, ConnectionConfig.Type.SFA));

        try {
            xmlRpcClient = new XmlRpcClient();

            //support <nil/>
            xmlRpcClient.setTypeFactory(new XmlRpcTypeNil(xmlRpcClient));

            final MockXmlRpcTransportFactory mockXmlRpcTransportFactory = new MockXmlRpcTransportFactory(xmlRpcClient);
            xmlRpcTransportFactory = mockXmlRpcTransportFactory;
            xmlRpcClient.setTransportFactory(xmlRpcTransportFactory);

            XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
            config.setServerURL(serverUrl);
            config.setEnabledForExtensions(true);
            config.setXmlRpcServer(mockXmlRpcTransportFactory.createServer(config));
            xmlRpcClient.setConfig(config);

            error = false;
        } catch (Exception e) {
            throw new JFedException("Error creating XmlRpcClient in MockSfaConnection constructor: " + e.getMessage(), e);
        }
    }

    public void close() {
        error = true;

        xmlRpcTransportFactory = null;
        xmlRpcClient = null;
    }


    //this is dirty, but it works  (it enables adding to these lists using TestCoreModule after injection)
    public void setMockReplies(List<MockXmlRpcReply> mockReplies) {
        this.mockReplies = mockReplies;
    }

    public void setNoMockListeners(List<OnMockXmlRpcListener> noMockListeners) {
        this.noMockListeners = noMockListeners;
    }

    public void setHistoryListeners(List<MockXmlRpcHistoryListener> historyListeners) {
        this.historyListeners = historyListeners;
    }

    public static class MockXmlRpcHistoryItem {
        public final XmlRpcRequest request;
        public final MockXmlRpcAnswer reply;

        public MockXmlRpcHistoryItem(XmlRpcRequest request, MockXmlRpcAnswer reply) {
            this.request = request;
            this.reply = reply;
        }
    }

    public interface MockXmlRpcHistoryListener {
        void onMockXmlRpcHistory(MockXmlRpcHistoryItem item);
    }


    public static class MockXmlRpcAnswer {
        private final String reply;
        private final Throwable ex;

        public MockXmlRpcAnswer(String reply) {
            this.reply = reply;
            this.ex = null;
        }

        public MockXmlRpcAnswer(Throwable ex) {
            this.reply = null;
            this.ex = ex;
        }

        public boolean isException() {
            return ex != null;
        }

        public String getString() {
            return reply;
        }

        public Throwable getEx() {
            return ex;
        }
    }

    public interface MockXmlRpcReply {
        MockXmlRpcAnswer matches(XmlRpcRequest request);

        void registerUsage();

        int usageCount();
    }

    public interface OnMockXmlRpcListener {
        void onNoMockFor(XmlRpcRequest request);
    }

    public static abstract class AbstractMockXmlRpcReply implements MockXmlRpcReply {
        private int usages = 0;

        public void registerUsage() {
            usages++;
        }

        public int usageCount() {
            return usages;
        }
    }


    private class MockXmlRpcTransportFactory extends XmlRpcTransportFactoryImpl implements HttpInfoXmlRpcTransportFactory {


        /**
         * Creates a new instance, operated by the given client.
         *
         * @param pClient The client, which will invoke the factory.
         */
        public MockXmlRpcTransportFactory(XmlRpcClient pClient) {
            super(pClient);
        }

        private final XmlRpcTransport LOCAL_TRANSPORT = new MockXmlRpcLocalTransport(getClient());

        public XmlRpcTransport getTransport() {
            return LOCAL_TRANSPORT;
        }


        /////////////////////////////////////
        // HttpInfoXmlRpcTransportFactory methods
        /////////////////////////////////////
        public String httpRecvHist = null;
        public String httpSentHist = null;
        public StatusLine httpStatusLine = null;
        public String httpRequestLine = null;
        public String httpRequestHeaders = null;
        public String httpResponseHeaders = null;

        @Override
        public String getHttpReceivedHistory() {
            return httpRecvHist;
        }

        @Override
        public String getHttpSentHistory() {
            return httpSentHist;
        }

        @Override
        public StatusLine getHttpStatusLine() {
            return httpStatusLine;
        }

        @Override
        public String getHttpRequestLine() {
            return httpRequestLine;
        }

        @Override
        public String getHttpRequestHeaders() {
            return httpRequestHeaders;
        }

        @Override
        public String getHttpResponseHeaders() {
            return httpResponseHeaders;
        }

        @Override
        public List<Header> getRawHttpResponseHeaders() {
            return null;
        }

        @Override
        public void clearLastCallData() {
            httpRecvHist = "";
            httpRequestHeaders = null;
            httpStatusLine = null;
            httpRequestLine = null;
            httpResponseHeaders = null;
            httpSentHist = "";
        }


        @Override
        public void close() {

        }

        @Override
        public void setProperty(String s, Object o) {

        }

        public XmlRpcRequestProcessor createServer(XmlRpcClientConfigImpl config) {
            return new MockXmlRpcServer(config);
        }

        private class MockXmlRpcLocalTransport extends XmlRpcLocalTransport {

            /**
             * Creates a new instance.
             *
             * @param pClient The client, which creates the transport.
             */
            public MockXmlRpcLocalTransport(XmlRpcClient pClient) {
                super(pClient);
            }


            private void logRequest(XmlRpcRequest request) {
                ByteArrayOutputStream pStream = new ByteArrayOutputStream();
                final XmlRpcStreamConfig config = (XmlRpcStreamConfig) request.getConfig();
                try {
                    ContentHandler h = getClient().getXmlWriterFactory().getXmlWriter(config, pStream);
                    XmlRpcWriter xw = new XmlRpcWriter(config, h, getClient().getTypeFactory());
                    try {
                        xw.write(request);
                    } catch (SAXException e) {
                        e.printStackTrace();
                    }
                } catch (XmlRpcException ex) {
                    LOG.error("Error while writing mock request", ex);
                }

                httpSentHist = pStream.toString();
            }


            @Override
            public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException {
                logRequest(pRequest);

                return super.sendRequest(pRequest);
            }
        }

        private class MockXmlRpcServer extends XmlRpcServer {
            private final XmlRpcClientConfigImpl config;

            public MockXmlRpcServer(XmlRpcClientConfigImpl config) {
                this.config = config;
            }

            @Override
            public Object execute(XmlRpcRequest pRequest) throws XmlRpcException {
                Throwable seenException = null;
                MockXmlRpcAnswer reply = null;
                for (MockXmlRpcReply mockXmlRpcReply : mockReplies) {
                    try {
                        reply = mockXmlRpcReply.matches(pRequest);
                    } catch (AssertionError e) {
                        LOG.error("AssertionError while matching for mock reply", e);
                        seenException = e;
                    } catch (Exception e) {
                        LOG.error("Exception while matching for mock reply", e);
                        seenException = e;
                    }

                    if (reply != null) {
                        mockXmlRpcReply.registerUsage();
                        MockXmlRpcHistoryItem historyItem = new MockXmlRpcHistoryItem(pRequest, reply);
                        for (MockXmlRpcHistoryListener histListener : historyListeners)
                            histListener.onMockXmlRpcHistory(historyItem);
                        break;
                    }
                }

                if (reply == null && seenException == null) {
                    for (OnMockXmlRpcListener l : noMockListeners)
                        l.onNoMockFor(pRequest);
                    throw new XmlRpcClientException("No mock answer for this request: " + pRequest, null);
                }

                if (reply == null /*&& seenException != null*/) {
                    throw new XmlRpcClientException("Got exception while looking for mock answer for " + pRequest, seenException);
                }

                if (reply.isException()) {
                    LOG.debug("Mocked exception", reply.getEx());
                    throw new XmlRpcClientException("Mocked exception", reply.getEx());
                } else {
                    httpRecvHist = reply.getString();
                    httpStatusLine = new StatusLine() {
                        @Override
                        public ProtocolVersion getProtocolVersion() {
                            return new HttpVersion(1, 1);
                        }

                        @Override
                        public int getStatusCode() {
                            return 200;
                        }

                        @Override
                        public String getReasonPhrase() {
                            return "OK";
                        }
                    };
                    return readResponse(reply.getString());
                }
            }

            protected XMLReader newXMLReader() throws XmlRpcException {
                return SAXParsers.newXMLReader();
            }

            private Object readResponse(String responseString) throws XmlRpcException {
                XMLReader xr = newXMLReader();
                XmlRpcResponseParser xp;
                try {
                    xp = new XmlRpcResponseParser(config, getTypeFactory());
                    xr.setContentHandler(xp);
                    xr.parse(new InputSource(new StringReader(responseString)));
                } catch (SAXException e) {
                    throw new XmlRpcClientException("Failed to parse mock response: " + e.getMessage(), e);
                } catch (IOException e) {
                    throw new XmlRpcClientException("Failed to read mock response: " + e.getMessage(), e);
                }
                if (xp.isSuccess()) {
                    return xp.getResult();
                }
                Throwable t = xp.getErrorCause();
                if (t == null) {
                    throw new XmlRpcException(xp.getErrorCode(), xp.getErrorMessage());
                }
                if (t instanceof XmlRpcException) {
                    throw (XmlRpcException) t;
                }
                if (t instanceof RuntimeException) {
                    throw (RuntimeException) t;
                }
                throw new XmlRpcException(xp.getErrorCode(), xp.getErrorMessage(), t);
            }

        }
    }


}
