package eu.dnetlib.functionality.alert.alerter.batch;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.log4j.Logger;

import eu.dnetlib.api.functionality.AlertServiceException;
import eu.dnetlib.domain.functionality.AlertSubscription;
import eu.dnetlib.functionality.alert.alerter.Alerter;
import eu.dnetlib.functionality.alert.alerter.AlerterException;
import eu.dnetlib.functionality.alert.alerter.batch.domain.BatchAlert;
import eu.dnetlib.functionality.alert.alerter.batch.domain.BatchSubscription;
import eu.dnetlib.functionality.alert.app.AlertServiceTransactionHelper;

/**
 * This class implements an alerter that is capable of sending batch alerts.
 * The batch alerter is actually a meta-alerter that caches incoming alerts, concatenates them periodically in batches using a scheduled executor service and then sends each batch as
 * a single alert, delegating the actual sending to another alerter.
 * In order to persist batch subscriptions and alerts a JDBC batch alerter DAO is used, while an alert service transaction helper is used for persisting simple alert subscriptions.
 * @author thanos@di.uoa.gr
 * @see eu.dnetlib.functionality.alert.alerter.Alerter
 * @see eu.dnetlib.functionality.alert.alerter.AlerterException
 * @see eu.dnetlib.functionality.alert.alerter.batch.dao.BatchAlerterDAO
 * @see eu.dnetlib.functionality.alert.alerter.batch.domain.BatchSubscription
 * @see eu.dnetlib.functionality.alert.alerter.batch.domain.BatchAlert
 *
 */
public class BatchAlerter extends Alerter {
	public static final String BATCH_ALERT_MODE_PREFIX = "Batch ";
	public static final String BATCH = "batch";
	private static final SortedSet<String> SUPPORTED_URI_SCHEMES = new TreeSet<String>();
	private static final Logger logger = Logger.getLogger(BatchAlerter.class);
	
	static {
		SUPPORTED_URI_SCHEMES.add(BATCH);
	}

	private final Map<String, Alerter> alerters;
	private final Map<String, Map<URL, Map<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>>>> schedules;
	private final DateFormat dateFormat;
	private BatchAlerterTransactionHelper batchAlerterTransactionHelper;
	private AlertServiceTransactionHelper alertServiceTransactionHelper;
	private SortedSet<String> supportedAlertModes;
	private ScheduledExecutorService scheduler;
	private String batchAlertTitle;
	private int pageSize;
	
	/**
	 * Construct a new batch alerter.
	 */
	public BatchAlerter() {
		alerters = new HashMap<String, Alerter>();
		supportedAlertModes = new TreeSet<String>();
		schedules = new HashMap<String, Map<URL, Map<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>>>>();
		dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm");
	}
	
	/**
	 * Set the batch alerter transaction helper.
	 * @param batchAlerterTransactionHelper the batch alerter transaction helper to use to manage alerts
	 */
	public void setBatchAlerterTransactionHelper(final BatchAlerterTransactionHelper batchAlerterTransactionHelper) {
		this.batchAlerterTransactionHelper = batchAlerterTransactionHelper;
	}
	
	/**
	 * Set the alert service transaction helper.
	 * @param alertServiceTransactionHelper the alert service transaction helper to use to manage subscriptions
	 */
	public void setAlertServiceTransactionHelper(final AlertServiceTransactionHelper alertServiceTransactionHelper) {
		this.alertServiceTransactionHelper = alertServiceTransactionHelper;
	}
	
	/**
	 * Set the alerters.
	 * @param alerters a set containing the alerters to use
	 */
	public void setAlerters(final Set<Alerter> alerters) {
		for (Alerter alerter : alerters) {
			for (String alertMode : alerter.getSupportedAlertModes()) {
				this.alerters.put(BATCH_ALERT_MODE_PREFIX + alertMode, alerter);
				supportedAlertModes.add(BATCH_ALERT_MODE_PREFIX + alertMode);
			}
		}
	}

