package de.hsadmin.core.model;

import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import de.hsadmin.core.qserv.EntityProcessorFactory;
import de.hsadmin.core.qserv.Processor;
import de.hsadmin.core.qserv.QueueTask;
import de.hsadmin.mods.user.UnixUser;

/**
 * Generic implementation of EntitySession interface.
 * 
 * @author peter
 */
public abstract class AbstractModuleImpl implements ModuleInterface {
	
	private Transaction transaction;

	public void construct(Transaction tx) {
		transaction = tx;
	}

	public Transaction getTransaction() {
		return transaction;
	}
	
	public AbstractEntity initialize(AbstractEntity newEntity)
			throws AuthorisationException {
		newEntity.initialize(transaction.getEntityManager(), transaction.getLoginUser());
		return newEntity;
	}

	public AbstractEntity add(AbstractEntity newEntity) throws HSAdminException {
		UnixUser loginUser = transaction.getLoginUser();
		EntityManager entityManager = transaction.getEntityManager();
		newEntity.complete(entityManager, loginUser);
		entityManager.persist(newEntity);
		if (!newEntity.isWriteAllowedFor(loginUser)) {
			throw new AuthorisationException(loginUser, "add", newEntity);
		}
		EntityProcessorFactory procFact = createProcessorFactory(newEntity.getClass());
		if (procFact != null) {
			Processor proc = procFact.createCreateProcessor(entityManager, newEntity);
			queueProcessor(proc, loginUser, newEntity, "hinzugefuegt");
		}
		return newEntity;
	}

	public AbstractEntity find(Class<? extends AbstractEntity> entityClass, Object key) throws HSAdminException {
		AbstractEntity entity = transaction.getEntityManager().find(entityClass, key);
		UnixUser loginUser = transaction.getLoginUser();
		if (!entity.isReadAllowedFor(loginUser)) {
			throw new AuthorisationException(loginUser, "add", entity);
		}
		return entity;
	}

	public AbstractEntity findByString(Class<? extends AbstractEntity> entityClass, String key) throws HSAdminException {
		Method method = null;
		try {
			method = entityClass.getDeclaredMethod("createQueryFromStringKey", String.class);
		} catch (SecurityException e) {
			throw new HSAdminException(e);
		} catch (NoSuchMethodException e) {
			method = null;
		}
		AbstractEntity entity = null;
		if (method == null) {
			entity = transaction.getEntityManager().find(entityClass, key);
		}
		else {
			String query = null;
			try {
				query = (String) method.invoke(null, key);
			} catch (Exception e) {
				throw new HSAdminException(e);
			}
			List<AbstractEntity> result = search(entityClass, query, null);
			if (result.size() > 1) throw new NonUniqueResultException();
			if (result.size() == 0) return null;
			entity = result.get(0);
		}
		return entity;
	}

	public List<AbstractEntity> search(Class<? extends AbstractEntity> entityClass, String condition, String orderBy) throws HSAdminException {
		UnixUser loginUser = transaction.getLoginUser();
		condition = restrict(entityClass, loginUser, condition);
		Entity entityAnnot = entityClass.getAnnotation(Entity.class);
		String queryString = "SELECT obj FROM " + entityAnnot.name() + " obj";
		if (condition != null && condition.length() > 0) {
			queryString += " WHERE " + condition;
		}
		if (condition != null && condition.contains("AND (FALSE)")) {
			return new LinkedList<AbstractEntity>();
		}
		if (orderBy != null) {
			queryString += " ";
			queryString += orderBy;
		}
		EntityManager entityManager = transaction.getEntityManager();
		entityManager.clear();
		Query query = entityManager.createQuery(queryString);
		setQueryParameter(query, queryString, "loginUser", loginUser);
		setQueryParameter(query, queryString, "loginUserName", loginUser.getName());
		setQueryParameter(query, queryString, "loginUserPac", loginUser.getPac());
		try {
			List<?> res = query.getResultList();
			List<AbstractEntity> ret = new LinkedList<AbstractEntity>();
			// remove entities where login user has no access rights
			for (Object entity : res) {
				if (entity instanceof AbstractEntity) {
					AbstractEntity returnedEntity = (AbstractEntity) entity;
					if (returnedEntity.isReadAllowedFor(transaction.getLoginUser())) {
						ret.add(returnedEntity);
					}
				}
			}
			return ret;
		} catch (Exception ex) {
			throw new HSAdminException(ex);
		}
	}

