package eu.eudat.logic.proxy.config.configloaders;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import eu.eudat.logic.proxy.config.ExternalUrls;
import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProviders;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

@Service("configLoader")
//@Profile("devel")
public class DefaultConfigLoader implements ConfigLoader {
    private static final Logger logger = LoggerFactory.getLogger(DefaultConfigLoader.class);
    private static final ObjectMapper mapper = new ObjectMapper();

    private ExternalUrls externalUrls;
    private List<String> rdaProperties;
    private XWPFDocument document;
    private XWPFDocument datasetDocument;
    private ConfigurableProviders configurableProviders;
    private Map<String, String> keyToSourceMap;

    @Autowired
    private Environment environment;

    private void setExternalUrls() {
        String fileUrl = this.environment.getProperty("configuration.externalUrls");
        logger.info("Loaded also config file: " + fileUrl);
        InputStream is = null;
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(ExternalUrls.class);
            Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
            is = getStreamFromPath(fileUrl);
            externalUrls = (ExternalUrls) jaxbUnmarshaller.unmarshal(is);
        } catch (Exception ex) {
           logger.error("Cannot find resource", ex);
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException | NullPointerException e) {
                logger.warn("Warning: Could not close a stream after reading from file: " + fileUrl, e);
            }
        }
    }

    private void setRdaProperties() {
        String filePath = environment.getProperty("configuration.rda");
        logger.info("Loaded also config file: " + filePath);
        BufferedReader reader;
        List<String> rdaList = new LinkedList<>();
        try {
            reader = new BufferedReader(new InputStreamReader(getStreamFromPath(filePath)));
            String line = reader.readLine();
            while (line != null) {
                rdaList.add(line);
                line = reader.readLine();
            }
            reader.close();
        } catch (IOException | NullPointerException e) {
            logger.error(e.getMessage(), e);
        }

        rdaProperties = rdaList;
    }

    private void setDocument() {
        String filePath = environment.getProperty("configuration.h2020template");
        logger.info("Loaded also config file: " + filePath);
        InputStream is = null;
        try {
            is = getStreamFromPath(filePath);
            this.document = new XWPFDocument(is);
        } catch (IOException | NullPointerException e) {
            logger.error(e.getMessage(), e);
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                logger.warn("Warning: Could not close a stream after reading from file: " + filePath, e);
            }
        }
    }

    private void setDatasetDocument() {
        String filePath = environment.getProperty("configuration.h2020datasettemplate");
        logger.info("Loaded also config file: " + filePath);
        InputStream is = null;
        try {
            is = getStreamFromPath(filePath);
            this.datasetDocument = new XWPFDocument(is);
        } catch (IOException | NullPointerException e) {
            logger.error(e.getMessage(), e);
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                logger.warn("Warning: Could not close a stream after reading from file: " + filePath, e);
            }
        }
    }

    private void setConfigurableProviders() {
        String filePath = environment.getProperty("configuration.configurable_login_providers");
        logger.info("Loaded also config file: " + filePath);
        InputStream is = null;
        try {
            is = getStreamFromPath(filePath);
            ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            this.configurableProviders = mapper.readValue(is, ConfigurableProviders.class);
        } catch (IOException | NullPointerException e) {
            logger.error(e.getMessage(), e);
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
               logger.warn("Warning: Could not close a stream after reading from file: " + filePath, e);
            }
        }
    }

    private void setKeyToSourceMap() {
        String filePath = this.environment.getProperty("configuration.externalUrls");
        logger.info("Loaded also config file: " + filePath);
        Document doc = getXmlDocumentFromFilePath(filePath);
        if (doc == null) {
            this.keyToSourceMap = null;
            return;
        }
        String xpathExpression = "//key";
        Map<String, String> keysToSourceMap = new HashMap<>();
        List<String> keys = getXmlValuesFromXPath(doc, xpathExpression);
        keys = keys.stream().distinct().collect(Collectors.toList());
        for (String key : keys) {
            String sourceExpression = String.format("//urlConfig[key='%s']/label", key);
            List<String> sources = getXmlValuesFromXPath(doc, sourceExpression);
            if (sources.size() != 0) {
                keysToSourceMap.put(key, sources.get(0));
            }
        }
        this.keyToSourceMap = keysToSourceMap;
    }



    public ExternalUrls getExternalUrls() {
        if (externalUrls == null) {
            externalUrls = new ExternalUrls();
            this.setExternalUrls();
        } else {
            while (externalUrls.getMaxresults() == null) {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }
        return externalUrls;
    }

    public List<String> getRdaProperties() {
        if (rdaProperties == null) {
            rdaProperties = new ArrayList<>();
            this.setRdaProperties();
        } else {
            while (rdaProperties.size() == 0) {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }
        return rdaProperties;
    }

    public XWPFDocument getDocument() {
        this.setDocument();
        return document;
    }

    public XWPFDocument getDatasetDocument() {
        this.setDatasetDocument();
        return datasetDocument;
    }

    public ConfigurableProviders getConfigurableProviders() {
        if (configurableProviders == null) {
            configurableProviders = new ConfigurableProviders();
            this.setConfigurableProviders();
        } else {
            while (configurableProviders.getProviders().size() == 0) {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }
        return configurableProviders;
    }

    public Map<String, String> getKeyToSourceMap() {
        if (keyToSourceMap == null) {
            keyToSourceMap = new HashMap<>();
            this.setKeyToSourceMap();
        } else {
            while (keyToSourceMap.size() == 0) {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }
        return keyToSourceMap;
    }


    private Document getXmlDocumentFromFilePath(String filePath) {
        InputStream is = null;
        Document doc;
        try {
            is = getStreamFromPath(filePath);
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            doc = documentBuilder.parse(is);
            return doc;
        } catch (IOException | ParserConfigurationException | SAXException | NullPointerException e) {
            logger.error(e.getMessage(), e);
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
               logger.warn("Warning: Could not close a stream after reading from file: " + filePath, e);
            }
        }
        return null;
    }

    private List<String> getXmlValuesFromXPath(Document doc, String expression) {
        XPath xPath = XPathFactory.newInstance().newXPath();
        NodeList nodeList = null;
        List<String> values = new LinkedList<>();
        try {
            nodeList = (NodeList) xPath.compile(expression).evaluate(doc, XPathConstants.NODESET);
        } catch (XPathExpressionException e) {
            logger.error(e.getMessage(), e);
        }
        if (nodeList != null) {
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (node.hasChildNodes()) {
                    values.add(nodeList.item(i).getChildNodes().item(0).getNodeValue());
                }
            }
        }
        return values;
    }

    private InputStream getStreamFromPath(String filePath) {
        try {
            return new FileInputStream(filePath);
        } catch (FileNotFoundException e) {
            logger.info("loading from classpath");
            return getClass().getClassLoader().getResourceAsStream(filePath);
        }
    }
}
