package eu.dnetlib.openaire.common;

import static eu.dnetlib.openaire.common.Utils.escape;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.google.common.collect.Lists;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;

import eu.dnetlib.DnetOpenaireExporterProperties;
import eu.dnetlib.enabling.datasources.common.DsmException;
import eu.dnetlib.enabling.datasources.common.DsmRuntimeException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpService;
import eu.dnetlib.openaire.context.ContextMappingUtils;
import eu.dnetlib.openaire.dsm.dao.utils.IndexDsInfo;
import eu.dnetlib.openaire.exporter.model.context.Context;

/**
 * Created by claudio on 20/10/2016.
 */
@Component
public class ISClientImpl implements ISClient {

	private static final Log log = LogFactory.getLog(ISClientImpl.class);

	@Autowired
	private DnetOpenaireExporterProperties config;

	@Autowired
	private ISLookUpService isLookUpService;

	@Override
	@Cacheable("indexdsinfo-cache")
	public IndexDsInfo calculateCurrentIndexDsInfo() throws Exception {
		log.warn("calculateCurrentIndexDsInfo(): not using cache");
		final String[] arr;
		try {
			arr = _isLookUp(_getQuery(config.getFindIndexDsInfo())).split("@@@");
			return new IndexDsInfo(
				_isLookUp(_getQuery(config.getFindSolrIndexUrl())),
				arr[0].trim(), arr[1].trim(), arr[2].trim());
		} catch (IOException | ISLookUpException e) {
			throw new DsmException("unable fetch index DS information from IS");
		}
	}

	@Override
	@Cacheable("objectstoreid-cache")
	public String getObjectStoreId(final String dsId) throws Exception {
		log.warn(String.format("getObjectStoreId(%s): not using cache", dsId));
		try {
			final String xqueryTemplate = _getQuery(config.getFindObjectStore());
			return _isLookUp(String.format(xqueryTemplate, dsId));
		} catch (IOException | ISLookUpException e) {
			throw new DsmException("unable to find objectstore for ds " + dsId);
		}
	}

	@Override
	@Cacheable("context-cache-funder")
	public Map<String, Context> getFunderContextMap() throws IOException {
		return _processContext(_getQuery(config.getFindFunderContexts()));
	}

	@Override
	@Cacheable("context-cache-community")
	public Map<String, Context> getCommunityContextMap() throws IOException {
		return _processContext(_getQuery(config.getFindCommunityContexts()));
	}

	@Override
	@Cacheable("context-cache")
	public Map<String, Context> getContextMap(final List<String> type) throws IOException {
		if (Objects.isNull(type) || type.isEmpty()) {
			return _processContext(_getQuery(config.getFindContextProfiles()));
		} else {
			final String xqueryTemplate = _getQuery(config.getFindContextProfilesByType());

			final String xquery = String.format(xqueryTemplate, type.stream()
				.map(t -> String.format("./RESOURCE_PROFILE/BODY/CONFIGURATION/context/@type = '%s'", t))
				.collect(Collectors.joining(" or ")));

			return _processContext(xquery);
		}
	}

	@Override
	@CacheEvict(value = {
		"context-cache", "context-cache-funder"
	}, allEntries = true)
	public void updateContextParam(final String id, final String name, final String value, final boolean toEscape) {
		if (getSize(id, name) > 0) {
			try {
				_quickSeachProfile(getXQuery(id, name, value, toEscape));
			} catch (final ISLookUpException e) {
				throw new DsmRuntimeException(String.format("unable to update context param [id: %s, name: %s, value: %s]", id, name, value), e);
			}
		} else {
			try {
				_quickSeachProfile(getInsertXQuery(id, name, value, toEscape));
			} catch (final ISLookUpException e) {
				throw new DsmRuntimeException(String.format("unable to insert context param [id: %s, name: %s, value: %s]", id, name, value), e);
			}
		}

	}

