package eu.dnetlib.enabling.aas.service;

import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.apache.log4j.Logger;
import org.opensaml.lite.common.SAMLObject;

import pl.edu.icm.yadda.aas.err.holder.IErrorHolder;
import pl.edu.icm.yadda.aas.oblig.analyzer.AnalyzerResultObject;
import pl.edu.icm.yadda.aas.oblig.analyzer.IInternalObligationAnalyzer;
import pl.edu.icm.yadda.aas.oblig.analyzer.InternalObligationAnalyzerException;
import pl.edu.icm.yadda.service2.aas.AAError;


import an.xacml.ExtendedRequest;
import an.xacml.context.Request;
import an.xacml.context.Response;
import an.xacml.engine.PDP;
import an.xacml.oblig.InternalObligationExtractor;
import an.xacml.policy.Obligation;

import eu.dnetlib.data.index.ws.nh.INotificationHandler;
import eu.dnetlib.enabling.aas.DNetAAError;
import eu.dnetlib.enabling.aas.DNetAuthenticateRequest;
import eu.dnetlib.enabling.aas.DNetAuthenticateResponse;
import eu.dnetlib.enabling.aas.DNetAuthorizeRequest;
import eu.dnetlib.enabling.aas.DNetAuthorizeResponse;
import eu.dnetlib.enabling.aas.IAAService;
import eu.dnetlib.enabling.aas.converter.an.xacml.context.XACMLConverterException;
import eu.dnetlib.enabling.aas.converter.an.xacml.context.XACMLRequestConverter;
import eu.dnetlib.enabling.aas.converter.an.xacml.context.XACMLResponseConverter;
import eu.dnetlib.enabling.aas.holder.IDataHolder;

/**
 * DNet Authorization & authentication service main class. 
 * @author mhorst
 *
 */
public class AAServiceImpl implements IAAService {

	protected static final Logger log = Logger.getLogger(AAServiceImpl.class);

	/**
	 * Service version.
	 */
	private String serviceVersion;
	
	/**
	 * SAML data holder for passing {@link SAMLObject} data from interceptors.
	 */
	private IDataHolder<SAMLObject[]> inputSamlDataHolder;
	
	/**
	 * SAML data holder for passing {@link SAMLObject} data to interceptors.
	 */
	private IDataHolder<SAMLObject[]> outputSamlDataHolder;
	
	/**
	 * Yadda Policy Decision Point.
	 */
	private PDP yaddaPDP;
	
	/**
	 * Internal obligations analyzer.
	 */
	private IInternalObligationAnalyzer internalObligationAnalyzer;
	
	/**
	 * Notification handler module.
	 */
	private INotificationHandler notificationHandler;
	
	/**
     * Auxiliary error holder.
     * Allows processing errors from XACML context.
     */
    private IErrorHolder auxErrorHolder;
	

