package eu.dnetlib.functionality.alert.app;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
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 org.apache.commons.lang.WordUtils;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.log4j.Logger;

import eu.dnetlib.api.functionality.AlertService;
import eu.dnetlib.api.functionality.AlertServiceException;
import eu.dnetlib.clients.functionality.notification.ws.NotificationWebService;
import eu.dnetlib.clients.functionality.notification.ws.NotificationWebServiceException;
import eu.dnetlib.domain.functionality.AlertSubscription;
import eu.dnetlib.domain.functionality.AlertTemplate;
import eu.dnetlib.domain.functionality.NotificationEvent;
import eu.dnetlib.domain.functionality.NotificationResult;
import eu.dnetlib.domain.functionality.ObjectPage;
import eu.dnetlib.domain.functionality.ResultPage;
import eu.dnetlib.functionality.alert.alerter.Alerter;
import eu.dnetlib.functionality.alert.alerter.AlerterException;
import gr.uoa.di.driver.app.DriverServiceImpl;

/**
 * This bean implements alert service using an alert DAO to manage topics and subscriptions and a scheduled executor service to update topics periodically.
 * @author thanos@di.uoa.gr
 * @see eu.dnetlib.api.functionality.AlertService
 * @see eu.dnetlib.domain.functionality.AlertTopic
 * @see eu.dnetlib.domain.functionality.AlertSubscription
 * @see eu.dnetlib.functionality.alert.dao.AlertDAO
 * @see eu.dnetlib.api.functionality.AlertDAOException;
 *
 */
public class AlertServiceImpl extends DriverServiceImpl implements AlertService {
	private static final String NOTIFICATION_SERVICE_VAR = "\\$notificationService";
	private static final String QUERY_ID_VAR = "\\$queryId";
	private static final String RESULT_ID_VAR = "\\$resultId";
	private static final String RESULT_NAME_CAPITALIZE_VAR = "\\$resultNameCapitalize";
	private static final String RESULT_NAME_VAR = "\\$resultName";
	private static final String VALUE_VAR = "\\$value";
	private static final String FROM_DATE_LONG_VAR = "\\$fromDateLong";
	private static final String FROM_DATE_STRING_DASHES_VAR = "\\$fromDateStringDashes";
	private static final String FROM_DATE_STRING_SLASHES_VAR = "\\$fromDateStringSlashes";
	private static final String TO_DATE_LONG_VAR = "\\$toDateLong";
	private static final String TO_DATE_STRING_DASHES_VAR = "\\$toDateStringDashes";
	private static final String TO_DATE_STRING_SLASHES_VAR = "\\$toDateStringSlashes";
	private static final Logger logger = Logger.getLogger(AlertServiceImpl.class);
	private static final DateFormat dateFormatDashes = new SimpleDateFormat("yyyy-MM-dd");
	private static final DateFormat dateFormatSlashes = new SimpleDateFormat("dd/MM/yyyy");
	
	private AlertServiceTransactionHelper transactionHelper;
	private final Map<String, Alerter> alerters;
	private final SortedSet<String> supportedAlertModes;
	private JaxWsProxyFactoryBean serviceFactory;
	private Map<URL, NotificationWebService> notificationServices;
	private int pageSize;

	/**
	 * Construct a new alert service implementation.
	 */
	public AlertServiceImpl() {
		alerters = new HashMap<String, Alerter>();
		supportedAlertModes = new TreeSet<String>();
		serviceFactory = new JaxWsProxyFactoryBean();
		serviceFactory.setServiceClass(NotificationWebService.class);
		notificationServices = new HashMap<URL, NotificationWebService>();
	}
	
	/**
	 * Set the transaction helper.
	 * @param transactionHelper the transaction helper to use for template, topic and subscription management
	 */
	public void setTransactionHelper(final AlertServiceTransactionHelper transactionHelper) {
		this.transactionHelper = transactionHelper;
	}
	
	/**
	 * Set the alerters.
	 * @param alerters a set containing alerters to use
	 */
	public void setAlerters(final Set<Alerter> alerters) {
		for (Alerter alerter : alerters) {
			supportedAlertModes.addAll(alerter.getSupportedAlertModes());
			for (String alertMode : alerter.getSupportedAlertModes())
				this.alerters.put(alertMode, alerter);
		}
	}
	
