package de.hsadmin.mods.user;

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

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

import de.hsadmin.core.model.AuthorisationException;
import de.hsadmin.core.model.AbstractEntity;
import de.hsadmin.core.model.AbstractModuleImpl;
import de.hsadmin.core.model.HSAdminException;
import de.hsadmin.core.model.Transaction;
import de.hsadmin.hostsharing.BasePacType;
import de.hsadmin.hostsharing.MultiOption;
import de.hsadmin.mods.pac.Pac;

public class UnixUserModuleImpl extends AbstractModuleImpl {

	public UnixUserModuleImpl() {
	}

	@Override
	public AbstractEntity initialize(AbstractEntity newEntity) throws AuthorisationException {
		UnixUser newUnixUser = (UnixUser) super.initialize(newEntity);
		newUnixUser.setName(getTransaction().getLoginUser().getPac().getName() + '-');
		return newUnixUser;
	}

	@Override
	public AbstractEntity find(Class<? extends AbstractEntity> entityClass, Object key) throws HSAdminException {
		UnixUser res = (UnixUser) super.find(entityClass, key);
		needsPartialAccessOnPacOf(res, "find");
		return res;
	}

	@Override
	public AbstractEntity findByString(Class<? extends AbstractEntity> entityClass, String key) throws HSAdminException {
		// do query
		UnixUser res = (UnixUser) super.findByString(entityClass, key);

		// return directly (checking rights already done in search within
		// findByString)
		return res;
	}

	@Override
	public List<AbstractEntity> search(Class<? extends AbstractEntity> entityClass, String condition, String orderBy) throws HSAdminException {
		// do query
		if (orderBy == null || orderBy.length() == 0) {
			orderBy = "ORDER BY obj.name ASC";
		}
		List<AbstractEntity> res = super.search(entityClass, condition, orderBy);
		List<AbstractEntity> ret = new LinkedList<AbstractEntity>();

		// remove entities where login user has no access rights
		if (res != null) {
			for (AbstractEntity entity : res) {
				try {
					UnixUser returnedUnixUser = (UnixUser) entity;
					needsPartialAccessOnPacOf(returnedUnixUser, "search");
					ret.add(returnedUnixUser);
				} catch (AuthorisationException exc) {
				} // ignore
			}
		}

		// return clean result
		return ret;
	}

