package be.iminds.ilabt.jfed.lowlevel;


import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.util.*;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.slf4j.LoggerFactory;
import org.w3c.dom.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.Date;
import java.util.Objects;
import java.util.Random;

/**
 * <p>SfaCredential is a wrapper for an XML Sfa Credential.
 * <p/>
 * <p>It parses some data from the credential.
 * More data parsing and support for the different type of Geni Credentials should be implemented
 * <p/>
 * <p>A method to check the validity of the credential is provided.
 * <p/>
 * <p>A name is stored alongside the actual credential, which is useful for listing the credential in GUI lists and comboboxes.
 *
 * @see <a href="http://groups.geni.net/geni/wiki/GeniApiCredentials">Geni API Credentials</a>
 * @see <a href="http://www.protogeni.net/ProtoGeni/wiki/Credentials">ProtoGeni Credentials</a>
 */
public class SfaCredential extends AnyCredential {
    public static boolean debug_expiredate_forceZinsteadOfZero = true;
    public static boolean debug_expiredate_forcezulu = true;
    public static boolean debug_expiredate_discardsubsecond = true;
    public static boolean debug_expiredate_smalltz = false;
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(SfaCredential.class);
    private static final Random random = new Random(System.currentTimeMillis());
    private String type;
    private String ownerUrn, targetUrn;
    private String ownerGid, targetGid;
    private String expires;
    private Date expiresDate;

    /**
     * Pre Geni API v3 credentials: type=geni_sfa version=2
     */
    public SfaCredential(String name, String credentialXml) throws CredentialException {
        this(name, credentialXml, "geni_sfa", "2");
    }

    public SfaCredential(String name, String credentialXml, String type, String version) throws CredentialException {
        super(name, credentialXml, type, version);

        assert type.equalsIgnoreCase("sfa") || type.equalsIgnoreCase("geni_sfa") :
                "Created SfaCredential not of type sfa, but of type=\"" + type + "\" version=\"" + version + "\"";

        initDoc();
        parseXml();
    }

    private static Element addElementHelper(Document doc, Element target, String name, String text) {
        Element e = doc.createElement(name);
        if (text != null)
            e.appendChild(doc.createTextNode(text));
        target.appendChild(e);
        return e;
    }