	/**
	 * Set the page size.
	 * @param pageSize the page size to use
	 */
	public void setPageSize(final int pageSize) {
		this.pageSize = pageSize;
	}
	
	@Override
	public void init() {
		super.init();
		try {
			for (int offset = 0; ; offset += pageSize) {
				final SortedSet<AlertSubscription> subscriptions = transactionHelper.getEnabledSubscriptions(pageSize, offset);
				for (AlertSubscription subscription : subscriptions) {
					try {
						if (!alerters.containsKey(subscription.getAlertMode())) {
							transactionHelper.disableSubscription(subscription.getTemplateId(), subscription.getNotificationService(), subscription.getQueryId(), subscription.getResultId(), subscription.getAlertMode(),
									subscription.getSubscriber());
							logger.info("Disabled subscription " + subscription + " because alert mode " + subscription.getAlertMode() + " is not supported");
						}
					} catch (final AlertServiceException e) {
						logger.warn("Error disabling subscription " + subscription + " with unsupported alert mode " + subscription.getAlertMode());
					}
				}
				if (subscriptions.size() < pageSize)
					break;
			}
			logger.info("Alert service initialization complete");
		} catch (final AlertServiceException e) {
			logger.warn("Error initializing alert service", e);
		}
	}
	
	@Override
	public SortedSet<String> getSupportedAlertModes() {
		return supportedAlertModes;
	}
	
	@Override
	public ObjectPage<AlertTemplate> getTemplates(final int pageNumber, final int pageSize) throws AlertServiceException {
		return transactionHelper.getTemplates(pageNumber, pageSize);
	}
	
	@Override
	public void addTemplate(final AlertTemplate template) throws AlertServiceException {
		transactionHelper.addTemplate(template);
	}
	
	@Override
	public void removeTemplate(final String templateId) throws AlertServiceException {
		transactionHelper.removeTemplate(templateId);
	}

	@Override
	public ObjectPage<AlertSubscription> getSubscriptions(final int pageNumber, final int pageSize) throws AlertServiceException {
		return transactionHelper.getSubscriptions(pageNumber, pageSize);
	}
	
	@Override
	public SortedSet<AlertSubscription> getSubscriptions(final String alertMode, final String subscriber, final int limit, final int offset) throws AlertServiceException {
		return transactionHelper.getSubscriptions(alertMode, subscriber, limit, offset);
	}

	@Override
	public void addSubscription(final AlertSubscription subscription) throws AlertServiceException {
		transactionHelper.addSubscription(subscription);
	}

	@Override
	public void enableSubscription(final String templateId, final URL notificationService, final String queryId, final String resultId, final String alertMode, final URI subscriber) throws AlertServiceException {
		transactionHelper.enableSubscription(templateId, notificationService, queryId, resultId, alertMode, subscriber);
	}

	@Override
	public void disableSubscription(final String templateId, final URL notificationService, final String queryId, final String resultId, final String alertMode, final URI subscriber) throws AlertServiceException {
		transactionHelper.disableSubscription(templateId, notificationService, queryId, resultId, alertMode, subscriber);
	}

	@Override
	public void removeSubscription(final String templateId, final URL notificationService, final String queryId, final String resultId, final String alertMode, final URI subscriber) throws AlertServiceException {
		transactionHelper.removeSubscription(templateId, notificationService, queryId, resultId, alertMode, subscriber);
	}
	
	@Override
	public int countAlertResults(final URL notificationService, final String queryId, final Date date, final String resultId) throws AlertServiceException {
		try {
			final NotificationWebService service = getNotificationService(notificationService);
			final NotificationResult result = service.getResult(queryId, date, resultId);
			if (result == null)
				throw new AlertServiceException("error counting alert results (notification service: " + notificationService + ", query: " + queryId + ", date: " + date + ", result: " + resultId +
						"): result (notification service: " + notificationService + ", query: " + queryId + ", date: " + date + ", resutl: " + resultId + ") does not exist");
			final NotificationResult previousResult = service.getPreviousResult(queryId, date, resultId);
			final int count = result.getValue() - ((previousResult == null) ? 0 : previousResult.getValue());
			logger.info("Counted " + count + " alert results (notification service: " + notificationService + ", query: " + queryId + ", date: " + date + ", result: " + resultId + ")");
			return count;
		} catch (final NotificationWebServiceException e) {
			throw new AlertServiceException("error counting alert results (notification service: " + notificationService + ", query: " + queryId + ", date: " + date + ", result: " + resultId + ")", e);
		}
	}

