/*
 * Copyright 2011 Federal Chancellery Austria and
 * Graz University of Technology
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package at.gv.util.data;

import java.io.FileOutputStream;
import java.io.Serializable;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import at.gv.util.BpkUtil;
import at.gv.util.DOMUtils;
import at.gv.util.MiscUtil;
import at.gv.util.ToStringUtil;
import at.gv.util.ex.EgovUtilException;
import at.gv.util.ex.InternalErrorException;
import at.gv.util.xsd.mandate.Mandate;
import at.gv.util.xsd.persondata.IdentificationType;
import at.gv.util.xsd.persondata.PersonDataType;
import at.gv.util.xsd.persondata.PhysicalPersonType;
import at.gv.util.xsd.saml.assertion.AssertionType;
import at.gv.util.xsd.saml.assertion.AttributeStatementType;
import at.gv.util.xsd.saml.assertion.AttributeType;
import at.gv.util.xsd.saml.assertion.NameIdentifierType;
import at.gv.util.xsd.saml.assertion.ObjectFactory;
import at.gv.util.xsd.saml.assertion.StatementAbstractType;
import at.gv.util.xsd.saml.assertion.SubjectConfirmationType;
import at.gv.util.xsd.saml.assertion.SubjectType;

/**
 * @author <a href="mailto:arne.tauber@egiz.gv.at">Arne Tauber</a>
 * @author <a href="mailto:thomas.knall@iaik.tugraz.at">Thomas Knall</a>
 */
public final class ElectronicIdentity implements Serializable, Empty, RoleContainer {

	private static final long serialVersionUID = 1L;

	private Logger log = Logger.getLogger(this.getClass().getName());

	private static final String FIRSTNAME = "firstname";
	private static final String LASTNAME = "lastname";
	private static final String TITLE = "title";
	private static final String DATEOFBIRTH = "dateofbirth";
	private static final String EMAIL = "email";
	private static final String BASEID = "baseid";
	private static final String BPK = "bpk";
	private static final String WBPK = "wbpk";
	private static final String VZBPK = "vzbpk";
	private static final String ZBPK = "zbpk";
	private static final String BKU_URL = "bkuurl";
	private static final String ROLES = "roles";
	private static final String ROLE = "role";
	private static final String NAME_QUALIFIER = "namequalifier";
	private static final String NAME_IDENTIFIER = "nameidentifier";
	private static final String MANDATE_ENABLED = "mandateenabled";

	private static final String BPK_NAME_QUALIFIER = "urn:publicid:gv.at:cdid+bpk";
	private static final String WBPK_NAME_QUALIFIER_PREFIX = "urn:publicid:gv.at:wbpk";
	private static final String BASE_NAME_QUALIFIER = "urn:publicid:gv.at:baseid";

	private String firstName;
	private String title;
	private String lastName;
	private Date dateOfBirth;
	private String bpk;
	private String wbpk;
	private String baseId;
	private String vzbpk;
	private String zbpk;
	private String email;
	private String bkuURL;
	private String nameQualifier;
	private String nameIdentifier;
	private Set<String> roles;
	private Object userdefinedData;
	private AssertionType samlAssertion;
	private Mandate mandate;
	
	public String getTitle() {
		return this.title;
	}

	public Object getUserdefinedData() {
		return this.userdefinedData;
	}