	/* (non-Javadoc)
	 * @see eu.dnetlib.common.rmi.BaseService#identify()
	 */
	@Override
	public String identify() {
		return serviceVersion;
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.common.rmi.BaseService#start()
	 */
	@Override
	public void start() {
		// does nothing
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.IAAService#authenticate(eu.dnetlib.enabling.aas.DNetAuthenticateRequest)
	 */
	@Override
	public DNetAuthenticateResponse authenticate(DNetAuthenticateRequest request) {
		try {
			DNetAuthenticateResponse response = new DNetAuthenticateResponse();
			if (request==null) {
				response.setErrors(new DNetAAError(DNetAAError.INVALID_REQ_ERROR, 
						"Null request object!"));
				return response;
			}
			XACMLRequestConverter reqConverter = new XACMLRequestConverter();
			Request reqCtx = reqConverter.convert2XACML(request.getAuthnQuery());
			if (reqCtx==null) {
				response.setErrors(new DNetAAError(DNetAAError.INVALID_REQ_ERROR, 
						"Couldn't retrieve valid XACML Request context!"));
				return response;
			}
			List<SAMLObject> samlObjects = null;
			if (inputSamlDataHolder!=null) {
				SAMLObject[] data = inputSamlDataHolder.getData();
				if (data!=null) {
					samlObjects = new ArrayList<SAMLObject>(Arrays.asList(data));
				} else {
					log.debug("no entry assertions found");
				} 
			} else {
				log.warn("no inputSamlDataHolder set!");
			}
			Response respCtx = (Response) yaddaPDP.handleRequest(
					new ExtendedRequest(reqCtx, samlObjects, null));
			if (respCtx==null) {
				response.setErrors(new DNetAAError(DNetAAError.EVALUATION_ERROR, 
				"Couldn't evaluate proper XACML Response!"));
				return response;
			}
			
			AnalyzerResultObject analyzerResultObject = processInternalObligations(respCtx, samlObjects);
//			TODO analyze result properties e.g. for checking if assertion should be sent, or stored internally
//			maybe one of modules should store Assertion inside AAS container and produce another assertion containing reference
			if (analyzerResultObject.getCurrentSAMLObject()!=null) {
				outputSamlDataHolder.storeData(new SAMLObject[] {analyzerResultObject.getCurrentSAMLObject()});
			}
			List<AAError> allErrors = processErrors(analyzerResultObject.getErrors());
			if (allErrors!=null) {
				response.setErrors(new DNetAAError[allErrors.size()]);
				int i=0;
				for (AAError sourceError : allErrors) {
					response.getErrors()[i] = new DNetAAError(sourceError.getErrorId(),
							sourceError.getMessage(), sourceError.getThrowable(),
							processErrorData(sourceError.getData()));
					i++;
				}
			}
			response.setXacmlResponse(new XACMLResponseConverter().convert2SAML(respCtx));
			return response;
			
		} catch (InternalObligationAnalyzerException e) {
			log.error("Exception occured when analyzing obligations!", e);
			DNetAuthenticateResponse response = new DNetAuthenticateResponse();
			if (e.getConveyedError()!=null) {
				response.setErrors(new DNetAAError(
						e.getConveyedError().getErrorId(), 
						e.getConveyedError().getMessage(), 
						e.getConveyedError().getThrowable(),
						e.getConveyedError().getData().toString()));
			} else {
				response.setErrors(new DNetAAError(DNetAAError.SYSTEM_ERROR, 
						"Exception occured when processing obligations!.", e));
			}
			return response;	
		} catch (XACMLConverterException e) {
			log.error("Exception occured when converting XACML objects!", e);
			DNetAuthenticateResponse response = new DNetAuthenticateResponse();
			response.setErrors(new DNetAAError(DNetAAError.SYSTEM_ERROR, 
					"Exception occured during XACML<->SAML model convertion.", e));
			return response;	
		} catch (Exception e) {
			log.error("Unknown exception caught!", e);
			DNetAuthenticateResponse response = new DNetAuthenticateResponse();
			response.setErrors(new DNetAAError(DNetAAError.SYSTEM_ERROR, null, e));
			return response;
		}
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.IAAService#authorize(eu.dnetlib.enabling.aas.DNetAuthorizeRequest)
	 */
	@Override
	public DNetAuthorizeResponse authorize(DNetAuthorizeRequest request) {
		try {
			DNetAuthorizeResponse response = new DNetAuthorizeResponse();
			if (request==null) {
				response.setErrors(new DNetAAError(DNetAAError.INVALID_REQ_ERROR, 
						"Null request object!"));
				return response;
			}
			XACMLRequestConverter reqConverter = new XACMLRequestConverter();
			Request reqCtx = reqConverter.convert2XACML(request.getAuthzQuery());
			if (reqCtx==null) {
				response.setErrors(new DNetAAError(DNetAAError.INVALID_REQ_ERROR, 
						"Couldn't retrieve valid XACML Request context!"));
				return response;
			}
			List<SAMLObject> samlObjects = null;
			if (inputSamlDataHolder!=null) {
				SAMLObject[] data = inputSamlDataHolder.getData();
				if (data!=null) {
					samlObjects = new ArrayList<SAMLObject>(Arrays.asList(data));
				} else {
					log.debug("no entry assertions found");
				} 
			} else {
				log.warn("no inputSamlDataHolder set!");
			}
			Response respCtx = (Response) yaddaPDP.handleRequest(
					new ExtendedRequest(reqCtx, samlObjects, null));
			if (respCtx==null) {
				response.setErrors(new DNetAAError(DNetAAError.EVALUATION_ERROR, 
				"Couldn't evaluate proper XACML Response!"));
				return response;
			}
			
			AnalyzerResultObject analyzerResultObject = processInternalObligations(respCtx, samlObjects);
//			TODO analyze result properties e.g. for checking if assertion should be sent, or stored internally
//			maybe one of modules should store Assertion inside AAS container and produce another assertion containing reference
			if (analyzerResultObject.getCurrentSAMLObject()!=null) {
				outputSamlDataHolder.storeData(new SAMLObject[] {analyzerResultObject.getCurrentSAMLObject()});
			}
			List<AAError> allErrors = processErrors(analyzerResultObject.getErrors());
			if (allErrors!=null) {
				response.setErrors(new DNetAAError[allErrors.size()]);
				int i=0;
				for (AAError sourceError : allErrors) {
					response.getErrors()[i] = new DNetAAError(sourceError.getErrorId(),
							sourceError.getMessage(), sourceError.getThrowable(),
							processErrorData(sourceError.getData()));
					i++;
				}
			}
			response.setXacmlResponse(new XACMLResponseConverter().convert2SAML(respCtx));
			return response;

		} catch (InternalObligationAnalyzerException e) {
			log.error("Exception occured when analyzing obligations!", e);
			DNetAuthorizeResponse response = new DNetAuthorizeResponse();
			if (e.getConveyedError()!=null) {
				response.setErrors(new DNetAAError(
						e.getConveyedError().getErrorId(), 
						e.getConveyedError().getMessage(), 
						e.getConveyedError().getThrowable(),
						e.getConveyedError().getData().toString()));
			} else {
				response.setErrors(new DNetAAError(DNetAAError.SYSTEM_ERROR, 
						"Exception occured when processing obligations!.", e));
			}
			return response;	
		} catch (XACMLConverterException e) {
			log.error("Exception occured when converting XACML objects!", e);
			DNetAuthorizeResponse response = new DNetAuthorizeResponse();
			response.setErrors(new DNetAAError(DNetAAError.SYSTEM_ERROR, 
					"Exception occured during XACML<->SAML model convertion.", e));
			return response;	
		} catch (Exception e) {
			log.error("Unknown exception caught!", e);
			DNetAuthorizeResponse response = new DNetAuthorizeResponse();
			response.setErrors(new DNetAAError(DNetAAError.SYSTEM_ERROR, null, e));
			return response;
		}
	}


	/* (non-Javadoc)
	 * @see eu.dnetlib.common.rmi.BaseService#notify(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public void notify(String subscrId, String topic, String isId,
			String message) {
		notificationHandler.notify(subscrId, topic, isId, message);
	}
	
	/**
	 * Common authz and authn Assertion preparation.
	 * @param respCtx XACML response context containing obligations
	 * @param sourceSAMLObjects list of source SAML objects
	 * @return AnalyzerResultObject 
	 * @throws XACMLConverterException
	 * @throws InternalObligationAnalyzerException
	 */
	protected AnalyzerResultObject processInternalObligations(Response respCtx, 
			final List<SAMLObject> sourceSAMLObjects) 
		throws InternalObligationAnalyzerException {
		List<Obligation> internalObligations = InternalObligationExtractor.extractInternalObligations(respCtx);
		AnalyzerResultObject analResultObj = internalObligationAnalyzer.analyze(null, 
				sourceSAMLObjects, internalObligations);
		return analResultObj;
	}

	/**
	 * Attaches auxilary errors if any found.
	 * @param source
	 * @return source list of errors with aux errors attached
	 */
	protected List<AAError> processErrors(List<AAError> source) {
		if (auxErrorHolder!=null) {
			Collection<AAError> auxErrors = auxErrorHolder.getAllErrors(); 
			if (auxErrors.isEmpty()) {
				return source;
			} else {
				if (source!=null) {
					source.addAll(auxErrors);
					return source;
				} else {
					return new ArrayList<AAError>(auxErrors); 
				}
			}
		} else {
			log.warn("Cannot process aux errors: no ErrorHolder module set!");
			return source;
		}
	}
	
	/**
	 * Processes {@link AAError} data into {@link DNetAAError} string data.
	 * @param object
	 * @return processed data
	 */
	protected String processErrorData(Object object) throws InvalidParameterException {
		if (object!=null) {
			if (object instanceof String) {
				return (String) object;
			} else {
				throw new InvalidParameterException("unsupported AAError#getData() object class: " +
						object.getClass().getCanonicalName() + ", currently only String is supported!");
			}
		} else {
			return null;
		}
	}
	
	/**
	 * Sets SAML data holder for passing {@link SAMLObject} data from interceptors.
	 * @param inputSamlDataHolder
	 */
	public void setInputSamlDataHolder(IDataHolder<SAMLObject[]> inputSamlDataHolder) {
		this.inputSamlDataHolder = inputSamlDataHolder;
	}

	/**
	 * Sets SAML data holder for passing {@link SAMLObject} data to interceptors.
	 * @param outputSamlDataHolder
	 */
	public void setOutputSamlDataHolder(IDataHolder<SAMLObject[]> outputSamlDataHolder) {
		this.outputSamlDataHolder = outputSamlDataHolder;
	}
	
	/**
	 * Sets Yadda PDP module.
	 * @param yaddaPDP
	 */
	public void setYaddaPDP(PDP yaddaPDP) {
		this.yaddaPDP = yaddaPDP;
	}

	/**
	 * Sets internal obligations analyzer
	 * @param internalObligationAnalyzer
	 */
	public void setInternalObligationAnalyzer(
			IInternalObligationAnalyzer internalObligationAnalyzer) {
		this.internalObligationAnalyzer = internalObligationAnalyzer;
	}
	
	/**
	 * Sets notification handler module.
	 * @param notificationHandler
	 */
	public void setNotificationHandler(INotificationHandler notificationHandler) {
		this.notificationHandler = notificationHandler;
	}

	/**
     * Sets auxiliary error holder.
     * @param auxErrorHolder
     */
    public void setAuxErrorHolder(IErrorHolder auxErrorHolder) {
            this.auxErrorHolder = auxErrorHolder;
    }

	/**
	 * Sets service version.
	 * @param serviceVersion
	 */
	public void setServiceVersion(String serviceVersion) {
		this.serviceVersion = serviceVersion;
	}

}