	@Override
	public ResultPage getAlertResults(final URL notificationService, final String queryId, final String resultId, final Date fromDate, final Date toDate, final int limit, final int offset) throws AlertServiceException {
		try {
			final NotificationWebService service = getNotificationService(notificationService);
			final ResultPage resultPage = service.executeQuery(queryId, resultId, fromDate, toDate, limit, offset);
			logger.info("Retrieved alert results " + resultPage + " (notification service: " + notificationService + ", query: " + queryId + ", result: " + resultId + ", from date: " + fromDate +
					", to date: " + toDate + ", limit: " + limit + ", offset: " + offset + ")");
			return resultPage;
		} catch (final NotificationWebServiceException e) {
			throw new AlertServiceException("error retrieving alert results (notification service: " + notificationService + ", query: " + queryId + ", result: " + resultId + ", from date: " +
					fromDate + ", to date: " + toDate + ", limit: " + limit + ", offset: " + offset + ")");
		}
	}
	
	@Override
	public void alert(final URL notificationService, final NotificationEvent event) throws AlertServiceException {
		String templateId = null;
		String resultId = null;
		AlertTemplate template = null;
		NotificationResult result = null;
		NotificationResult previousResult = null;
		String title = null;
		String message = null;
		URL link = null;
		for (int offset = 0; ; offset += pageSize) {
			final SortedSet<AlertSubscription> subscriptions = transactionHelper.getEnabledSubscriptions(notificationService, event.getQueryId(), pageSize, offset); 
			for (AlertSubscription subscription : subscriptions) {
				try {
					if ((!subscription.getTemplateId().equals(templateId)) || (!subscription.getResultId().equals(resultId))) { // template or result changed
						if (!subscription.getTemplateId().equals(templateId)) { // template changed
							templateId = subscription.getTemplateId();
							template = transactionHelper.getTemplate(templateId);
							if (template == null)
								logger.warn("Error alerting subscription " + subscription + ": template " + templateId + " does not exist");
						}
						if (!subscription.getResultId().equals(resultId)) { // result changed
							resultId = subscription.getResultId();
							final NotificationWebService service = getNotificationService(notificationService);
							result = service.getResult(event.getQueryId(), event.getDate(), resultId);
							if (result == null)
								logger.warn("Error alerting subscription " + subscription + ": result (notification service: " + notificationService + ", query: " + event.getQueryId() + ", date: " + event.getDate() +
										", resutl: " + resultId + ") does not exist");
							previousResult = service.getPreviousResult(event.getQueryId(), event.getDate(), resultId); 
						}
						title = ((template == null) || (result == null)) ? null : replaceVars(template.getTitle(), notificationService, event.getQueryId(), resultId, result.getName(), result.getValue() - ((previousResult ==
								null) ? 0 :previousResult.getValue()), (previousResult == null) ? new Date(0) : previousResult.getDate(), result.getDate(), false);
						message = ((template == null) || (result == null)) ? null : replaceVars(template.getMessage(), notificationService, event.getQueryId(), resultId, result.getName(), result.getValue() - ((previousResult ==
								null) ? 0 :previousResult.getValue()), (previousResult == null) ? new Date(0) : previousResult.getDate(), result.getDate(), false); 
						link = ((template == null) || (result == null) || (template.getLink() == null)) ? null : new URL(replaceVars(template.getLink().toString(), notificationService, event.getQueryId(), resultId,
								result.getName(), result.getValue() - ((previousResult == null) ? 0 :previousResult.getValue()), (previousResult == null) ? new Date(0) : previousResult.getDate(), result.getDate(), true));
					}
					if ((title != null) && (message != null)) {
						final Alerter alerter = alerters.get(subscription.getAlertMode());
						if (alerter == null) { // unsupported alert mode
							transactionHelper.disableSubscription(subscription.getTemplateId(), subscription.getNotificationService(), subscription.getQueryId(), subscription.getResultId(), subscription.getAlertMode(),
									subscription.getSubscriber());				
							logger.warn("Disabled subscription " + subscription + " because alert mode " + subscription.getAlertMode() + " is not supported");
						} else {
							alerter.alert(subscription, title, message, link);
							logger.info("Alerted subscription " + subscription);
						}
					}
				} catch (final AlerterException e) {
					logger.warn("Error alerting subscription " + subscription, e);
				} catch (final AlertServiceException e) {
					logger.warn("Error alerting subscription " + subscription, e);
				} catch (final MalformedURLException e){
					logger.warn("Error alerting subscription " + subscription, e);
				} catch (final NotificationWebServiceException e) {
					logger.warn("Error alerting subscription " + subscription, e);
				}
			}
			if (subscriptions.size() < pageSize)
				break;
		}
	}
		