	public void setUserdefinedData(Object userdefinedData) {
		this.userdefinedData = userdefinedData;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getBkuURL() {
		return this.bkuURL;
	}

	public void setBkuURL(String bkuURL) {
		this.bkuURL = bkuURL;
	}

	public String getEmail() {
		return this.email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getWbpk() {
		return this.wbpk;
	}

	public void setWbpk(String wbpk) {
		this.wbpk = wbpk;
	}

	public String getNameQualifier() {
		return this.nameQualifier;
	}

	public void setNameQualifier(String nameQualifier) {
		this.nameQualifier = nameQualifier;
	}

	public String getNameIdentifier() {
		return this.nameIdentifier;
	}

	public void setNameIdentifier(String nameIdentifier) {
		this.nameIdentifier = nameIdentifier;
	}

	public String getZbpk() {
		return this.zbpk;
	}

	public void setZbpk(String zbpk) {
		this.zbpk = zbpk;
	}

	public String getVzbpk() {
		return this.vzbpk;
	}

	public void setVzbpk(String vzbpk) {
		this.vzbpk = vzbpk;
	}

	public String getBaseId() {
		return this.baseId;
	}

	public void setBaseId(String baseId) {
		this.baseId = baseId;
	}

	public String getBpk() {
		return this.bpk;
	}

	public void setBpk(String bpk) {
		this.bpk = bpk;
	}

	public AssertionType getSamlAssertion() {
		return this.samlAssertion;
	}

	public Date getDateOfBirth() {
		return this.dateOfBirth;
	}

	public void setDateOfBirth(Date dateOfBirth) {
		this.dateOfBirth = dateOfBirth;
	}

	public String getFirstName() {
		return this.firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return this.lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public Set<String> getRoles() {
		return this.roles;
	}

	public void setRoles(Set<String> roles) {
		MiscUtil.assertNotNull(roles, "Roles");
		this.roles = roles;
	}

	public ElectronicIdentity addRole(String role) {
		MiscUtil.assertNotEmpty(role, "Role");
		this.roles.add(role);
		return this;
	}

	public ElectronicIdentity() {
	}

	public ElectronicIdentity(String firstName, String lastName, Date dateOfBirth) {
		this();
		this.setFirstName(firstName);
		this.setLastName(lastName);
		this.setDateOfBirth(dateOfBirth);
	}

	public ElectronicIdentity(String firstName, String lastName, String email) {
		this();
		this.setFirstName(firstName);
		this.setLastName(lastName);
		this.setEmail(email);
	}

	private void updateAll() {
		if (this.getNameQualifier() != null
		    && this.getNameQualifier().startsWith(WBPK_NAME_QUALIFIER_PREFIX)
		    && MiscUtil.isEmpty(this.getNameIdentifier())) {
			log.debug("NameQualifier starts with \"" + WBPK_NAME_QUALIFIER_PREFIX
			    + "\" and BaseId is present. Calculating NameIdentifier as wbpk.");
			this.nameIdentifier = BpkUtil.calcWBPK(this.getBaseId(),
			    this.getNameQualifier());
		}

		if (this.getNameQualifier() != null
		    && this.getNameQualifier().startsWith(WBPK_NAME_QUALIFIER_PREFIX)
		    && MiscUtil.isEmpty(this.getWbpk())
		    && MiscUtil.isNotEmpty(this.getNameIdentifier())) {
			log.debug("NameQualifier starts with \"" + WBPK_NAME_QUALIFIER_PREFIX
			    + "\". We have a wbpk.");
			this.wbpk = this.getNameIdentifier();
		}

		if (BPK_NAME_QUALIFIER.equals(this.getNameQualifier())
		    && MiscUtil.isEmpty(this.getBpk())
		    && MiscUtil.isNotEmpty(this.getNameIdentifier())) {
			log.debug("NameQualifier equals to \"" + BPK_NAME_QUALIFIER
			    + "\". We have a bpk.");
			this.bpk = this.getNameIdentifier();
		}

		if (MiscUtil.isNotEmpty(this.getBaseId())) {
			log.debug("BaseId present -> calculating zbpk");
			this.zbpk = BpkUtil.calcZBPK(this.getBaseId());
		}

	}

	void setSamlAssertion(AssertionType samlAssertion) {
		this.samlAssertion = samlAssertion;
	}

	public ElectronicIdentity(Document doc) throws EgovUtilException {
		this(doc, false);
	}
	
	public ElectronicIdentity(Document doc, boolean isMOAIDAssertion) throws EgovUtilException {
		this();
		try {
			MiscUtil.assertNotNull(doc, "Document");

			JAXBContext ctx = JAXBContext.newInstance(AssertionType.class
			    .getPackage().getName());
			JAXBElement<AssertionType> assertionElement = (JAXBElement<AssertionType>) ctx
			    .createUnmarshaller().unmarshal(doc.getDocumentElement());

			if (isMOAIDAssertion) {
				initializeCitizenCardWithMOAIDAssertion(assertionElement.getValue());
			} else {
				initializeCitizenCard(assertionElement.getValue());	
			}
			
		} catch (JAXBException e) {
			throw new EgovUtilException(e);
		}
	}

	public ElectronicIdentity(AssertionType assertion) throws EgovUtilException {
		this(assertion, false);
	}
	
	public ElectronicIdentity(AssertionType assertion, boolean isMOAIDAssertion) throws EgovUtilException {
		this();
		// debug moa-id response
		log.trace("Debug response: " + System.getProperty("debug.moaid.log.path") != null);
		if (System.getProperty("debug.moaid.log.path") != null) {
			try {
				ObjectFactory of = new ObjectFactory();
				JAXBContext ctx = JAXBContext.newInstance(AssertionType.class.getPackage().getName());
				
				String file = System.getProperty("debug.moaid.log.path") + "/" + MiscUtil.formatDate(new Date(), "yyyyMMdd-HHmmss") +".xml";
				log.trace("Writing MOA-ID response to: " + file);
				FileOutputStream fos = new FileOutputStream(file);
				ctx.createMarshaller().marshal(of.createAssertion(assertion), fos);
				fos.flush();
				fos.close();
			} catch(Exception e) {
				log.debug(e);
			}
		}
		if (isMOAIDAssertion) {
			initializeCitizenCardWithMOAIDAssertion(assertion);
		} else {
			initializeCitizenCard(assertion);	
		}
	}
	
	private void initializeCitizenCard(AssertionType assertion)
	    throws EgovUtilException {
		MiscUtil.assertNotNull(assertion, "SAMLAssertion");

		try {
			for (StatementAbstractType sat : assertion
			    .getStatementOrSubjectStatementOrAuthenticationStatement()) {
				if (sat instanceof AttributeStatementType) {
					AttributeStatementType attrStmt = (AttributeStatementType) sat;
					SubjectType subject = attrStmt.getSubject();
					for (JAXBElement<?> subChild : subject.getContent()) {
						if (subChild.getValue() instanceof SubjectConfirmationType) {
							SubjectConfirmationType sct = (SubjectConfirmationType) subChild
							    .getValue();
							Element scdNode = (Element) sct.getSubjectConfirmationData();
							Element personNode = (Element) DOMUtils
							    .getChildElements(scdNode).get(0);
							JAXBContext ctx = JAXBContext.newInstance(PhysicalPersonType.class
							    .getPackage().getName());
							JAXBElement<PhysicalPersonType> pptElement = (JAXBElement<PhysicalPersonType>) ctx
							    .createUnmarshaller().unmarshal(personNode);
							PhysicalPersonType ppt = pptElement.getValue();
							this.baseId = ppt.getIdentification().get(0).getValue().getValue();
							this.firstName = ppt.getName().getGivenName().get(0);
							this.lastName = ppt.getName().getFamilyName().get(0).getValue();
							this.dateOfBirth = MiscUtil.parseXMLDate(ppt.getDateOfBirth());
						}
					}
				}
			}
		} catch(JAXBException e) {
			throw new EgovUtilException(e);
		}
	}

	private void initializeCitizenCardWithMOAIDAssertion(AssertionType assertion)
	    throws EgovUtilException {
		MiscUtil.assertNotNull(assertion, "SAMLAssertion");
		try {
			AttributeStatementType attrStmt = (AttributeStatementType) assertion
			    .getStatementOrSubjectStatementOrAuthenticationStatement().get(0);
			// parse subject
			SubjectType subject = attrStmt.getSubject();
			for (JAXBElement<?> subChild : subject.getContent()) {
				if (subChild.getValue() instanceof SubjectConfirmationType) {
					SubjectConfirmationType sct = (SubjectConfirmationType) subChild
					    .getValue();
					Element scdNode = (Element) sct.getSubjectConfirmationData();
					if (scdNode.hasChildNodes()) {
						Element assertionNode = (Element) DOMUtils.getChildElements(scdNode)
						    .get(0);
						JAXBContext ctx = JAXBContext.newInstance(AssertionType.class.getPackage().getName());
						JAXBElement<AssertionType> assertionElement = (JAXBElement<AssertionType>) ctx
						    .createUnmarshaller().unmarshal(assertionNode);
						AssertionType subjectAssertion = assertionElement.getValue();
						for (StatementAbstractType sat : subjectAssertion
						    .getStatementOrSubjectStatementOrAuthenticationStatement()) {
							if (sat instanceof AttributeStatementType) {
								AttributeStatementType ast = (AttributeStatementType) sat;
								for (AttributeType attr : ast.getAttribute()) {
									if ("bPK".equals(attr.getAttributeName())) {
										Element attrValueNode = (Element) attr.getAttributeValue()
										    .get(0);
										Element idNode = (Element) DOMUtils.getChildElements(
										    attrValueNode).get(0);
										ctx = JAXBContext.newInstance(IdentificationType.class
										    .getPackage().getName());
										JAXBElement<IdentificationType> idElement = (JAXBElement<IdentificationType>) ctx
										    .createUnmarshaller().unmarshal(idNode);
										IdentificationType idt = (IdentificationType) idElement
										    .getValue();
										//this.setBpk(idt.getValue().getValue());
									}
								}
							}
						}
					}
				} else if (subChild.getValue() instanceof NameIdentifierType) {
					NameIdentifierType nit = (NameIdentifierType) subChild.getValue();
					this.setNameQualifier(nit.getNameQualifier());
					this.setNameIdentifier(nit.getValue());
				}
			}

			for (AttributeType attr : attrStmt.getAttribute()) {
				if ("PersonData".equals(attr.getAttributeName())) {
					Element attrValueNode = (Element) attr.getAttributeValue().get(0);
					Element personNode = (Element) DOMUtils.getChildElements(
					    attrValueNode).get(0);
					JAXBContext ctx = JAXBContext.newInstance(PhysicalPersonType.class
					    .getPackage().getName());
					JAXBElement<PhysicalPersonType> pptElement = (JAXBElement<PhysicalPersonType>) ctx
					    .createUnmarshaller().unmarshal(personNode);
					PhysicalPersonType ppt = pptElement.getValue();
					String baseId = ppt.getIdentification().get(0).getValue().getValue();
					this.setBaseId(baseId);
					this.setZbpk(BpkUtil.calcZBPK(baseId));
					this.setVzbpk(BpkUtil.calcVZBPK(baseId));
					this.setDateOfBirth(MiscUtil.parseXMLDate(ppt.getDateOfBirth()));
					this.setFirstName(ppt.getName().getGivenName().get(0));
					this.setLastName(ppt.getName().getFamilyName().get(0).getValue());
				} else if ("bkuURL".equals(attr.getAttributeName())) {
					Node attrValueNode = (Node) attr.getAttributeValue().get(0);
					this.setBkuURL(attrValueNode.getFirstChild().getNodeValue());
				} else if ("Mandate".equals(attr.getAttributeName())) {
					Element attrValueNode = (Element) attr.getAttributeValue().get(0);
					List mandateElementList = DOMUtils.getChildElements(attrValueNode);
					if (mandateElementList != null && mandateElementList.size() > 0) {
						// parse mandate
						JAXBContext ctx = JAXBContext.newInstance(Mandate.class.getPackage().getName());
						this.mandate = (Mandate) ctx.createUnmarshaller().unmarshal((Element) mandateElementList.get(0));
					}
				}
			}
		} catch (JAXBException e) {
			throw new EgovUtilException(e);
		}

	}

	/**
	 * Creates a wrapper for buergerkarte person data.<br/>
	 * Important note: properties-files are supposed to contain ISO 8859-1
	 * character encoding
	 * 
	 * @param properties
	 *          Properties containing buergerkarte person data as key/value pairs
	 */
	public ElectronicIdentity(Properties properties) {
		this();
		this.evaluateProperties(properties);
	}

	/**
	 * Fills wrapper with buergerkarte person data from a Properties file.<br/>
	 * Important note: properties-files are supposed to contain ISO 8859-1
	 * character encoding
	 * 
	 * @param properties
	 *          Properties containing buergerkarte person data as key/value pairs
	 * @throws CannotResetException
	 */
	private void evaluateProperties(Properties properties) {
		if (properties != null) {
			this.setFirstName(properties.getProperty(FIRSTNAME));
			this.setLastName(properties.getProperty(LASTNAME));
			if (properties.getProperty(DATEOFBIRTH) != null) {
				try {
					this.setDateOfBirth(DateUtils.parseDate(
					    properties.getProperty(DATEOFBIRTH), new String[] { "yyyy-MM-dd",
					        "dd.MM.yyyy", }));
				} catch (ParseException e) {
					log.error(e);
				}
			}
			this.setTitle(properties.getProperty(TITLE));
			this.setBpk(properties.getProperty(BPK));
			this.setWbpk(properties.getProperty(WBPK));
			this.setBaseId(properties.getProperty(BASEID));
			this.setNameIdentifier(properties.getProperty(NAME_IDENTIFIER));
			this.setNameQualifier(properties.getProperty(NAME_QUALIFIER));
			if (MiscUtil.isEmpty(this.getBaseId())
			    && BASE_NAME_QUALIFIER.equals(this.getNameQualifier())) {
				this.setBaseId(this.getNameIdentifier());
			}
			this.setEmail(properties.getProperty(EMAIL));
			this.setVzbpk(properties.getProperty(VZBPK));
			this.setZbpk(properties.getProperty(ZBPK));
			this.setBkuURL(properties.getProperty(BKU_URL));

			String roles = properties.getProperty(ROLES);
			if (MiscUtil.isNotEmpty(roles)) {
				StringTokenizer tokenizer = new StringTokenizer(roles, ",");
				while (tokenizer.hasMoreTokens()) {
					String role = StringUtils.trim(tokenizer.nextToken());
					if (MiscUtil.isNotEmpty(role)) {
						this.roles.add(role);
					}
				}
			}
			String role = StringUtils.trim(properties.getProperty(ROLE));
			if (MiscUtil.isNotEmpty(role)) {
				this.roles.add(role);
			}

			this.updateAll();

		}
	}

	public void calcBpk(String domain) {
		if (MiscUtil.isEmpty(this.getBaseId())) {
			throw new InternalErrorException(
			    "Unable to calculate bpk. BaseId has to be set.");
		}
		if (MiscUtil.isEmpty(domain)) {
			throw new IllegalArgumentException(
			    "Unable to calculate bpk. Target/sector/domain must not be empty.");
		}
		this.bpk = BpkUtil.calcBPK(this.getBaseId(), domain);
	}

	public void calcWbpk() {
		MiscUtil.assertNotEmpty(this.getBaseId(), "BaseId");
		MiscUtil.assertNotEmpty(this.getNameQualifier(), "NameQualifier");
		this.wbpk = BpkUtil.calcWBPK(this.getBaseId(), this.getNameQualifier());
		this.nameIdentifier = this.wbpk;
	}

	protected void calcVzbpk(byte[] rsaPublicKey, String domain) {
		MiscUtil.assertNotEmpty(domain, "Domain");
		if (MiscUtil.isEmpty(this.getBaseId())) {
			throw new InternalErrorException(
			    "Unable to calculate bpk. BaseId has to be set.");
		}
		MiscUtil.assertNotEmpty(rsaPublicKey, "RSAPublicKey");
		PublicKey publicKey;
		try {

			KeyFactory rsaKeyFac = KeyFactory.getInstance("RSA");
			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(rsaPublicKey);
			publicKey = (RSAPublicKey) rsaKeyFac.generatePublic(keySpec);
		} catch (InvalidKeySpecException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new InternalErrorException(e);
		}
		this.vzbpk = BpkUtil.calcVZBPK(this.getBaseId(), publicKey);
	}

	public void calcVzbpk(byte[] rasPublicKey) {
		this.calcVzbpk(rasPublicKey, BpkUtil.SECTOR_DELIVERY);
	}

	public void calcVzbpk() {
		this.calcVzbpk(BpkUtil.PUBLIC_KEY_ZUSEKOPF_SN01_BASE64.getBytes(),
		    BpkUtil.SECTOR_DELIVERY);
	}

	public void calcZbpk() {
		if (MiscUtil.isEmpty(this.getBaseId())) {
			throw new InternalErrorException(
			    "Unable to calculate bpk. BaseId has to be set.");
		}
		this.setZbpk(BpkUtil.calcZBPK(this.getBaseId()));
	}

	@Override
	public String toString() {
		return new ToStringBuilder(this)
		    .append("firstName", this.firstName)
		    .append("lastName", this.lastName)
		    .append(
		        "dateOfBirth",
		        this.dateOfBirth != null ? DateFormatUtils.format(this.dateOfBirth,
		            "yyyy-MM-dd") : this.dateOfBirth)
		    .append("title", this.title)
		    .append("email", this.email)
		    //.append("baseId", this.baseId)
		    .append("nameQualifier", this.nameQualifier)
		    .append("nameIdentifier", this.nameIdentifier)
		    .append("bpk", this.bpk)
		    .append("wbpk", this.wbpk)
		    .append("zbpk", this.zbpk)
		    .append("vzbpk", this.vzbpk)
		    .append("bkuURL", this.bkuURL)
		    .append("userdefinedData", this.userdefinedData)
		    .append(
		        "roles",
		        this.roles != null ? ToStringUtil.toString(this.roles, ", ", "\"")
		            : null)
		    .append("samlAssertion",
		        this.samlAssertion != null ? "<set>" : "<not set>").toString();
	}

	public boolean isEmpty() {
		boolean stringsEmpty = MiscUtil.areAllEmpty(this.wbpk, this.nameQualifier,
		    this.nameIdentifier, this.baseId, this.bpk, this.firstName,
		    this.lastName, this.vzbpk, this.zbpk, this.email, this.bkuURL);
		boolean udEmpty = true;
		if (this.userdefinedData != null) {
			if (this.userdefinedData instanceof Empty) {
				udEmpty = ((Empty) this.userdefinedData).isEmpty();
			} else {
				udEmpty = false;
			}
		}
		return stringsEmpty && udEmpty && this.dateOfBirth == null
		    && MiscUtil.isEmpty(this.roles) && (this.samlAssertion != null);
	}

	public boolean hasRole(String role) {
		return this.roles.contains(role);
	}

	public void setMandate(Mandate mandate) {
	  this.mandate = mandate;
  }

	public Mandate getMandate() {
	  return mandate;
  }

}