package be.iminds.ilabt.jfed.call_log_output;

import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import java.io.StringWriter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;

import static be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetailsConstants.*;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;

/**
 * User: twalcari
 * Date: 4/8/2015
 * Time: 09:27
 */
public class SerializableApiCallDetailsReader {
    private static final Logger LOG = LoggerFactory.getLogger(SerializableApiCallDetailsReader.class);

    public static SerializableApiCallDetails readCall(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, ApiCallDetailsParseException {
        assert (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_CALL));


        int id = -1;

        if (e.asStartElement().getAttributeByName(Q_CALL_ID) != null)
            id = Integer.parseInt(e.asStartElement().getAttributeByName(Q_CALL_ID).getValue());

        String authorityName = null;
        String authorityUrn = null;
        String baseServerUrl = null;
        String callServerUrl = null;

        /**
         * proxy used for the connection. May be null.
         */
        JFedConnection.ProxyInfo proxyInfo = null;

        String connectionUserUrn = null;
        String connectionBasicHttpAuthenticationUsername = null;
        String connectionId = null;
        String connectionUserCertificates = null;

        String connectionAuthenticationMethod = null;
        String connectionProtocol = null;
        String connectionType = null;

        String httpRequestStatusLine = null;
        String httpRequestHeaders = null;
        String httpRequest = null;
        String httpResponseStatusLine = null;
        String httpResponseHeaders = null;
        String httpResponse = null;

        String apiName = null;
        String javaMethodName = null;
        String geniMethodName = null;
        String methodParameters = null;

        String xmlRpcRequestJsonString = null;
        String xmlRpcResponseJsonString = null;

        String xmlRpcGeniResponseValue = null;
        String xmlRpcGeniResponseOutput = null;
        String xmlRpcGeniResponseCode = null;

        Integer geniResponseCode = null;
        String geniResponseCodeDescription = null;
        Boolean geniResponseCodeIsSuccess = null;
        String geniResponseOutput = null;

        String rawValue = null;


        Date startTime = null;
        Date stopTime = null;

        String protogeniSpewLogUrl = null;

        String exceptionString = null;


        while (eventReader.hasNext()) {
            e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (e.asStartElement().getName().equals(Q_AUTHORITY_NAME) || e.asStartElement().getName().equals(Q_AUTHORITY_NAME_ALT)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        authorityName = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_AUTHORITY_URN)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        authorityUrn = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_BASE_SERVER_URL)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        baseServerUrl = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_CALL_SERVER_URL)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        callServerUrl = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_CONNECTION_USER_URN)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        connectionUserUrn = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_CONNECTION_HTTP_BASIC_AUTHENTICATION_USERNAME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        connectionBasicHttpAuthenticationUsername = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_CONNECTION_CONFIG_TYPE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters()) {
                        connectionType = e.asCharacters().getData();
//                        assert connectionType != null;
//                        assert !connectionType.trim().isEmpty();
                    }
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_CONNECTION_CONFIG_AUTH_METHOD)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        connectionAuthenticationMethod = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_CONNECTION_CONFIG_PROTOCOL)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        connectionProtocol = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_PROXY_INFO)) {
                    proxyInfo = readProxyInfo(eventReader, e);

                } else if (Objects.equals(e.asStartElement().getName(), Q_CONNECTION_ID)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        connectionId = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_CONNECTION_USER_CERTS)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        connectionUserCertificates = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_HTTP_REQUEST_LINE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        httpRequestStatusLine = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_HTTP_REQUEST_HEADERS)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        httpRequestHeaders = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_HTTP_REQUEST)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        httpRequest = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_HTTP_STATUS_LINE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        httpResponseStatusLine = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_HTTP_RESPONSE_HEADERS)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        httpResponseHeaders = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_HTTP_REPLY)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        httpResponse = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_API_NAME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        apiName = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_JAVA_METHOD_NAME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        javaMethodName = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_METHOD_NAME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        geniMethodName = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_METHOD_PARAMETERS)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        methodParameters = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_XMLRPC_REQUEST)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        xmlRpcRequestJsonString = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_XMLRPC_REPLY)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        xmlRpcResponseJsonString = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_VALUE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        xmlRpcGeniResponseValue = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_OUTPUT)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        xmlRpcGeniResponseOutput = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_CODE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        xmlRpcGeniResponseCode = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_RESPONSE_CODE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        geniResponseCode = Integer.parseInt(e.asCharacters().getData());
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_RESPONSE_CODE_DESCRIPTION)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        geniResponseCodeDescription = e.asCharacters().getData();
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_CODE_IS_SUCCESS)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        geniResponseCodeIsSuccess = Boolean.parseBoolean(e.asCharacters().getData());
                    else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_GENI_RESPONSE_OUTPUT)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters()) {
                        geniResponseOutput = e.asCharacters().getData();

                    } else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_RAW_RESPONSE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters()) {
                        rawValue = e.asCharacters().getData();
                    } else {
                        assert (e.isEndElement());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_START_TIME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    assert (e.isCharacters());

                    startTime = getDateFromString(e.asCharacters().getData());

                } else if (e.asStartElement().getName().equals(Q_END_TIME) || e.asStartElement().getName().equals(Q_END_TIME_ALT)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    assert (e.isCharacters());
                    stopTime = getDateFromString(e.asCharacters().getData());
                } else if (Objects.equals(e.asStartElement().getName(), Q_PROTOGENI_SPEW_LOG_URL)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        protogeniSpewLogUrl = e.asCharacters().getData();
                    else
                        assert (e.isEndElement());
                } else if (Objects.equals(e.asStartElement().getName(), Q_EXCEPTION)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        exceptionString = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else {
                    logUnexpectedElement(Q_CALL, e);
                }
            } else if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_CALL)) {
//                    assert connectionType != null;
//                    assert !connectionType.trim().isEmpty();
                    return new SerializableApiCallDetails(id, authorityName, authorityUrn, baseServerUrl, callServerUrl,
                            proxyInfo,
                            connectionUserUrn, connectionBasicHttpAuthenticationUsername, connectionId, connectionUserCertificates,
                            connectionAuthenticationMethod, connectionProtocol, connectionType,

                            httpRequestStatusLine, httpRequestHeaders, httpRequest,
                            httpResponseStatusLine, httpResponseHeaders, httpResponse,

                            apiName, javaMethodName, geniMethodName, methodParameters,

                            xmlRpcRequestJsonString, xmlRpcResponseJsonString,

                            xmlRpcGeniResponseValue, xmlRpcGeniResponseOutput,
                            xmlRpcGeniResponseCode,

                            geniResponseCode, geniResponseCodeDescription,
                            geniResponseCodeIsSuccess, geniResponseOutput,

                            rawValue,

                            startTime, stopTime,
                            protogeniSpewLogUrl,
                            exceptionString);
                } else {
                    logUnexpectedElement(Q_CALL, e);
                }
            } else {
                logUnexpectedElement(Q_CALL, e);
            }
        }
        throw createUnexpectedEndException(Q_CALL, e.getLocation());
    }

    public static JFedConnection.ProxyInfo readProxyInfo(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, ApiCallDetailsParseException {

        String hostname = null;
        int port = -1;

        String username = "";
        String hostkey = "";


        while (eventReader.hasNext()) {
            e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_HOSTNAME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        hostname = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_PORT)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        port = Integer.parseInt(e.asCharacters().getData());
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_USERNAME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        username = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else if (Objects.equals(e.asStartElement().getName(), Q_HOST_KEY)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (e.isCharacters())
                        hostkey = e.asCharacters().getData();
                    else
                        assert e.isEndElement();
                } else {
                    logUnexpectedElement(Q_PROXY_INFO, e);
                }
            } else if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_PROXY_INFO)) {
                    return new JFedConnection.SshProxyInfo(hostname, port, username, null, hostkey, false);
                } else {
                    logUnexpectedElement(Q_PROXY_INFO, e);
                }
            } else {
                logUnexpectedElement(Q_PROXY_INFO, e);
            }
        }
        throw createUnexpectedEndException(Q_PROXY_INFO, e.getLocation());

    }

    private static final DateTimeFormatter DATE_TIME_FORMATTER = ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault());

    private static Date getDateFromString(String input) {
        ZonedDateTime dateTime = ZonedDateTime.from(DATE_TIME_FORMATTER.parse(input));
        return Date.from(dateTime.toInstant());

    }

    public static class ApiCallDetailsParseException extends Exception {
        private final Location location;

        public ApiCallDetailsParseException(Location location) {
            this.location = location;
        }

        public ApiCallDetailsParseException(Throwable cause, Location location) {
            super(cause);
            this.location = location;
        }

        public ApiCallDetailsParseException(String message, Location location) {
            super(message);
            this.location = location;
        }

        public ApiCallDetailsParseException(String message, Throwable cause, Location location) {
            super(message, cause);
            this.location = location;
        }

        public ApiCallDetailsParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Location location) {
            super(message, cause, enableSuppression, writableStackTrace);
            this.location = location;
        }
    }


    private static ApiCallDetailsParseException createUnexpectedEndException(QName name, Location location) {
        return new ApiCallDetailsParseException("Expected to encounter end-tag of " + name.getLocalPart() + ", but never found it before end of stream.", location);

    }

    private static void logUnexpectedElement(QName parent, XMLEvent event) {
        if (event.isCharacters() && event.asCharacters().getData().trim().isEmpty())
            return; //this is to be expected!
        if (event.isEndElement())
            return; //we also want to ignore these!

        StringWriter stringWriter = new StringWriter();
        try {
            event.writeAsEncodedUnicode(stringWriter);
        } catch (XMLStreamException e) {
            e.printStackTrace();
        }
        String eventContents = stringWriter.toString().trim();


        LOG.warn("Encountered unexpected event {} ({}) while parsing {}@{}", event, eventContents, parent.getLocalPart(), event.getLocation());
    }
}
