package eu.dnetlib.espas.sosservice.cxs;

import eu.dnetlib.espas.exception.OwsException;
import eu.dnetlib.espas.exception.OwsExceptionCode;
import eu.dnetlib.espas.sosservice.*;
import org.apache.log4j.Logger;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;

@SuppressWarnings({"UnusedDeclaration", "unused", "FieldCanBeLocal", "ThrowFromFinallyBlock"})
@Transactional
public class CxsSOSDataProvider implements SOSDataProvider {
    private final Logger logger = Logger.getLogger(CxsSOSDataProvider.class);

    private SOSConfiguration cxsSOSConfiguration = null;
    private String timeZone = null;
    private VelocityEngine velocityEngine = null;

    public void init() {
        velocityEngine = new VelocityEngine();

        velocityEngine.addProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.Log4JLogChute");
        velocityEngine.addProperty("runtime.log.logsystem.log4j.logger", logger.getName());

        velocityEngine.addProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        velocityEngine.addProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        velocityEngine.addProperty(RuntimeConstants.UBERSPECT_CLASSNAME, org.apache.velocity.util.introspection.SecureUberspector.class.getName());

        velocityEngine.init();
    }

    @Override
    public String getResultTemplate(Map<String, String> parameters) throws OwsException {
        Properties properties = cxsSOSConfiguration.getProperties();
        Exception exception = null;
        SOSHelper helper = new SOSHelper();
        String resourceTemplate = null;
        boolean hasErrors = false;

        String observedProperty = parameters.get("observedproperty");
        String offering = parameters.get("offering");

        String[] processInfo = offering.split("\\/");
        try {
            if (processInfo.length > 0) {
                String process = processInfo[0];
                String namespace = processInfo[1];
                String localID = processInfo[2];
                String version = processInfo[3];
                String capabilityName = "";

                if (processInfo.length > 4)
                    capabilityName = processInfo[4];

                String cxsPrefix = "cxs.sos.";
                VelocityContext context = new VelocityContext();
                Template velocityTemplate = velocityEngine.getTemplate(properties.getProperty(cxsPrefix + "template"));
                String resourceFile;

                resourceTemplate = helper.getResourceTemplate(velocityEngine, properties, cxsPrefix, cxsSOSConfiguration,
                        process, namespace, localID, version, observedProperty, capabilityName);

            } else {
                throw new OwsException("No parameters available. Check servlet for the parsed parameters", OwsExceptionCode.MISSING_PARAMETER);
            }
        } catch (XPathExpressionException e) {
            logger.error("XPathExpressionException in getResultTemplate", e);
            exception = e;
        } catch (ParserConfigurationException e) {
            logger.error("ParserConfigurationException in getResultTemplate", e);
            exception = e;
        } catch (IOException e) {
            logger.error("IOException in getResultTemplate", e);
            exception = e;
        } catch (SAXException e) {
            logger.error("SAXException in getResultTemplate", e);
            exception = e;
        } catch (Exception e) {
            logger.error("Exception in getResultTemplate", e);
            exception = e;
        } finally {
            if (exception != null) {
                throw new OwsException(exception.getMessage(), OwsExceptionCode.NO_APPLICABLE_CODE);
            }
        }

        return resourceTemplate;
    }

