/*
 * Hibernate OGM, Domain model persistence for NoSQL datastores
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.ogm.datastore.redis.utils;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.ogm.OgmSessionFactory;
import org.hibernate.ogm.cfg.OgmProperties;
import org.hibernate.ogm.datastore.document.options.AssociationStorageType;
import org.hibernate.ogm.datastore.redis.AbstractRedisDialect;
import org.hibernate.ogm.datastore.redis.Redis;
import org.hibernate.ogm.datastore.redis.RedisHashDialect;
import org.hibernate.ogm.datastore.redis.RedisJsonDialect;
import org.hibernate.ogm.datastore.redis.RedisProperties;
import org.hibernate.ogm.datastore.redis.dialect.value.Entity;
import org.hibernate.ogm.datastore.redis.impl.RedisDatastoreProvider;
import org.hibernate.ogm.datastore.spi.DatastoreConfiguration;
import org.hibernate.ogm.datastore.spi.DatastoreProvider;
import org.hibernate.ogm.model.key.spi.EntityKey;
import org.hibernate.ogm.utils.TestHelper;
import org.hibernate.ogm.utils.GridDialectTestHelper;
import com.lambdaworks.redis.api.sync.RedisKeyCommands;
import com.lambdaworks.redis.cluster.api.sync.RedisClusterCommands;
import org.json.JSONException;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class RedisTestHelper implements GridDialectTestHelper {

	static {
		// Read host and port from environment variable
		// Maven's surefire plugin set it to the string 'null'
		String redisHostName = System.getenv( "REDIS_HOSTNAME" );
		if ( isNotNull( redisHostName ) ) {
			System.getProperties().setProperty( OgmProperties.HOST, redisHostName );
		}
		String redisPort = System.getenv( "REDIS_PORT" );
		if ( isNotNull( redisPort ) ) {
			System.getProperties().setProperty( OgmProperties.PORT, redisPort );
		}

		String redisCluster = System.getenv( "REDIS_CLUSTER" );
		if ( isNotNull( redisCluster ) ) {
			System.getProperties().setProperty( RedisProperties.CLUSTER, redisCluster );
		}
	}

	private static boolean isNotNull(String value) {
		return value != null && value.length() > 0 && !"null".equals( value );
	}

	@Override
	public Map<String, Object> extractEntityTuple(Session session, EntityKey key) {
		RedisDatastoreProvider castProvider = getProvider( session.getSessionFactory() );
		AbstractRedisDialect gridDialect = getGridDialect( castProvider );

		if ( gridDialect instanceof RedisJsonDialect ) {
			return extractFromJsonDialect( key, (RedisJsonDialect) gridDialect );
		}

		if ( gridDialect instanceof RedisHashDialect ) {
			return extractFromHashDialect( session.getSessionFactory(), key, (RedisHashDialect) gridDialect );
		}

		throw new IllegalStateException( "Unsupported dialect " + gridDialect );
	}


	private Map<String, Object> extractFromJsonDialect(
			EntityKey key,
			RedisJsonDialect gridDialect
	) {

		Entity entity = gridDialect.getEntityStorageStrategy().getEntity(
				gridDialect.entityId( key )
		);

		if ( entity == null ) {
			return null;
		}

		for ( int i = 0; i < key.getColumnNames().length; i++ ) {
			entity.set( key.getColumnNames()[i], key.getColumnValues()[i] );
		}
		return new HashMap<>( entity.getProperties() );
	}

	private Map<String, Object> extractFromHashDialect(
			SessionFactory sessionFactory,
			EntityKey key,
			RedisHashDialect gridDialect) {

		RedisClusterCommands<String, String> connection = getConnection( sessionFactory );

		String entityId = gridDialect.entityId( key );

		if ( !connection.exists( entityId ) ) {
			return null;
		}

		Map<String, String> hgetall = connection.hgetall( entityId );

		Map<String, Object> result = new HashMap<>();
		result.putAll( hgetall );

		for ( int i = 0; i < key.getColumnNames().length; i++ ) {
			result.put( key.getColumnNames()[i], key.getColumnValues()[i] );
		}

		return result;
	}

	private static RedisDatastoreProvider getProvider(SessionFactory sessionFactory) {
		DatastoreProvider provider = ( (SessionFactoryImplementor) sessionFactory ).getServiceRegistry().getService(
				DatastoreProvider.class
		);
		if ( !( RedisDatastoreProvider.class.isInstance( provider ) ) ) {
			throw new RuntimeException( "Not testing with RedisDatastoreProvider, cannot extract underlying map." );
		}

		return RedisDatastoreProvider.class.cast( provider );
	}

	@Override
	public AbstractRedisDialect getGridDialect(DatastoreProvider datastoreProvider) {
		return getDialect( (RedisDatastoreProvider) datastoreProvider );
	}

	public static AbstractRedisDialect getDialect(RedisDatastoreProvider datastoreProvider) {
		if ( TestHelper.getCurrentGridDialectClass().equals( RedisHashDialect.class ) ) {
			return new RedisHashDialect( datastoreProvider );
		}
		return new RedisJsonDialect( datastoreProvider );
	}

	@Override
	public boolean backendSupportsTransactions() {
		return false;
	}

	@Override
	public void dropSchemaAndDatabase(SessionFactory sessionFactory) {
		RedisClusterCommands<String, String> connection = getConnection( sessionFactory );
		connection.flushall();
	}

	private RedisClusterCommands<String, String> getConnection(SessionFactory sessionFactory) {
		RedisDatastoreProvider castProvider = getProvider( sessionFactory );
		return castProvider.getConnection();
	}

	@Override
	public Map<String, String> getAdditionalConfigurationProperties() {
		return Collections.emptyMap();
	}

	@Override
	public long getNumberOfEntities(Session session) {
		return getNumberOfEntities( session.getSessionFactory() );
	}

	@Override
	public long getNumberOfEntities(SessionFactory sessionFactory) {
		RedisClusterCommands<String, String> connection = getConnection( sessionFactory );
		List<String> keys = connection.keys( "*" );

		long result = 0;
		for ( String key : keys ) {
			if ( key.startsWith( AbstractRedisDialect.ASSOCIATIONS ) || key.startsWith( AbstractRedisDialect.IDENTIFIERS ) ) {
				continue;
			}

			String type = connection.type( key );

			if ( type.equals( "hash" ) || type.equals( "string" ) ) {
				result++;
			}
		}

		return result;
	}

	public static void assertDbObject(
			OgmSessionFactory sessionFactory,
			String hash,
			String key,
			String expectedDbObject) {
		RedisDatastoreProvider provider = getProvider( sessionFactory );
		try {

			String actual = provider.getConnection().get( hash + ":" + key );
			JSONAssert.assertEquals( expectedDbObject, actual, JSONCompareMode.LENIENT );
		}
		catch (JSONException e) {
			throw new IllegalStateException( e );
		}
	}

	@Override
	public long getNumberOfAssociations(Session session) {
		return getNumberOfAssociations( session.getSessionFactory() );
	}

	@Override
	public long getNumberOfAssociations(SessionFactory sessionFactory) {
		long associationCount = getNumberOfGlobalAssociations( sessionFactory );
		associationCount += getNumberOfEmbeddedAssociations( sessionFactory );

		return associationCount;
	}

	@Override
	public long getNumberOfAssociations(SessionFactory sessionFactory, AssociationStorageType type) {
		switch ( type ) {
			case ASSOCIATION_DOCUMENT:
				return getNumberOfGlobalAssociations( sessionFactory );
			case IN_ENTITY:
				return getNumberOfEmbeddedAssociations( sessionFactory );
			default:
				throw new IllegalArgumentException( "Unexpected association storaget type " + type );
		}
	}

	public long getNumberOfGlobalAssociations(SessionFactory sessionFactory) {
		RedisKeyCommands<String, String> connection = getConnection( sessionFactory );
		return connection.keys( AbstractRedisDialect.ASSOCIATIONS + ":*" ).size();
	}

	public long getNumberOfEmbeddedAssociations(SessionFactory sessionFactory) {
		RedisClusterCommands<String, String> connection = getConnection( sessionFactory );

		long associationCount = 0;
		List<String> keys = connection.keys( "*" );

		for ( String key : keys ) {

			if ( key.startsWith( AbstractRedisDialect.ASSOCIATIONS ) || key.startsWith( AbstractRedisDialect.IDENTIFIERS ) ) {
				continue;
			}

			String type = connection.type( key );

			if ( "string".equalsIgnoreCase( type ) ) {
				String value = connection.get( key );
				JsonNode jsonNode = fromJSON( value );

				for ( JsonNode node : jsonNode ) {
					if ( node.isArray() ) {
						associationCount++;
					}
				}
			}
		}

		return associationCount;
	}

	public static JsonNode fromJSON(String json) {
		if ( json == null || json.length() == 0 ) {
			return null;
		}

		try {
			ObjectMapper objectMapper = new ObjectMapper().configure( JsonParser.Feature.ALLOW_SINGLE_QUOTES, true );
			return objectMapper.reader().readTree( json );
		}
		catch (IOException e) {
			throw new IllegalStateException( e );
		}
	}

	@Override
	public void prepareDatabase(SessionFactory sessionFactory) {
	}

	@Override
	public Class<? extends DatastoreConfiguration<?>> getDatastoreConfigurationType() {
		return Redis.class;
	}
}