package eu.dnetlib.enabling.aas.utils;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;


import com.sun.xacml.ParsingException;
import com.sun.xacml.UnknownIdentifierException;
import com.sun.xacml.attr.AttributeFactory;
import com.sun.xacml.attr.AttributeValue;
import com.sun.xacml.attr.BagAttribute;
import com.sun.xacml.ctx.RequestCtx;
import com.sun.xacml.ctx.ResponseCtx;
import com.sun.xacml.ctx.Result;
import com.sun.xacml.ctx.Status;
import com.sun.xacml.ctx.Subject;

import eu.dnetlib.enabling.aas.ctx.SecCtxInfoDTO;
import eu.dnetlib.enabling.aas.rmi.A2Error;
import eu.dnetlib.enabling.aas.rmi.Attribute;
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.Obligation;
import eu.dnetlib.enabling.aas.rmi.TypedString;

/**
 * Utility for converting between XACML and AAS model objects. 
 * @author mhorst
 *
 */
public class DriverConverter {
	
	protected static final Logger log = Logger.getLogger(DriverConverter.class);

//	public final static String RESOURCE_PREFIX					= "urn:oasis:names:tc:xacml:1.0:resource:";
	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 DEFAULT_RESOURCE_ATTRIBUTE_TYPE	= "http://www.w3.org/2001/XMLSchema#string";
	public final static String DEFAULT_ACTION_ATTRIBUTE_TYPE	= "http://www.w3.org/2001/XMLSchema#string";
	public final static String DEFAULT_SUBJECT_ATTRIBUTE_TYPE	= "http://www.w3.org/2001/XMLSchema#string";
	
	public final static String IDENTITIES_IDENTIFIER			= "driver.security.obligation.identities";
	public final static String SECURITY_CONTEXT_OBLIGATION		= "driver.security.obligation.security_context";
	public final static String SECURITY_CONTEXT_RESOURCEID		= "resourceId";
	
