package eu.eudat.logic.managers;

import eu.eudat.data.dao.criteria.LockCriteria;
import eu.eudat.logic.services.ApiContext;
import eu.eudat.models.data.lock.Lock;
import eu.eudat.models.data.security.Principal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import javax.persistence.NoResultException;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.UUID;

@Component
public class LockManager {
	private final Comparator<eu.eudat.data.entities.Lock> compareByTouchedAt = Comparator.comparing(o ->  o.getTouchedAt().getTime());

	private ApiContext apiContext;
	private Environment environment;

	@Autowired
	public LockManager(ApiContext apiContext, Environment environment) {
		this.apiContext = apiContext;
		this.environment = environment;
	}

	public eu.eudat.data.entities.Lock createOrUpdate(Lock lock, Principal principal) throws Exception {
		if (lock.getId() != null) {
			try {
				eu.eudat.data.entities.Lock entity = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().find(lock.getId());
				if (entity != null) {
					if (!entity.getLockedBy().getId().equals(principal.getId())) {
						throw new Exception("Is not locked by that user");
					}
				}
			}catch(NoResultException e) {
				lock.setId(null);
			}
		}
		eu.eudat.data.entities.Lock newLock = lock.toDataModel();
		newLock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().createOrUpdate(newLock);

		return newLock;
	}

	public boolean isLocked(String targetId, Principal principal) throws Exception {
		LockCriteria criteria = new LockCriteria();
		criteria.setTarget(UUID.fromString(targetId));
		Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
		if (availableLocks == 1) {
			eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle();
			if (lock.getLockedBy().getId().equals(principal.getId())) {
				lock.setTouchedAt(new Date());
				this.createOrUpdate(new Lock().fromDataModel(lock), principal);
				return false;
			}
			return this.forceUnlock(targetId) > 0;
		} else if (availableLocks > 1) {
			this.forceUnlock(targetId);
			return this.isLocked(targetId, principal);
		}
		return false;
	}

	private Long forceUnlock(String targetId) {
		LockCriteria criteria = new LockCriteria();
		criteria.setTarget(UUID.fromString(targetId));
		Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
		long deletedLocks = 0L;
		if (availableLocks > 0) {
			List<eu.eudat.data.entities.Lock> locks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).toList();
			for (eu.eudat.data.entities.Lock lock : locks) {
				if (new Date().getTime() - lock.getTouchedAt().getTime() > environment.getProperty("database.lock-fail-interval", Integer.class)) {
					this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock);
					deletedLocks++;
				}
			}
			if (deletedLocks == 0) {
				eu.eudat.data.entities.Lock recentlock = locks.stream().max(compareByTouchedAt).get();
				for (eu.eudat.data.entities.Lock lock : locks) {
					if (lock != recentlock) {
						this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock);
						deletedLocks++;
					}
				}
			}
		}
		return availableLocks - deletedLocks;
	}

	public void unlock(String targetId, Principal principal) throws Exception {
		LockCriteria criteria = new LockCriteria();
		criteria.setTarget(UUID.fromString(targetId));
		Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
		if (availableLocks == 1) {
			eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle();
			if (!lock.getLockedBy().getId().equals(principal.getId())) {
				throw new Exception("Only the user who created that lock can delete it");
			}
			this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock);
		} else if (availableLocks > 1) {
			List<eu.eudat.data.entities.Lock> locks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).toList();
			locks.stream().filter(lock -> lock.getLockedBy().getId().equals(principal.getId())).forEach(lock -> this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock));
		}
	}

	public Lock getFromTarget(String targetId, Principal principal) throws Exception {
		LockCriteria criteria = new LockCriteria();
		criteria.setTarget(UUID.fromString(targetId));
		Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
		if (availableLocks > 0) {
			eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle();
			if (!lock.getLockedBy().getId().equals(principal.getId())) {
				throw new Exception("Only the user who created that lock can access it");
			}
			return new Lock().fromDataModel(lock);
		}
		return null;
	}
}
