package be.recipe.common.wss4j.handler;

import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;

import org.apache.wss4j.common.bsp.BSPEnforcer;
import org.apache.wss4j.common.bsp.BSPRule;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.util.DOM2Writer;
import org.apache.wss4j.common.util.XMLUtils;
import org.apache.wss4j.dom.WSConstants;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

/**
 * Timestamp according to SOAP Message Security 1.0, chapter 10 / appendix A.2
 */
public class Timestamp {

	private Element element;
	private Instant created;
	private Instant expires;
	private String createdString;

	/**
	 * Constructs a <code>Timestamp</code> object and parses the <code>wsu:Timestamp</code> element to initialize it.
	 *
	 * @param timestampElement
	 *            the <code>wsu:Timestamp</code> element that contains the timestamp data
	 * @param bspEnforcer
	 *            a BSPEnforcer instance to enforce BSP rules
	 */
	public Timestamp(Element timestampElement, BSPEnforcer bspEnforcer) throws WSSecurityException {

		element = timestampElement;

		String strExpires = null;

		for (Node currentChild = element.getFirstChild(); currentChild != null; currentChild = currentChild.getNextSibling()) {
			if (Node.ELEMENT_NODE == currentChild.getNodeType()) {
				Element currentChildElement = (Element) currentChild;
				if (WSConstants.CREATED_LN.equals(currentChild.getLocalName()) && WSConstants.WSU_NS.equals(currentChild.getNamespaceURI())) {
					if (createdString == null) {
						String valueType = currentChildElement.getAttributeNS(null, "ValueType");
						if (valueType != null && !"".equals(valueType)) {
							// We can't have a ValueType attribute as per the BSP spec
							bspEnforcer.handleBSPRule(BSPRule.R3225);
						}
						createdString = ((Text) currentChildElement.getFirstChild()).getData();
					} else {
						// Test for multiple Created elements
						bspEnforcer.handleBSPRule(BSPRule.R3203);
					}
				} else if (WSConstants.EXPIRES_LN.equals(currentChild.getLocalName()) && WSConstants.WSU_NS.equals(currentChild.getNamespaceURI())) {
					if (createdString == null) {
						// Created must appear before Expires
						bspEnforcer.handleBSPRule(BSPRule.R3221);
					}
					if (strExpires != null) {
						// We can't have multiple Expires elements
						bspEnforcer.handleBSPRule(BSPRule.R3224);
					} else {
						String valueType = currentChildElement.getAttributeNS(null, "ValueType");
						if (valueType != null && !"".equals(valueType)) {
							// We can't have a ValueType attribute as per the BSP spec
							bspEnforcer.handleBSPRule(BSPRule.R3226);
						}
						strExpires = ((Text) currentChildElement.getFirstChild()).getData();
					}
				} else {
					bspEnforcer.handleBSPRule(BSPRule.R3222);
				}
			}
		}

		// We must have a Created element
		if (createdString == null) {
			bspEnforcer.handleBSPRule(BSPRule.R3203);
		}

		// Parse the dates
		if (createdString != null) {
			try {
				ZonedDateTime createdDateTime = ZonedDateTime.parse(createdString);
				if (!ZoneOffset.UTC.equals(createdDateTime.getZone())) {
					bspEnforcer.handleBSPRule(BSPRule.R3217);
				}

				created = createdDateTime.toInstant();
			} catch (DateTimeParseException e) {
				throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY, e);
			}

			if (created.getNano() > 0) {
				int milliseconds = created.get(ChronoField.MILLI_OF_SECOND);
				if (milliseconds * 1000000 != created.getNano()) {
					bspEnforcer.handleBSPRule(BSPRule.R3220);
				}
			}
		}

		if (strExpires != null) {
			try {
				ZonedDateTime expiresDateTime = ZonedDateTime.parse(strExpires);
				if (!ZoneOffset.UTC.equals(expiresDateTime.getZone())) {
					bspEnforcer.handleBSPRule(BSPRule.R3223);
				}

				expires = expiresDateTime.toInstant();
			} catch (DateTimeParseException e) {
				throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY, e);
			}

