package eu.dnetlib.espas.api.sos;

import eu.dnetlib.espas.api.exception.OwsException;
import eu.dnetlib.espas.api.exception.OwsExceptionCode;
import eu.dnetlib.espas.sos.client.SOSRequestManager;
import eu.dnetlib.espas.sos.client.SOSRequestStatus;
import org.apache.commons.io.FileUtils;
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.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.sql.DataSource;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.net.*;
import java.sql.*;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Sos {
    private static Logger logger = Logger.getLogger(Sos.class);

    private DataSource dataSource;
    private String templateFile;
    private String tokenseparator;
    private String blockseparator;
    private SOSRequestManager sosManager;


    /**
     * SOS GetResultTemplate implementation
     *
     * @param parameters input parameters containing offering and observedProperty
     * @return the getResultTemplateResponse as String
     * @throws OwsException
     */
    public String getResultTemplate(Map<String, String> parameters) throws OwsException {
        StringBuilder response = new StringBuilder();
        Map<String, Map<String, String>> fields = new HashMap<String, Map<String, String>>();
        try {
            HashSet<String> processCapabilities = getUniqueCapabilityProcesses(parameters.get("offering"), parameters.get("observedproperty"));
            VelocityEngine velocityEngine = new VelocityEngine();
            init(velocityEngine);

            VelocityContext context = new VelocityContext();
            Template velocityTemplate = velocityEngine.getTemplate(templateFile);

            StringWriter writer = new StringWriter();

            for (String processCapability : processCapabilities) {
                HashSet<String> urls = getUniqueUrls(processCapability);

                for (String url : urls) {
                    String request = url +
                            "/sos" +
                            "?service=" + parameters.get("service") +
                            "&version=" + parameters.get("version") +
                            "&request=GetResultTemplate" +
                            "&offering=" + processCapability.replaceAll("#", "%23") +
                            "&observedProperty=" + parameters.get("observedproperty");


                    String templateResponse = makeUrlCall(request, parameters.get("httpmethod"));
                    ResultTemplate resultTemplate;

                    if (templateResponse != null && !templateResponse.isEmpty()) {
                        resultTemplate = getResultTemplate(templateResponse);

                        for (SweField sweField : resultTemplate.getFields()) {
                            if (!fields.containsKey(sweField.getUnit())) {
                                Map<String, String> field = new HashMap<String, String>();
                                field.put("name", sweField.getName());
                                field.put("definition", sweField.getDefinition());
                                field.put("unit", sweField.getUnit());
                                fields.put(sweField.getUnit(), field);
                            }
                        }
                    }
                }
            }

            context.put("fields", fields.values());
            context.put("tokenSeparator", tokenseparator);
            context.put("blockSeparator", blockseparator);

            velocityTemplate.merge(context, writer);

            response.append(writer.toString());

            writer.flush();
            writer.close();

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (response.toString().isEmpty())
            throw new OwsException("Error getting template from providers", "getResultTemplate", OwsExceptionCode.NO_APPLICABLE_CODE);
        return response.toString();
    }

    /**
     * SOS GetResult implementation
     * For the offering parameter a processCapability is retrieved and the query is
     * rewritten and transmitted to the corresponding data provider.
     *
     * @param parameters input parameters (contains both offering and observedProperty parameters)
     * @return the getResultResponse as String
     * @throws OwsException
     */
    public String getResult(Map<String, String> parameters) throws OwsException {
        Matcher matcher1 = Pattern.compile("^.*,(.*)/(.*)$").matcher(parameters.get("temporalfilter"));
        Properties springConfProps = new Properties();
        InputStream propsStream = SOSRequestManager.class.getResourceAsStream("applicationContext-espas-sos-client.properties");
        String requestId;
        String espasKey = parameters.get("espas-key");
        String offering = parameters.get("offering").replace("%23", "#");
        String result = "";
        int days = 0;

        try {
            if (matcher1.find()) {
                springConfProps.load(propsStream);
                /* dates */
                String startDate = matcher1.group(1);
                String endDate = matcher1.group(2);
                DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
                Date in = format.parse(startDate);
                Date out = format.parse(endDate);
                Calendar calendar = new GregorianCalendar();
                calendar.setTime(in);

                while (calendar.getTime().before(out)) {
                    ++days;
                    calendar.add(Calendar.DATE, 1);
                }

                if (days < 32) {

                    SOSRequestStatus sosRequestResult;

                    if (!espasKey.isEmpty()) {
                        requestId = sosManager.submitRequest(offering, parameters.get("observedproperty"), in, out, espasKey);
                        sosRequestResult = sosManager.getDataRequestStatus(requestId, espasKey);

                        logger.info("Request ID is " + requestId);

                        while (sosRequestResult.getStatus() != SOSRequestStatus.RequestStatus.COMPLETED
                                && sosRequestResult.getStatus() != SOSRequestStatus.RequestStatus.FAILED) {
                            sosRequestResult = sosManager.getDataRequestStatus(requestId, espasKey);
                        }


                        if (sosRequestResult.getStatus() != SOSRequestStatus.RequestStatus.FAILED) {

                            File exportedDirectory = new File(springConfProps.getProperty("sos.client.store.location") + "sos_store/requests/");

                            if (requestId != null && !requestId.isEmpty()) {
                                final String requestKati = requestId;
                                File[] outputDirectoryList = exportedDirectory.listFiles(new FilenameFilter() {
                                    @Override
                                    public boolean accept(File dir, String name) {
                                        return name.contains(requestKati);
                                    }
                                });
                                File outputDirectory;
                                if (outputDirectoryList.length > 0) {
                                    outputDirectory = outputDirectoryList[0].listFiles(new FilenameFilter() {
                                        @Override
                                        public boolean accept(File dir, String name) {
                                            return name.contains("sos_response.xml");
                                        }
                                    })[0];

//                                result = getResultResponseValues(outputDirectory);
                                    result = FileUtils.readFileToString(outputDirectory);
                                }
                            }
                        }
                    }
                }
            }
        } catch (ParseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (days > 31)
            throw new OwsException("Invalid time period. Valid period is within a month", "getResult", OwsExceptionCode.INVALID_PARAMETER);
        else if (result == null || result.isEmpty())
            throw new OwsException("Error getting results from providers", "getResult", OwsExceptionCode.NO_APPLICABLE_CODE);
        return result;
    }

    /**
     * Initializes the VelocityEngine instance. A Velocity Template is used to write the getResultTemplate response
     *
     * @param velocityEngine the VelocityEngine instance
     * @throws Exception
     */
    public void init(VelocityEngine velocityEngine) throws Exception {
        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();
    }

    /**
     * Makes a call to a specific URL and
     *
     * @param request the URL to communicate
     * @param method  the type of call (GET or POST)
     * @return a string with the result from the URL call
     * @throws OwsException
     */
    public String makeUrlCall(String request, String method) throws OwsException {
        URL url;
        StringBuilder response = new StringBuilder();
        try {
            url = new URL(request);
            logger.debug("Sending 'GET' request to URL : " + url);
            HttpURLConnection con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod(method.toUpperCase());

            con.setRequestProperty("User-Agent", "Mozilla/5.0");
            int responseCode = con.getResponseCode();
            logger.debug("Response Code : " + responseCode);

            if (responseCode == 200) {
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String inputLine;

                while ((inputLine = in.readLine()) != null) {
                    response.append("\n").append(inputLine);
                }

                in.close();
            }
            con.disconnect();

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ProtocolException e) {
            e.printStackTrace();
        } catch (ConnectException e) {
            logger.error("Timeout connection with remote server", e);
            throw new OwsException("Timeout connection with remote server", "URLCall", OwsExceptionCode.NO_APPLICABLE_CODE);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return response.toString();
    }

    /**
     * Parses a response of a GetResultTemplate request and returns an object containing
     *
     * @param xml GetResultTemplate response
     * @return a ResultTemplate object containing the getResultTemplate response fields
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     */
    public ResultTemplate getResultTemplate(String xml) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser saxParser = factory.newSAXParser();
        final ResultTemplate resultTemplate = new ResultTemplate();

        DefaultHandler handler = new DefaultHandler() {
            SweField sweField = null;
            String value = "";

            public void startElement(String uri, String localName, String qName,
                                     Attributes attributes) throws SAXException {
                if (qName.contains("swe:field")) {
                    sweField = new SweField();
                    sweField.setName(attributes.getValue("name"));

                } else if (qName.contains("swe:Quantity") || qName.contains("swe:Time") && sweField != null) {
                    sweField.setDefinition(attributes.getValue("definition"));
                } else if (qName.contains("swe:uom") && sweField != null) {
                    sweField.setUnit(attributes.getValue("code"));
                } else if (qName.contains("swe:TextEncoding")) {
                    resultTemplate.setTokenSeparator(attributes.getValue("tokenSeparator"));
                    resultTemplate.setBlockSeparator(attributes.getValue("blockSeparator"));
                }
            }

            public void endElement(String uri, String localName,
                                   String qName) throws SAXException {
                if (qName.contains("swe:field")) {
                    if (sweField != null)
                        resultTemplate.getFields().add(sweField);
                }
            }

            public void characters(char ch[], int start, int length) throws SAXException {
                value = new String(ch, start, length);
            }
        };
        saxParser.parse(new InputSource(new StringReader(xml.trim())), handler);
        return resultTemplate;
    }

    public String getResultResponseValues(File url) {
        SAXParserFactory factory;
        SAXParser saxParser;
        final StringBuilder values = new StringBuilder();
        final StringBuilder stringBuilder = new StringBuilder();

        try {
            factory = SAXParserFactory.newInstance();
            saxParser = factory.newSAXParser();
            DefaultHandler handler = new DefaultHandler() {
                boolean track = false;

                public void startElement(String uri, String localName, String qName,
                                         Attributes attributes) throws SAXException {
                    if (qName.contains("sos:resultValues")) {
                        track = true;
                    }
                }

                public void endElement(String uri, String localName,
                                       String qName) throws SAXException {
                    if (qName.contains("sos:resultValues")) {
                        track = false;
                    }
                }

                public void characters(char ch[], int start, int length) throws SAXException {
                    stringBuilder.append(new String(ch, start, length));
                }
            };
            FileInputStream fs = new FileInputStream(url);
            saxParser.parse(fs, handler);
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        List<String> sortingList = new ArrayList<String>(Arrays.asList(stringBuilder.toString().split("@@")));

        Collections.sort(sortingList);

        for (String val : sortingList) {
            values.append(val).append("@@");
        }

        return values.toString();
    }

    public Capability getProcessCapability(String url, final String observedProperty, final String processCapability) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser saxParser = factory.newSAXParser();
        final List<Capability> capabilities = new ArrayList<Capability>();

        DefaultHandler handler = new DefaultHandler() {
            boolean foundCapability = false;
            String value = "";
            Capability capability = null;


            public void startElement(String uri, String localName, String qName,
                                     Attributes attributes) throws SAXException {
                if (qName.contains("ESPAS_ProcessCapability")) {
                    capability = new Capability();
                } else if (qName.contains("observedProperty")) {
                    if (attributes.getValue("xlink:href").equals(observedProperty)) {
                        foundCapability = true;
                        capability.setObservedProperty(attributes.getValue("xlink:href"));
                    }
                } else if (qName.contains("dimensionalityInstance")) {
                    capability.setDimensionalityInstance(attributes.getValue("xlink:href"));
                } else if (qName.contains("dimensionalityTimeline")) {
                    capability.setDimensionalityTimeline(attributes.getValue("xlink:href"));
                } else if (qName.contains("units")) {
                    capability.setUnit(attributes.getValue("xlink:href"));
                }
            }

            public void endElement(String uri, String localName,
                                   String qName) throws SAXException {

                if (qName.contains("ESPAS_ProcessCapability")) {
                    if (foundCapability) {
                        if (processCapability.isEmpty()) {
                            capabilities.add(capability);
                        } else {
                            if (capability.getName().equals(processCapability))
                                capabilities.add(capability);
                        }
                    }
                    capability = null;
                    foundCapability = false;
                } else if (qName.equalsIgnoreCase("name")) {
                    capability.setName(value.trim());
                } else if (qName.equalsIgnoreCase("validmin")) {
                    capability.setValidMin(value.trim());
                } else if (qName.equalsIgnoreCase("validmax")) {
                    capability.setValidMax(value.trim());
                } else if (qName.equalsIgnoreCase("fillvalue")) {
                    capability.setFillValue(value.trim());
                }
            }

            public void characters(char ch[], int start, int length) throws SAXException {
                value = new String(ch, start, length);
            }
        };

        saxParser.parse(url, handler);
        return capabilities.get(0);
    }

    public String getObservedProperty(String observedProperty) {
        String urlLabel = null;
        Pattern pattern = Pattern.compile("http://ontology.espas-fp7.eu/observedProperty/(.*)");
        Matcher matcher = pattern.matcher(observedProperty);
        if (matcher.find()) {
            urlLabel = matcher.group(1);
        }

        return urlLabel;
    }

    public String getResourceTemplate(VelocityEngine velocityEngine, Properties properties,
                                      String process, String namespace, String localID, String version,
                                      String observedProperty, String capabilityName) throws Exception {
        VelocityContext context = new VelocityContext();
        Template velocityTemplate = velocityEngine.getTemplate(properties.getProperty("template"));
        String url;
        String resourceTemplate = null;
        String altName = getObservedProperty(observedProperty);

        String catalogueService = properties.getProperty("service");
        String constraint = "Constraint=%22Type+like+%27" + process + "%27%22";
        String id = "http://resources.espas-fp7.eu/" + process + "/" + namespace + "/" + localID + "/" + version;

        url = catalogueService + "?service=CSW" +
                "&version=2.0.2" +
                "&outputFormat=application/xml" +
                "&request=GetRecordById&resulttype=results" +
                "&outputSchema=http://www.opengis.net/cat/csw/2.0.2" +
                "&Id=" + id;

        logger.debug("Creating getRecordById request for the csw service:\n" + url);

        Capability capability = getProcessCapability(url, observedProperty, capabilityName);

        if (capability != null) {
            StringWriter writer = new StringWriter();

            context.put("capability", "capability");
            context.put("name", capability.getName());
            context.put("definition", capability.getObservedProperty());
            context.put("unit", capability.getUnit());
            context.put("dimensionalityInstance", capability.getDimensionalityInstance());
            context.put("dimensionalityTimeline", capability.getDimensionalityTimeline());
            context.put("validMin", capability.getValidMin());
            context.put("validMax", capability.getValidMax());
            context.put("fillValue", capability.getFillValue());
            context.put("tokenSeparator", properties.getProperty("tokenseparator"));
            context.put("blockSeparator", properties.getProperty("blockseparator"));

            velocityTemplate.merge(context, writer);
            resourceTemplate = writer.toString();

            writer.flush();
            writer.close();
        }

        return resourceTemplate;
    }

    public String getQuery(String resource, String offering) {
        String queryFormat = "select observation from %s where %s='%s'";
        if (resource.matches("computation")) {
            return String.format(queryFormat, "views.observation_computation", resource.toLowerCase(), offering);
        } else if (resource.matches("acquisition")) {
            return String.format(queryFormat, "views.observation_acquisition", resource.toLowerCase(), offering);
        } else if (resource.matches("composite[P|p]rocess")) {
            return String.format(queryFormat, "views.observation_compositeprocess", resource.toLowerCase(), offering);
        } else if (resource.matches("instrument")) {
            return String.format(queryFormat, "views.observation_instrument", resource.toLowerCase(), offering);
        } else if (resource.matches("platform")) {
            return String.format(queryFormat, "views.observation_platform", resource.toLowerCase(), offering);
        } else if (resource.matches("project")) {
            return String.format(queryFormat, "views.observation_project", resource.toLowerCase(), offering);
        } else if (resource.matches("observation[C|c]ollection")) {
            return String.format(queryFormat, "views.observation_observationcollection", resource.toLowerCase(), offering);
        } else if (resource.matches("observation")) {
            return String.format("select id as observation from views.observation where id='%s'", offering);
        }

        return null;
    }

    public void executeQuery(String query, String[] fields, String fieldType, HashSet<String> uniqueResults) throws OwsException {
        try {
            Connection connection = dataSource.getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement(query);
            ResultSet resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                for (String field : fields) {
                    if (fieldType.equalsIgnoreCase("array")) {
                        Array array = resultSet.getArray(field);

                        if (array != null) {
                            String[] results = (String[]) array.getArray();
                            Collections.addAll(uniqueResults, results);
                        }
                    } else {
                        String result = resultSet.getString(field);
                        if (result != null && !result.isEmpty())
                            uniqueResults.add(result);
                    }
                }
            }

            resultSet.close();
            preparedStatement.close();
            connection.close();
        } catch (SQLException e) {
            logger.error("Error when calling remote database", e);
            throw new OwsException("Error when calling remote database", "SOSDatabase", OwsExceptionCode.NO_APPLICABLE_CODE);
        }
    }

    public HashSet<String> getUniqueCapabilityProcesses(String offering, String observedProperty) throws OwsException {
        HashSet<String> processCapabilities = new HashSet<String>();
        Matcher match = Pattern.compile("http://resources\\.espas-fp7\\.eu/(.*)/(.*)/(.*)/([0-9]+)(%23[a-zA-Z0-9\\-\\_]+)?").matcher(offering);

        try {
            if (match.find()) {
                if (match.group(5) != null && !match.group(5).isEmpty()) {
                    processCapabilities.add(offering);
                } else {
                    String resource = match.group(1);
                    String queryObservations = getQuery(resource, offering);

                    String queryProcessCapabilities = "select views.observation_processcapability.capability " +
                            "from views.observation_processcapability join views.processcapability on views.processcapability.id=views.observation_processcapability.capability " +
                            "join (" + queryObservations + ") as observations on views.observation_processcapability.observation=observations.observation " +
                            "and views.processcapability.observedproperty='" + observedProperty + "'";

                    logger.debug("Query for observations:\n" + queryObservations);
                    logger.debug("Query for capability processes:\n" + queryProcessCapabilities);

                    String[] fields = {"capability"};
                    executeQuery(queryProcessCapabilities, fields, "String", processCapabilities);
                }
            }
        } catch (NullPointerException e) {
            logger.error("Error getting ProcessCapability", e);
            throw new OwsException("Error getting ProcessCapability", "GetProcessCapability", OwsExceptionCode.NO_APPLICABLE_CODE);
        }

        return processCapabilities;
    }

    public HashSet<String> getUniqueUrls(String processCapability) throws OwsException {
        HashSet<String> urls = new HashSet<String>();
        Matcher matchCapability = Pattern.compile("http://resources\\.espas-fp7\\.eu/(.*)/(.*)/(.*)/([0-9]+)(#[a-zA-Z0-9\\-\\_]+)?").matcher(processCapability);
        if (matchCapability.find()) {
            String namespace = matchCapability.group(2);

            String queryUrls = "select url from public.dataprovider where namespace = 'http://resources.espas-fp7.eu/provider/" + namespace + "';";
            String[] fieldsUrls = {"url"};
            executeQuery(queryUrls, fieldsUrls, "string", urls);
        }

        return urls;
    }

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public String getTemplateFile() {
        return templateFile;
    }

    public void setTemplateFile(String template) {
        this.templateFile = template;
    }

    public String getTokenseparator() {
        return tokenseparator;
    }

    public void setTokenseparator(String tokenseparator) {
        this.tokenseparator = tokenseparator;
    }

    public String getBlockseparator() {
        return blockseparator;
    }

    public void setBlockseparator(String blockseparator) {
        this.blockseparator = blockseparator;
    }

    public SOSRequestManager getSosManager() {
        return sosManager;
    }

    public void setSosManager(SOSRequestManager sosManager) {
        this.sosManager = sosManager;
    }
}
