package be.iminds.ilabt.jfed.call_log_output;

import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsRef;
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.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.stream.StreamSource;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

import static be.iminds.ilabt.jfed.call_log_output.CallReportConstants.*;

/**
 * User: twalcari
 * Date: 4/9/2015
 * Time: 11:55
 */
public class CallReportReader {
    private static final Logger LOG = LoggerFactory.getLogger(CallReportReader.class);

    private final CallRestoreContext callRestoreContext;
    private final CallReportFactory callReportFactory;

    public CallReportReader(CallRestoreContext callRestoreContext, CallReportFactory callReportFactory) {
        this.callRestoreContext = callRestoreContext;
        this.callReportFactory = callReportFactory;
    }

    public CallReport readCallReport(Reader reader) throws XMLStreamException, CallReportParseException {
        XMLInputFactory xif = XMLInputFactory.newFactory();
        StreamSource xml = new StreamSource(reader);
        XMLEventReader eventReader = xif.createXMLEventReader(xml);

        assert (eventReader.hasNext());
        XMLEvent e = eventReader.nextEvent();
        while (!(e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_CALL_REPORT))) {
            e = eventReader.nextEvent();
        }

        return readCallReport(eventReader, e);
    }

    public CallReport readCallReport(InputStream inputStream) throws XMLStreamException, CallReportParseException {
        XMLInputFactory xif = XMLInputFactory.newFactory();
        StreamSource xml = new StreamSource(inputStream);
        XMLEventReader eventReader = xif.createXMLEventReader(xml);

        assert (eventReader.hasNext());
        XMLEvent e = eventReader.nextEvent();

        return readCallReport(eventReader, e);
    }

    public CallReport readCallReport(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, CallReportParseException {
        assert (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_CALL_REPORT));

        String reportTitle = e.asStartElement().getAttributeByName(Q_TITLE).getValue();
        String reportObject = e.asStartElement().getAttributeByName(Q_REPORTER).getValue();
        List<CallReport.State> states = new ArrayList<>();

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

            if (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_STATES)) {
                states.addAll(readStates(eventReader, e));
            } else if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_CALL_REPORT)) {
                return callReportFactory.createCallReport(reportTitle, reportObject, states);
            } else {
                logUnexpectedElement(Q_CALL_REPORT, e);
            }
        }

        throw createUnexpectedEndException(Q_CALL_REPORT, e.getLocation());

    }
    private List<CallReport.State> readStates(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, CallReportParseException {
        assert (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_STATES));

        List<CallReport.State> res = new ArrayList<>();

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

            if (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_STATE)) {
                res.add(readState(eventReader, e));

            } else if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_STATES)) {
                return res;
            } else {
                logUnexpectedElement(Q_STATES, e);
            }
        }
        throw createUnexpectedEndException(Q_STATES, e.getLocation());
    }

    private CallReport.State readState(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, CallReportParseException {
        assert (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_STATE));

        ZonedDateTime startDate = null;
        ZonedDateTime endDate = null;
        String name = null;
        List<LogOutput.LogEntry> logLines = Collections.emptyList();
        List<ApiCallDetailsRef> calls = Collections.emptyList();
        LogOutput.TestResultState state = null;

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

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

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at start", e.getLocation());

                    startDate =
                            ZonedDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()).parse(e.asCharacters().getData()));
                } else if (Objects.equals(e.asStartElement().getName(), Q_END)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at end, but got " + e.getEventType(), e.getLocation());

                    endDate =
                            ZonedDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()).parse(e.asCharacters().getData()));
                } else if (Objects.equals(e.asStartElement().getName(), Q_NAME)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at name", e.getLocation());

                    name = e.asCharacters().getData();
                } else if (Objects.equals(e.asStartElement().getName(), Q_LOGENTRIES)) {
                    logLines = readLogLines(eventReader, e);
                } else if (Objects.equals(e.asStartElement().getName(), Q_CALLS)) {
                    calls = readCalls(eventReader, e);
                } else if (Objects.equals(e.asStartElement().getName(), Q_STATESTATE)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at " + Q_STATESTATE, e.getLocation());

                    state = LogOutput.TestResultState.valueOf(e.asCharacters().getData());
                } else {
                    logUnexpectedElement(Q_STATE, e);
                }
            } else if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_STATE)) {
                CallReport.State res =
                        callReportFactory.createCallReportState(name,
                                startDate != null ? Date.from(startDate.toInstant()) : null);

                if (endDate != null)
                    res.setEnd(Date.from(endDate.toInstant()));
                logLines.forEach(res::log);
                calls.forEach(res::logCall);
                res.setState(state);
                return res;
            } else {
                logUnexpectedElement(Q_STATE, e);
            }


        }

        throw createUnexpectedEndException(Q_STATE, e.getLocation());

    }

    private List<ApiCallDetailsRef> readCalls(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, CallReportParseException {
        assert (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_CALLS));


        List<ApiCallDetailsRef> res = new ArrayList<>();

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

            if (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_CALL_REF)) {
                int oldId = Integer.parseInt(e.asStartElement().getAttributeByName(Q_CALL_REF_ID).getValue());

                ApiCallDetailsRef newRef = callRestoreContext.getNewApiCallDetailsRef(oldId);

                if (newRef == null) {
                    LOG.warn("Could not retrieve ApiCallDetails with ref {}", oldId);
                }


                res.add(newRef);
            } else if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_CALLS)) {
                return res;
            } else {
                logUnexpectedElement(Q_CALLS, e);
            }
        }
        throw createUnexpectedEndException(Q_CALLS, e.getLocation());

    }


    private List<LogOutput.LogEntry> readLogLines(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, CallReportParseException {
        assert (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_LOGENTRIES));

        List<LogOutput.LogEntry> res = new ArrayList<>();

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

            if (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_LOGENTRY)) {
                res.add(readLogLine(eventReader, e));
            } else if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_LOGENTRIES)) {
                return res;
            } else {
                logUnexpectedElement(Q_LOGENTRIES, e);
            }
        }
        throw createUnexpectedEndException(Q_LOGENTRIES, e.getLocation());
    }


    private LogOutput.LogEntry readLogLine(XMLEventReader eventReader, XMLEvent e) throws XMLStreamException, CallReportParseException {
        assert (e.isStartElement() && Objects.equals(e.asStartElement().getName(), Q_LOGENTRY));

        LogOutput.LogLineType type = null;
        String text = null;
        long timeMs = 0;
        String exception = null;

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

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

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at start", e.getLocation());

                    try {
                        type = LogOutput.LogLineType.valueOf(e.asCharacters().getData());
                    } catch (IllegalArgumentException ex) {
                        throw new CallReportParseException("Could not parse the log line type correctly. Unexpected type: " + e.asCharacters().getData(), ex, e.getLocation());
                    }
                } else if (Objects.equals(e.asStartElement().getName(), Q_TEXT)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at name", e.getLocation());

                    text = e.asCharacters().getData();
                } else if (Objects.equals(e.asStartElement().getName(), Q_TIME_IN_MS)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at name", e.getLocation());

                    timeMs = Long.parseLong(e.asCharacters().getData());
                } else if (Objects.equals(e.asStartElement().getName(), Q_EXCEPTION)) {
                    assert (eventReader.hasNext());
                    e = eventReader.nextEvent();

                    if (!e.isCharacters())
                        throw new CallReportParseException("Expected characters at name", e.getLocation());

                    exception = e.asCharacters().getData();
                } else {
                    logUnexpectedElement(Q_LOGENTRY, e);
                }
            } else if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_LOGENTRY)) {
                    return new LogOutput.LogEntry(type, text, timeMs, exception, false);
                } else {
                    logUnexpectedElement(Q_LOGENTRY, e);
                }
            } else {
                logUnexpectedElement(Q_LOGENTRY, e);
            }


        }

        throw createUnexpectedEndException(Q_LOGENTRY, e.getLocation());
    }


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

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

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

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

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

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

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

    }

    private 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());
    }

}
