package eu.dnetlib.lbs.subscriptions;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.search.MatchQuery.ZeroTermsQuery;

import eu.dnetlib.lbs.elasticsearch.Event;
import eu.dnetlib.lbs.events.input.MapValueType;
import eu.dnetlib.lbs.utils.DateParser;

public class MapCondition {

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

	private String field;
	private MapValueType fieldType;
	private ConditionOperator operator;
	private List<ConditionParams> listParams = new ArrayList<>();

	public MapCondition() {}

	public MapCondition(final String field, final MapValueType fieldType, final ConditionOperator operator, final List<ConditionParams> listParams) {
		this.field = field;
		this.fieldType = fieldType;
		this.operator = operator;
		this.listParams = listParams;
	}

	public String getField() {
		return this.field;
	}

	public void setField(final String field) {
		this.field = field;
	}

	public MapValueType getFieldType() {
		return this.fieldType;
	}

	public void setFieldType(final MapValueType fieldType) {
		this.fieldType = fieldType;
	}

	public ConditionOperator getOperator() {
		return this.operator;
	}

	public void setOperator(final ConditionOperator operator) {
		this.operator = operator;
	}

	public List<ConditionParams> getListParams() {
		return this.listParams;
	}

	public void setListParams(final List<ConditionParams> listParams) {
		this.listParams = listParams;
	}

	public boolean verifyEvent(final Event event) {
		final Object val = event.getMap().get(this.field);

		return getListParams().stream().anyMatch(cp -> {
			try {
				switch (this.fieldType) {
				case STRING:
					return cp.verify(val.toString(), this.operator);
				case INTEGER:
					return cp.verify(val instanceof Long ? (long) val : NumberUtils.toInt(val.toString()), this.operator);
				case FLOAT:
					return cp.verify(val instanceof Double ? (double) val : NumberUtils.toDouble(val.toString()), this.operator);
				case BOOLEAN:
					return cp.verify(val instanceof Boolean ? (boolean) val : val.toString().equalsIgnoreCase("true"), this.operator);
				case DATE:
					return cp.verify(val instanceof Date ? (Date) val : DateParser.parse(val.toString()), this.operator);
				case LIST_STRING:
					return ((List<?>) val).stream().map(Object::toString).anyMatch(s -> cp.verify(s, this.operator));
				case LIST_DATE:
					return ((List<?>) val).stream().map(o -> o instanceof Date ? (Date) o : DateParser.parse(o.toString()))
							.anyMatch(s -> cp.verify(s, this.operator));
				case LIST_INTEGER:
					return ((List<?>) val).stream().map(Object::toString).map(NumberUtils::toInt).anyMatch(n -> cp.verify(n, this.operator));
				case LIST_FLOAT:
					return ((List<?>) val).stream().map(Object::toString).map(NumberUtils::toDouble).anyMatch(n -> cp.verify(n, this.operator));
				default:
					break;
				}
			} catch (final Throwable e) {
				log.error("Error verifying condition " + this + " on event: " + event, e);
			}
			return false;
		});
	}

	public QueryBuilder asQueryBuilder() {
		if (this.listParams == null || this.listParams.isEmpty()) { return null; }

		if (this.operator == ConditionOperator.RANGE) {
			if (this.listParams.size() == 1) {
				return createSimpleRangeOperator(this.listParams.get(0));
			} else {
				return createListRangeOperator(this.listParams);
			}
		} else {
			if (this.listParams.size() == 1) {
				return createSimpleOperator(this.listParams.get(0));
			} else {
				return createListOperator(this.listParams);
			}
		}
	}

	private QueryBuilder createSimpleOperator(final ConditionParams p) {
		if (StringUtils.isNotBlank(p.getValue())) {
			return QueryBuilders.matchQuery("map." + this.field, convertToType(p.getValue())).operator(Operator.AND)
					.zeroTermsQuery(ZeroTermsQuery.ALL);
		} else {
			return null;
		}
	}

	private QueryBuilder createListOperator(final List<ConditionParams> list) {
		final BoolQueryBuilder query = QueryBuilders.boolQuery();
		for (final ConditionParams p : list) {
			query.should(QueryBuilders.matchQuery("map." + this.field, convertToType(p.getValue())).operator(Operator.AND)
					.zeroTermsQuery(ZeroTermsQuery.ALL));
		}
		return query;
	}

	private QueryBuilder createSimpleRangeOperator(final ConditionParams p) {
		if (StringUtils.isNotBlank(p.getValue()) || StringUtils.isNotBlank(p.getOtherValue())) {
			return QueryBuilders.rangeQuery("map." + this.field)
					.from(convertToType(p.getValue()))
					.to(convertToType(p.getOtherValue()));
		} else {
			return null;
		}
	}

	private QueryBuilder createListRangeOperator(final List<ConditionParams> list) {
		final BoolQueryBuilder query = QueryBuilders.boolQuery();

		for (final ConditionParams p : list) {
			query.should(QueryBuilders
					.rangeQuery("map." + this.field)
					.from(convertToType(p.getValue()))
					.to(convertToType(p.getOtherValue())));
		}
		return query;
	}

	private Object convertToType(final String s) {
		switch (this.fieldType) {
		case STRING:
		case LIST_STRING:
			return s;
		case INTEGER:
		case LIST_INTEGER:
			return NumberUtils.toLong(s, 0);
		case FLOAT:
		case LIST_FLOAT:
			return NumberUtils.toDouble(s, 0);
		case DATE:
		case LIST_DATE:
			return DateParser.parse(s);
		case BOOLEAN:
		case LIST_BOOLEAN:
			return "true".equalsIgnoreCase(s);
		}
		return null;
	}

	@Override
	public String toString() {
		return String.format("[ %s %s %s ]", this.field, this.operator, this.listParams);
	}
}
