<?php


namespace iDServer\Message\Strategy;


use DOMDocument;
use DOMElement;
use DOMXPath;
use iDServer\Constants;
use iDServer\Saml\AbstractSamlData;
use iDServer\Saml\AttributeData;
use iDServer\Saml\AttributeQueryData;
use iDServer\Saml\AuthnRequestData;
use iDServer\Saml\ExtensionData;
use iDServer\Saml\LogoutRequestData;
use iDServer\Saml\LogoutResponseData;
use iDServer\Saml\NameId;
use iDServer\Saml\NameIdMappingData;
use iDServer\Saml\NameIdPolicy;

abstract class AbstractMessageHandlingStrategy
{
    /**
     * @param NameIdPolicy|null $nameIdPolicy
     * @param DOMDocument $doc
     */
    protected function addNameIDPolicy(?NameIdPolicy $nameIdPolicy, DOMDocument $doc)
    {
        if (!$nameIdPolicy) {
            return;
        }

        $element = $doc->createElementNS(Constants\NS_SAMLP, 'saml2p:NameIDPolicy');
        $element->setAttribute('AllowCreate', $nameIdPolicy->isAllowCreate() ? 'true' :  'false');
        if ($nameIdPolicy->getFormat()) {
            $element->setAttribute('Format', $nameIdPolicy->getFormat());
        }
        if ($nameIdPolicy->getSpNameQualifier()) {
            $element->setAttribute('SPNameQualifier', $nameIdPolicy->getSpNameQualifier());
        }

        $doc->documentElement->appendChild($element);
    }


    /**
     * @param NameId $nameId
     * @param DOMDocument $doc
     * @return DOMElement
     */
    protected function createNameID(NameId $nameId, DOMDocument $doc): DOMElement
    {
        $elem = $doc->createElementNS(Constants\NS_SAML, 'saml2:NameID', $nameId->getValue());
        if ($nameId->getFormat()) {
            $elem->setAttribute('Format', $nameId->getFormat());
        }
        if ($nameId->getSpProvidedId()) {
            $elem->setAttribute('SPProvidedID', $nameId->getSpProvidedId());
        }
        if ($nameId->getSPNameQualifier()) {
            $elem->setAttribute('SPNameQualifier', $nameId->getSPNameQualifier());
        }

        return $elem;
    }

    /**
     * @param NameId $nameId
     * @param DOMDocument $doc
     * @param DOMElement|null $parent
     */
    protected function addNameID(NameId $nameId, DOMDocument $doc, ?DOMElement $parent = null)
    {
        if (!$parent) {
            $parent = $doc->documentElement;
        }
        $parent->appendChild($this->createNameID($nameId, $doc));
    }

    /**
     * @param string[] $audiences
     * @param DOMDocument $doc
     */
    protected function addAudiences(array $audiences, DOMDocument $doc)
    {
        if ($audiences) {
            $conditions = $doc->createElementNS(Constants\NS_SAML, 'saml2:Conditions');
            $restriction = $doc->createElementNS(Constants\NS_SAML, 'saml2:AudienceRestriction');
            foreach ($audiences as $audienceName) {
                $restriction->appendChild($doc->createElementNS(Constants\NS_SAML, 'saml2:Audience', $audienceName));
            }
            $conditions->appendChild($restriction);
            $doc->documentElement->appendChild($conditions);
        }
    }

    /**
     * @param ExtensionData|null $extensionData
     * @param DOMDocument $doc
     */
    protected function addExtensions(?ExtensionData $extensionData, DOMDocument $doc)
    {
        if ($extensionData) {
            $extensions = $doc->createElementNS(Constants\NS_SAMLP, 'saml2p:Extensions');
            $this->addAttributes($extensionData->getAttributes(), $doc, $extensions);
            $this->addRequestedAttributes($extensionData->getRequestedAttributes(), $doc, $extensions);
            $this->addSPType($extensionData->getSpType(), $doc, $extensions);
            $doc->documentElement->appendChild($extensions);
        }
    }