    @Override
    public String getResult(Map<String, String> parameters) throws OwsException {
        Properties properties = cxsSOSConfiguration.getProperties();
        SOSHelper helper = new SOSHelper();
        String offering;
        String observedProperty;
        boolean hasError = false;
        String toolXml;
        String resource = null;

        try {
            offering = parameters.get("offering");
            observedProperty = helper.getObservedProperty(parameters.get("observedproperty"));

            String[] processInfo = offering.split("\\/");

            if (processInfo.length > 0) {
                String process = processInfo[0];
                String namespace = processInfo[1];
                String localID = processInfo[2];
                String version = processInfo[3];
                String capabilityName = "";

                if (processInfo.length > 4)
                    capabilityName = processInfo[4];

                String cxsPrefix = "cxs.sos.";

                if (capabilityName.isEmpty())
                    toolXml = createToolXML(properties, localID, observedProperty, parameters.get("temporalfilter"));
                else {
                    toolXml = createToolXML(properties, localID, capabilityName, parameters.get("temporalfilter"));
                }

                String redirectedUrl = jobSubmit(properties.getProperty(cxsPrefix + "service"), toolXml);

                jobStatus(redirectedUrl);
                resource = jobGetResult(properties, redirectedUrl, cxsPrefix, toolXml);

                jobDelete(redirectedUrl);

            } else {
                throw new OwsException("No parameters available. Check servlet for the parsed parameters.", OwsExceptionCode.MISSING_PARAMETER);
            }
        } catch (XPathExpressionException e) {
            logger.error("XPathExpressionException", e);
            hasError = true;
        } catch (ParserConfigurationException e) {
            logger.error("ParserConfigurationException", e);
            hasError = true;
        } catch (IOException e) {
            logger.error("IOException", e);
            hasError = true;
        } catch (SAXException e) {
            logger.error("SAXException", e);
            hasError = true;
        } catch (InterruptedException e) {
            logger.error("InterruptedException", e);
            hasError = true;
        } catch (Exception e) {
            logger.error("Exception", e);
            hasError = true;
        } finally {
            if (hasError)
                throw new OwsException("Check log file for errors", OwsExceptionCode.NO_APPLICABLE_CODE);
        }

        return resource;
    }

    public SOSConfiguration getCxsSOSConfiguration() {
        return cxsSOSConfiguration;
    }

    public void setCxsSOSConfiguration(SOSConfiguration cxsSOSConfiguration) {
        this.cxsSOSConfiguration = cxsSOSConfiguration;
    }

    public String getTimeZone() {
        return timeZone;
    }

    public void setTimeZone(String timeZone) {
        this.timeZone = timeZone;
    }

    private String createToolXML(Properties properties, String localID, String observedProperty, String temporalFilter) throws IOException, OwsException {
        String cxsPrefix = "cxs.sos.";
        String configurationPrefix = cxsPrefix + localID + "." + observedProperty + ".";
        Template velocityTemplate = velocityEngine.getTemplate(properties.getProperty(cxsPrefix + "toolxmltemplate"));
        VelocityContext context = new VelocityContext();
        SOSHelper helper = new SOSHelper();
        String resource;

        String tool = helper.retrieveProperty(observedProperty, localID, cxsPrefix, "tool", properties);
        String dateParameterNames = helper.retrieveProperty(observedProperty, localID, cxsPrefix, "dateparameters", properties);
        String inputParameterNames = helper.retrieveProperty(observedProperty, localID, cxsPrefix, "inputparameters", properties);
        String outputParameterNames = helper.retrieveProperty(observedProperty, localID, cxsPrefix, "outputparameters", properties);

        String[] dateParameterNamesArray = dateParameterNames.split(",");
        String[] inputParameterNamesArray = inputParameterNames.split(",");
        String[] outputParameterNamesArray = outputParameterNames.split(",");
        List<Map> inputParameters = new ArrayList<Map>();
        List<Map> outputParameters = new ArrayList<Map>();

        context.put("tool", tool);
        if (dateParameterNamesArray.length > 0) {
            if (temporalFilter != null) {
                Set<Map<String, String>> temporalQueryParsedSet = helper.temporalQueryParser(temporalFilter);
                String startDate = null;
                String endDate = null;
                for (Map<String, String> temporalQueryParsed : temporalQueryParsedSet) {
                    for (String key : temporalQueryParsed.keySet()) {
                        if (key.contains("from")) {
                            startDate = temporalQueryParsed.get(key);
                        }
                        if (key.contains("to")) {
                            endDate = temporalQueryParsed.get(key);
                        }
                        if (key.contains("instant")) {
                            startDate = temporalQueryParsed.get(key);
                            endDate = temporalQueryParsed.get(key);
                        }
                    }
                }
                for (String parameterName : dateParameterNamesArray) {
                    Map<String, String> dateParameter = new HashMap<String, String>();
                    if (parameterName.toLowerCase().contains("start")) {
                        dateParameter.put("name", parameterName.trim());
                        dateParameter.put("value", startDate);
                    } else {
                        dateParameter.put("name", parameterName.trim());
                        dateParameter.put("value", endDate);
                    }
                    inputParameters.add(dateParameter);
                }
            }
        }

        for (String parameterName : inputParameterNamesArray) {
            Map<String, String> inputParameter = new HashMap<String, String>();
            inputParameter.put("name", parameterName.trim());
            String value = helper.retrieveProperty(observedProperty, localID, cxsPrefix, parameterName.trim(), properties);
            inputParameter.put("value", value);
            inputParameters.add(inputParameter);
        }

        for (String parameterName : outputParameterNamesArray) {
            Map<String, String> outputParameter = new HashMap<String, String>();
            outputParameter.put("name", parameterName.trim());
            String value = helper.retrieveProperty(observedProperty, localID, cxsPrefix, parameterName.trim(), properties);
            outputParameter.put("value", value);
            outputParameters.add(outputParameter);
        }

        context.put("input", inputParameters);
        context.put("output", outputParameters);

        StringWriter writer = new StringWriter();
        velocityTemplate.merge(context, writer);
        resource = writer.toString();
        writer.flush();
        writer.close();

        logger.debug("TOOL XML:" + resource);
        return resource;
    }