			if (expires.getNano() > 0) {
				int milliseconds = expires.get(ChronoField.MILLI_OF_SECOND);
				if (milliseconds * 1000000 != expires.getNano()) {
					bspEnforcer.handleBSPRule(BSPRule.R3229);
				}
			}
		}
	}

	/**
	 * Constructs a <code>Timestamp</code> object according to the defined parameters.
	 *
	 * @param doc
	 *            the SOAP envelope as <code>Document</code>
	 * @param ttl
	 *            the time to live (validity of the security semantics) in seconds
	 */
	public Timestamp(boolean milliseconds, Document doc, int ttl) {
		this(milliseconds, doc, new be.recipe.common.wss4j.handler.WSCurrentTimeSource(), ttl);
	}

	/**
	 * Constructs a <code>Timestamp</code> object according to the defined parameters.
	 *
	 * @param doc
	 *            the SOAP envelope as <code>Document</code>
	 * @param ttl
	 *            the time to live (validity of the security semantics) in seconds
	 */
	public Timestamp(boolean milliseconds, Document doc, be.recipe.common.wss4j.handler.WSTimeSource timeSource, int ttl) {

		element = doc.createElementNS(WSConstants.WSU_NS, WSConstants.WSU_PREFIX + ":" + WSConstants.TIMESTAMP_TOKEN_LN);

		Element elementCreated = doc.createElementNS(WSConstants.WSU_NS, WSConstants.WSU_PREFIX + ":" + WSConstants.CREATED_LN);
		created = timeSource.now().toInstant();

		DateTimeFormatter formatter = DateUtil.getDateTimeFormatter(milliseconds);
		elementCreated.appendChild(doc.createTextNode(created.atZone(ZoneOffset.UTC).format(formatter)));

		element.appendChild(elementCreated);
		if (ttl != 0) {
			expires = created.plusSeconds(ttl);

			Element elementExpires = doc.createElementNS(WSConstants.WSU_NS, WSConstants.WSU_PREFIX + ":" + WSConstants.EXPIRES_LN);
			elementExpires.appendChild(doc.createTextNode(expires.atZone(ZoneOffset.UTC).format(formatter)));
			element.appendChild(elementExpires);
		}
	}

	/**
	 * Add the WSU Namespace to this T. The namespace is not added by default for efficiency purposes.
	 */
	public void addWSUNamespace() {
		XMLUtils.setNamespace(element, WSConstants.WSU_NS, WSConstants.WSU_PREFIX);
	}

	/**
	 * Returns the dom element of this <code>Timestamp</code> object.
	 *
	 * @return the <code>wsse:UsernameToken</code> element
	 */
	public Element getElement() {
		return element;
	}

	/**
	 * Returns the string representation of the token.
	 *
	 * @return a XML string representation
	 */
	@Override
	public String toString() {
		return DOM2Writer.nodeToString(element);
	}

	/**
	 * Get the time of creation.
	 *
	 * @return the "created" time
	 */
	public Instant getCreated() {
		return created;
	}

	/**
	 * Get the time of creation as a String
	 *
	 * @return the time of creation as a String
	 */
	public String getCreatedString() {
		return createdString;
	}

	/**
	 * Get the time of expiration.
	 *
	 * @return the "expires" time
	 */
	public Instant getExpires() {
		return expires;
	}

	/**
	 * Set wsu:Id attribute of this timestamp
	 * 
	 * @param id
	 */
	public void setID(String id) {
		element.setAttributeNS(WSConstants.WSU_NS, WSConstants.WSU_PREFIX + ":Id", id);
	}

	/**
	 * @return the value of the wsu:Id attribute
	 */
	public String getID() {
		return element.getAttributeNS(WSConstants.WSU_NS, "Id");
	}

	/**
	 * Return true if the current Timestamp is expired, meaning if the "Expires" value is before the current time. It returns false if there
	 * is no Expires value.
	 */
	public boolean isExpired() {
		if (expires != null) {
			Instant rightNow = Instant.now();
			return expires.isBefore(rightNow);
		}
		return false;
	}

	/**
	 * Return true if the "Created" value is before the current time minus the timeToLive argument, and if the Created value is not "in the
	 * future".
	 *
	 * @param timeToLive
	 *            the value in seconds for the validity of the Created time
	 * @param futureTimeToLive
	 *            the value in seconds for the future validity of the Created time
	 * @return true if the timestamp is before (now-timeToLive), false otherwise
	 */
	public boolean verifyCreated(int timeToLive, int futureTimeToLive) {
		return DateUtil.verifyCreated(created, timeToLive, futureTimeToLive);
	}

	@Override
	public int hashCode() {
		int result = 17;
		if (created != null) {
			result = 31 * result + created.hashCode();
		}
		if (expires != null) {
			result = 31 * result + expires.hashCode();
		}
		return result;
	}

	@Override
	public boolean equals(Object object) {
		if (!(object instanceof Timestamp)) {
			return false;
		}
		Timestamp timestamp = (Timestamp) object;
		if (!compare(timestamp.getCreated(), getCreated())) {
			return false;
		}
		if (!compare(timestamp.getExpires(), getExpires())) {
			return false;
		}
		return true;
	}

	private boolean compare(Instant item1, Instant item2) {
		if (item1 == null && item2 != null) {
			return false;
		} else if (item1 != null && !item1.equals(item2)) {
			return false;
		}
		return true;
	}

}
