package eu.dnetlib.enabling.aas.service;

import java.util.ArrayList;
import java.util.List;

import javax.jws.WebService;

import org.apache.commons.lang.NotImplementedException;

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.retrievers.ISLookupAttributeRetrieverConstants;
import eu.dnetlib.enabling.aas.rmi.A2Error;
import eu.dnetlib.enabling.aas.rmi.A2Response;
import eu.dnetlib.enabling.aas.rmi.A2Service;
import eu.dnetlib.enabling.aas.rmi.AuthenticateRequest;
import eu.dnetlib.enabling.aas.rmi.AuthenticateResp;
import eu.dnetlib.enabling.aas.rmi.AuthorizeRequest;
import eu.dnetlib.enabling.aas.rmi.AuthorizeResp;
import eu.dnetlib.enabling.aas.rmi.InvalidateRequest;
import eu.dnetlib.enabling.aas.rmi.InvalidateResp;
import eu.dnetlib.enabling.aas.xacml.ctx.ActionType;
import eu.dnetlib.enabling.aas.xacml.ctx.AttributeType;
import eu.dnetlib.enabling.aas.xacml.ctx.AttributeValueType;
import eu.dnetlib.enabling.aas.xacml.ctx.RequestType;
import eu.dnetlib.enabling.aas.xacml.ctx.ResourceType;
import eu.dnetlib.enabling.aas.xacml.ctx.ResultType;
import eu.dnetlib.enabling.aas.xacml.ctx.SubjectType;
import eu.dnetlib.enabling.aas.xacml.ctx.DecisionType.DECISION;
import eu.dnetlib.enabling.aas.xacml.profile.saml.XACMLAuthzDecisionQueryType;

/**
 * {@link A2Service} from icm-enabling-api facade adapting
 * AAS2 frontend to AAS1 frontend.
 * 
 * @author mhorst
 *
 */
@WebService(serviceName="A2Service",
		endpointInterface="eu.dnetlib.enabling.aas.rmi.A2Service",
		targetNamespace="http://rmi.aas.enabling.dnetlib.eu/")
public class A2ServiceAdapter implements A2Service {

	public final static String SUBJECT_ATTRIBUTE_ID				= "urn:oasis:names:tc:xacml:1.0:subject:subject-id";
	public final static String RESOURCE_ATTRIBUTE_ID			= "urn:oasis:names:tc:xacml:1.0:resource:resource-id";
	public final static String ACTION_ATTRIBUTE_ID				= "urn:oasis:names:tc:xacml:1.0:action:action-id";

	public final static String ATTRIBUTE_TYPE_STRING 			= "http://www.w3.org/2001/XMLSchema#string";
	
	public final static String DEFAULT_RESOURCE_ATTRIBUTE_TYPE	= ATTRIBUTE_TYPE_STRING;
	public final static String DEFAULT_ACTION_ATTRIBUTE_TYPE	= ATTRIBUTE_TYPE_STRING;
	public final static String DEFAULT_SUBJECT_ATTRIBUTE_TYPE	= ATTRIBUTE_TYPE_STRING;
	
	/**
	 * AAS2 encapsulated service.
	 */
	private IAAService aaService;
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.rmi.A2Service#authenticate(eu.dnetlib.enabling.aas.rmi.AuthenticateRequest)
	 */
	@Override
	public AuthenticateResp authenticate(AuthenticateRequest request) {
		return adaptAuthnResponse(aaService.authenticate(
				adaptAuthnRequest(request)));
	}