	private int getSize(final String id, final String name) {
		int size = 0;
		try {
			size = _quickSeachProfile(String
				.format("for $x in  collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')  where $x//context[@id='%s']/param[@name='%s'] return $x", id, name))
					.size();
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException("unable to execute search query", e);
		}
		return size;
	}

	@Override
	@CacheEvict(value = {
		"context-cache", "context-cache-funder"
	}, allEntries = true)
	public void updateContextAttribute(final String id, final String name, final String value) {
		final Escaper esc = XmlEscapers.xmlAttributeEscaper();
		try {
			_quickSeachProfile(String.format("update value collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']/@%s with '%s'", id, name, escape(esc, value)));
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException(String.format("unable update context attribute [id: %s, name: %s, data: %s]", id, name, value), e);
		}
	}

	@Override
	@CacheEvict(value = {
		"context-cache", "context-cache-funder"
	}, allEntries = true)
	public void addConcept(final String id, final String categoryId, final String data) {
		try {
			_quickSeachProfile(String.format("update insert %s into collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']/category[./@id = '%s']", data, id, categoryId));
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException(String.format("unable add concept [id: %s, categoryId: %s, data: %s]", id, categoryId, data), e);
		}
	}

	@Override
	@CacheEvict(value = {
		"context-cache", "context-cache-funder"
	}, allEntries = true)
	public void removeConcept(final String id, final String categoryId, final String conceptId) {
		try {
			_quickSeachProfile(String.format("for $concept in collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']" +
				"/category[./@id = '%s']/concept[./@id = '%s'] " +
				"return update delete $concept", id, categoryId, conceptId));
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException(String.format("unable remove concept [id: %s, categoryId: %s, conceptId: %s]", id, categoryId, conceptId), e);
		}
	}

	@Override
	@CacheEvict(value = {
		"context-cache", "context-cache-community", "context-cache-funder"
	}, allEntries = true)
	public void updateConceptAttribute(final String id, final String name, final String value) {
		final Escaper esc = XmlEscapers.xmlAttributeEscaper();
		try {
			_quickSeachProfile(String.format("update value collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context/category/concept[./@id = '%s']/@%s with '%s'", id, name, escape(esc, value)));
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException(String.format("unable update concept attribute [id: %s, name: %s, value: %s]", id, name, value), e);
		}

	}

	@Override
	@CacheEvict(value = {
		"context-cache", "context-cache-funder"
	}, allEntries = true)
	public void updateConceptParam(final String id, final String name, final String value) {
		try {
			_quickSeachProfile(getConceptXQuery(id, name, value));
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException(String.format("unable update concept param [id: %s, name: %s, value: %s]", id, name, value), e);
		}
	}

	@Override
	@CacheEvict(value = {
		"context-cache", "context-cache-funder"
	}, allEntries = true)
	public void updateConceptParamNoEscape(final String id, final String name, final String value) {
		try {
			_quickSeachProfile(getConceptXQueryNoEscape(id, name, value));
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException(String.format("unable update concept param [id: %s, name: %s, value: %s]", id, name, value), e);
		}
	}

	/// HELPERS

	private String getInsertXQuery(final String id, final String paramName, final String paramValue, final boolean toEscape) {
		String value;
		if (toEscape) {
			value = escape(XmlEscapers.xmlContentEscaper(), paramValue);
		} else {
			value = paramValue;
		}
		if (StringUtils.isNotBlank(value)) {
			return String.format("update insert <param name='%s'>%s</param> into collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']", paramName, value, id);
		} else {
			return String.format("update insert <param name='%s'/> into collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']", paramName, id);
		}
	}