    /**
     * @param type "privilege" or "speaksfor"
     */
    public static SfaCredential create(String type,
                                       String ownerUrn, String targetUrn,
                                       X509Certificate ownerCert, X509Certificate targetCert,
                                       X509Certificate signerCert, PrivateKey signerPrivateKey,
                                       Date expireDate, String privilegeName, boolean canDelegate,
                                       String credentialjFedName) throws CredentialException {

        //manually build the Xml document
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        docFactory.setNamespaceAware(true);
        DocumentBuilder docBuilder;
        try {
            docBuilder = docFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new CredentialException("Failed to create credential: " + e.getMessage(), e);
        }


        /*<signed-credential
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:noNamespaceSchemaLocation="http://www.protogeni.net/resources/credential/credential.xsd"
                xsi:schemaLocation="http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd">
        */
        // root elements
        Document doc = docBuilder.newDocument();
        Element rootElement = doc.createElement("signed-credential");

        rootElement.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
                "xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd");
        rootElement.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
                "xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd");

        doc.appendChild(rootElement);

        Element credential = doc.createElement("credential");
        Attr idAttr = doc.createAttribute("xml:id");
        String credentialId = "cred" + random.nextInt();
        idAttr.setValue(credentialId);
        credential.setAttributeNode(idAttr);
        credential.setIdAttributeNode(idAttr, true);
        rootElement.appendChild(credential);


        addElementHelper(doc, credential, "type", type);
        addElementHelper(doc, credential, "serial", "" + Math.random()); //no idea what to put here
        addElementHelper(doc, credential, "owner_gid", KeyUtil.x509certificateToCredentialXmlGid(ownerCert));
        addElementHelper(doc, credential, "owner_urn", ownerUrn);
        addElementHelper(doc, credential, "target_gid", KeyUtil.x509certificateToCredentialXmlGid(targetCert));
        addElementHelper(doc, credential, "target_urn", targetUrn);
        addElementHelper(doc, credential, "uuid", null);
        //ISO 8601 date and time of when the credential becomes invalid. If no timezone is specified,
        //   times are assumed to be in UTC. Note: Implementations are preferred to use RFC3339 compliant strings.
        addElementHelper(doc, credential, "expires", getExpiresDateString(expireDate));
        Element privileges = addElementHelper(doc, credential, "privileges", null);
        Element privilege = addElementHelper(doc, privileges, "privilege", null);
        addElementHelper(doc, privilege, "name", privilegeName);
        addElementHelper(doc, privilege, "can_delegate", canDelegate ? "1" : "0");

        Element signatures = addElementHelper(doc, rootElement, "signatures", null);

        String xml;
        try {
            xml = XmlUtil.signXml(doc, credentialId, "signatures", signerCert, signerPrivateKey);
        } catch (TransformerException | XMLSecurityException e) {
            throw new CredentialException("Failed to sign generated credential: " + e.getMessage(), e);
        }

        //http://groups.geni.net/geni/wiki/GeniApiCredentials describes geni_sfa version 3
        return new SfaCredential(credentialjFedName, xml, "geni_sfa", "3");

    }

    //  /*spokenForUrn*/, /*speakerUrn*/, /*spokenForCert*/, /*speakerCert*/, /*spokenForPrivateKey*/, /*expireDate*/, /*privilegeName*/, /*canDelegate*/
    public static SfaCredential createSpeaksFor(String spokenForUrn, String speakerUrn,
                                                X509Certificate spokenForCert, X509Certificate speakerCert,
                                                PrivateKey spokenForPrivateKey, Date expireDate,
                                                String privilegeName, boolean canDelegate) throws CredentialException {
        return create("speaksfor",
                speakerUrn/*owner*/, spokenForUrn/*target*/,
                speakerCert/*owner*/, spokenForCert/*target*/,
                spokenForCert/*signer*/, spokenForPrivateKey/*signer*/, expireDate,
                privilegeName, canDelegate,
                "SpeaksFor Credential"); //TODO: add usernames extracted from URNs to name
    }

    /*
     * for server debugging purposes, we have modifiers that can alter how these dates are created.
     * */
    private static String getExpiresDateString(Date expireDate) {
//        return RFC3339Util.dateToRFC3339String(expireDate, true/*force zulu time*/, true /*discardSubSeconds*/); //safest date to return
        String res =
                RFC3339Util.dateToRFC3339String(expireDate, debug_expiredate_forcezulu, debug_expiredate_discardsubsecond, debug_expiredate_forceZinsteadOfZero);
        if (debug_expiredate_smalltz) {
            res = res.replace('T', 't');
            res = res.replace('Z', 'z');
        }

        //for debugging
        try {
            Date reconstructExpires = RFC3339Util.rfc3339StringToDate(res);

            if (debug_expiredate_discardsubsecond)
                expireDate = new Date(expireDate.getTime() - (expireDate.getTime() % 1000));

            if (reconstructExpires.getTime() != expireDate.getTime())
                throw new RuntimeException("ERROR reconstructing date: expireDate=" + expireDate + " res=\"" + res + "\" " +
                        "reconstructExpires=" + reconstructExpires +
                        "   -> " + reconstructExpires.getTime() + " != " + expireDate.getTime());
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }

        return res;
    }

    /*
   * Example from http://groups.geni.net/geni/wiki/GeniApiCredentials:
   *
   <?xml version="1.0"?>
   <signed-credential>
           <credential xml:id="ref0">
                   <type>privilege</type>
                   <serial>8</serial>
                   <owner_gid>certificate here</owner_gid>
                   <owner_urn>urn:publicid:IDN+plc:gpo:site2+user+jkarlin</owner_urn>
                   <target_gid>certificate here</target_gid>
                   <target_urn>urn:publicid:IDN+plc:gpo:site2+user+jkarlin</target_urn>
                   <uuid/>
                   <expires>2012-07-14T19:52:08Z</expires>
                   <privileges>
                           <privilege>
                                   <name>refresh</name>
                                   <can_delegate>true</can_delegate>
                           </privilege>
                   </privileges>
           </credential>

           <signatures>
                   signature information here
           </signatures>
   </signed-credential>

   <Signature xml:id="Sig_ref0" xmlns="http://www.w3.org/2000/09/xmldsig#">
       <SignedInfo>
         <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
         <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
         <Reference URI="#ref0">
         <Transforms>
           <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
         </Transforms>
         <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
         <DigestValue></DigestValue>
         </Reference>
       </SignedInfo>
       <SignatureValue />
         <KeyInfo>
           <X509Data>
             <X509SubjectName/>
             <X509IssuerSerial/>
             <X509Certificate/>
           </X509Data>
         <KeyValue />
         </KeyInfo>
       </Signature>
   * */

    private void parseXml() throws CredentialException {
        assert xmlDoc != null; //initDoc should have been called

        assert Objects.equals(xmlDoc.getDocumentElement().getTagName(), "signed-credential") :
                "Document element is not <signed-credential> but \"" + xmlDoc.getDocumentElement().getTagName() + "\"";

        NodeList nl = xmlDoc.getDocumentElement().getChildNodes();    // getElementsByTagName("credential");
        if (nl.getLength() == 0) {
            throw new CredentialException("Cannot find any <signed-credential> element children!");
        }
        Element credentialEl = null;
        for (int i = 0; i < nl.getLength(); i++) {
            if (!(nl.item(i) instanceof Element)) {
                //ignore whitespace
                if (!(nl.item(i) instanceof Text && nl.item(i).getTextContent().trim().isEmpty()))
                    LOG.warn("<signed-credential> contains non credential: class=" + nl.item(i).getClass().getName() + " value=" + nl.item(i).toString());
                continue;
            }
            Element e = (Element) nl.item(i);
            if (Objects.equals(e.getTagName(), "credential")) {
                if (credentialEl != null)
                    throw new CredentialException("No support for multiple credentials implemented.");
                credentialEl = e;
            }
        }
        if (credentialEl == null) {
            throw new CredentialException("No credential element found in credential.");
        }

        Element typeEl = (Element) credentialEl.getElementsByTagName("type").item(0);
        if (typeEl == null) throw new CredentialException("XML credential element does not contain type element");
        type = typeEl.getTextContent();

        Element ownerUrnEl = (Element) credentialEl.getElementsByTagName("owner_urn").item(0);
        if (ownerUrnEl == null)
            throw new CredentialException("XML credential element does not contain owner_urn element");
        ownerUrn = ownerUrnEl.getTextContent();

        Element targetUrnEl = (Element) credentialEl.getElementsByTagName("target_urn").item(0);
        targetUrn = targetUrnEl.getTextContent();

        if (credentialEl.getElementsByTagName("owner_gid") == null)
            throw new CredentialException("XML credential element does not contain owner_gid element");
        if (credentialEl.getElementsByTagName("owner_gid").getLength() > 0) {
            Element ownerGidEl = (Element) credentialEl.getElementsByTagName("owner_gid").item(0);
            ownerGid = ownerGidEl.getTextContent();
        }

        if (credentialEl.getElementsByTagName("target_gid") == null)
            throw new CredentialException("XML credential element does not contain target_gid element");
        if (credentialEl.getElementsByTagName("target_gid").getLength() > 0) {
            Element targetGidEl = (Element) credentialEl.getElementsByTagName("target_gid").item(0);
            targetGid = targetGidEl.getTextContent();
        }

        if (credentialEl.getElementsByTagName("expires") == null)
            throw new CredentialException("XML credential element does not contain expires element");
        if (credentialEl.getElementsByTagName("expires").getLength() > 0) {
            Element expiresEl = (Element) credentialEl.getElementsByTagName("expires").item(0);
            expires = expiresEl.getTextContent();
            try {
                expiresDate = RFC3339Util.iso8601StringToDate(expires); //able to parse a lot of dates
            } catch (ParseException e) {
                LOG.error("XML credential expires element is not a valid ISO8601 date: \"" + expires + "\"", e);
                expiresDate = null;
                //ignore
//                throw new CredentialException("XML credential expires element is not a valid RFC3339 date: \""+expires+"\"", e);
            }
        }
    }

    public String getType() {
        return type;
    }

    public String getTargetUrn() {
        return targetUrn;
    }

    public String getOwnerUrn() {
        return ownerUrn;
    }

    public String getOwnerGid() {
        return ownerGid;
    }

    public String getTargetGid() {
        return targetGid;
    }

    public String getExpires() {
        return expires;
    }

    public Date getExpiresDate() {
        return expiresDate;
    }

    @Override
    public Boolean isTargetSubAuthority() {
        if (targetUrn == null) return null;
        GeniUrn targetU = GeniUrn.parse(targetUrn);
        if (targetU == null) return null;
        return targetU.getEncodedSubAuthName() != null;
    }

    /**
     * Check if the credential is valid.
     *
     * @return {@code true} if the credential is valid
     */
    @Override
    public boolean check(TestbedInfoSource testbedInfoSource) throws CredentialException {
        JFedTrustStore jFedTrustStore = new JFedTrustStore();

        if (ownerUrn != null) {
            Server auth = testbedInfoSource.getFromAnyUrn(ownerUrn, TestbedInfoSource.SubAuthMatchAllowed.ALLOW_OTHER_SUBAUTHORITY, TestbedInfoSource.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY);
            if (auth != null)
                jFedTrustStore.addTrustedPemCertificateIfNotAdded(auth.getCertificateChain());
        }
        if (targetUrn != null) {
            Server auth = testbedInfoSource.getFromAnyUrn(targetUrn, TestbedInfoSource.SubAuthMatchAllowed.ALLOW_OTHER_SUBAUTHORITY, TestbedInfoSource.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY);
            if (auth != null)
                jFedTrustStore.addTrustedPemCertificateIfNotAdded(auth.getCertificateChain());
        }
        return check(jFedTrustStore.getTrustStore());
    }

    /**
     * @param delegatedRights Currently, SFA assigns ['refresh', 'resolve', and 'info'] rights to user credentials. Slice credentials have "slice" rights. Use * for all rights.
     */