	/**
	 * Adapts authentication request.
	 * @param internalReq
	 * @return adapted request
	 */
	protected DNetAuthenticateRequest adaptAuthnRequest(
			AuthenticateRequest internalReq) {
		if (internalReq==null) 	return null;
		
		DNetAuthenticateRequest dnetRequest = new DNetAuthenticateRequest();
		XACMLAuthzDecisionQueryType authzQuery = new XACMLAuthzDecisionQueryType();
		dnetRequest.setAuthnQuery(authzQuery);
		RequestType xacmlRequest = new RequestType();
		authzQuery.setRequest(xacmlRequest);
		
//		authentication - predefined action
		AttributeType actionAttr = new AttributeType();
		actionAttr.setAttributeID(ACTION_ATTRIBUTE_ID);
		actionAttr.setDataType(DEFAULT_ACTION_ATTRIBUTE_TYPE);
		actionAttr.setAttributeValues(new AttributeValueType[] {
				new AttributeValueType("authentication")});
		xacmlRequest.setAction(new ActionType(new AttributeType[] {actionAttr}));
//		handling principals
		if (internalReq.getPrincipals()!=null) {
			List<SubjectType> subjectList = new ArrayList<SubjectType>();
			for (int i=0; i<internalReq.getPrincipals().length; i++) {
				{
//					currently in AAS2 scope type is set as subjectId
					AttributeType subjectAttr = new AttributeType();
					subjectAttr.setAttributeID(SUBJECT_ATTRIBUTE_ID);
					subjectAttr.setDataType(DEFAULT_SUBJECT_ATTRIBUTE_TYPE);
					subjectAttr.setAttributeValues(new AttributeValueType[] {
							new AttributeValueType(internalReq.getPrincipals()[i].getType())
					});
					subjectList.add(new SubjectType(
							new AttributeType[] {subjectAttr}));
				}
				{
//					text is considered to be an indentifier of security entity in AAS2 scope
					AttributeType subjectAttr = new AttributeType();
					subjectAttr.setAttributeID(ISLookupAttributeRetrieverConstants.SUBJECT_PARAM_ID);
					subjectAttr.setDataType(DEFAULT_SUBJECT_ATTRIBUTE_TYPE);
					subjectAttr.setAttributeValues(new AttributeValueType[] {
							new AttributeValueType(internalReq.getPrincipals()[i].getText())
					});
					subjectList.add(new SubjectType(
							new AttributeType[] {subjectAttr}, 
							ISLookupAttributeRetrieverConstants.SUBJECT_CATEGORY));
				}
			}
			xacmlRequest.setSubjects(subjectList.toArray(new SubjectType[subjectList.size()]));
		}
//		handling credentials
		if (internalReq.getCredentials()!=null) {
//			all credentials set as resources with credential type set as suffix
			String credentialResourcePrefix = "urn:oasis:names:tc:xacml:1.0:resource:credential";
			xacmlRequest.setResources(new ResourceType[internalReq.getCredentials().length]);
			for (int i=0; i<internalReq.getCredentials().length; i++) {
				String credType = internalReq.getCredentials()[i].getType();
				AttributeType resourceAttr = new AttributeType();
				if (credType!=null) {
					resourceAttr.setAttributeID(credentialResourcePrefix + ':' + credType);
				} else {
					resourceAttr.setAttributeID(credentialResourcePrefix);
				}
				resourceAttr.setDataType(DEFAULT_RESOURCE_ATTRIBUTE_TYPE);
				resourceAttr.setAttributeValues(new AttributeValueType[] {
						new AttributeValueType(internalReq.getCredentials()[i].getText())
				});
				xacmlRequest.getResources()[i] = new ResourceType(
						new AttributeType[] {resourceAttr});
			}
		}
		return dnetRequest;
	}
	
	/**
	 * Adapts authentication response.
	 * @param internalResp
	 * @return adapted response
	 */
	protected AuthenticateResp adaptAuthnResponse(
			DNetAuthenticateResponse internalResp) {
		if (internalResp==null) return null;
		AuthenticateResp authnResp = new AuthenticateResp();
		processResult(internalResp.getResult(), authnResp);
		if (internalResp.getErrors()!=null && internalResp.getErrors().length>0) {
			for (int i=0; i<internalResp.getErrors().length; i++) {
				authnResp.addError(adaptError(internalResp.getErrors()[i]));
			}
		}
//		FIXME identities and obligations are not currently being processed
		return authnResp;
	}
	
	/**
	 * Adapts {@link DNetAAError} into {@link A2Error}.
	 * @param internalErr
	 * @return adapted {@link A2Error}
	 */
	protected A2Error adaptError(DNetAAError internalErr) {
		if (internalErr==null) return null;
		return new A2Error(internalErr.getErrorId(),
				internalErr.getMessage(),
				internalErr.getThrowable());
	}
	
