package de.hsadmin.core.model;

import java.util.LinkedList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

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 static final long serialVersionUID = 2693948730004920437L;
	private static Log log = LogFactory.getLog(AbstractModuleImpl.class);

	private UnixUser loginUser;
	private Transaction transaction;

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

	/**
	 *  apply access restriction to JPA-QL condition.
	 * @param entityClass
	 * @param loginUser
	 * @param condition
	 * @return
	 */
	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 Transaction getTransaction() {
		return transaction;
	}
	
	public AbstractEntity initialize(AbstractEntity newEntity)
			throws AuthorisationException {
		newEntity.initialize(transaction.getEntityManager(), getLoginUser());
		return newEntity;
	}

	public AbstractEntity add(AbstractEntity newEntity) throws HSAdminException {
		// get the user who is logged in
		UnixUser loginUser = getLoginUser();

		// create record in database
		log.debug("merging?");
		newEntity.complete(transaction.getEntityManager(), loginUser);
		try {
			transaction.getEntityManager().persist(newEntity);
		} catch (Throwable exc) {
			log.error("exception: " + exc);
		} finally {
			log.debug("finally");
		}
		log.debug("merged");

		// check rights
		if (!newEntity.isWriteAllowedFor(loginUser))
			throw new AuthorisationException(loginUser, "add", newEntity);

		// generically create the processor
		EntityProcessorFactory procFact = createProcessorFactory(newEntity.getClass());
		if (procFact == null) {
			log.debug("no procFact found :-(");
			return newEntity;
		}
		log.debug("procFact found :-)");
		Processor proc = procFact.createCreateProcessor(transaction.getEntityManager(), newEntity);

		// queue the processor
		queueProcessor(proc, loginUser, newEntity, "hinzugefuegt");

		// return the added entity
		return newEntity;
	}

	public AbstractEntity find(Class<? extends AbstractEntity> entityClass, Object key) throws HSAdminException {
		AbstractEntity entity = transaction.getEntityManager().find(entityClass, key);

		// check rights
		UnixUser loginUser = getLoginUser();
		if (!entity.isReadAllowedFor(loginUser))
			throw new AuthorisationException(loginUser, "add", entity);

		return entity;
	}

	public AbstractEntity findByString(Class<? extends AbstractEntity> entityClass, String key) throws HSAdminException {
		// find a static method which creates the query
		java.lang.reflect.Method method = null;
		try {
			method = entityClass.getDeclaredMethod("createQueryFromStringKey", String.class);
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			method = null;
		}

		AbstractEntity entity;
		if (method == null)
			entity = transaction.getEntityManager().find(entityClass, key);
		else {
			// get the query expression from the static method (query part after
			// WHERE)
			String query;
			try {
				query = (String) method.invoke(null, key);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}

			// perform the query
			List<AbstractEntity> result = search(entityClass, query, null);
			if (result.size() > 1)
				throw new javax.persistence.NonUniqueResultException();
			if (result.size() == 0)
				return null;
			entity = result.get(0);

			// this was maybe thought as a fallback
			// but is wrong when the above result is empty due to accessibility
			// entity = em.find(entityClass, key);
		}

		// return (checking rights already done in search)
		return entity;
	}

	public List<AbstractEntity> search(Class<? extends AbstractEntity> entityClass, String condition, String orderBy) throws HSAdminException {
		// restrict query
		UnixUser loginUser = getLoginUser();
		condition = restrict(entityClass, loginUser, condition);

		// get the entities name (query part from FROM to WHERE)
		// TODO: beware SQL injections!!!
		javax.persistence.Entity entityAnnot = entityClass.getAnnotation(javax.persistence.Entity.class);
		String queryString = "SELECT obj FROM " + entityAnnot.name() + " obj";
		if (condition != null && condition.length() > 0)
			queryString += " WHERE " + condition;

		// Fix problem with queries WHERE .. AND (FALSE) -- pe
		if (condition != null && condition.contains("AND (FALSE)")) {
			return new LinkedList<AbstractEntity>();
		}
		
		if (orderBy != null) {
			queryString += " ";
			queryString += orderBy;
		}

		// set parameters
		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());

		// do query
		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(getLoginUser())) {
						ret.add(returnedEntity);
					}
				}
			}

			// return clean result
			return ret;
		} catch (Exception ex) {
			throw new HSAdminException(ex);
		}
	}

	public AbstractEntity update(AbstractEntity existingEntity) throws HSAdminException {
		// get the user who is logged in
		UnixUser loginUser = getLoginUser();

		// update record in database
		log.debug("merging:");
		try {
			existingEntity = existingEntity.merge(transaction.getEntityManager(), loginUser);
		} catch (Throwable exc) {
			log.error("exception: " + exc);
			throw new RuntimeException(exc);
		} finally {
			log.debug("finally");
		}
		log.debug("merged!");

		// check rights
		if (!existingEntity.isWriteAllowedFor(loginUser))
			throw new AuthorisationException(loginUser, "update",
					existingEntity);

		// generically create the processor
		EntityProcessorFactory procFact = 
			createProcessorFactory(existingEntity.getClass());
		if (procFact != null) {
			log.debug("creating processor");
			Processor proc = procFact.createUpdateProcessor(transaction.getEntityManager(), existingEntity);

			// queue the processor
			queueProcessor(proc, loginUser, existingEntity, "aktualisiert");
		}

		// return the merged entity
		return existingEntity;
	}

	public void delete(AbstractEntity existingEntity) throws HSAdminException {
		// get the user who is logged in
		UnixUser user = getLoginUser();

		// re-attach the entity
		log.debug("merging:");
		try {
			existingEntity = transaction.getEntityManager().find(existingEntity.getClass(), existingEntity.id());
		} catch (Throwable exc) {
			log.error("exception: " + exc);
			throw new RuntimeException(exc);
		} finally {
			log.debug("finally");
		}
		log.debug("merged!");

		// check rights
		if (!existingEntity.isWriteAllowedFor(loginUser))
			throw new AuthorisationException(loginUser, "add", existingEntity);

		// delete record in database
		log.debug("deleting:");
		try {
			transaction.getEntityManager().remove(existingEntity);
		} catch (Throwable exc) {
			log.error("exception: " + exc);
		} finally {
			log.debug("finally");
		}
		log.debug("deleted!");

		// generically create the processor
		EntityProcessorFactory procFact = createProcessorFactory(existingEntity.getClass());
		if (procFact == null) {
			log.debug("no procFact found :-(");
			return;
		}
		log.debug("procFact found :-)");
		Processor proc = procFact.createDeleteProcessor(transaction.getEntityManager(), existingEntity);

		// queue the processor
		queueProcessor(proc, user, existingEntity, "geloescht");
	}

	public EntityProcessorFactory createProcessorFactory(Class<? extends AbstractEntity> entityClass) {
		try {
			String procFactName = entityClass.getCanonicalName()
					+ "ProcessorFactory";
			Class<?> procFactClass = Class.forName(procFactName);
			if (procFactClass == null)
				return null;
			Object procFact = procFactClass.newInstance();
			return (EntityProcessorFactory) procFact;
		} catch (Exception exc) {
			log.error("exception creating instance: " + exc);
			return null;
		}
	}

	/**
	 *  get current login user from session context.
	 * @return
	 */
	public UnixUser getLoginUser() {
		if (loginUser == null) {
			loginUser = transaction.getLoginUser();
		}
		return loginUser;
	}

	public void queueProcessor(Processor proc, UnixUser user, AbstractEntity entity, String action) {
		log.debug("queueing processor for user " + user.getId() + "/"
				+ user.getUserId() + "/" + user.getName());
		EntityInfo entityInfo = 
			entity.getClass().getAnnotation(EntityInfo.class);
		String entityTypeName = 
			entityInfo != null ? entityInfo.name() : entity.getClass().getSimpleName();
		StringBuilder details = new StringBuilder();
		
		// TODO: add properties of entity to details

		String title = 
			entityTypeName + " (" + entity.createStringKey() + ") " + action;
		QueueTask task = new QueueTask(user, title, details.toString(), proc);
		transaction.getEntityManager().persist(task);
		transaction.enqueue(entity.getHiveName(), task);
		log.debug("processor queued");
	}

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

	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;
		}
	}
}
