package com.sun.identity.saml2.protocol.impl;

import com.sun.identity.saml2.assertion.AssertionFactory;
import com.sun.identity.saml2.assertion.Issuer;
import com.sun.identity.saml2.common.SAML2Exception;
import com.sun.identity.saml2.common.SAML2SDKUtils;
import com.sun.identity.saml2.protocol.Extensions;
import com.sun.identity.saml2.protocol.ProtocolFactory;
import com.sun.identity.saml2.protocol.RequestAbstract;
import com.sun.identity.saml2.xmlsig.SigManager;
import com.sun.identity.shared.DateUtils;
import com.sun.identity.shared.xml.XMLUtils;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.ListIterator;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public abstract class RequestAbstractImpl implements RequestAbstract {
   protected Issuer nameID;
   protected Extensions extensions;
   protected String requestId;
   protected String version;
   protected Date issueInstant;
   protected String destinationURI;
   protected String consent;
   protected boolean isSigned;
   protected Boolean isSignatureValid;
   protected PublicKey publicKey;
   protected boolean isMutable;
   protected String signatureString;
   protected String signedXMLString;
   protected String elementName = "";

   @Override
   public void setIssuer(Issuer nameID) throws SAML2Exception {
      if (!this.isMutable) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("objectImmutable"));
      }

      this.nameID = nameID;
   }

   @Override
   public Issuer getIssuer() {
      return this.nameID;
   }

   @Override
   public String getSignature() {
      return this.signatureString;
   }

   @Override
   public void sign(PrivateKey privateKey, X509Certificate cert) throws SAML2Exception {
      Element signatureEle = SigManager.getSigInstance().sign(this.toXMLString(true, true), this.getID(), privateKey, cert);
      this.signatureString = XMLUtils.print(signatureEle);
      this.signedXMLString = XMLUtils.print(signatureEle.getOwnerDocument().getDocumentElement());
      this.isSigned = true;
      this.makeImmutable();
   }

   @Override
   public void setExtensions(Extensions extensions) throws SAML2Exception {
      if (!this.isMutable) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("objectImmutable"));
      }

      this.extensions = extensions;
   }

   @Override
   public Extensions getExtensions() {
      return this.extensions;
   }

   @Override
   public void setID(String id) throws SAML2Exception {
      if (!this.isMutable) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("objectImmutable"));
      }

      this.requestId = id;
   }

   @Override
   public String getID() {
      return this.requestId;
   }

   @Override
   public void setVersion(String version) throws SAML2Exception {
      if (!this.isMutable) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("objectImmutable"));
      }

      this.version = version;
   }

   @Override
   public String getVersion() {
      return this.version;
   }

   @Override
   public void setIssueInstant(Date dateTime) throws SAML2Exception {
      if (!this.isMutable) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("objectImmutable"));
      }

      this.issueInstant = dateTime;
   }

   @Override
   public Date getIssueInstant() {
      return this.issueInstant;
   }

   @Override
   public void setDestination(String destinationURI) throws SAML2Exception {
      if (!this.isMutable) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("objectImmutable"));
      }

      this.destinationURI = destinationURI;
   }

   @Override
   public String getDestination() {
      return this.destinationURI;
   }

   @Override
   public void setConsent(String consent) throws SAML2Exception {
      if (!this.isMutable) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("objectImmutable"));
      }

      this.consent = consent;
   }

   @Override
   public String getConsent() {
      return this.consent;
   }

   @Override
   public boolean isSigned() {
      return this.isSigned;
   }

   @Override
   public boolean isSignatureValid(List<X509Certificate> senderCerts) throws SAML2Exception {
      if (this.isSignatureValid == null) {
         this.isSignatureValid = SigManager.getSigInstance().verify(this.signedXMLString, this.getID(), senderCerts);
      }

      return this.isSignatureValid;
   }

   @Override
   public String toXMLString() throws SAML2Exception {
      return this.toXMLString(true, false);
   }

   @Override
   public String toXMLString(boolean includeNSPrefix, boolean declareNS) throws SAML2Exception {
      if (this.isSigned && this.signedXMLString != null) {
         return this.signedXMLString;
      }

      StringBuilder attrs = new StringBuilder();
      StringBuilder childElements = new StringBuilder();
      this.getXMLString(attrs, childElements, includeNSPrefix, declareNS);
      StringBuilder xmlString = new StringBuilder(1000);
      xmlString.append("<");
      if (includeNSPrefix) {
         xmlString.append("samlp:");
      }

      xmlString.append(this.elementName);
      xmlString.append(attrs).append(">").append("\n").append(childElements).append("<").append("/");
      if (includeNSPrefix) {
         xmlString.append("samlp:");
      }

      xmlString.append(this.elementName).append(">");
      return xmlString.toString();
   }

   protected String getAttributesString() {
      StringBuilder xml = new StringBuilder();
      xml.append("ID=\"");
      xml.append(this.requestId);
      xml.append("\" ");
      xml.append("Version=\"");
      xml.append(this.version);
      xml.append("\" ");
      xml.append("IssueInstant=\"");
      xml.append(DateUtils.toUTCDateFormat(this.issueInstant));
      xml.append("\" ");
      if (this.destinationURI != null && !this.destinationURI.isEmpty()) {
         xml.append("Destination=\"");
         xml.append(this.destinationURI);
         xml.append("\" ");
      }

      if (this.consent != null && !this.consent.isEmpty()) {
         xml.append("Consent=\"");
         xml.append(this.consent);
         xml.append("\" ");
      }

      return xml.toString();
   }

   protected String getElements(boolean includeNSPrefix, boolean declareNS) throws SAML2Exception {
      StringBuilder xml = new StringBuilder();
      if (this.nameID != null) {
         xml.append(this.nameID.toXMLString(includeNSPrefix, declareNS));
      }

      if (this.signatureString != null && !this.signatureString.isEmpty()) {
         xml.append(this.signatureString);
      }

      if (this.extensions != null) {
         xml.append(this.extensions.toXMLString(includeNSPrefix, declareNS));
      }

      return xml.toString();
   }

   @Override
   public void makeImmutable() {
      if (this.isMutable) {
         if (this.nameID != null && this.nameID.isMutable()) {
            this.nameID.makeImmutable();
         }

         if (this.extensions != null && this.extensions.isMutable()) {
            this.extensions.makeImmutable();
         }

         this.isMutable = false;
      }
   }

   @Override
   public boolean isMutable() {
      return this.isMutable;
   }

   protected static void validateID(String requestID) throws SAML2Exception {
      if (requestID == null || requestID.isEmpty()) {
         SAML2SDKUtils.DEBUG.message("ID is missing in the SAMLRequest");
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("missingIDAttr"));
      }
   }

   protected static void validateVersion(String version) throws SAML2Exception {
      if (version == null || version.isEmpty()) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("missingVersion"));
      }

      if (!version.equals("2.0")) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("incorrectVersion"));
      }
   }

   protected void validateIssueInstant(String issueInstantStr) throws SAML2Exception {
      if (issueInstantStr != null && !issueInstantStr.isEmpty()) {
         try {
            this.issueInstant = DateUtils.stringToDate(issueInstantStr);
         } catch (ParseException e) {
            SAML2SDKUtils.DEBUG.message("Error parsing IssueInstant", e);
            throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("incorrectIssueInstant"));
         }
      } else {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("missingIssueInstant"));
      }
   }

   protected void validateData() throws SAML2Exception {
      validateID(this.requestId);
      validateVersion(this.version);
      if (this.issueInstant == null) {
         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("incorrectIssueInstant"));
      }

      this.validateIssueInstant(DateUtils.dateToString(this.issueInstant));
   }

   private void getXMLString(StringBuilder attrs, StringBuilder childElements, boolean includeNSPrefix, boolean declareNS) throws SAML2Exception {
      this.validateData();
      attrs.append(" ")
         .append("ID")
         .append("=")
         .append("\"")
         .append(this.requestId)
         .append("\"")
         .append(" ")
         .append("Version")
         .append("=")
         .append("\"")
         .append(this.version)
         .append("\"")
         .append(" ")
         .append("IssueInstant")
         .append("=")
         .append("\"")
         .append(DateUtils.toUTCDateFormat(this.issueInstant))
         .append("\"");
      if (this.destinationURI != null && !this.destinationURI.isEmpty()) {
         attrs.append(" ").append("Destination").append("=").append("\"").append(this.destinationURI).append("\"");
      }

      if (this.consent != null && !this.consent.isEmpty()) {
         attrs.append(" ").append("Consent").append("=").append("\"").append(this.consent).append("\"");
      }

      if (this.nameID != null) {
         childElements.append(this.nameID.toXMLString(includeNSPrefix, declareNS)).append("\n");
      }

      if (this.signatureString != null && !this.signatureString.isEmpty()) {
         childElements.append(this.signatureString).append("\n");
      }

      if (this.extensions != null) {
         childElements.append(this.extensions.toXMLString(includeNSPrefix, declareNS)).append("\n");
      }
   }

   protected void parseDOMElement(Element element) throws SAML2Exception {
      this.parseDOMAttributes(element);
      List<Node> childElementList = new ArrayList<>();
      NodeList nList = element.getChildNodes();
      if (nList != null && nList.getLength() > 0) {
         for (int i = 0; i < nList.getLength(); i++) {
            Node childNode = nList.item(i);
            if (childNode.getNodeType() == 1) {
               childElementList.add(childNode);
            }
         }
      }

      ListIterator<Node> iter = childElementList.listIterator();
      this.parseDOMChileElements(iter);
      if (iter.hasNext()) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("RequestAbstractImpl.parseDOMElement: Unexpected child element found");
         }

         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("schemaViolation"));
      }
   }

   private void parseDOMAttributes(Element element) throws SAML2Exception {
      this.requestId = element.getAttribute("ID");
      validateID(this.requestId);
      this.version = element.getAttribute("Version");
      validateVersion(this.version);
      String issueInstantStr = element.getAttribute("IssueInstant");
      this.validateIssueInstant(issueInstantStr);
      this.destinationURI = element.getAttribute("Destination");
      this.consent = element.getAttribute("Consent");
   }

   private void parseDOMChileElements(ListIterator<Node> iter) throws SAML2Exception {
      AssertionFactory assertionFactory = AssertionFactory.getInstance();
      ProtocolFactory protoFactory = ProtocolFactory.getInstance();

      while (iter.hasNext()) {
         Element childElement = (Element)iter.next();
         String localName = childElement.getLocalName();
         if ("Issuer".equals(localName)) {
            this.validateIssuer();
            this.nameID = assertionFactory.createIssuer(childElement);
         } else if ("Signature".equals(localName)) {
            this.validateSignature();
            this.signatureString = XMLUtils.print(childElement);
            this.isSigned = true;
         } else {
            if (!"Extensions".equals(localName)) {
               iter.previous();
               break;
            }

            this.validateExtensions();
            this.extensions = protoFactory.createExtensions(childElement);
         }
      }
   }

   private void validateIssuer() throws SAML2Exception {
      if (this.nameID != null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("RequestAbstractImpl.validateIssuer: Too many Issuer Element");
         }

         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("schemaViolation"));
      } else if (this.signatureString != null || this.extensions != null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("RequestAbstractImpl.validateIssuer: Issuer Element should be the first element in the Request");
         }

         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("schemaViolation"));
      }
   }

   private void validateSignature() throws SAML2Exception {
      if (this.signatureString != null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("RequestAbstractImpl.validateSignature: Too many Signature Elements");
         }

         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("schemaViolation"));
      } else if (this.extensions != null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("RequestAbstractImpl.validateSignature: Signature should be in front of Extensions");
         }

         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("schemaViolation"));
      }
   }

   private void validateExtensions() throws SAML2Exception {
      if (this.extensions != null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("RequestAbstractImpl.validateExtensions: Too many Extension Elements");
         }

         throw new SAML2Exception(SAML2SDKUtils.BUNDLE.getString("schemaViolation"));
      }
   }
}
