package info.bukova.isspst.services.requirement;

import info.bukova.isspst.Constants;
import info.bukova.isspst.Module;
import info.bukova.isspst.ModuleUtils;
import info.bukova.isspst.data.AuthItem;
import info.bukova.isspst.data.DataModel;
import info.bukova.isspst.data.JobMapping;
import info.bukova.isspst.data.RequirementBase;
import info.bukova.isspst.data.RequirementState;
import info.bukova.isspst.data.Role;
import info.bukova.isspst.data.SettingsData;
import info.bukova.isspst.data.SignedDocument;
import info.bukova.isspst.data.SignedDocumentItem;
import info.bukova.isspst.data.User;
import info.bukova.isspst.data.Workflow;
import info.bukova.isspst.data.Workgroup;
import info.bukova.isspst.mail.MailMessage;
import info.bukova.isspst.mail.Mailer;
import info.bukova.isspst.mail.MessageBuilder;
import info.bukova.isspst.reporting.Generator;
import info.bukova.isspst.reporting.GeneratorFactory;
import info.bukova.isspst.reporting.ParamFiller;
import info.bukova.isspst.reporting.Report;
import info.bukova.isspst.reporting.ReportDefinition;
import info.bukova.isspst.services.AbstractOwnedService;
import info.bukova.isspst.services.HqlDataFilter;
import info.bukova.isspst.services.LazyLoader;
import info.bukova.isspst.services.Service;
import info.bukova.isspst.services.settings.GlobalSettingsService;
import info.bukova.isspst.services.settings.SeasonService;
import info.bukova.isspst.services.signeddocs.SignedDocumentService;
import info.bukova.isspst.services.users.UserService;
import info.bukova.isspst.services.workgroups.WorkgroupService;
import info.bukova.isspst.signapi.SignData;
import info.bukova.isspst.storage.FileStorage;
import info.bukova.isspst.storage.ReportFileStorage;
import info.bukova.isspst.storage.StorageException;
import org.hibernate.LazyInitializationException;
import org.hibernate.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.ServletContext;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * @author Pepa Rokos
 * @author Franta Přibyl
 *
 * Abstraktní bázová třída všech požadavků
 *
 * @param <T> Třída požadavku
 */
