package eu.dnetlib.msro.logging;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.beans.factory.annotation.Required;

import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;

public class DnetLoggerMongoDao implements DnetLoggerDao {

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

	private MongoDatabase db;

	@Override
	public void init(final String collection) {
		final MongoCollection<Document> coll = db.getCollection(collection);
		if (coll == null) {
			log.info(String.format("creating collection %s", collection));
			db.createCollection(collection);
		}
	}

	@Override
	public void configureIndex(final String collection, final Map<String, IndexConf> conf) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		coll.dropIndexes();
		for (final String key : conf.keySet()) {
			coll.createIndex(new BasicDBObject(key, 1));
		}
	}

	@Override
	public void writeLog(final String collection, final Map<String, Object> map) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		final DBObject obj = BasicDBObjectBuilder.start(replaceKeyNames(map)).get();
		coll.insertOne(obj);
	}

	private Map<String, Object> replaceKeyNames(final Map<String, Object> inputMap) {
		final Map<String, Object> outMap = new HashMap<>();

		if (inputMap != null) {
			for (final Entry<String, Object> e : inputMap.entrySet()) {
				final String k = e.getKey();
				if (StringUtils.isNotBlank(k)) {
					final Object v = e.getValue();
					outMap.put(k.replaceAll("\\.", "_"), v != null ? v : "null");
				}
			}
		}
		return outMap;
	}

	@Override
	public Map<String, String> findOne(final String collection, final String key, final String value) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		final DBObject obj = coll.find(Filters.eq(key, value)).sort(new BasicDBObject(LogMessage.LOG_DATE_FIELD, -1)).first();
		return dbObject2Map(obj);
	}

	@Override
	public Map<String, String> findOne(final String collection, final Map<String, Object> criteria) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		final DBObject obj = coll.find(new BasicDBObject(criteria)).sort(new BasicDBObject(LogMessage.LOG_DATE_FIELD, -1)).first();
		return dbObject2Map(obj);
	}

	@Override
	public Iterator<Map<String, String>> obtainLogIterator(final String collection) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		return iter(collection, coll.find().iterator());
	}

	@Override
	public Iterator<Map<String, String>> find(final String collection, final String key, final String value) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		return iter(collection, coll.find(Filters.eq(key, value)).iterator());
	}

	@Override
	public Iterator<Map<String, String>> find(final String collection, final Map<String, Object> criteria) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		return iter(collection, coll.find(new BasicDBObject(criteria)).iterator());
	}

	@Override
	public Iterator<Map<String, String>> findByDateRange(final String collection, final LocalDateTime startDate, final LocalDateTime endDate) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		final Bson dateQuery = dateRangeQuery(startDate, endDate);
		// System.out.println(mongoQueryObject);
		return iter(collection, coll.find(dateQuery).iterator());
	}

	@Override
	public Iterator<Map<String, String>> findByDateRange(final String collection,
			final LocalDateTime startDate,
			final LocalDateTime endDate,
			final String key,
			final String value) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		final Bson dateQuery = dateRangeQuery(startDate, endDate);
		final BasicDBObject customQuery = new BasicDBObject(key, value);

		return iter(collection, coll.find(Filters.and(dateQuery, customQuery)).iterator());
	}

	@Override
	public Iterator<Map<String, String>> findByDateRange(final String collection,
			final LocalDateTime startDate,
			final LocalDateTime endDate,
			final Map<String, Object> criteria) {
		final MongoCollection<DBObject> coll = db.getCollection(collection, DBObject.class);
		final Bson dateQuery = dateRangeQuery(startDate, endDate);
		final BasicDBObject customQuery = new BasicDBObject(criteria);

		return iter(collection, coll.find(Filters.and(dateQuery, customQuery)).iterator());
	}

	private Bson dateRangeQuery(final LocalDateTime startDate, final LocalDateTime endDate) {

		final long start = startDate.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
		final long end = endDate.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();

		final Bson dateFilter =
				Filters.and(Filters.gte(LogMessage.LOG_DATE_FIELD, start), Filters.lt(LogMessage.LOG_DATE_FIELD, end));
		log.debug("Date filter created: " + dateFilter);
		// return new BasicDBObject(LogMessage.LOG_DATE_FIELD, BasicDBObjectBuilder.start("$gte", startDate.getTime()).append("$lt",
		// endDate.getTime()).get());
		return dateFilter;
	}

	private Iterator<Map<String, String>> iter(final String collection, final MongoCursor<DBObject> cursor) {
		return new Iterator<Map<String, String>>() {

			@Override
			public boolean hasNext() {
				return cursor.hasNext();
			}

			@Override
			public Map<String, String> next() {
				return dbObject2Map(cursor.next());
			}

			@Override
			public void remove() {
				throw new RuntimeException("NOT IMPLEMENTED");
			}
		};
	}

	public MongoDatabase getDb() {
		return db;
	}

	@Required
	public void setDb(final MongoDatabase db) {
		this.db = db;
	}

	private Map<String, String> dbObject2Map(final DBObject obj) {
		return obj != null ? obj.keySet()
				.stream()
				.filter(k -> !k.startsWith("_"))
				.collect(Collectors.toMap(k -> k, k -> "" + obj.get(k))) : new HashMap<>();

	}

}