	/**
	 * Converts AAS AuthorizeRequest into XACML RequestCTX.
	 * @param authReq
	 * @return XACML RequestCTX
	 * @throws URISyntaxException 
	 * @throws ParsingException 
	 * @throws UnknownIdentifierException 
	 */
	public static RequestCtx convertRequest(AuthorizeRequest authReq) {
		if (authReq==null)
			return null;
		
		AttributeFactory attrFactory = AttributeFactory.getInstance();
		
		Set<Subject> subjects = new HashSet<Subject>();
		Set<com.sun.xacml.ctx.Attribute> resource = new HashSet<com.sun.xacml.ctx.Attribute>();
		Set<com.sun.xacml.ctx.Attribute> action = new HashSet<com.sun.xacml.ctx.Attribute>();
		Set<com.sun.xacml.ctx.Attribute> environment = new HashSet<com.sun.xacml.ctx.Attribute>();
		try {
//			warn! subject is empty:
			Set<com.sun.xacml.ctx.Attribute> attrs = new HashSet<com.sun.xacml.ctx.Attribute>();
			Subject subject = new Subject(attrs);
			subjects.add(subject);
			
			if (authReq.getResource()!=null) {
				URI resourceIdURI = new URI(RESOURCE_ATTRIBUTE_ID);
				URI type;
				if (authReq.getResource().getType() != null &&
						authReq.getResource().getType().length()>0)
					type = new URI(authReq.getResource().getType());
				else
					type = new URI(DEFAULT_RESOURCE_ATTRIBUTE_TYPE);
				
				AttributeValue attrValue = attrFactory.createValue(type, authReq.getResource().getText());
				com.sun.xacml.ctx.Attribute resourceAttr = new com.sun.xacml.ctx.Attribute(resourceIdURI, null, null, attrValue);
				resource.add(resourceAttr);
			}
			if (authReq.getAction()!=null) {
				URI resourceIdURI = new URI(ACTION_ATTRIBUTE_ID);
				URI type;
				if (authReq.getAction().getType() != null &&
						authReq.getAction().getType().length()>0)
					type = new URI(authReq.getAction().getType());
				else
					type = new URI(DEFAULT_ACTION_ATTRIBUTE_TYPE);
				AttributeValue attrValue = attrFactory.createValue(type, authReq.getAction().getText());
				com.sun.xacml.ctx.Attribute resourceAttr = new com.sun.xacml.ctx.Attribute(resourceIdURI, null, null, attrValue);
				action.add(resourceAttr);
			}
		} catch (URISyntaxException e) {
			log.error("URISyntaxException occured during converting attributes!",e);
			return null;
		} catch (UnknownIdentifierException e) {
			log.error("UnknownIdentifierException occured during converting attributes!",e);
			return null;
		} catch (ParsingException e) {
			log.error("ParsingException occured during converting attributes!",e);
			return null;
		}
		return new RequestCtx(subjects, resource, action, environment);
				
	}
	
	
	/**
	 * Converts XACML ResponseCTX into AAS AuthorizeResp.
	 * @param responseCtx
	 * @return
	 */
	public static AuthorizeResp convertAuthorizeResponse(ResponseCtx responseCtx) {
		if (responseCtx==null)
			return null;
		AuthorizeResp authResponse = new AuthorizeResp();
		if (responseCtx.getResults() != null) {
			Set results = responseCtx.getResults();
			Iterator it = results.iterator();
//			only first result is processed (multiple results only for hierarchical resources)
			if (it.hasNext()) {
				Result res = (Result) it.next();
				if (res.getDecision() == Result.DECISION_PERMIT) {
					authResponse.setAuthorized(true);
				} else if (res.getDecision() == Result.DECISION_DENY) {
					authResponse.setAuthorized(false);
				} else {
					authResponse.setAuthorized(false);
					if (res.getDecision() == Result.DECISION_INDETERMINATE)
						authResponse.addError(new A2Error(A2Error.DECISION_INDETERMINATE));
					else 
						authResponse.addError(new A2Error(A2Error.DECISION_NOT_APPLICABLE));
				}
				
				authResponse.setObligations(DriverConverter.convertObligations(res.getObligations(),res.getResource()));
				if (res.getStatus()!=null) {
					String c = (String)(res.getStatus().getCode().iterator().next());
					if (!c.equals(Status.STATUS_OK)) {
						authResponse.addError(new A2Error(A2Error.EVALUATION_ERROR,c));
					}
				}

			}
		} else {
			authResponse.setAuthorized(false);
			authResponse.addError(new A2Error(A2Error.UNKNOWN_ERROR,"No evaluation result!"));
		}
		return authResponse;
	}
	
	
	/**
	 * Converts XACML ResponseCTX into AAS AuthorizeResp.
	 * Returns Object[] with AuthenticateResp as Object[0] 
	 * and SecCtxInfoDTO as Object[1].
	 * @param responseCtx
	 * @return Object[] with AuthenticateResp as Object[0] and SecCtxInfoDTO as Object[1]
	 */
	public static Object[] convertAuthenticateResponse(ResponseCtx responseCtx) {
		if (responseCtx==null)
			return null;
		AuthenticateResp authResponse = new AuthenticateResp();
		SecCtxInfoDTO secCtxInfoDTO = null;
		
		if (responseCtx.getResults() != null) {
			Set results = responseCtx.getResults();
			Iterator it = results.iterator();
//			only first result is processed (multiple results only for hierarchical resources)
			if (it.hasNext()) {
				Result res = (Result) it.next();

				if (res.getDecision() == Result.DECISION_PERMIT) {
//					do nothing
				}
				else if (res.getDecision() == Result.DECISION_DENY)
					authResponse.addError(new A2Error(A2Error.DECISION_NOT_AUTHENTICATED));
				else if (res.getDecision() == Result.DECISION_INDETERMINATE) 
					authResponse.addError(new A2Error(A2Error.DECISION_INDETERMINATE));
				else 
					authResponse.addError(new A2Error(A2Error.DECISION_NOT_APPLICABLE));
				
//				extracting identities from obligations
				authResponse.setIdentities(DriverConverter.extractIdetitiesFromObligations(res.getObligations()));
				List<Obligation> obligations = DriverConverter.convertObligationsWithoutIdentities(res.getObligations(),res.getResource()); 
				secCtxInfoDTO = checkForSecCtxObligationAndRemove(obligations);
				
				if (obligations!=null && obligations.size()>0) {
					authResponse.setObligations(obligations.toArray(new Obligation[0]));
				}
				if (res.getStatus()!=null) {
					String c = (String)(res.getStatus().getCode().iterator().next());
					if (!c.equals(Status.STATUS_OK)) {
						authResponse.addError(new A2Error(A2Error.EVALUATION_ERROR,c));
					}
				}

			}
		} else {
			authResponse.addError(new A2Error(A2Error.UNKNOWN_ERROR,"No evaluation result!"));
		}
		return new Object[] {authResponse, secCtxInfoDTO};
	}
	