	private String getXQuery(final String id, final String name, final String paramValue, final boolean toEscape) {
		String value = paramValue;
		if (toEscape) {
			value = escape(XmlEscapers.xmlContentEscaper(), paramValue);
		}

		if (StringUtils.isNotBlank(value)) {
			return String.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']/param[./@name = '%s'] with <param name='%s'>%s</param>", id, name, name, value);
		} else {
			return String.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
				"/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']/param[./@name = '%s'] with <param name='%s'/>", id, name, name);
		}
	}

	// private String getXQueryNoEscape(final String id, final String name, final String value) {
	// if (StringUtils.isNotBlank(value)) {
	// return String.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
	// "/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']/param[./@name = '%s'] with <param name='%s'>%s</param>", id, name, name,
	// value);
	// } else {
	// return String.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')" +
	// "/RESOURCE_PROFILE/BODY/CONFIGURATION/context[./@id = '%s']/param[./@name = '%s'] with <param name='%s'/>", id, name, name);
	// }
	// }

	private String getConceptXQuery(final String id, final String name, final String value) {
		final Escaper esc = XmlEscapers.xmlContentEscaper();
		if (StringUtils.isNotBlank(value)) {
			return String.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')//" +
				"concept[./@id = '%s']/param[./@name = '%s'] with <param name='%s'>%s</param>", id, name, name, escape(esc, value));
		} else {
			return String
				.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')//concept[./@id = '%s']/param[./@name = '%s'] with <param name='%s'/>", id, name, name);
		}
	}

	private String getConceptXQueryNoEscape(final String id, final String name, final String value) {

		if (StringUtils.isNotBlank(value)) {
			return String.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')//" +
				"concept[./@id = '%s']/param[./@name = '%s'] with <param name='%s'>%s</param>", id, name, name, value);
		} else {
			return String
				.format("update replace collection('/db/DRIVER/ContextDSResources/ContextDSResourceType')//concept[./@id = '%s']/param[./@name = '%s'] with <param name='%s'/>", id, name, name);
		}
	}

	private Map<String, Context> _processContext(final String xquery) throws IOException {
		return _processContext(new LinkedBlockingQueue<>(), xquery);
	}

	private Map<String, Context> _processContext(final Queue<Throwable> errors, final String xquery) throws IOException {
		try {
			return getContextProfiles(errors, xquery).stream()
				.filter(StringUtils::isNotBlank)
				.map(s -> ContextMappingUtils.parseContext(s, errors))
				.collect(Collectors.toMap(Context::getId, Function.identity(), (c1, c2) -> {
					log.warn(String.format("found duplicate context profile '%s'", c1.getId()));
					return c1;
				}));
		} finally {
			if (!errors.isEmpty()) {
				log.error(errors);
				errors.forEach(Throwable::printStackTrace);
			}
		}
	}

	private List<String> getContextProfiles(final Queue<Throwable> errors, final String xquery) throws IOException {
		log.warn("getContextProfiles(): not using cache");
		try {
			return _quickSeachProfile(xquery);
		} catch (final ISLookUpException e) {
			throw new DsmRuntimeException("unable to get context profiles", e);
		}
	}

	private String _getQuery(final ClassPathResource resource) throws IOException {
		return IOUtils.toString(resource.getInputStream(), Charset.defaultCharset());
	}

	private String _isLookUp(final String xquery) throws ISLookUpException {
		log.debug(String.format("running xquery:\n%s", xquery));
		// log.debug(String.format("query result: %s", res));
		return isLookUpService.getResourceProfileByQuery(xquery);
	}

	private List<String> _quickSeachProfile(final String xquery) throws ISLookUpException {
		final List<String> res = Lists.newArrayList();

		log.debug(String.format("running xquery:\n%s", xquery));
		final List<String> list = isLookUpService.quickSearchProfile(xquery);
		if (list != null) {
			res.addAll(list);
		}
		log.debug(String.format("query result size: %s", res.size()));
		return res;
	}

	@Override
	@CacheEvict(cacheNames = {
		"context-cache", "indexdsinfo-cache", "objectstoreid-cache"
	}, allEntries = true)
	@Scheduled(fixedDelayString = "${openaire.exporter.cache.ttl}")
	public void dropCache() {
		log.debug("dropped dsManager IS cache");
	}

}