	@Override
	public AbstractEntity add(AbstractEntity newEntity) throws HSAdminException {
		Transaction transaction = getTransaction();
		EntityManager em = transaction.getEntityManager();
		UnixUser loginUser = transaction.getLoginUser();
		// only allow pac which matches the username (TODO: hard coded
		// Hostsharing convention)
		UnixUser newUnixUser = (UnixUser) newEntity;
		// validation of username and password
		String name = newUnixUser.getName();
		if (name == null) {
			throw new HSAdminException("username is required");
		}
		String userName = name.toLowerCase().trim();
		for (char c : userName.toCharArray()) {
			if (!(Character.isLetterOrDigit(c) || c == '.' || c == '-' || c == '_')) {
				throw new AuthorisationException(loginUser, "add", newUnixUser, "userId");
			}
		}
		if (userName.length() < 7 || userName.charAt(5) != '-' || userName.lastIndexOf('-') > 5) {
			throw new AuthorisationException(loginUser, "add", newUnixUser, "userId");
		}
		String passWord = newUnixUser.getPassword();
		if (passWord == null || passWord.length() == 0) {
			throw new HSAdminException("password is required");
		}
		if (passWord.indexOf(':') >= 0) {
			throw new AuthorisationException(loginUser, "add", newUnixUser, "userId");
		}
		Query qPac = em.createQuery("SELECT obj FROM Pacs obj WHERE obj.name = :pacName");
		qPac.setParameter("pacName", userName.substring(0, 5));
		Object singleResult = qPac.getSingleResult();
		Pac pac = (Pac) singleResult;
		newUnixUser.setName(userName);
		newUnixUser.setHomedir("/home/pacs/" + userName.substring(0, 5) + "/users/" + userName.substring(6));

		// no appropriate uid?
		if (newUnixUser.getUserId() < 1000) {
			// determine next free uid
			long nUID = 20000;
			Query qUID = em.createQuery("SELECT MAX(u.userId) FROM UnixUsers u");
			Long maxUid = (Long) qUID.getSingleResult();
			if (maxUid >= nUID)
				nUID = maxUid + 1;
			newUnixUser.setUserId(nUID);
		} else {
			// given uid belongs to same pac?
			Query q = em.createQuery("SELECT u.userId FROM UnixUsers u WHERE u.userId = :userId");
			q.setParameter("userId", newUnixUser.getUserId());
			List<?> idExists = q.getResultList();
			q = em.createQuery("SELECT u.userId FROM UnixUsers u WHERE u.userId = :userId AND u.pac = :pac");
			q.setParameter("userId", newUnixUser.getUserId());
			q.setParameter("pac", pac);
			List<?> idOfSamePac = q.getResultList();
			if (idOfSamePac.isEmpty() && !idExists.isEmpty()) {
				throw new AuthorisationException(loginUser, "add", newUnixUser, "userId");
			}
		}
		//TODO: Needs better implementation
		String pacType = pac.getBasepac().getName();
		if (!pacType.equals(BasePacType.PAC_WEB) && !pacType.equals(BasePacType.PAC_DW) && !pacType.equals(BasePacType.PAC_SW)) {
			throw new HSAdminException("not allowed for this packet type");
	    }		
		//TODO: Needs better implementation
		Query qUnixUsers = em.createQuery("SELECT obj FROM UnixUsers obj WHERE obj.id = :pacId");
		qUnixUsers.setParameter("pacId", pac.getId());
		if (qUnixUsers.getResultList().size() >= MultiOption.UNIXUSERS_PER_OPTION * pac.getQuantityByComponentName("MULTI")) {
			throw new HSAdminException("included unix users exceeded");
		}	
		// don't move this up, it will update the new entity still with wrong
		// userid!
		newUnixUser.setPac(pac);
		newUnixUser.init();

		// authorisation check
		needsFullAccessOnPacOf(newUnixUser, "add");

		// add new entity
		return super.add(newEntity);
	}

	@Override
	public AbstractEntity update(AbstractEntity existingEntity) throws HSAdminException {
		Transaction transaction = getTransaction();
		UnixUser loginUser = transaction.getLoginUser();
		EntityManager em = transaction.getEntityManager();
		UnixUser detachedUnixUser = (UnixUser) existingEntity;
		UnixUser attachedUnixUser = em.find(detachedUnixUser.getClass(), detachedUnixUser.getId());
		needsFullAccessOnUser(attachedUnixUser, "update");
		if (attachedUnixUser.getUserId() != detachedUnixUser.getUserId()) {
			throw new AuthorisationException(loginUser, "update", detachedUnixUser, "id");
		}
		if (!attachedUnixUser.getName().equals(detachedUnixUser.getName())) {
			throw new AuthorisationException(loginUser, "update", detachedUnixUser, "name");
		}
		attachedUnixUser.setPassword(detachedUnixUser.getPassword());
		if (hasFullAccessOnPacOf(attachedUnixUser)) {
			attachedUnixUser.setComment(detachedUnixUser.getComment());
			attachedUnixUser.setHomedir(detachedUnixUser.getHomedir());
		} else {
			if (!attachedUnixUser.getComment().equals(detachedUnixUser.getComment())) {
				throw new AuthorisationException(loginUser, "update", detachedUnixUser, "comment");
			}
			if (!attachedUnixUser.getHomedir().equals(detachedUnixUser.getHomedir())) {
				throw new AuthorisationException(loginUser, "update", detachedUnixUser, "homedir");
			}
		}
		if (!attachedUnixUser.getShell().equals(detachedUnixUser.getShell()))
			if (hasFullAccessOnPacOf(attachedUnixUser)
					|| isLoginShell(attachedUnixUser.getShell())
					|| !isLoginShell(detachedUnixUser.getShell()))
				attachedUnixUser.setShell(detachedUnixUser.getShell());
			else
				throw new AuthorisationException(loginUser, "update",
						detachedUnixUser, "shell");
		if (attachedUnixUser.isLocked() != detachedUnixUser.isLocked()) {
			throw new AuthorisationException(loginUser, "update", detachedUnixUser, "locked");
		}
		if (detachedUnixUser.getQuotaSoftlimit() != null) {
			if (hasFullAccessOnPacOf(attachedUnixUser)) {
				attachedUnixUser.setQuotaSoftlimit(detachedUnixUser.getQuotaSoftlimit());
				if (detachedUnixUser.getQuotaHardlimit() != null) {
					attachedUnixUser.setQuotaHardlimit(detachedUnixUser.getQuotaHardlimit());
				}
			}
			else {
				Integer oldQuota = attachedUnixUser.getQuotaSoftlimit();
				Integer newQuota = detachedUnixUser.getQuotaSoftlimit();
				if (oldQuota != newQuota && !oldQuota.equals(newQuota)) {
					throw new AuthorisationException(loginUser, "update", detachedUnixUser, "quota");
				}
			}
		}
		return super.update(attachedUnixUser);
	}