//  /*newOwnerUrn*/, /*newOwnerCert*/, /*originalOwnerPrivateKey*/, /*expireDate*/, /*delegatedRights*/, /*canDelegate*/
    public SfaCredential delegate(String newOwnerUrn, X509Certificate newOwnerCert,
                                  PrivateKey originalOwnerPrivateKey,
                                  Date expireDate,
                                  String delegatedRights, boolean canDelegate) throws CredentialException {
        Element originalCredential = (Element) xmlDoc.getElementsByTagName("credential").item(0);
        Element originalSignatures = (Element) xmlDoc.getElementsByTagName("signatures").item(0);
        String originalTargetUrn = originalCredential.getElementsByTagName("target_urn").item(0).getTextContent();
        String originalTargetCertString =
                originalCredential.getElementsByTagName("target_gid").item(0).getTextContent();
        String originalOwnerUrn = originalCredential.getElementsByTagName("owner_urn").item(0).getTextContent();
        String originalOwnerCertString = originalCredential.getElementsByTagName("owner_gid").item(0).getTextContent();

        Date originalCredentialExpires = getExpiresDate();
        if (originalCredentialExpires != null && originalCredentialExpires.before(expireDate)) {
            LOG.info("when delegating credential, requested expire date was after expire date of credential being " +
                    "delegated. This is not allowed, using original date of date of credential being delegated as " +
                    "expires date instead. requested date = " + expires + ". original expires date = " + originalCredentialExpires);
            expireDate = originalCredentialExpires;
        }

        //manually build the Xml document
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        docFactory.setNamespaceAware(true);
        DocumentBuilder docBuilder;
        try {
            docBuilder = docFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new CredentialException("Failed to create delegated credential: " + e.getMessage(), e);
        }

        // root elements
        Document doc = docBuilder.newDocument();
        Element rootElement = doc.createElement("signed-credential");

        rootElement.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
                "xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd");
        rootElement.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
                "xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd");

        doc.appendChild(rootElement);

        Element credential = doc.createElement("credential");
        Attr idAttr = doc.createAttribute("xml:id");
        //TODO make SURE this is different from id in original doc   (now randomness make it almost certain, but not fully)
        String credId = "cred" + Math.round(Math.random() * 1000);
        idAttr.setValue(credId);
        credential.setAttributeNode(idAttr);
        credential.setIdAttributeNode(idAttr, true);
        rootElement.appendChild(credential);


        addElementHelper(doc, credential, "type", "privilege");
        addElementHelper(doc, credential, "serial", "" + Math.random()); //no idea what to put here
        addElementHelper(doc, credential, "owner_gid", KeyUtil.x509certificateToCredentialXmlGid(newOwnerCert));
        addElementHelper(doc, credential, "owner_urn", newOwnerUrn);
        addElementHelper(doc, credential, "target_gid", originalTargetCertString);
        addElementHelper(doc, credential, "target_urn", originalTargetUrn);
        addElementHelper(doc, credential, "uuid", null);
        addElementHelper(doc, credential, "expires", getExpiresDateString(expireDate));
        Element privileges = addElementHelper(doc, credential, "privileges", null);
        Element privilege = addElementHelper(doc, privileges, "privilege", null);
        addElementHelper(doc, privilege, "name", delegatedRights);
        addElementHelper(doc, privilege, "can_delegate", canDelegate ? "1" : "0");
        Element parent = addElementHelper(doc, credential, "parent", null);

        Node importedOriginalCredential = doc.importNode(originalCredential, true);
        parent.appendChild(importedOriginalCredential);

        Element signatures = addElementHelper(doc, rootElement, "signatures", null);

        //add existing signature(s) of delegated credential to new credential <signatures>
        NodeList origSignatures = originalSignatures.getChildNodes();
        for (int i = 0; i < origSignatures.getLength(); i++) {
            Node importedOriginalSig = doc.importNode(origSignatures.item(i), true);
            signatures.appendChild(importedOriginalSig);
        }

        X509Certificate originalOwnerCert = KeyUtil.pemToX509Certificate(originalOwnerCertString);

        String xml;
        try {
            xml = XmlUtil.signXml(doc, credId, "signatures", originalOwnerCert, originalOwnerPrivateKey);
        } catch (TransformerException | XMLSecurityException e) {
            throw new CredentialException("Failed to sign delegated credential: " + e.getMessage(), e);
        }

        //http://groups.geni.net/geni/wiki/GeniApiCredentials describes geni_sfa version 3
        return new SfaCredential("Delegated Credential", xml, "geni_sfa", "3"); //TODO: add usernames extracted from URNs to name
    }
}