	public static RequestCtx convertRequest(AuthenticateRequest authReq) {
		if (authReq==null)
			return null;
		
		AttributeFactory attrFactory = AttributeFactory.getInstance();
		
		Set<Subject> subjects = new HashSet<Subject>();
		Set<com.sun.xacml.ctx.Attribute> resource = new HashSet<com.sun.xacml.ctx.Attribute>();
		Set<com.sun.xacml.ctx.Attribute> action = new HashSet<com.sun.xacml.ctx.Attribute>();
		Set<com.sun.xacml.ctx.Attribute> environment = new HashSet<com.sun.xacml.ctx.Attribute>();
		try {
			if (authReq.getPrincipals()!=null) {
				URI type = new URI(DEFAULT_SUBJECT_ATTRIBUTE_TYPE);
				for (int i=0; i<authReq.getPrincipals().length; i++) {
					String princType = authReq.getPrincipals()[i].getType();
					if (princType!=null) {
						URI subjectIdURI = new URI(SUBJECT_ATTRIBUTE_ID);
						AttributeValue attrValue = attrFactory.createValue(type, princType);
						Set<com.sun.xacml.ctx.Attribute> attrs = new HashSet<com.sun.xacml.ctx.Attribute>();
						com.sun.xacml.ctx.Attribute resourceAttr = new com.sun.xacml.ctx.Attribute(subjectIdURI, null, null, attrValue);
						attrs.add(resourceAttr);
						Subject subject = new Subject(attrs);
						subjects.add(subject);
					}
				}
				
//				authentication - resource
				URI resourceIdURI = new URI(RESOURCE_ATTRIBUTE_ID);
				URI typeRes = new URI(DEFAULT_RESOURCE_ATTRIBUTE_TYPE);
				AttributeValue attrValue = attrFactory.createValue(typeRes, "authentication");
				com.sun.xacml.ctx.Attribute resourceAttr = new com.sun.xacml.ctx.Attribute(resourceIdURI, null, null, attrValue);
				resource.add(resourceAttr);
			
			}
			
		} catch (URISyntaxException e) {
			log.error("URISyntaxException occured during converting attributes!",e);
			return null;
		} catch (UnknownIdentifierException e) {
			log.error("UnknownIdentifierException occured during converting attributes!",e);
			return null;
		} catch (ParsingException e) {
			log.error("ParsingException occured during converting attributes!",e);
			return null;
		}
		return new RequestCtx(subjects, resource, action, environment);
				
	}
	
	
	
