package info.bukova.isspst.services;

import info.bukova.isspst.Module;
import info.bukova.isspst.ModuleUtils;
import info.bukova.isspst.SessionData;
import info.bukova.isspst.dao.BaseDao;
import info.bukova.isspst.dao.QueryDao;
import info.bukova.isspst.data.DataModel;
import info.bukova.isspst.data.NumberSeries;
import info.bukova.isspst.filters.Filter;
import info.bukova.isspst.reporting.Report;
import info.bukova.isspst.services.numberseries.NumberSeriesService;
import info.bukova.isspst.storage.DocumentFileStorage;
import org.hibernate.NonUniqueResultException;
import org.hibernate.Query;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.GenericTypeResolver;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;

import static ch.lambdaj.Lambda.filter;

public abstract class AbstractService<T extends DataModel> implements Service<T> {

	protected BaseDao<T> dao;
	private Validator validator;
	@Autowired
	protected SessionData sessionData;
	@Autowired
	protected  HqlDataFilter hqlFilter;
	@Autowired
	protected QueryDao queryDao;
	private DocumentFileStorage documentFileStorage;

	private NumberSeriesService numberSeriesService;
	private Class<T> entityClass;


	public AbstractService() {
		entityClass = (Class<T>) GenericTypeResolver.resolveTypeArgument(getClass(), AbstractService.class);
	}

	@Override
	public List<Class<?>> getEntityClasses() {
		List<Class<?>> classes = new ArrayList<Class<?>>();
		classes.add(entityClass);
		return classes;
	}

	public void setDocumentFileStorage(DocumentFileStorage storage) {
		this.documentFileStorage = storage;
	}

	public NumberSeriesService getNumberSeriesService()
	{
		return numberSeriesService;
	}

	public void setNumberSeriesService(NumberSeriesService numberSeriesService)
	{
		this.numberSeriesService = numberSeriesService;
	}

	public void setDao(BaseDao<T> dao) {
		this.dao = dao;
	}
	
	public String getUpdateEntityPermission() {
		return "";
	}
	
	public String getDeleteEntityPermission() {
		return "";
	}

	protected void maintainStorrage() {
		if (documentFileStorage != null) {
			documentFileStorage.purge();
		}
	}
	
	protected Query filterQuery(Query q) {
		return filterQuery(q, null);
	}
	
	protected Query filterQuery(Query q, HqlDataFilter aditFilter) {
		HqlDataFilter f = hqlFilter;
		try {
			if (((f.getWhere() == null || f.getWhere().isEmpty())
					&& (f.getOrderBy() == null || f.getOrderBy().isEmpty()))
				&& ((aditFilter.getWhere() == null || aditFilter.getWhere().isEmpty())
					&& (aditFilter.getOrderBy() == null || aditFilter.getOrderBy().isEmpty()))) {
				return q;
			}
		} catch (Exception e) {
			f = new HqlDataFilter(); // pri volani mimo web request filtr z requestu neresime
		}

		boolean isAplicable = false;
		if (f.getDataClass() != null) {
			for (Class<?> clazz : getEntityClasses()) {
				if (f.getDataClass().isAssignableFrom(clazz)) {
					isAplicable = true;
				}
			}
		}

		String hql;
		if (isAplicable) {
			hql = HqlUtils.buildHql(q.getQueryString(), f);
		} else {
			hql = q.getQueryString();
		}

		hql = HqlUtils.addOrderBy(HqlUtils.addAndWhere(hql, aditFilter), aditFilter);
		Query newQuery = dao.getQuery(hql);

		if (isAplicable) {
			HqlUtils.addParameters(newQuery, f);
		}

		HqlUtils.addParameters(newQuery, aditFilter);
		return newQuery;
	}
	