	/**
	 * Set the thread pool size.
	 * @param threadPoolSize the thread pool size of the underlying scheduled executor service
	 */
	public void setThreadPoolSize(final int threadPoolSize) {
		scheduler = Executors.newScheduledThreadPool(threadPoolSize);
	}
	
	/**
	 * Set the batch alert title.
	 * @param batchAlertTitle the title to use when sending batch alerts
	 */
	public void setBatchAlertTitle(final String batchAlertTitle) {
		this.batchAlertTitle = batchAlertTitle;
	}
	
	/**
	 * Set the page size.
	 * @param pageSize the page size to use
	 */
	public void setPageSize(final int pageSize) {
		this.pageSize = pageSize;
	}
	
	@Override
	public void init() {
		logger.info("Batch alerter initialization complete");
	}
	
	@Override
	public SortedSet<String> getSupportedAlertModes() {
		return supportedAlertModes;
	}

	@Override
	public void alert(final AlertSubscription subscription, final String title, final String message, final URL link) throws AlerterException {
		validateSubscription(subscription);
		final BatchSubscription batchSubscription = new BatchSubscription(subscription);
		batchAlerterTransactionHelper.addAlert(new BatchAlert(batchSubscription, new Date(), title, message, link));
		schedule(batchSubscription.getTemplateId(), batchSubscription.getNotificationService(), batchSubscription.getQueryId(), batchSubscription.getResultId(), batchSubscription.getAlertMode(),
				batchSubscription.getSubscriber(), batchSubscription.getAlertPeriod());
	}

	@Override
	protected SortedSet<String> getSupportedUriSchemes() {
		return SUPPORTED_URI_SCHEMES;
	}
	