	@Override
	public void delete(AbstractEntity existingEntity) throws HSAdminException {
		// get the entity from the database
		EntityManager entityManager = getTransaction().getEntityManager();
		UnixUser detachedUnixUser = (UnixUser) existingEntity;
		UnixUser attachedUnixUser = entityManager.find(detachedUnixUser.getClass(),
				detachedUnixUser.getId());

		// authorisation check
		if (attachedUnixUser.getName().length() < 7) {
			throw new AuthorisationException(attachedUnixUser, "delete");
		}
		needsFullAccessOnPacOf(attachedUnixUser, "delete");

		// delete entity
		super.delete(attachedUnixUser);
	}

	// throws an AuthorisationException if the login user has no write acess
	// on the pac of the given UnixUser
	private boolean hasFullAccessOnPacOf(UnixUser user) {
		UnixUser loginUser = getTransaction().getLoginUser();
		String loginUserName = loginUser.getName();
		return loginUser.hasHostmasterRole()
			|| loginUserName.equals(user.getPac().getName()) 
			|| loginUserName.equals(user.getPac().getCustomer().getName());
	}

	// throws an AuthorisationException if the login user has no write acess
	// on the pac of the given UnixUser
	private void needsFullAccessOnPacOf(UnixUser user, String method)
			throws AuthorisationException {
		if (!hasFullAccessOnPacOf(user))
			throw new AuthorisationException(getTransaction().getLoginUser(), method, user);
	}

	private void needsPartialAccessOnPacOf(UnixUser user, String method) throws AuthorisationException {
		UnixUser loginUser = getTransaction().getLoginUser();
		if (!hasFullAccessOnPacOf(user) && loginUser.getPac().id() != user.getPac().id()) {
			throw new AuthorisationException(loginUser, method, user);
		}
	}

	private void needsFullAccessOnUser(UnixUser user, String method) throws AuthorisationException {
		UnixUser loginUser = getTransaction().getLoginUser();
		if (!hasFullAccessOnPacOf(user) && !loginUser.sameIdAs(user)) {
			throw new AuthorisationException(loginUser, method, user);
		}
	}

	private static boolean isLoginShell(String shell) {
		if (shell.equals("/bin/sh"))
			return true;
		if (shell.equals("/bin/bash"))
			return true;
		if (shell.equals("/bin/csh"))
			return true;
		if (shell.equals("/bin/tcsh"))
			return true;
		if (shell.equals("/bin/zsh"))
			return true;
		if (shell.equals("/bin/ksh"))
			return true;
		return false;
	}

}