	/**
	 * Processes XACML result. Sets according error or flag according to 
	 * {@link A2Response} type. Supports both {@link AuthorizeResp} and 
	 * {@link AuthenticateResp} response objects.
	 * @param resultType
	 * @param a2Response
	 */
	protected void processResult(ResultType resultType, A2Response a2Response) {
		if (resultType==null) {
			a2Response.addError(
					new A2Error(A2Error.UNKNOWN_ERROR,"No evaluation result!"));
			return;
		} else {
			if (resultType.getDecision().getDecision() == DECISION.Permit) {
				if (a2Response instanceof AuthorizeResp) {
					((AuthorizeResp)a2Response).setAuthorized(true);
				} else {
//					no errors added
				}
			} else if (resultType.getDecision().getDecision() == DECISION.Deny) {
				if (a2Response instanceof AuthorizeResp) {
					((AuthorizeResp)a2Response).setAuthorized(false);
				} else {
					a2Response.addError(new A2Error(A2Error.DECISION_NOT_AUTHENTICATED));
				}
				
			} else if (resultType.getDecision().getDecision() == DECISION.Indeterminate) {
				a2Response.addError(new A2Error(A2Error.DECISION_INDETERMINATE));
			} else {
				a2Response.addError(new A2Error(A2Error.DECISION_NOT_APPLICABLE));
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.rmi.A2Service#authorize(eu.dnetlib.enabling.aas.rmi.AuthorizeRequest)
	 */
	@Override
	public AuthorizeResp authorize(AuthorizeRequest request) {
		return adaptAuthzResponse(aaService.authorize(
				adaptAuthzRequest(request)));
	}

	/**
	 * Adapts authorization request.
	 * @param internalReq
	 * @return adapted request
	 */
	protected DNetAuthorizeRequest adaptAuthzRequest(
			AuthorizeRequest internalReq) {
		if (internalReq==null) return null;
		DNetAuthorizeRequest dnetRequest = new DNetAuthorizeRequest();
		XACMLAuthzDecisionQueryType authzQuery = new XACMLAuthzDecisionQueryType();
		dnetRequest.setAuthzQuery(authzQuery);
		RequestType xacmlRequest = new RequestType();
		authzQuery.setRequest(xacmlRequest);
//		subject is empty, should be added at conversion performed on AAS2 side
		
		if (internalReq.getResource()!=null) {
			String resourceId = RESOURCE_ATTRIBUTE_ID;
			String type;
			if (internalReq.getResource().getType() != null &&
					internalReq.getResource().getType().length()>0) {
				type = internalReq.getResource().getType();
			} else {
				type = DEFAULT_RESOURCE_ATTRIBUTE_TYPE;
			}
			AttributeType resourceAttr = new AttributeType();
			resourceAttr.setAttributeID(resourceId);
			resourceAttr.setDataType(type);
			resourceAttr.setAttributeValues(new AttributeValueType[] {
					new AttributeValueType(internalReq.getResource().getText())
			});
			xacmlRequest.setResources(new ResourceType[] {
					new ResourceType(new AttributeType[] {resourceAttr})	
			});
		}
		if (internalReq.getAction()!=null) {
			String actionId = ACTION_ATTRIBUTE_ID;
			String type;
			if (internalReq.getAction().getType() != null &&
					internalReq.getAction().getType().length()>0) {
				type = internalReq.getAction().getType();
			} else {
				type = DEFAULT_ACTION_ATTRIBUTE_TYPE;
			}
			AttributeType actionAttr = new AttributeType();
			actionAttr.setAttributeID(actionId);
			actionAttr.setDataType(type);
			actionAttr.setAttributeValues(new AttributeValueType[] {
					new AttributeValueType(internalReq.getAction().getText())});
			xacmlRequest.setAction(new ActionType(new AttributeType[] {actionAttr}));
		}
		return dnetRequest;
	}
	
	/**
	 * Adapts authorization response.
	 * @param internalResp
	 * @return adapted response
	 */
	protected AuthorizeResp adaptAuthzResponse(
			DNetAuthorizeResponse internalResp) {
		if (internalResp==null) return null;
		AuthorizeResp authzResp = new AuthorizeResp();
		processResult(internalResp.getResult(), authzResp);
		if (internalResp.getErrors()!=null && internalResp.getErrors().length>0) {
			for (int i=0; i<internalResp.getErrors().length; i++) {
				authzResp.addError(adaptError(internalResp.getErrors()[i]));
			}
		}
//		FIXME identities and obligations are not currently being processed
		return authzResp;
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.rmi.A2Service#identify()
	 */
	@Override
	public String identify() {
		return aaService.identify();
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.rmi.A2Service#invalidate(eu.dnetlib.enabling.aas.rmi.InvalidateRequest)
	 */
	@Override
	public InvalidateResp invalidate(InvalidateRequest request) {
		throw new UnsupportedOperationException("AAS2 is stateless therefore " +
				"invalidation is unsupported!");
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.nh.rmi.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) {
		aaService.notify(subscrId, topic, isId, message);
		return true;
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.validator.rmi.IProfileValidator#validate(java.lang.String, java.lang.String)
	 */
	@Override
	public boolean validate(String profId, String secProfId) {
		throw new NotImplementedException("Validation is not implemented!");
	}

	/**
	 * Sets AAS2 service to be adapted.
	 * @param aaService
	 */
	public void setAaService(IAAService aaService) {
		this.aaService = aaService;
	}

}