    private String jobSubmit(String cxsService, String toolXml) throws IOException {
        URL url = new URL(cxsService);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        String redirectedUrl;

        conn.setRequestMethod("POST");

        // Set follow redirected page as false. Redirected address will be handled later
        conn.setInstanceFollowRedirects(false);
        conn.setRequestProperty("Content-Type", "text/xml;charset=\"utf-8\"");
        conn.setRequestProperty("Accept", "text/xml");
        conn.setRequestProperty("Cache-Control", "no-cache");
        conn.setRequestProperty("Pragma", "no-cache");
        conn.setRequestProperty("SOAPAction", "\"run\"");
        // Set length of the message (toolXml produced in createTollXML)
        conn.setRequestProperty("Content-length", String.valueOf(toolXml.length()));
        conn.setDoOutput(true);
        conn.setDoInput(true);
        // Send data
        DataOutputStream output = new DataOutputStream(conn.getOutputStream());
        output.writeBytes(toolXml);
        output.close();

        // Handle redirected address. Get JobId and resend url at phase page with phase = RUN
        redirectedUrl = conn.getHeaderField("Location");

        return redirectedUrl;
    }

    private void jobStatus(String redirectedUrl) throws XPathExpressionException, ParserConfigurationException, IOException, SAXException, InterruptedException {
        XPathFactory factory = XPathFactory.newInstance();
        XPath xPath = factory.newXPath();
        xPath.setNamespaceContext(new SOSNamespace());

        XPathExpression phaseExpression = xPath.compile("//uws:phase");

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        Node phase;
        Document doc;

        while (true) {
            doc = db.parse(redirectedUrl + "/phase");
            phase = (Node) phaseExpression.evaluate(doc, XPathConstants.NODE);
            logger.error("PHASE: " + phase.getTextContent());
            if (phase.getTextContent().equalsIgnoreCase(ExecutionPhase.EXECUTING.toString())
                    || phase.getTextContent().equalsIgnoreCase(ExecutionPhase.QUEUED.toString())) {
                Thread.sleep(1000);
            } else if (phase.getTextContent().equalsIgnoreCase(ExecutionPhase.PENDING.toString())) {
                URL url = new URL(redirectedUrl + "/phase");
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("POST");
                conn.setInstanceFollowRedirects(false);
                conn.setRequestProperty("Content-length", String.valueOf("phase=RUN".length()));
                conn.setDoOutput(true);
                conn.setDoInput(true);
                DataOutputStream output = new DataOutputStream(conn.getOutputStream());
                output.writeBytes("phase=RUN");
                output.close();

                // read the response
                DataInputStream input = new DataInputStream(conn.getInputStream());
                int c;
                StringBuilder resultBuf = new StringBuilder();
                while ((c = input.read()) != -1) {
                    resultBuf.append((char) c);
                }
                input.close();
                Thread.sleep(1000);
            } else {
                break;
            }
        }

        if (phase.getTextContent().equalsIgnoreCase(ExecutionPhase.ERROR.toString())
                || phase.getTextContent().equalsIgnoreCase(ExecutionPhase.ABORTED.toString())
                || phase.getTextContent().equalsIgnoreCase(ExecutionPhase.HELD.toString())
                || phase.getTextContent().equalsIgnoreCase(ExecutionPhase.SUSPENDED.toString())
                || phase.getTextContent().equalsIgnoreCase(ExecutionPhase.UNKNOWN.toString())
                ) {
            throw new IOException("Request is stated at " + phase.getTextContent() + " in CXS Server");
        }
    }

