package eu.dnetlib.enabling.aas.retrievers;

import java.io.StringReader;
import java.net.URI;

import javax.xml.ws.wsaddressing.W3CEndpointReference;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import an.xacml.CachedDataObjectHolder;
import an.xacml.ExtendedRequest;
import an.xacml.IndeterminateException;
import an.xacml.policy.AttributeValue;
import eu.dnetlib.common.ws.epr.W3CEPRUtils;
import eu.dnetlib.common.ws.nh.NotificationConstants;
import eu.dnetlib.common.ws.nh.deleg.INotificationDelegatorClient;
import eu.dnetlib.enabling.aas.retrievers.cache.IProfilesCache;
import eu.dnetlib.enabling.aas.ws.nh.NotificationUtils;
import eu.dnetlib.enabling.is.sn.rmi.ISSNException;
import eu.dnetlib.enabling.is.sn.rmi.ISSNService;
import eu.dnetlib.enabling.tools.ServiceLocator;


/**
 * Cachable attribute retriever which caches profiles 
 * retrieved by {@link ISLookupAttributeRetriever}.
 * Works only in getFullProfileFlag mode enabled.
 * 
 * @author mhorst
 *
 */
public class CachableISLookupAttributeRetriever extends ISLookupAttributeRetriever 
	implements INotificationDelegatorClient {

	public static final String DEFAULT_SUPPORTED_ID_PATH = "RESOURCE_PROFILE/HEADER/RESOURCE_IDENTIFIER/@value";
	
	public static final String DEFAULT_SUPPORTED_KIND_PATH = "RESOURCE_PROFILE/HEADER/RESOURCE_KIND/@value";
	
	public static final String DEFAULT_SUPPORTED_TYPE_PATH = "RESOURCE_PROFILE/HEADER/RESOURCE_TYPE/@value";
	
	/**
	 * Supported id path, cannot be null, set to profId by default 
	 * but can handle any other unique identifier.
	 * Only single specific path is allowed per single retriever instance.
	 */
	private String supportedIdPath;
	
	/**
	 * Supported profile kind. Optional paramter, 
	 * if not specified all kinds will be supported.
	 */
	private String supportedProfileKind = null;
	
	/**
	 * Supported profile type. Optional paramter, 
	 * if not specified all types will be supported.
	 */
	private String supportedProfileType = null;
	
	/**
	 * Adjustable topic regexPattern.
	 */
	private String topicRegexPattern;
	
	/**
	 * Profiles cache module
	 */
	private IProfilesCache cache;
	
	/**
	 * ISSN service locator used for subscribing to notifications.
	 */
	private ServiceLocator<ISSNService> issnServiceLocator;
	
	/**
	 * Notification consumer reference URL.
	 */
	private String consumerReferenceURL;
	
	/**
	 * Notification consumer reference WSDL location.
	 */
	private String consumerReferenceWSDL;
	
	/**
	 * Notification consumer reference EPR generated at service startup.
	 */
	private W3CEndpointReference generatedConsumerReferenceEPR;
	
	
	/**
	 * Default constructor.
	 */
	public CachableISLookupAttributeRetriever() {
		this.getFullProfileFlag = true;
		this.supportedIdPath = DEFAULT_SUPPORTED_ID_PATH;
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.retrievers.ISLookupAttributeRetriever#init()
	 */
	@Override
	public void init() {
		super.init();
//		initializing local EPR
		this.generatedConsumerReferenceEPR = (consumerReferenceWSDL!=null)?
				W3CEPRUtils.buildEPR(consumerReferenceURL, consumerReferenceWSDL):
					W3CEPRUtils.buildEPR(consumerReferenceURL);
//		generating topic if not set
		if (this.topicRegexPattern==null && this.supportedProfileType!=null) {
			log.debug("generating topic pattern based on supportedProfileType");
//			notice: no subscription is being made, this is only pattern used by
//			notification dispatcher to select proper notification consumer
			this.topicRegexPattern = "("+
	            NotificationConstants.TOPIC_PREFIX_DELETE+"\\.|"+
	            NotificationConstants.TOPIC_PREFIX_UPDATE+"\\.)"+
	            this.supportedProfileType +
	            ".*";
		}
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.retrievers.ISLookupAttributeRetriever#retrieveAttributeValuesFromLookup(an.xacml.ExtendedRequest, eu.dnetlib.enabling.aas.retrievers.ContextPathDTO, java.net.URI, java.net.URI)
	 */
	@Override
	protected AttributeValue[] retrieveAttributeValuesFromLookup(
			ExtendedRequest req, ContextPathDTO ctxPathDTO, URI dataType, URI attrIdURI)
			throws IndeterminateException {
		if (isApplicable(ctxPathDTO)) {
			return super.retrieveAttributeValuesFromLookup(
				req, ctxPathDTO, dataType, attrIdURI);
		} else {
			log.debug("request is not applicable, idPath: " + ctxPathDTO.getIdPath() +
					", profileKind: " + ctxPathDTO.getProfileKind() +
					", profileType: " + ctxPathDTO.getProfileType());
			return new AttributeValue[0];
		}
	}

	/**
	 * Checks whether given request is applicable for this
	 * particular attribute retriever instance.
	 * @param ctxPathDTO
	 * @return
	 */
	protected boolean isApplicable(ContextPathDTO ctxPathDTO) {
		if (supportedIdPath.equals(ctxPathDTO.getIdPath())) {
			if (supportedProfileKind==null || 
					supportedProfileKind.equals(ctxPathDTO.getProfileKind())) {
				if (supportedProfileType==null ||
						supportedProfileType.equals(ctxPathDTO.getProfileType())) {
					return true;
				}
			}
		}
		return false;
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.retrievers.ISLookupAttributeRetriever#getCachedData(eu.dnetlib.enabling.aas.retrievers.ContextPathDTO, an.xacml.ExtendedRequest)
	 */
	@Override
	protected CachedDataObjectHolder getCachedData(ContextPathDTO ctxPathDTO,
			ExtendedRequest req) {
//		1) checking in request scoped cache (potentially faster)
		CachedDataObjectHolder resultCache = super.getCachedData(ctxPathDTO, req);
		if (resultCache!=null) {
			log.debug("found profile in request cache for id: " + ctxPathDTO.getId() + 
					", profile content: " + resultCache.getSingleDataObject());
			return resultCache;
		}
//		2) checking in application scoped cache
		if (ctxPathDTO.getId()!=null) {
			String result = cache.getProfile(ctxPathDTO.getId());
			if (result!=null) {
				log.debug("found profile in application cache for id: " + ctxPathDTO.getId() + 
						", profile content: " + result);
				return new CachedDataObjectHolder(result);
			}
		}
		return null;
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.retrievers.ISLookupAttributeRetriever#storeDataInCache(an.xacml.CachedDataObjectHolder, eu.dnetlib.enabling.aas.retrievers.ContextPathDTO, an.xacml.ExtendedRequest)
	 */
	@Override
	protected void storeDataInCache(CachedDataObjectHolder data,
			ContextPathDTO ctxPathDTO, ExtendedRequest req) {
//		1) storing data in request scoped cache
		super.storeDataInCache(data, ctxPathDTO, req);
//		2) additionally storing data in application scoped cache
		if (data.isSingleObject()) {
			subscribe((String) data.getSingleDataObject());
			cache.setProfile(ctxPathDTO.getId(), (String) data.getSingleDataObject());
		} else {
			log.warn("only single-object results are supported by " +
					this.getClass().getCanonicalName() + 
					", result will not be stored in application scoped cache!");
		}
	}
	
	/**
	 * Subscribes to DELETE and UPDATE notifications of given profile.
	 * @param profile
	 */
	protected void subscribe(String profile) {
		try {
			String profId = getSingleValueForXPath(profile, DEFAULT_SUPPORTED_ID_PATH);
			String resourceType = getSingleValueForXPath(profile, DEFAULT_SUPPORTED_TYPE_PATH);
			if (profId!=null && resourceType!=null) {
				String delSubscrId = issnServiceLocator.getService().subscribe(
						generatedConsumerReferenceEPR, 
						NotificationUtils.buildTopicForProfId(
								NotificationConstants.TOPIC_PREFIX_DELETE, resourceType, profId), 
						NotificationConstants.TERMINATION_TIME_INFINITE);
				log.debug("subscribed to " + NotificationConstants.TOPIC_PREFIX_DELETE +
						" topic for profId: " + profId + ", with subscrId: " + delSubscrId);
				String updateSubscrId = issnServiceLocator.getService().subscribe(
						generatedConsumerReferenceEPR, 
						NotificationUtils.buildTopicForProfId(
								NotificationConstants.TOPIC_PREFIX_UPDATE, resourceType, profId), 
						NotificationConstants.TERMINATION_TIME_INFINITE);
				log.debug("subscribed to " + NotificationConstants.TOPIC_PREFIX_UPDATE +
						" topic for profId: " + profId + ", with subscrId: " + updateSubscrId);
			} else {
				log.error("neither profId nor resource type can be null! " +
						"Couldn't subscribe to notifications for profile: " + profile);
				return;
			}
		} catch(XPathExpressionException e) {
			log.error("exception occured when extracting profId from profile: " 
					+ profile, e);
		} catch (ISSNException e) {
			log.error("exception occured when subscribing to notifications for profile: "
					+ profile, e);
		}
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.ws.nh.INotificationHandler#notify(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public boolean notify(String subscrId, String topic, String isId,
			String message) {
		try {
		if (topic.startsWith(NotificationConstants.TOPIC_PREFIX_DELETE)) {
			if (isTypeAndKindSupported(message)) {
				String resourceId = getSingleValueForXPath(message, supportedIdPath);
				if (resourceId==null) {
					log.warn("couldn't find resource id using xpath: " + supportedIdPath);
					return true;
				}
				log.debug("removing profile for resourceId: " + resourceId);
				String removedProfile = cache.removeProfile(resourceId);
				log.debug("removed profile content: " + removedProfile);
				return true;
			} else {
				log.warn("profile type or kind is not supported " +
						"by this attribute retriever instance! " +
						"Profile content: " + message);
				return true;
			}
		} else if (topic.startsWith(NotificationConstants.TOPIC_PREFIX_UPDATE)) {
			if (isTypeAndKindSupported(message)) {
				String resourceId = getSingleValueForXPath(message, supportedIdPath);
				if (resourceId==null) {
					log.warn("couldn't find resource id using xpath: " + supportedIdPath);
					return true;
				}
				String oldProfile = cache.getProfile(resourceId);
				if (oldProfile!=null) {
					log.debug("updating profile for resourceId: " + resourceId);
					cache.setProfile(resourceId, message);
					log.debug("old profile content" + oldProfile);
					log.debug("new profile content" + message);
					return true;
				} else {
					log.warn("cannot update profile for resourceId: " + resourceId +
							", no profile found in cache");
					return true;
				}
			} else {
				log.warn("profile type or kind is not supported " +
						"by this attribute retriever instance! " +
						"Profile content: " + message);
				return true;
			}
		} else {
			log.error("unsupported topic prefix: " + topic);
			return false;
		}
		} catch (XPathExpressionException e) {
			log.error("Exception occured when running xquery on profile!", e);
			return false;
		}
	}

	/**
	 * Checks whether given profile is supported.
	 * @param profile
	 * @return true if given profile is supported
	 * @throws XPathExpressionException
	 */
	protected boolean isTypeAndKindSupported(String profile) throws XPathExpressionException {
		if (supportedProfileKind==null || supportedProfileKind.equals(
				getSingleValueForXPath(profile, DEFAULT_SUPPORTED_KIND_PATH))) {
			if (supportedProfileType==null || supportedProfileType.equals(
					getSingleValueForXPath(profile, DEFAULT_SUPPORTED_TYPE_PATH))) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Returns single value for given profile and xPath expression.
	 * @param profile
	 * @param xPath
	 * @return single value for given profile and xPath expression
	 * @throws XPathExpressionException
	 */
	protected String getSingleValueForXPath(String profile, String xPath) throws XPathExpressionException {
		NodeList nList = (NodeList)xpath.evaluate(
				xPath, new InputSource(new StringReader(profile)), 
            XPathConstants.NODESET);
		if (nList==null || nList.getLength()==0) {
			return null;
		} else {
			if (nList.getLength()>1) {
				log.warn("got multiple results, returning first one");
			}
			return nList.item(0).getNodeValue();
		}
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.ws.nh.deleg.INotificationDelegatorClient#getTopicRegexPattern()
	 */
	@Override
	public String getTopicRegexPattern() {
		return topicRegexPattern;
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.retrievers.ISLookupAttributeRetriever#setGetFullProfileFlag(boolean)
	 */
	@Override
	public void setGetFullProfileFlag(boolean getFullProfileFlag) {
		throw new RuntimeException("Setting getFullProfileFlag is not " +
				"allowed in " + this.getClass().getCanonicalName() + 
				" as its value is locked on [true]");
	}
	
	/**
	 * Sets topic regex pattern.
	 * @param topicRegexPattern
	 */
	public void setTopicRegexPattern(String topicRegexPattern) {
		this.topicRegexPattern = topicRegexPattern;
	}

	/**
	 * Sets cache module.
	 * @param cache
	 */
	public void setCache(IProfilesCache cache) {
		this.cache = cache;
	}

	/**
	 * Sets required supported id path.
	 * @param supportedIdPath
	 */
	public void setSupportedIdPath(String supportedIdPath) {
		this.supportedIdPath = supportedIdPath;
	}

	/**
	 * Sets optional supported profile kind.
	 * @param supportedProfileKind
	 */
	public void setSupportedProfileKind(String supportedProfileKind) {
		this.supportedProfileKind = supportedProfileKind;
	}

	/**
	 * Sets optional supported profile type.
	 * @param supportedProfileType
	 */
	public void setSupportedProfileType(String supportedProfileType) {
		this.supportedProfileType = supportedProfileType;
	}

	/**
	 * Sets issn service locator.
	 * @param issnServiceLocator
	 */
	public void setIssnServiceLocator(ServiceLocator<ISSNService> issnServiceLocator) {
		this.issnServiceLocator = issnServiceLocator;
	}

	/**
	 * Sets consumerReferenceURL.
	 * @param consumerReferenceURL
	 */
	public void setConsumerReferenceURL(String consumerReferenceURL) {
		this.consumerReferenceURL = consumerReferenceURL;
	}

	/**
	 * Sets consumerReferenceWSDL.
	 * @param consumerReferenceWSDL
	 */
	public void setConsumerReferenceWSDL(String consumerReferenceWSDL) {
		this.consumerReferenceWSDL = consumerReferenceWSDL;
	}
	
}