public abstract class RequirementBaseServiceImpl<T extends RequirementBase> extends
		AbstractOwnedService<T> implements RequirementBaseService<T> {
	
	@Autowired
	private Mailer mailer;
	@Autowired
	private MessageBuilder messageBuilder;
	@Autowired
	private GlobalSettingsService settingsService;
	@Autowired
	private UserService userService;
	@Autowired
	protected WorkgroupService workgroupService;
	@Autowired
	private ReportDefinition reportDefinition;
	@Autowired
	private ParamFiller paramFiller;
	@Autowired
	private GeneratorFactory genFactory;
	@Autowired
	private ServletContext servletContext;
	@Autowired
	private FileStorage storage;
	@Autowired
	private SignedDocumentService signedDocumentService;
	@Autowired
	private ReportFileStorage signedDocStorage;

	@Autowired
	private SeasonService seasonService;

	private final static Logger logger = LoggerFactory.getLogger(RequirementBaseServiceImpl.class);

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_ADD')")
	public void add(T entity) {
		Workgroup reqWorkgroup = null;
		for (Workgroup w : workgroupService.getUserWorkgroups(getLoggedInUser())) {
			if (workgroupService.getMembers(entity.getCentre()).contains(w)) {
				for (Role r : workgroupService.getUserWorkgroupRoles(w, getLoggedInUser())) {
					if (r.getAuthority().equals(Constants.ROLE_USER)) {
						reqWorkgroup = w;
					}
				}
			}
		}
		
		entity.setWorkgroup(reqWorkgroup);
			
		checkEnable();
		
		if (!canAdd(entity)) {
			throw new AddRequirementException();
		}

		entity.setSeason(seasonService.getActive());

		this.addWorkflow(entity);
		entity.setNumser(this.getNumberSerie());
		
		super.add(entity);
		
		this.postAdd(entity);
	}
	
	private void checkEnable() {
		if (!settingsService.getSettings().isEnableRequirements()) {
			throw new AddRequirementException("RequirementsDisabled");
		}
	}
	
	protected boolean canAdd(T entity) {
		return true;
	}
	
	private void sendToApprovers(T entity) {
		SettingsData settings = settingsService.getSettings();
		List<User> approvers = this.getNextApprover(entity);
		if (approvers != null && !approvers.isEmpty()) {
			MailMessage message = messageBuilder.buildMessage(settings.getNewReqTemplate(), entity);
			message.setTo(userService.getEmailsForSend(approvers));
			
			mailer.send(message);
		}
	}

	protected void postAdd(T entity) {
		Workflow wf = getNextWorkflow(entity);
		if (canApprove(entity) && (wf.getSignature() == null || !wf.getSignature())) {
			approve(entity);
		} else {
			this.sendToApprovers(entity);
		}
	}

	protected void addWorkflow(T entity) {
		if (entity.getType() == null) {
			return;
		}
		
		for (Workflow w : entity.getType().getWorkflow()) {
			if ((entity.getWorkgroup() != null && !w.getCentre()) || w.getCentre()) {
				Workflow workflow = new Workflow();
				workflow.setCentre(w.getCentre());
				workflow.setLimit(w.getLimit());
				workflow.setOrder(w.getOrder());
				workflow.setRole(w.getRole());
				workflow.setSignature(w.getSignature());
				entity.getWorkflow().add(workflow);
			}
		}
	}

	@Override
	@Transactional
	public List<User> getNextApprover(T entity) {
		T e = (T) dao.getById(entity.getId());
		
		List<User> users = new ArrayList<User>();
		Workgroup wg;
		Workflow wf = getNextWorkflow(e);
		Role nextRole;
		
		if (wf == null) {
			return null;
		} else {
			nextRole = wf.getRole();
		}
		
		if (wf.getCentre() || e.getWorkgroup() == null) {
			wg = e.getCentre();
		} else {
			wg = e.getWorkgroup();
		}
		
		workgroupService.loadMembers(wg);
		
		for (JobMapping jm : wg.getMembers())
		{
			if (jm.getRole().equals(nextRole) && jm.getMember().isPerson()) {
				users.add((User) jm.getMember());
			}
		}
		
		return users;
	}

	@Override
	@Transactional
	@LazyLoader({"grid", "form"})
	public void loadAuthItems(T entity) {
		try {
			if (entity.getAuthorization() == null) {
				throw new LazyInitializationException("");
			}
			entity.getAuthorization().size();
		} catch (LazyInitializationException ex) {
			T e = dao.getById(entity.getId());
			e.getAuthorization().size();
			entity.setAuthorization(e.getAuthorization());
		}
	}
	

	@Override
	@Transactional
	@LazyLoader("grid")
	public void loadType(T data) {
		try {
			if (data.getType() == null) {
				throw new LazyInitializationException("");
			}
			data.getType().getType();
		} catch (LazyInitializationException ex) {
			T e = dao.getById(data.getId());
			
			if (e.getType() == null) {
				return;
			}
			
			e.getType().getType();
			data.setType(e.getType());
		}
	}

	@Override
	@Transactional
	@LazyLoader({"grid", "form"})
	public void loadWorkflow(T data) {
		try {
			if (data.getWorkflow() == null) {
				throw new LazyInitializationException("");
			}
			data.getWorkflow().size();
		} catch (LazyInitializationException ex) {
			T e = dao.getById(data.getId());
			e.getWorkflow().size();
			e.getOwnedBy().getFullName();
			data.setWorkflow(e.getWorkflow());
			data.setOwnedBy(e.getOwnedBy());
		}
	}

	protected void approve(T entity, User user, Date approveDate, byte[] signedPdf) {
		T e = (T) dao.getById(entity.getId());

		if (e.getReqDate().getTime() > approveDate.getTime()) {
			throw new ApproveException("ErrApproveBeforeRequirement");
		}

		if (e.getLastApproveDate() != null && e.getLastApproveDate().getTime() > approveDate.getTime()) {
			throw new ApproveException("ErrAppreveBeforeLastApprove");
		}

		Workflow wf = getNextWorkflow(e);
		if (wf == null) {
			return;
		}

		Role role = wf.getRole();
		AuthItem auth = new AuthItem();
		auth.setApprover(user);
		auth.setRole(role);
		auth.setAuthDate(approveDate);

		e.getAuthorization().add(auth);

		if (getNextWorkflow(e) == null) {
			e.setState(RequirementState.APPROVED);
		} else {
			e.setState(RequirementState.PARTIALLY);
		}

		super.update(e);

		if (signedPdf != null) {
			saveSignedDoc(e, signedPdf);
		} else if (wf.getSignature() != null && wf.getSignature() && !signatureNotRequired(wf)) {
			throw new ApproveException("ErrApproveMustBeSigned");
		}

		if (!autoApprove(e, approveDate)) {
			this.sendToApprovers(e);

			SettingsData settings = settingsService.getSettings();
			MailMessage message = null;

			if (e.getOwnedBy().getEmail() != null
					&& !e.getOwnedBy().getEmail().isEmpty()
					&& e.getOwnedBy().isNotify()) {
				if (e.getState() == RequirementState.APPROVED && settings.getConfReqTemplate() != null) {
					message = messageBuilder.buildMessage(settings.getConfReqTemplate(), e);
				} else if (settings.getAuthReqTemplate() != null) {
					message = messageBuilder.buildMessage(settings.getAuthReqTemplate(), e);
				}

				if (message != null) {
					message.setFrom(getLoggedInUser().getEmail());
					message.setTo(e.getOwnedBy().getEmail());
					mailer.send(message);
				}
			}
		}

		postApprove(e);
	}

	/**
	 * Check pro automatické schválení, kdy není třeba podepisovat- nadlimitní nákupy se neautorizují...
	 * @param wf Další krok schválení
	 * @return true pokud není potřeba podepisovat. Zde vrací vždy false, v případě potřeby nutno překrýt.
	 */
	protected boolean signatureNotRequired(Workflow wf) {
		return false;
	}

	protected void approve(T entity, User user) {
		approve(entity, user, new Date(), null);
	}
	
	@Override
	@Transactional
	@PreAuthorize("this.canApprove(#entity)")
	public void approve(T entity) {
		approve(entity, getLoggedInUser());
	}

	@Override
	@Transactional
	@PreAuthorize("this.canApprove(#entity)")
	public void approve(T entity, Date approveDate, byte[] signedPdf) {
		approve(entity, getLoggedInUser(), approveDate, signedPdf);
	}

	/**
	 * Volá se z metody approve pro automatické schválení dalším schvalovatelem v pořadí. Metoda z báze nedělá nic.
	 * 
	 * @param entity Požadavek
	 * @param approveDate
	 * @return true pokud se provedlo automatické schválení.
	 */
	protected boolean autoApprove(T entity, Date approveDate) {
		return false;
	}
	
	/**
	 * Činnost prováděná po schválení požadavku (i dílčím schválení).
	 * 
	 * @param entity Požadavek
	 */
	protected void postApprove(T entity) {
		
	}

	protected void saveSignedDoc(T entity, byte[] signedPdf) {
		DataModel reportEntity = entityForSignReport(entity);
		SignedDocumentItem signedItem = signedDocumentService.getItem(entityForSignReport(entity), getSignReport(entity).getReportId());

		if (signedItem == null) {
			SignedDocument signDoc = new SignedDocument();
			signDoc.setDescription(entity.getDescription());
			signDoc.setNumser(entity.getNumser());
			signDoc.setRecordId(reportEntity.getId());
			signDoc.setModuleName(ModuleUtils.getModule(reportEntity, servletContext).getId());
			signDoc.setSignDate(entity.getLastApproveDate());

			SignedDocumentItem item = new SignedDocumentItem();
			item.setReportId(reportDefinition.getReport().getReportId());
			item.setReportName(reportDefinition.getReport().getName());
			signDoc.addItem(item);

			signedDocStorage.saveFile(signedPdf, item);
			signedDocumentService.addFromApprove(signDoc);
		} else {
			signedDocStorage.saveFile(signedPdf, signedItem);
		}
	}

	@Override
	@Transactional
	public boolean canApprove(T entity) {
		List<User> nextApprovers = getNextApprover(entity); 
		if (nextApprovers != null && nextApprovers.contains(getLoggedInUser())) {
			return true;
		}
		
		return false;
	}

	@Override
	@Transactional
	public Workflow getNextWorkflow(T entity) {
		T e = dao.getById(entity.getId());

		AuthItem authItem = null;
		if (e.getWorkflow() == null) {
			return null;
		}
		
		if (e.getAuthorization().size() > 0) {
			authItem = e.getAuthorization().get(e.getAuthorization().size() - 1); 
		}
		
		Workflow wf = null;
		for (int i = 0; i < e.getWorkflow().size(); i++) {
			wf = e.getWorkflow().get(i);
			if (authItem != null && wf.getRole().equals(authItem.getRole())) {
				if (i + 1 < e.getWorkflow().size()) {
					wf = e.getWorkflow().get(i + 1);
					break;
				} else {
					return null;
				}
			}
			
			if (authItem == null) {
				break;
			}
		}
		
		return wf;
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_READ')")
	public List<T> getMy()
	{
		Query q = dao.getQuery("from " + dao.getEntityName());
		HqlDataFilter f = new HqlDataFilter("ownedBy = :owner");
		f.setParam("owner", getLoggedInUser());
		return filterQuery(q, f).list();
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_SHOW_CENTRE_REQ')")
	@PostFilter("hasPermission(filterObject, 'PERM_SHOW_CENTRE_REQ')")
	public List<T> getCentreReq()
	{
		List<Workgroup> wgList = workgroupService.getUserCentres(getLoggedInUser());
		Query q = dao.getQuery("select tr from " + dao.getEntityName() + " tr join fetch tr.ownedBy join tr.centre c");
		HqlDataFilter f = new HqlDataFilter("c in (:wgList)", "tr.numser desc");
		f.setParam("wgList", wgList);
		return filterQuery(q, f).list();
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_SHOW_WORKGROUP_REQ')")
	@PostFilter("hasPermission(filterObject, 'PERM_SHOW_WORKGROUP_REQ')")
	public List<T> getWorkgroupReq()
	{
		List<Workgroup> wgList = workgroupService.getUserWorkgroups(getLoggedInUser());
		Query q = dao.getQuery("select tr from " + dao.getEntityName() + " tr join fetch tr.ownedBy join tr.workgroup w");
		HqlDataFilter f = new HqlDataFilter("w in (:wgList)", "tr.numser desc");
		f.setParam("wgList", wgList);
		return filterQuery(q, f).list();
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_SHOW_ALL_REQ')")
	public List<T> getAll()
	{
		Query q = dao.getQuery("from " + dao.getEntityName() + " as tr join fetch tr.ownedBy");
		HqlDataFilter f = new HqlDataFilter("", "tr.numser desc");
		return filterQuery(q, f).list();
	}

	@Override
	public String getUpdateEntityPermission() {
		return Constants.PERM_EDIT_NEW;
	}
	
	@Override
	public String getDeleteEntityPermission() {
		return Constants.PERM_DELETE_NEW;
	}

	@Override
	@Transactional
	public boolean prepareSignData(T entity, Date approveDate) {
		SignData data = new SignData();
		sessionData.setProperty(Constants.KEY_SIGN_DATA, data);
		data.setSignGuid(UUID.randomUUID().toString());
		sessionData.setProperty(Constants.KEY_SIGN_GUID, data.getSignGuid());

		reportDefinition.clear();
		reportDefinition.setSingleObject(entityForSignReport(entity));

		Report report = getSignReport(entity);

		if (report == null) {
			return false;
		}

		reportDefinition.setReport(report);
		Module module = ModuleUtils.getModule(entityForSignReport(entity), servletContext);
		reportDefinition.setService((Service<Object>) ModuleUtils.getServiceInstance(module, servletContext));

		paramFiller.fill();
		Generator gen = genFactory.createGenerator(reportDefinition);
		data.setPdfData(gen.generate());
		data.setDescription(entity.getDescription());
		data.setNumser(entity.getNumser());
		data.setSignDate(approveDate);
		data.setSigned(signedDocumentService.getItem(entityForSignReport(entity), report.getReportId()) != null);

		if (getNextWorkflow(entity).getCentre()) { // viditelné podpisy lze pouze na urovni strediska
			if (entity.getState() == RequirementState.NEW) {
				data.setAreaId(1);
			} else {
				loadAuthItems(entity);
				int wgCount = 0;
				for (Workflow wf : entity.getWorkflow()) {
					if (!wf.getCentre()) {
						++wgCount;
					}
				}
				data.setAreaId(entity.getAuthorization().size() - wgCount + 1);
			}

			if (userService.getUserSettings().getSignatureFile() != null) {
				data.setSignImg(storage.fileData(userService.getUserSettings().getSignatureFile()));
			}
		}

		return true;
	}

	protected DataModel entityForSignReport(T entity) {
		return entity;
	}

	private Report getSignReport(T entity) {
		Module module = ModuleUtils.getModule(entityForSignReport(entity), servletContext);
		Report report = null;
		int reportId = Constants.SIGN_REPORT_MAP.get(entity.getClass()) == null ? 0 : Constants.SIGN_REPORT_MAP.get(entity.getClass());

		for (Report r : module.getReports()) {
			if (r.isSignable()) {
				if (reportId == 0) {
					report = r;
					break;
				} else if (reportId == r.getReportId()) {
					report = r;
					break;
				}
			}
		}

		return report;
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_EDIT') or hasPermission(#entity, this.getUpdateEntityPermission())")
	public void update(T entity) {
		entity.getAuthorization().clear();
		entity.setState(RequirementState.NEW);
		super.update(entity);

		try {
			signedDocumentService.deleteForEntity(entity);
		} catch (StorageException e) {
			logger.warn(e.getMessage());
		}

		sendToApprovers(entity);
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_DELETE') or hasPermission(#entity, this.getDeleteEntityPermission())")
	public void delete(T entity) {
		super.delete(entity);

		try {
			signedDocumentService.delFromApprove(signedDocumentService.getForEntity(entity));
		} catch (StorageException e) {
			logger.warn(e.getMessage());
		}
	}
}
