package eu.dnetlib.openaire.dsm.dao;

import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.jpa.domain.Specification;

import eu.dnetlib.enabling.datasources.common.DsmRuntimeException;
import eu.dnetlib.openaire.dsm.domain.db.DatasourceApiDbEntry;
import eu.dnetlib.openaire.dsm.domain.db.DatasourceDbEntry;
import eu.dnetlib.openaire.exporter.model.dsm.FilterName;
import eu.dnetlib.openaire.exporter.model.dsm.RequestFilter;
import eu.dnetlib.openaire.exporter.model.dsm.RequestSort;
import eu.dnetlib.openaire.exporter.model.dsm.RequestSortOrder;

public class DatasourceSpecs {

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

	public static final String WILDCARD = "%";

	public static Specification<DatasourceDbEntry> dsRegisteredbyNotNullSpec() {
		return (ds, query, cb) -> cb.and(cb.isNull(ds.get(FilterName.registeredby.name())).not(), cb.isNull(ds.get("registrationdate")).not());
	}

	public static Specification<DatasourceDbEntry> dsSpec(final RequestSort requestSortBy, final RequestSortOrder order, final RequestFilter requestFilter) {
		log.debug(String.format("RequestFilter:'%s', RequestSort:'%s', RequestSortOrder:'%s'", requestFilter, requestSortBy, order));
		return (ds, query, cb) -> {
			final Predicate p = cb.conjunction();
			final List<Expression<Boolean>> expressions = p.getExpressions();

			expressions.add(cb.equal(ds.get("eoscType"), "Data Source"));
			expressions.add(cb.equal(ds.get("dedupMainService"), true));

			if (requestFilter != null) {
				requestFilter.entrySet()
					.stream()
					.forEach(e -> {
						switch (FilterName.type(e.getKey())) {

						case exact:
							expressions.add(exactSearch(ds, cb, e));

							break;
						case search:
							expressions.add(likeSearch(ds, cb, e));

							break;

						case multiSearch:
							expressions.add(likeSearchList(ds, cb, e));

							break;
						case searchOrgs:
							// search by case insensitive organization's country
							expressions.add(cb.equal(cb.lower(ds.join("organizations").get(FilterName.country.name())), getValue(e)));
							break;
						}
					});
			}
			if (requestSortBy != null) {
				if (order != null) {
					final Path<Object> orderField = ds.get(requestSortBy.name());
					switch (order) {
					case ASCENDING:
						query.orderBy(cb.asc(orderField));
						break;
					case DESCENDING:
						query.orderBy(cb.desc(orderField));
						break;
					}
				}
			}
			query.distinct(true);

			return p;
		};
	}

	public static Specification<DatasourceApiDbEntry> apiSpec(final RequestFilter requestFilter) {
		log.debug(String.format("RequestFilter:'%s'", requestFilter));
		return (api, query, cb) -> {
			final Predicate p = cb.conjunction();
			if (requestFilter != null) {
				final List<Expression<Boolean>> expressions = p.getExpressions();
				requestFilter.entrySet()
					.stream()
					.forEach(e -> {
						switch (FilterName.type(e.getKey())) {

						case exact:
							expressions.add(exactSearch(api, cb, e));
							break;
						case search:

							expressions.add(likeSearch(api, cb, e));
							break;

						case multiSearch:
							expressions.add(likeSearchList(api, cb, e));

							break;
						case searchOrgs:
							throw new DsmRuntimeException("not implemented");
						}
					});
			}
			query.distinct(true);
			return p;
		};
	}

	// HELPERS

	// substring, case insensitive, like based search
	private static Predicate likeSearch(final Root<?> r, final CriteriaBuilder cb, final Entry<FilterName, Object> e) {
		return cb.like(cb.lower(r.get(e.getKey().name())), WILDCARD + getValue(e) + WILDCARD);
	}

	// substring, case insensitive, like based search
	private static Predicate likeSearchList(final Root<?> r, final CriteriaBuilder cb, final Entry<FilterName, Object> e) {
		final String key = e.getKey().name();

		final Predicate[] arr =
			Arrays.stream(e.getValue().toString().split(","))
				.map(String::trim)
				.map(String::toLowerCase)
				.filter(StringUtils::isNotBlank)
				.map(s -> cb.like(cb.lower(r.get(key)), WILDCARD + s + WILDCARD))
				.toArray(size -> new Predicate[size]);

		return cb.or(arr);
	}

	// search by ID, managed. exact match
	private static Predicate exactSearch(final Root<?> r, final CriteriaBuilder cb, final Entry<FilterName, Object> e) {
		return cb.equal(r.get(e.getKey().name()), getValue(e));
	}

	private static Object getValue(final Entry<FilterName, Object> e) {
		if (e.getValue() instanceof String) {
			final String s = (String) e.getValue();

			if (!e.getKey().equals(FilterName.country)) {
				final Boolean b = BooleanUtils.toBooleanObject(s);
				if (b != null) { return b; }
			}
			return e.getKey().equals(FilterName.id) ? s : StringUtils.lowerCase(s);
		}
		if (e.getValue() instanceof Boolean) { return e.getValue(); }

		return e.getValue();
	}

}