    /**
     * @param AttributeData $item
     * @param DOMDocument $doc
     * @return DOMElement
     */
    protected function createAttribute(AttributeData $item, DOMDocument $doc)
    {
        $attribute = $doc->createElementNS(Constants\NS_SAML, 'saml2:Attribute');
        $attribute->setAttribute('Name', $item->getName());
        $attribute->setAttribute('NameFormat', $item->getNameFormat());

        $attributeValue = $doc->createElementNS(Constants\NS_SAML, 'saml2:AttributeValue');
        $attributeValue->setAttributeNS(Constants\NS_XML_SCHEMA, 'xsi:type', $item->getType());
        if (empty($item->getValue())) {
            $attributeValue->setAttributeNS(Constants\NS_XML_SCHEMA, 'xsi:nil', 'true');
        } else {
            $attributeValue->nodeValue = $item->getValue();
        }
        $attribute->appendChild($attributeValue);
        return $attribute;
    }

    /**
     * @param AttributeData[] $attributeData
     * @param DOMDocument $doc
     * @param DOMElement $extensions
     */
    protected function addAttributes(array $attributeData, DOMDocument $doc, DOMElement $extensions)
    {
        if ($attributeData) {
            $statement = $doc->createElementNS(Constants\NS_SAML, 'saml2:AttributeStatement');
            foreach ($attributeData as $item) {
                $statement->appendChild($this->createAttribute($item, $doc));
            }
            $extensions->appendChild($statement);
        }
    }

    /**
     * @param AttributeData[] $attributeData
     * @param DOMDocument $doc
     * @param DOMElement $extensions
     */
    protected function addRequestedAttributes(array $attributeData, DOMDocument $doc, DOMElement $extensions)
    {
        if ($attributeData) {
            $attributes = $doc->createElementNS(Constants\NS_EIDAS, 'eidas:RequestedAttributes');
            foreach ($attributeData as $item) {
                $attribute = $doc->createElementNS(Constants\NS_EIDAS, 'eidas:RequestedAttribute');
                $attribute->setAttribute('Name', $item->getName());
                $attribute->setAttribute('NameFormat', $item->getNameFormat());
                $attribute->setAttribute('isRequired', $item->isRequired() ? 'true' : 'false');
                $attributes->appendChild($attribute);
            }
            $extensions->appendChild($attributes);
        }
    }

    /**
     * @param string|null $spTypeData
     * @param DOMDocument $doc
     * @param DOMElement $extensions
     */
    protected function addSPType(?string $spTypeData, DOMDocument $doc, DOMElement $extensions)
    {
        if ($spTypeData) {
            $spTypeElem = $doc->createElementNS(Constants\NS_EIDAS, 'eidas:SPType', $spTypeData);
            $extensions->appendChild($spTypeElem);
        }
    }

    /**
     * @param AbstractSamlData $data
     * @param string $namespaceURI
     * @param string $qualifiedName
     * @return DOMDocument
     */
    protected function createSamlRequest(AbstractSamlData $data, string $namespaceURI, string $qualifiedName): DOMDocument
    {
        $doc = new DOMDocument('1.0', 'UTF-8');
        $root = $doc->createElementNS($namespaceURI, $qualifiedName);
        if ($data->getDestination()) {
            $root->setAttribute('Destination', $data->getDestination());
        }
        $root->setAttribute('ID', $data->getId());
        $root->setAttribute('IssueInstant', $data->getIssueInstant());
        $root->setAttribute('Version', '2.0');
        $root->setAttributeNS('http://www.w3.org/2000/xmlns/', "xmlns:xs", "http://www.w3.org/2001/XMLSchema");
        $elem = $doc->createElementNS(Constants\NS_SAML, "saml2:Issuer", (string) $data->getIssuer());
        $root->appendChild($elem);
        $doc->appendChild($root);

        return $doc;
    }

    /**
     * @param AuthnRequestData $data
     * @return DOMDocument
     */
    public function createAuthnRequest(AuthnRequestData $data): DOMDocument
    {
        $doc = $this->createSamlRequest($data, Constants\NS_SAMLP, "saml2p:AuthnRequest");
        $root = $doc->documentElement;
        $root->setAttribute('AssertionConsumerServiceURL', $data->getAssertionConsumerServiceURL());
        $root->setAttribute('ProtocolBinding', Constants\SAML_BINDING_POST);

        $this->addExtensions($data->getExtensions(), $doc);
        $this->addNameIDPolicy($data->getNameIdPolicy(), $doc);
        $this->addAudiences($data->getAudiences(), $doc);

        return $doc;
    }