	public AbstractEntity update(AbstractEntity existingEntity) throws HSAdminException {
		UnixUser loginUser = transaction.getLoginUser();
		existingEntity = existingEntity.merge(transaction.getEntityManager(), loginUser);
		if (!existingEntity.isWriteAllowedFor(loginUser)) {
			throw new AuthorisationException(loginUser, "update", existingEntity);
		}
		EntityProcessorFactory procFact = createProcessorFactory(existingEntity.getClass());
		if (procFact != null) {
			Processor proc = procFact.createUpdateProcessor(transaction.getEntityManager(), existingEntity);
			queueProcessor(proc, loginUser, existingEntity, "aktualisiert");
		}
		return existingEntity;
	}

	public void delete(AbstractEntity existingEntity) throws HSAdminException {
		UnixUser loginUser = transaction.getLoginUser();
		EntityManager entityManager = transaction.getEntityManager();
		existingEntity = entityManager.find(existingEntity.getClass(), existingEntity.id());
		if (!existingEntity.isWriteAllowedFor(loginUser)) {
			throw new AuthorisationException(loginUser, "add", existingEntity);
		}
		entityManager.remove(existingEntity);
		EntityProcessorFactory procFact = createProcessorFactory(existingEntity.getClass());
		if (procFact != null) {
			Processor proc = procFact.createDeleteProcessor(entityManager, existingEntity);
			queueProcessor(proc, loginUser, existingEntity, "geloescht");
		}
	}

	protected EntityProcessorFactory createProcessorFactory(Class<? extends AbstractEntity> entityClass) 
			throws HSAdminException {
		String procFactName = entityClass.getCanonicalName() + "ProcessorFactory";
		Class<?> procFactClass = null;
		EntityProcessorFactory procFact = null;
		try {
			procFactClass = Class.forName(procFactName);
			if (procFactClass != null) {
				procFact = (EntityProcessorFactory) procFactClass.newInstance();
			}
		} catch (ClassNotFoundException e) {
			// no processor defined
		} catch (InstantiationException e) {
			throw new HSAdminException(e);
		} catch (IllegalAccessException e) {
			throw new HSAdminException(e);
		}
		return procFact;
	}

	protected void queueProcessor(Processor proc, UnixUser user, AbstractEntity entity, String action) {
		Entity entityInfo = entity.getClass().getAnnotation(Entity.class);
		String entityTypeName = entityInfo != null ? entityInfo.name() : entity.getClass().getSimpleName();
		StringBuilder details = new StringBuilder();
		String title = entityTypeName + " (" + entity.createStringKey() + ") " + action;
		QueueTask task = new QueueTask(user, title, details.toString(), proc);
		transaction.getEntityManager().persist(task);
		transaction.enqueue(entity.getHiveName(), task);
	}

	public String toString(StackTraceElement[] stackTrace) {
		StringBuilder stack = new StringBuilder();
		for (StackTraceElement e : stackTrace) {
			stack.append(e.getFileName() + ":" + e.getLineNumber() + "\n");
		}
		return stack.toString();
	}

	/**
	 *  apply access restriction to JPA-QL condition.
	 */
	private String restrict(Class<?> entityClass, UnixUser loginUser, String condition) {
		String restriction = AbstractEntity.restriction(entityClass, loginUser);
		if (restriction == null)
			return condition;
		if (condition != null && condition.length() > 0)
			condition = "(" + condition + ") AND (" + restriction + ")";
		else
			condition = restriction;
		return condition;
	}

	public static void setQueryParameter(Query query, String queryString,
			String argName, Object argValue) {
		int argLen = argName.length();
		int iMax = queryString.length();
		int i = 0;
		while ((i = queryString.indexOf(argName, i)) >= 0) {
			if ((i + argLen) >= iMax || queryString.charAt(i + argLen) < 'A') {
				query.setParameter(argName, argValue);
				break;
			}
			++i;
		}
	}
}