	/**
	 * Converts obligations from source XACML Obligations type to AAS Obligations
	 * @param sourceObligations
	 * @param resource
	 * @return AAS Obligation[]
	 */
	@SuppressWarnings("unchecked")
	public static Obligation[] convertObligations(Set sourceObligations, String resource) {
		if (sourceObligations==null || sourceObligations.size()==0)
			return null;
		
		Obligation[] resObligations = new Obligation[sourceObligations.size()];
		int i=0;
		for (Iterator iter = sourceObligations.iterator(); iter.hasNext();) {
			com.sun.xacml.Obligation sourceObligation = (com.sun.xacml.Obligation) iter.next();
			resObligations[i] = new Obligation();
			resObligations[i].setResource(resource);
			resObligations[i].setObligation(sourceObligation.getId().toString());
			if (sourceObligation.getAssignments() != null) {
				resObligations[i].setAttributes(convertAttributes(sourceObligation.getAssignments()));
			}
			i++;
		} 
		return resObligations;
	}
	
	/**
	 * Converts obligations from source XACML Obligations type to AAS Obligations.
	 * Excludes identities.
	 * @param sourceObligations
	 * @param resource
	 * @return List of obligations
	 */
	@SuppressWarnings("unchecked")
	public static List<Obligation> convertObligationsWithoutIdentities(Set sourceObligations, String resource) {
		if (sourceObligations==null || sourceObligations.size()==0)
			return null;
		
		List<Obligation> resObligationsList = new ArrayList<Obligation>();
		for (Iterator iter = sourceObligations.iterator(); iter.hasNext();) {
			com.sun.xacml.Obligation sourceObligation = (com.sun.xacml.Obligation) iter.next();
			String obligationId = sourceObligation.getId().toString();
			if (!IDENTITIES_IDENTIFIER.equals(obligationId)) {
				Obligation currentObligation = new Obligation();
				currentObligation.setResource(resource);
				currentObligation.setObligation(obligationId);
				if (sourceObligation.getAssignments() != null) {
					currentObligation.setAttributes(convertAttributes(sourceObligation.getAssignments()));
				}
				resObligationsList.add(currentObligation);
			}
		} 
		return resObligationsList;
	}

	/**
	 * Converts xacml attributes to AAS attributes.
	 * @param sourceAttrs
	 * @return Array of AAS attributes
	 */
	private static Attribute[] convertAttributes(List<com.sun.xacml.ctx.Attribute> sourceAttrs) {
		if (sourceAttrs==null)
			return null;
		List<Attribute> attributes = new ArrayList<Attribute>();
		
		for (Iterator itAssignments = sourceAttrs.iterator(); itAssignments.hasNext();) {
			com.sun.xacml.ctx.Attribute sourceAttr = (com.sun.xacml.ctx.Attribute) itAssignments.next();
			AttributeValue currentAttrValue = sourceAttr.getValue();
			if (currentAttrValue instanceof BagAttribute) {
				List<String> bagValues = retrieveValuesFromBagAttribute((BagAttribute)currentAttrValue);
				if (bagValues!=null) {
					for (Iterator<String> iterator = bagValues.iterator(); iterator.hasNext();) {
						Attribute currentAttribute = new Attribute();
						currentAttribute.setKey(sourceAttr.getId().toString());
						currentAttribute.setType(sourceAttr.getType().toString());
						currentAttribute.setValue(iterator.next());
						attributes.add(currentAttribute);
					}
				}
			} else {
				Attribute currentAttribute = new Attribute();
				currentAttribute.setKey(sourceAttr.getId().toString());
				currentAttribute.setType(sourceAttr.getType().toString());
				currentAttribute.setValue(currentAttrValue.encode());
				attributes.add(currentAttribute);
			}
		}
		return attributes.toArray(new Attribute[0]);
	}
	