	private NotificationWebService getNotificationService(final URL notificationService) throws AlertServiceException {
		NotificationWebService service = notificationServices.get(notificationService);
		if (service == null) {
			serviceFactory.setAddress(notificationService.toString());
			service = (NotificationWebService) serviceFactory.create();
			if (service == null)
				throw new AlertServiceException("error connecting to notification service " + notificationService);
			notificationServices.put(notificationService, service);
		}
		return service;
	}
	
	private String replaceVars(final String string, final URL notificationService, final String queryId, final String resultId, final String resultName, final int value, final Date fromDate,
			final Date toDate, final boolean urlEncode) throws AlertServiceException {
		try {
			final String notificationServiceString = urlEncode ? URLEncoder.encode(notificationService.toString(), "UTF-8") : notificationService.toString();
			final String queryIdString = urlEncode ? URLEncoder.encode(queryId, "UTF-8") : queryId;
			final String resultIdString = urlEncode ? URLEncoder.encode(resultId, "UTF-8") : resultId;
			final String resultNameString = urlEncode ? URLEncoder.encode(resultName, "UTF-8") : resultName;
			final String resultNameCapitalizeString = urlEncode ? URLEncoder.encode(WordUtils.capitalize(resultName), "UTF-8") : WordUtils.capitalize(resultName);
			final String valueString = urlEncode ? URLEncoder.encode(Integer.toString(value), "UTF-8") : Integer.toString(value);
			final String fromDateLongString = urlEncode ? URLEncoder.encode(Long.toString(fromDate.getTime()), "UTF-8") : Long.toString(fromDate.getTime());
			final String fromDateDashesString = urlEncode ? URLEncoder.encode(dateFormatDashes.format(fromDate), "UTF-8") : dateFormatDashes.format(fromDate);
			final String fromDateSlashesString = urlEncode ? URLEncoder.encode(dateFormatSlashes.format(fromDate), "UTF-8") : dateFormatSlashes.format(fromDate);
			final String toDateLongString = urlEncode ? URLEncoder.encode(Long.toString(toDate.getTime()), "UTF-8") : Long.toString(toDate.getTime());
			final String toDateDashesString = urlEncode ? URLEncoder.encode(dateFormatDashes.format(toDate), "UTF-8") : dateFormatDashes.format(toDate);
			final String toDateSlashesString = urlEncode ? URLEncoder.encode(dateFormatSlashes.format(toDate), "UTF-8") : dateFormatSlashes.format(toDate);
			return string.replaceAll(NOTIFICATION_SERVICE_VAR, notificationServiceString).replaceAll(QUERY_ID_VAR, queryIdString).replaceAll(RESULT_ID_VAR, resultIdString).replaceAll(RESULT_NAME_CAPITALIZE_VAR,
					resultNameCapitalizeString).replaceAll(RESULT_NAME_VAR, resultNameString).replaceAll(VALUE_VAR, valueString).replaceAll(FROM_DATE_LONG_VAR, fromDateLongString).replaceAll(FROM_DATE_STRING_DASHES_VAR,
					fromDateDashesString).replaceAll(FROM_DATE_STRING_SLASHES_VAR, fromDateSlashesString).replaceAll(TO_DATE_LONG_VAR, toDateLongString).replaceAll(TO_DATE_STRING_DASHES_VAR, toDateDashesString).
					replaceAll(TO_DATE_STRING_SLASHES_VAR, toDateSlashesString);
		} catch (final UnsupportedEncodingException e) {
			throw new AlertServiceException("error replacing variables", e);
		}
	}
}