	@Override
	@PreAuthorize("hasPermission(this, 'PERM_ADD')")
	public final T create() {
		return createEntity();
	}
	
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_ADD')")
	public void add(T entity) {
		if (dao == null) {
			throw new IsspstException("DAO is null");
		}
		
		if (getModule() != null && !getModule().isActive()) {
			throw new ModuleNotActiveException();
		}
		
		validate(entity);
		entity.setCreated(new Date());
		dao.add(entity);
	}
	
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_EDIT') or hasPermission(#entity, this.getUpdateEntityPermission())")
	public void update(T entity) {
		if (dao == null) {
			throw new IsspstException("DAO is null");
		}
		
		if (getModule() != null && !getModule().isActive()) {
			throw new ModuleNotActiveException();
		}
		
		validate(entity);
		entity.setModified(new Date());
		dao.modify(entity);
		maintainStorrage();
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_DELETE') or hasPermission(#entity, this.getDeleteEntityPermission())")
	public void delete(T entity) {
		if (dao == null) {
			throw new IsspstException("DAO is null");
		}
		
		if (getModule() != null && !getModule().isActive()) {
			throw new ModuleNotActiveException();
		}

		dao.delete(entity);

		maintainStorrage(); // poklidit přiložené soubory
	}
	
	@Override
	public void validate(T entity) {
		if (validator == null) {
			entity.setValid(true);
			return;
		}
		
		Set<ConstraintViolation<T>> violations = validator.validate(entity);
		
		if (!violations.isEmpty()) {
			ValidationException ex = new ValidationException();
			for(ConstraintViolation<T> v : violations) {
				String errKye = v.getPropertyPath().toString();
				ex.addMessage(errKye.isEmpty() ? "CLASS_ERR" : errKye, v.getMessage());
			}
			
			throw ex;
		}
		
		entity.setValid(true);
	}
	
	@Override
	public List<T> filterList(List<T> sourceList, Filter<T> filter) {
		return filter(filter.matcher(), sourceList);
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_READ')")
	public T getById(int id) {
		if (dao == null) {
			throw new IsspstException("DAO is null");
		}
		
		if (getModule() != null && !getModule().isActive()) {
			throw new ModuleNotActiveException();
		}
		
		return dao.getById(id);
	}
	
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_READ')")
	public List<T> getAll() {
		if (dao == null) {
			throw new IsspstException("DAO is null");
		}
		
		if (getModule() != null && !getModule().isActive()) {
			throw new ModuleNotActiveException();
		}

		Query q = dao.getQuery("from " + dao.getEntityName());
		
		return filterQuery(q).list();
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_READ')")
	public List<T> execQuery(String query) {
		if (dao == null) {
			throw new IsspstException("DAO is null");
		}
		
		if (getModule() != null && !getModule().isActive()) {
			throw new ModuleNotActiveException();
		}
		
		return dao.execQuery(query);
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_READ')")
	public T selectSingle(String query) {
		if (dao == null) {
			throw new IsspstException("DAO is null");
		}
		
		if (getModule() != null && !getModule().isActive()) {
			throw new ModuleNotActiveException();
		}
		
		try {
			Query q = dao.getQuery(query);
			return (T) q.uniqueResult();
		} catch (NonUniqueResultException e) {
			return null;
		}
	}

	public void setValidator(Validator validator) {
		this.validator = validator;
	}
	
	protected T createEntity() {
		return null;
	}
	
	@Override
	public Module getModule() {
		return ModuleUtils.getModule(this.getClass());
	}
	
	public List<Report> getReports() {
		if (getModule() != null) {
			return getModule().getReports();
		} else {
			return null;
		}
	}

	protected String getNumberSerie()
	{
		String currentNumber = "";
		
		if (numberSeriesService != null)
		{
			String moduleName = this.getModule().getId();
			NumberSeries ns = numberSeriesService.getNumberSerie(moduleName);
			
			if (ns != null)
			{
				currentNumber = ns.getCurrentNumber();
				numberSeriesService.increase(ns);
			}
		}
		
		return currentNumber;
	}

	@Override
	@Transactional
	public void loadLazyData(T entity) {
		loadLazyData("", entity);
	}

	@Override
	@Transactional
	public void loadLazyData(String group, T entity) {
		if (entity == null) {
			return;
		}

		Method[] methods = this.getClass().getMethods();
		
		for (Method m : methods) {
			Annotation[] annotations = m.getAnnotations();
			for (Annotation a : annotations) {
				if (a instanceof LazyLoader) {
					String[] values = ((LazyLoader) a).value();
					if (group.isEmpty() || values.length == 0 || Arrays.asList(values).contains(group)) {
						try {
							m.invoke(this, entity);
						} catch (IllegalArgumentException e) {
							throw new LazyLoadException(e.getCause());
						} catch (IllegalAccessException e) {
							throw new LazyLoadException(e.getCause());
						} catch (InvocationTargetException e) {
							throw new LazyLoadException(e.getCause());
						}
					}
				}
			}
		}
	}

	@Override
	public boolean canPrintRecord(T entity) {
		return true;
	}
}