	private void schedule(final String templateId, final URL notificationService, final String queryId, final String resultId, final String alertMode, final URI subscriber, final long alertPeriod) {
		if (!schedules.containsKey(templateId))
			schedules.put(templateId, new HashMap<URL, Map<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>>>());
		final Map<URL, Map<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>>> templateIdSchedules = schedules.get(templateId);
		if (!templateIdSchedules.containsKey(notificationService))
			templateIdSchedules.put(notificationService, new HashMap<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>>());
		final Map<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>> notificationServiceSchedules = templateIdSchedules.get(notificationService);
		if (!notificationServiceSchedules.containsKey(queryId))
			notificationServiceSchedules.put(queryId, new HashMap<String, Map<String, Map<URI, ScheduledFuture<?>>>>());
		final Map<String, Map<String, Map<URI, ScheduledFuture<?>>>> queryIdSchedules = notificationServiceSchedules.get(queryId);
		if (!queryIdSchedules.containsKey(resultId))
			queryIdSchedules.put(resultId, new HashMap<String, Map<URI, ScheduledFuture<?>>>());
		final Map<String, Map<URI, ScheduledFuture<?>>> resultIdSchedules = queryIdSchedules.get(resultId);
		if (!resultIdSchedules.containsKey(alertMode))
			resultIdSchedules.put(alertMode, new HashMap<URI, ScheduledFuture<?>>());
		final Map<URI, ScheduledFuture<?>> alertModeSchedules = resultIdSchedules.get(alertMode);
		if (!alertModeSchedules.containsKey(subscriber)) {
			alertModeSchedules.put(subscriber, scheduler.scheduleAtFixedRate(new Runnable() {
				@Override
				public void run() {
					try {
						final BatchSubscription subscription  = batchAlerterTransactionHelper.getSubscription(templateId, notificationService, queryId, resultId, alertMode, subscriber);
						if (subscription == null) {
							logger.info("Error sending batch alert to subscription (template: " + templateId + ", notification service: " + notificationService + ", query: " + queryId + ", result: " + resultId +
									", alert mode: " + alertMode + ", subscriber: " + subscriber + "): subscription does not exist");
							return;
						}
						final Alerter alerter = alerters.get(subscription.getAlertMode());
						if (alerter == null) { // alert mode is not supported
							alertServiceTransactionHelper.disableSubscription(templateId, notificationService, queryId, resultId, alertMode, subscriber);
							cancel(templateId, notificationService, queryId, resultId, alertMode, subscriber);
							logger.info("Disabled subscription " + subscription + " because alert mode " + subscription.getAlertMode() + " is not supported");
							return;
						}
						final StringBuilder batch = new StringBuilder();
						for (int offset = 0; ; offset += pageSize) {
							final SortedSet<BatchAlert> alerts = batchAlerterTransactionHelper.getAlerts(subscription, pageSize, offset);
							for (BatchAlert alert : alerts) {
								batch.append("<p>").append(alert.getTitle()).append(" (").append(dateFormat.format(alert.getDate())).append(")<br />");
								if (alert.getLink() != null)
									batch.append("<a href=\"").append(StringEscapeUtils.escapeXml(alert.getLink().toString())).append("\">");
								batch.append(alert.getMessage());
								if (alert.getLink() != null)
									batch.append("</a>");
								batch.append("</p>");
							}
							if ((alerts.size() == 0) && (offset == 0)) { // if no alerts have been generated during the last period it is safe to cancel subscription - it will be rescheduled with the first alert to arrive
								cancel(templateId, notificationService, queryId, resultId, alertMode, subscriber);
								return;
							}
							if (alerts.size() < pageSize)
								break;
						}
						alerter.alert(subscription.getBasicSubscription(), batchAlertTitle, batch.toString(), null);
						batchAlerterTransactionHelper.removeAlerts(subscription); // remove sent alerts
						logger.info("Sent batch alert to subscription " + subscription);
					} catch (final AlerterException e) {
						logger.warn("Error sending batch alert to " + subscriber, e);
					} catch (final AlertServiceException e) {
						logger.warn("Error sending batch alert to " + subscriber, e);
					} catch (final URISyntaxException e) {
						logger.warn("Error sending batch alert to " + subscriber, e);
					}
				}
			}, 0, alertPeriod, TimeUnit.MINUTES));
		}
	}
	
	private void cancel(final String templateId, final URL notificationService, final String queryId, final String resultId, final String alertMode, final URI subscriber) {
		final Map<URL, Map<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>>> templateIdSchedules = schedules.get(templateId);
		if (templateIdSchedules != null) {
			final Map<String, Map<String, Map<String, Map<URI, ScheduledFuture<?>>>>> notificationServiceSchedules = templateIdSchedules.get(notificationService);
			if (notificationServiceSchedules != null) {
				final Map<String, Map<String, Map<URI, ScheduledFuture<?>>>> queryIdSchedules = notificationServiceSchedules.get(queryId);
				if (queryIdSchedules != null) {
					final Map<String, Map<URI, ScheduledFuture<?>>> resultIdSchedules = queryIdSchedules.get(resultId);
					if (resultIdSchedules != null) {
						final Map<URI, ScheduledFuture<?>> alertModeSchedules = resultIdSchedules.get(alertMode);
						if (alertModeSchedules != null) {
							final ScheduledFuture<?> schedule = alertModeSchedules.get(subscriber);
							if (schedule != null) {
								schedule.cancel(false);
								alertModeSchedules.remove(subscriber);
							}
							if (alertModeSchedules.isEmpty())
								resultIdSchedules.remove(alertMode);
						}
						if (resultIdSchedules.isEmpty())
							queryIdSchedules.remove(resultId);
					}
					if (queryIdSchedules.isEmpty())
						notificationServiceSchedules.remove(queryId);
				}
				if (notificationServiceSchedules.isEmpty())
					templateIdSchedules.remove(notificationService);
			}
			if (templateIdSchedules.isEmpty())
				schedules.remove(templateId);
		}
	}
}