    /**
     * @param LogoutRequestData $data
     * @return DOMDocument
     */
    public function createLogoutRequest(LogoutRequestData $data): DOMDocument
    {
        $doc = $this->createSamlRequest($data, Constants\NS_SAMLP, 'saml2p:LogoutRequest');

        $this->addNameID($data->getNameId(), $doc);
        $elem = $doc->createElementNS(Constants\NS_SAMLP, "saml2:SessionIndex", $data->getSessionIndex());
        $doc->documentElement->appendChild($elem);

        return $doc;
    }

    /**
     * @param LogoutResponseData $data
     * @return DOMDocument
     */
    public function createLogoutResponse(LogoutResponseData $data): DOMDocument
    {
        $doc = $this->createSamlRequest($data, Constants\NS_SAMLP, 'saml2p:LogoutResponse');

        $status = $doc->createElementNS(Constants\NS_SAMLP, 'saml2p:Status');
        $statusCode = $doc->createElementNS(Constants\NS_SAMLP, 'saml2p:StatusCode');
        $statusCode->setAttribute("Value", $data->getStatus()->getCode());
        $status->appendChild($statusCode);

        if (!empty($this->statusMessage)) {
            $statusMessage = $doc->createElementNS(
                Constants\NS_SAMLP,
                'saml2p:statusMessage',
                $data->getStatus()->getMessage()
            );
            $status->appendChild($statusMessage);
        }

        $doc->documentElement->appendChild($status);

        return $doc;
    }

    /**
     * @param AttributeQueryData $data
     * @return DOMDocument
     */
    public function createAttributeQuery(AttributeQueryData $data): DOMDocument
    {
        $doc = $this->createSamlRequest($data, Constants\NS_SAMLP, 'saml2p:AttributeQuery');

        $this->addExtensions($data->getExtensions(), $doc);

        $xpath = new DOMXPath($doc);
        $xpath->registerNamespace('saml2p', Constants\NS_SAMLP);
        $xpath->registerNamespace('saml2', Constants\NS_SAML);

        $attributeStatement = $xpath->query('saml2p:Extensions/saml2:AttributeStatement', $doc->documentElement)->item(0);

        foreach ($data->getAudiences() as $audience) {
            $item = new AttributeData();
            $item->setNameFormat('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified:audience');
            $item->setName('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified:audience');
            $item->setValue($audience);
            $item->setType('xs:string');
            $attributeStatement->appendChild($this->createAttribute($item, $doc));
        }

        if ($data->getNameId()) {
            $subject = $doc->createElementNS(Constants\NS_SAML, 'saml2:Subject');
            $this->addNameID($data->getNameId(), $doc, $subject);
            $doc->documentElement->appendChild($subject);
        }

        $attributes = $data->getAttributes();

        foreach ($attributes as $attribute) {
            $doc->documentElement->appendChild($this->createAttribute($attribute, $doc));
        }

        return $doc;
    }

    /**
     * @param NameIdMappingData $data
     * @return DOMDocument
     */
    public function createNameIdMappingRequest(NameIdMappingData $data): DOMDocument
    {
        $doc = $this->createSamlRequest($data, Constants\NS_SAMLP, 'saml2p:NameIDMappingRequest');

        $this->addExtensions($data->getExtensions(), $doc);

        $xpath = new DOMXPath($doc);
        $xpath->registerNamespace('saml2p', Constants\NS_SAMLP);
        $xpath->registerNamespace('saml2', Constants\NS_SAML);

        $attributeStatement = $xpath->query('saml2p:Extensions/saml2:AttributeStatement', $doc->documentElement)->item(0);

        foreach ($data->getAudiences() as $audience) {
            $item = new AttributeData();
            $item->setNameFormat('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified:audience');
            $item->setName('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified:audience');
            $item->setValue($audience);
            $item->setType('xs:string');
            $attributeStatement->appendChild($this->createAttribute($item, $doc));
        }

        if ($data->getNameId()) {
            $this->addNameID($data->getNameId(), $doc);
        }

        $this->addNameIDPolicy($data->getNameIdPolicy(), $doc);

        $attributes = $data->getAttributes();

        foreach ($attributes as $attribute) {
            $doc->documentElement->appendChild($this->createAttribute($attribute, $doc));
        }

        return $doc;
    }
}