	/**
	 * Extracts Identities from XACML Obligations.
	 * @param sourceObligations
	 * @return TypedString[]
	 */
	private static TypedString[] extractIdetitiesFromObligations(Set sourceObligations) {
		if (sourceObligations==null || sourceObligations.size()==0)
			return null;
		
		List<TypedString> identitiesList = new ArrayList<TypedString>();
		for (Iterator iter = sourceObligations.iterator(); iter.hasNext();) {
			com.sun.xacml.Obligation sourceObligation = (com.sun.xacml.Obligation) iter.next();
			String obligationId = sourceObligation.getId().toString();
			if (IDENTITIES_IDENTIFIER.equals(obligationId)) {
				if (sourceObligation.getAssignments() != null) {
					for (Iterator itAssignments = sourceObligation.getAssignments().iterator(); itAssignments
							.hasNext();) {
						com.sun.xacml.ctx.Attribute sourceAttr = (com.sun.xacml.ctx.Attribute) itAssignments.next();
						AttributeValue currentAttrValue = sourceAttr.getValue();
						if (currentAttrValue instanceof BagAttribute) {
							List<String> bagValues = retrieveValuesFromBagAttribute((BagAttribute)currentAttrValue);
							if (bagValues!=null) {
								for (Iterator<String> iterator = bagValues.iterator(); iterator.hasNext();) {
									TypedString currentIdentity = new TypedString(iterator.next(), sourceAttr.getId().toString());
									identitiesList.add(currentIdentity);
								}
							}
						} else {
							TypedString currentIdentity = new TypedString(currentAttrValue.encode(), sourceAttr.getId().toString());
							identitiesList.add(currentIdentity);
						}
					}
				}
			}
		} 
		return identitiesList.toArray(new TypedString[0]);
	}
	
	/**
	 * Checks for SecurityContext obligation. 
	 * If found returs obligation string and removes this obligation from obligations list.
	 * @param obligations
	 * @return SecurityContext obligation if found, null otherwise 
	 */
	static SecCtxInfoDTO checkForSecCtxObligationAndRemove(List<Obligation> obligations) {
		if (obligations==null || obligations.size()==0)
			return null;
		Iterator it = obligations.iterator();
		while (it.hasNext()) {
			Obligation currentObligation = (Obligation) it.next();
			String currentObligStr = currentObligation.getObligation();
			if (currentObligStr!=null &&
					currentObligStr.startsWith(SECURITY_CONTEXT_OBLIGATION)) {
				SecCtxInfoDTO secCtxInfoDTO = new SecCtxInfoDTO();
				String workStr = currentObligStr.substring(DriverConverter.SECURITY_CONTEXT_OBLIGATION.length());
				if (workStr.length()>0 && 
						(workStr.charAt(0)=='.' || workStr.charAt(0)==':'))
					secCtxInfoDTO.setSecCtxType(workStr.substring(1));
				Attribute[] attrs = currentObligation.getAttributes();
				if (attrs!=null) {
					for (int i = 0; i < attrs.length; i++) {
						Attribute currentAttr = attrs[i];
						if (currentAttr.getKey()!=null &&
								SECURITY_CONTEXT_RESOURCEID.equals(currentAttr.getKey()))
							secCtxInfoDTO.setResourceId(currentAttr.getValue());
					}
				}
				it.remove();
				return secCtxInfoDTO;
			}
		}
		return null;
	}
	
	/**
	 * Retrieves String values from BagAttribute.
	 * @param bagAttribute
	 * @return List of String values retrieved from BagAttribute.
	 */
	public static List<String> retrieveValuesFromBagAttribute(BagAttribute bagAttribute) {
		List<String> allValues = new ArrayList<String>();
		if (bagAttribute!=null) {
			Iterator bagIt = bagAttribute.iterator();
			while (bagIt.hasNext()) {
				AttributeValue currentAttrValue = (AttributeValue) bagIt.next();
				if (currentAttrValue instanceof BagAttribute) {
					allValues.addAll(retrieveValuesFromBagAttribute((BagAttribute) currentAttrValue));
				} else {
					allValues.add(currentAttrValue.encode());
				}
			}
		}
		return allValues;
	}
	
	
}