    private String getOutputFile(Properties properties, String redirectedUrl, String cxsPrefix, String resource) throws XPathExpressionException, ParserConfigurationException, IOException, SAXException {
        XPathFactory factory = XPathFactory.newInstance();
        XPath xPath = factory.newXPath();
        xPath.setNamespaceContext(new SOSNamespace());
        XPathExpression outputFileExpression = xPath.compile("//cea:output/cea:parameter/cea:value");
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        Node outputFileNode;
        InputSource inputSource = new InputSource(new ByteArrayInputStream(resource.getBytes("utf-8")));
        Document doc = db.parse(inputSource);
        outputFileNode = (Node) outputFileExpression.evaluate(doc, XPathConstants.NODE);

        return outputFileNode.getTextContent().replace("internalstorage:", "");
    }

    private String jobGetResult(Properties properties, String redirectedUrl, String cxsPrefix, String resource) throws XPathExpressionException, ParserConfigurationException, IOException, SAXException {
        File cxsOUT = new File(properties.getProperty(cxsPrefix + "serviceoutput"));
        String outputFile;
        String output = null;

        if (cxsOUT.isDirectory()) {
            String jobId = redirectedUrl.substring(properties.getProperty(cxsPrefix + "service").length()).replace("/", "");
            outputFile = getOutputFile(properties, redirectedUrl, cxsPrefix, resource);
            File currentJob = new File(properties.getProperty(cxsPrefix + "serviceoutput") + jobId + outputFile);


            BufferedReader bufferedReader = new BufferedReader(new FileReader(currentJob));
            StringBuilder stringBuilder = new StringBuilder();
            String xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                    "<sos:GetResultResponse " +
                    "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
                    "xmlns:swe=\"http://www.opengis.net/swe/2.0\" " +
                    "xmlns:swes=\"http://www.opengis.net/swes/2.0\" " +
                    "xmlns:ows=\"http://www.opengis.net/ows/1.1\" " +
                    "xmlns:sos=\"http://www.opengis.net/sos/2.0\" " +
                    "xmlns:fes=\"http://www.opengis.net/fes/2.0\" " +
                    "xmlns:gml=\"http://www.opengis.net/gml/3.2\" " +
                    "xmlns:ogc=\"http://www.opengis.net/ogc\" " +
                    "xmlns:om=\"http://www.opengis.net/om/2.0\" " +
                    "xmlns:xlink=\"http://www.w3.org/1999/xlink\" " +
                    "xsi:schemaLocation=\"http://www.opengis.net/sos/2.0 " +
                    "http://schemas.opengis.net/sos/2.0/sos.xsd\">\n" +
                    "<sos:resultValues>";
            String xmlFooter = "</sos:resultValues>\n" +
                    "</sos:GetResultResponse>";
            String tokenseparator = properties.getProperty(cxsPrefix + "tokenseparator");
            String blockseparator = properties.getProperty(cxsPrefix + "blockseparator");

            stringBuilder.append(xmlHeader);

            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line);
            }
            stringBuilder.append(blockseparator);
            stringBuilder.append(xmlFooter);

            output = stringBuilder.toString();
        }

        return output;
    }

    private void jobDelete(String redirectedUrl) throws IOException {
        URL url = new URL(redirectedUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setInstanceFollowRedirects(false);
        conn.setRequestProperty("Content-length", String.valueOf("ACTION=DELETE".length()));
        conn.setDoOutput(true);
        conn.setDoInput(true);
        DataOutputStream output = new DataOutputStream(conn.getOutputStream());
        output.writeBytes("ACTION=DELETE");
        output.close();

        // read the response
        DataInputStream input = new DataInputStream(conn.getInputStream());
        int c;
        StringBuilder resultBuf = new StringBuilder();
        while ((c = input.read()) != -1) {
            resultBuf.append((char) c);
        }
        input.close();
    }
}
