package eu.dnetlib.functionality.notification.app;

import java.net.URL;
import java.util.Date;
import java.util.SortedSet;

import org.apache.log4j.Logger;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import eu.dnetlib.api.functionality.NotificationServiceException;
import eu.dnetlib.domain.functionality.NotificationEvent;
import eu.dnetlib.domain.functionality.NotificationQuery;
import eu.dnetlib.domain.functionality.NotificationResult;
import eu.dnetlib.domain.functionality.NotificationSchedule;
import eu.dnetlib.domain.functionality.NotificationSubscription;
import eu.dnetlib.domain.functionality.ObjectPage;
import eu.dnetlib.functionality.notification.dao.NotificationDAO;
import eu.dnetlib.functionality.notification.dao.NotificationDAOException;
import eu.dnetlib.functionality.notification.executor.NotificationQueryExecutorException;

public class NotificationServiceTransactionHelper {
	private static final Logger logger = Logger.getLogger(NotificationServiceTransactionHelper.class);
	
	private NotificationDAO dao;
	
	/**
	 * Set the DAO.
	 * @param dao the DAO of this notification service transaction helper
	 */
	public void setDao(final NotificationDAO dao) {
		this.dao = dao;
	}
	
	/**
	 * Retrieve a page of queries.
	 * @param pageNumber the number of the page to retrieve
	 * @param pageSize the size of the page to retrieve
	 * @return an object page containing queries
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public ObjectPage<NotificationQuery> getQueries(final int pageNumber, final int pageSize) throws NotificationServiceException {
		try {
			final ObjectPage<NotificationQuery> queries = new ObjectPage<NotificationQuery>(pageNumber, pageSize, dao.countQueries(), dao.getQueries(pageSize, pageNumber * pageSize));
			logger.info("Retrieved queries " + queries);
			return queries;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving queries (page " + pageNumber + ", page size " + pageSize + ")", e);
		}
	}
	
	/**
	 * Retrieve a query.
	 * @param queryId the unique identifier of the query to retrieve
	 * @return the specified query or null if no such query exists
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public NotificationQuery getQuery(final String queryId) throws NotificationServiceException {
		try {
			final NotificationQuery query = dao.getQuery(queryId);
			logger.info((query == null) ? ("Query " + queryId + " does not exist") : ("Retrieved query " + query));
			return query;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving query " + queryId, e);
		}
	}
	
	/**
	 * Add a query.
	 * @param query the query to add
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void addQuery(final NotificationQuery query) throws NotificationServiceException {
		try {
			if (dao.getQuery(query.getQueryId()) != null)
				throw new NotificationServiceException("error adding query " + query + ": query already exists");
			dao.saveQuery(query);
			logger.info("Added query " + query);
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error adding query " + query, e);
		}
	}
	
	/**
	 * Remove a query.
	 * @param queryId the unique identifier of the query to remove
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void removeQuery(final String queryId) throws NotificationServiceException {
		try {
			if (dao.getQuery(queryId) == null)
				throw new NotificationServiceException("error removing query " + queryId + ": query does not exist");
			dao.deleteQuery(queryId);
			logger.info("Removed query " + queryId);
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error removing query " + queryId, e);
		}
	}
	
	/**
	 * Retrieve a page of schedules.
	 * @param pageNumber the number of the page to retrieve
	 * @param pageSize the size of the page to retrieve
	 * @return an object page containing schedules
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public ObjectPage<NotificationSchedule> getSchedules(final int pageNumber, final int pageSize) throws NotificationServiceException {
		try {
			final ObjectPage<NotificationSchedule> schedules = new ObjectPage<NotificationSchedule>(pageNumber, pageSize, dao.countSchedules(), dao.getSchedules(pageSize, pageNumber * pageSize));
			logger.info("Retrieved schedules " + schedules);
			return schedules;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving schedules (page: " + pageNumber + ", page size: " + pageSize + ")", e);
		}
	}
	
	/**
	 * Get enabled schedules.
	 * @param limit the maximum number of schedules to retrieve
	 * @param offset the offset to start at
	 * @return a sorted set containing schedules
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public SortedSet<NotificationSchedule> getEnabledSchedules(final int limit, final int offset) throws NotificationServiceException {
		try {
			final SortedSet<NotificationSchedule> schedules = dao.getEnabledSchedules(limit, offset);
			logger.info("Retrieved " + schedules.size() + " enabled schedules (limit: " + limit + ", offset: " + offset + ")");
			return schedules;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving enabled schedules (limit: " + limit + ", offset: " + offset + ")", e);
		}
	}
		
	/**
	 * Retrieve a schedule.
	 * @param queryId the unique identifier of the query of the schedule to retrieve
	 * @return the schedule of the specified query or null if no such schedule exists
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public NotificationSchedule getSchedule(final String queryId) throws NotificationServiceException {
		try {
			final NotificationSchedule schedule = dao.getSchedule(queryId);
			logger.info((schedule == null) ? ("Schedule " + queryId + " does not exist") : ("Retrieved schedule " + schedule));
			return schedule;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving schedule " + queryId, e);
		}
	}

	/**
	 * Add a schedule.
	 * @param schedule the schedule to add
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void addSchedule(final NotificationSchedule schedule) throws NotificationServiceException {
		try {
			if (dao.getSchedule(schedule.getQueryId()) != null)
				throw new NotificationServiceException("error adding schedule " + schedule + ": schedule already exists");
			dao.saveSchedule(schedule);
			logger.info("Added schedule " + schedule);
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error adding schedule " + schedule, e);
		}
	}
	
	/**
	 * Enable a schedule.
	 * @param queryId the unique identifier of the query of the schedule to enable
	 * @return the schedule enabled
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public NotificationSchedule enableSchedule(final String queryId) throws NotificationServiceException {
		try {
			final NotificationSchedule schedule = dao.getSchedule(queryId);
			if (schedule == null)
				throw new NotificationServiceException("error enabling schedule " + queryId + ": schedule does not exist");
			if (schedule.isEnabled())
				throw new NotificationServiceException("error enabling schedule " + queryId + ": schedule is already enabled");
			schedule.setEnabled(true);
			dao.saveSchedule(schedule);
			logger.info("Enabled schedule " + schedule);
			return schedule;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error enabling schedule " + queryId, e);
		}
	}
	
	/**
	 * Disable a schedule.
	 * @param queryId the unique identifier of the query of the schedule to disable
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void disableSchedule(final String queryId) throws NotificationServiceException {
		try {
			final NotificationSchedule schedule = dao.getSchedule(queryId);
			if (schedule == null)
				throw new NotificationServiceException("error disabling schedule " + queryId + ": schedule does not exist");
			if (!schedule.isEnabled())
				throw new NotificationServiceException("error disabling schedule " + queryId + ": schedule is already disabled");
			schedule.setEnabled(false);
			dao.saveSchedule(schedule);
			logger.info("Disabled schedule " + schedule);
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error disabling schedule " + queryId, e);
		}
	}
	
	/**
	 * Remove a schedule.
	 * @param queryId the unique identifier of the query of the schedule to remove
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void removeSchedule(final String queryId) throws NotificationServiceException {
		try {
			if (dao.getSchedule(queryId) == null)
				throw new NotificationServiceException("error removing schedule " + queryId + ": schedule does not exist");
			dao.deleteSchedule(queryId);
			logger.info("Removed schedule " + queryId);
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error removing schedule " + queryId, e);
		}
	}
	
	/**
	 * Retrieve a page of events.
	 * @param pageNumber the number of the page to retrieve
	 * @param pageSize the size of the page to retrieve
	 * @return an object page containing events
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public ObjectPage<NotificationEvent> getEvents(final int pageNumber, final int pageSize) throws NotificationServiceException {
		try {
			final ObjectPage<NotificationEvent> events = new ObjectPage<NotificationEvent>(pageNumber, pageSize, dao.countEvents(), dao.getEvents(pageSize, pageNumber * pageSize));
			logger.info("Retrieved events " + events);
			return events;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving events (page: " + pageNumber + ", page size: " + pageSize, e);
		}
	}
	
	/**
	 * Retrieve a page of results.
	 * @param pageNumber the number of the page to retrieve
	 * @param pageSize the size of the page to retrieve
	 * @return an object page containing results
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public ObjectPage<NotificationResult> getResults(final int pageNumber, final int pageSize) throws NotificationServiceException {
		try {
			final ObjectPage<NotificationResult> results = new ObjectPage<NotificationResult>(pageNumber, pageSize, dao.countResults(), dao.getResults(pageSize, pageNumber * pageSize));
			logger.info("Retrieved results " + results);
			return results;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving results (page: " + pageNumber + ", page size: " + pageSize + ")", e);
		}
	}
	
	/**
	 * Retrieve a result.
	 * @param queryId the unique identifier of the query of the event of the result to retrieve
	 * @param date the date corresponding to the event of the result to retrieve
	 * @param resultId the unique identifier of the result to retrieve
	 * @return the result specified or null if no such result exists
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public NotificationResult getResult(final String queryId, final Date date, final String resultId) throws NotificationServiceException {
		try {
			final NotificationResult result = dao.getResult(queryId, date, resultId);
			logger.info((result == null) ? ("Result (query: " + queryId + ", date: " + date + ", result: " + resultId + ") does not exist") : ("Retrieved result " + result));
			return result;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving result (query: " + queryId + ", date: " + date + ", result: " + resultId + ")", e);
		}
	}
	
	/**
	 * Retrieve the a result of the previous event of an event of a query.
	 * @param queryId the unique identifier of the query of the event of the result to retrieve
	 * @param date the date corresponding to the next event of the event to retrieve
	 * @param resultId the unique identifier of the result to retrieve
	 * @return the result specified or null if no such result exists
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public NotificationResult getPreviousResult(final String queryId, final Date date, final String resultId) throws NotificationServiceException {
		try {
			final NotificationResult result = dao.getPreviousResult(queryId, date, resultId);
			logger.info((result == null) ? ("Previous result of result (query: " + queryId + ", date: " + date + ", result: " + resultId + ") does not exist") : ("Retrieved result " + result +
					" as previous of result (query: " + queryId + ", date: " + date + ", result: " + resultId + ")"));
			return result;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving previous result of result (query: " + queryId + ", date: " + date + ", result: " + resultId + ")", e);
		}
	}
	
	/**
	 * Retrieve a result of the last event of a query. 
	 * @param queryId the unique identifier of the query of the event of the result to retrieve
	 * @param resultId the unique identifier of the result to retrieve
	 * @return the result specified or null if no such result exists
	 * @throws NotificationQueryExecutorException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationQueryExecutorException.class)
	public NotificationResult getLastResult(final String queryId, final String resultId) throws NotificationQueryExecutorException {
		try {
			final NotificationResult result = dao.getLastResult(queryId, resultId);
			logger.info((result == null) ? ("Result " + resultId + " of last event of query " + queryId + " does not exist") : ("Retrieved result " + result + " as result " + resultId + " of last event of query " + queryId));
			return result;
		} catch (final NotificationDAOException e) {
			throw new NotificationQueryExecutorException("error retrieving result " + resultId + " of last event of query " + queryId, e);
		}
	}
	
	/**
	 * Add a new result.
	 * @param result the result to add
	 * @throws NotificationQueryExecutorException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationQueryExecutorException.class)
	public void addResult(final NotificationResult result) throws NotificationServiceException {
		try {
			if (dao.getResult(result.getQueryId(), result.getDate(), result.getResultId()) != null)
				throw new NotificationServiceException("error adding result " + result + ": result already exists");
			dao.saveResult(result);
			logger.info("Added result " + result);
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error adding result " + result, e);
		}
	}
	
	/**
	 * Retrieve a page of subscriptions.
	 * @param pageNumber the number of the page to retrieve
	 * @param pageSize the size of the page to retrieve
	 * @return an object page containing subscriptions
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public ObjectPage<NotificationSubscription> getSubscriptions(final int pageNumber, final int pageSize) throws NotificationServiceException {
		try {
			final ObjectPage<NotificationSubscription> subscriptions = new ObjectPage<NotificationSubscription>(pageNumber, pageSize, dao.countSubscriptions(), dao.getSubscriptions(pageSize, pageNumber * pageSize));
			logger.info("Retrieved subscriptions " + subscriptions);
			return subscriptions;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving subscriptions (page: " + pageNumber + ", page size: " + pageSize + ")", e);
		}
	}
	
	/**
	 * Retrieve enabled subscriptions.
	 * @param queryId the unique identifier of the query whose subscriptions to retrieve
	 * @param limit the maximum number of subscriptions to retrieve
	 * @param offset the offset to use
	 * @return a sorted set of subscriptions
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public SortedSet<NotificationSubscription> getEnabledSubscriptions(final String queryId, final int limit, final int offset) throws NotificationServiceException {
		try {
			final SortedSet<NotificationSubscription> subscriptions = dao.getEnabledSubscriptions(queryId, limit, offset);
			logger.info("Retrieved " + subscriptions.size() + " enabled subscriptions of query " + queryId + " (limit: " + limit + ", offset: " + offset + ")");
			return subscriptions;
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error retrieving enabled subscriptions of query " + queryId + " (limit: " + limit + ", offset: " + offset + ")", e);
		}
	}
	
	/**
	 * Add a subscription.
	 * @param subscription the subscription to add
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true, rollbackFor = NotificationServiceException.class)
	public void addSubscription(final NotificationSubscription subscription) throws NotificationServiceException {
		try {
			if (dao.getSubscription(subscription.getQueryId(), subscription.getAlertService()) != null)
				throw new NotificationServiceException("error adding subscription " + subscription + ": subscription already exists");
			dao.saveSubscription(subscription);
			logger.info("Added subscription " + subscription);
		} catch (final NotificationDAOException e) {
			throw new NotificationServiceException("error adding subscription " + subscription, e);
		}
	}
	
	/**
	 * Enable a subscription.
	 * @param queryId the unique identifier of the query of the schedule of the subscription to enable
	 * @param alertService the URL of the alert service of the subscription to enable
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void enableSubscription(final String queryId, final URL alertService) throws NotificationServiceException {
		try {
			final NotificationSubscription subscription = dao.getSubscription(queryId, alertService);
			if (subscription == null)
				throw new NotificationServiceException("error enabling subscription (query: " + queryId + ", alert service: " + alertService + "): subscription does not exist");
			if (subscription.isEnabled())
				throw new NotificationServiceException("error enabling subscription (query: " + queryId + ", alert service: " + alertService + "): subscription is already enabled");
			subscription.setEnabled(true);
			dao.saveSubscription(subscription);
			logger.info("Enabled subscription (query: " + queryId + ", alert service: " + alertService + ")");
		} catch (NotificationDAOException e) {
			throw new NotificationServiceException("error enabling subscription (query: " + queryId + ", alert service: " + alertService + ")", e);
		}
	}	
	
	/**
	 * Disable a subscription.
	 * @param queryId the unique identifier of the query of the schedule of the subscription to disable
	 * @param alertService the URL of the alert service of the subscription to disable
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void disableSubscription(final String queryId, final URL alertService) throws NotificationServiceException {
		try {
			final NotificationSubscription subscription = dao.getSubscription(queryId, alertService);
			if (subscription == null)
				throw new NotificationServiceException("error disabling subscription (query: " + queryId + ", alert service: " + alertService + "): subscription does not exist");
			if (!subscription.isEnabled())
				throw new NotificationServiceException("error disabling subscription (query: " + queryId + ", alert service: " + alertService + "): subscription is already disabled");
			subscription.setEnabled(false);
			dao.saveSubscription(subscription);
			logger.info("Disabled subscription (query: " + queryId + ", alert service: " + alertService + ")");
		} catch (NotificationDAOException e) {
			throw new NotificationServiceException("error disabling subscription (query: " + queryId + ", alert service: " + alertService + ")", e);
		}
	}	
	
	/**
	 * Remove a subscription.
	 * @param queryId the unique identifier of the query of the schedule of the subscription to remove
	 * @param alertService the URL of the alert service of the subscription to remove
	 * @throws NotificationServiceException if any errors occur
	 */
	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = NotificationServiceException.class)
	public void removeSubscription(final String queryId, final URL alertService) throws NotificationServiceException {
		try {
			if (dao.getSubscription(queryId, alertService) == null)
				throw new NotificationServiceException("error removing subscription (query: " + queryId + ", alert service: " + alertService + "): subscription does not exist");
			dao.deleteSubscription(queryId, alertService);
			logger.info("Removed subscription (query: " + queryId + ", alert service: " + alertService + ")");
		} catch (NotificationDAOException e) {
			throw new NotificationServiceException("error removing subscription (query: " + queryId + ", alert service: " + alertService + ")", e);
		}
	}
}
