<?php

namespace AuthStack\SAMLServiceProvider\Model;

use LightSaml\Credential\KeyHelper;
use LightSaml\Credential\X509Certificate;
use LightSaml\Model\Context\SerializationContext;
use LightSaml\Model\Metadata\AssertionConsumerService;
use LightSaml\Model\Metadata\ContactPerson;
use LightSaml\Model\Metadata\EntityDescriptor;
use LightSaml\Model\Metadata\KeyDescriptor;
use LightSaml\Model\Metadata\SingleLogoutService;
use LightSaml\Model\Metadata\SpSsoDescriptor;
use LightSaml\Model\XmlDSig\SignatureWriter;
use RobRichards\XMLSecLibs\XMLSecurityKey;

class ServiceProvider
{
    /**
     * @return null|string
     */
    public function getEntityId(): ?string
    {
        return config('saml.sp.EntityId');
    }

    /**
     * @return string
     */
    public function getIdPEntityId(): string
    {
        return config('saml.sp.IdentityProvider');
    }

    /**
     * @return string
     */
    public function getBaseUrl(): string
    {
        return config('saml.sp.baseUrl');
    }

    /**
     * @return null|string
     */
    public function getNameIdFormat(): ?string
    {
        return config('saml.sp.nameIdFormat');
    }

    /**
     * @return bool
     */
    public function wantsSignedMessages(): bool
    {
        return config('saml.sp.wantsSignedMessages');
    }

    /**
     * @return bool
     */
    public function wantsSignedAssertions(): bool
    {
        return config('saml.sp.wantsSignedAssertions');
    }

    /**
     * @return bool
     */
    public function wantsEncryptedAssertions(): bool
    {
        return config('saml.sp.wantsEncryptedAssertions');
    }

    /**
     * @return string
     */
    public function getACSPath(): string
    {
        return config('saml.sp.AssertionConsumerService.location');
    }

    /**
     * @return string
     */
    public function getACSUrl(): string
    {
        return sprintf("%s%s", $this->getBaseUrl(), $this->getACSPath());
    }

    /**
     * @return string
     */
    public function getACSBinding(): string
    {
        return config('saml.sp.AssertionConsumerService.binding');
    }

    /**
     * @return string
     */
    public function getLogoutServicePath(): string
    {
        return config('saml.sp.SLO.location');
    }

    /**
     * @return string
     */
    public function getLogoutServiceUrl(): string
    {
        return sprintf("%s%s", $this->getBaseUrl(), $this->getLogoutServicePath());
    }

    /**
     * @return string
     */
    public function getLogoutServiceBinding(): string
    {
        return config('saml.sp.SLO.binding');
    }

    public function validateSignature(): bool
    {
        return config('saml.sp.validate.signature');
    }

    /**
     * @return bool
     */
    public function validateStatus(): bool
    {
        return config('saml.sp.validate.status');
    }

    /**
     * @return bool
     */
    public function validateIssuer(): bool
    {
        return config('saml.sp.validate.issuer');
    }

    /**
     * @return bool
     */
    public function validateDestination(): bool
    {
        return config('saml.sp.validate.destination');
    }

    /**
     * @return bool
     */
    public function validateLifetime(): bool
    {
        return config('saml.sp.validate.lifetime');
    }

    /**
     * @return \DateTime
     */
    public function getUpperDT(): \DateTime
    {
        return new \DateTime($this->getValidationUpperTimeBoundary(), new \DateTimeZone('UTC'));
    }

    /**
     * @return \DateTime
     */
    public function getLowerDT(): \DateTime
    {
        return new \DateTime($this->getValidationLowerTimeBoundary(), new \DateTimeZone('UTC'));
    }

    /**
     * @return string
     */
    public function getValidationUpperTimeBoundary(): string
    {
        return config('saml.sp.validate.upper');
    }

    /**
     * @return string
     */
    public function getValidationLowerTimeBoundary(): string
    {
        return config('saml.sp.validate.lower');
    }

    /**
     * @return array
     */
    public function getSigningKey(): array
    {
        return config('saml.sp.keys.signing', []);
    }

    /**
     * @return SignatureWriter
     */
    public function getSignatureWriter(): SignatureWriter
    {
        $x509 = new X509Certificate();

        $x509->setData($this->getSigningKeyX509Cert());

        $priv = KeyHelper::createPrivateKey(
            $this->getSigningPrivateKey(),
            $this->getSigningPrivateKeyPassphrase(),
            false,
            $this->getSigningDigestAlgorithm()
        );

        return new SignatureWriter($x509, $priv);
    }

    /**
     * @return XMLSecurityKey
     */
    public function getSigningPublicKey(): XMLSecurityKey
    {
        return KeyHelper::createPublicKey(
            (new X509Certificate())->setData($this->getSigningKeyX509Cert())
        );
    }

    /**
     * @return string
     */
    public function getSigningKeyX509Cert(): string
    {
        return config('saml.sp.keys.signing.x509');
    }

    /**
     * @return string
     */
    public function getSigningPrivateKey(): string
    {
        return config('saml.sp.keys.signing.private');
    }

    /**
     * @return null|string
     */
    public function getSigningPrivateKeyPassphrase(): ?string
    {
        return config('saml.sp.keys.signing.passphrase');
    }

    /**
     * @return string
     */
    public function getSigningDigestAlgorithm(): string
    {
        return config('saml.sp.keys.signing.algorithm');
    }

    /**
     * @return array
     */
    public function getDecryptionKey(): array
    {
        return config('saml.sp.keys.encryption', []);
    }

    /**
     * @return XMLSecurityKey
     */
    public function getDecrypter(): XMLSecurityKey
    {
        return KeyHelper::createPrivateKey(
            $this->getDecryptionKeyPrivateKey(),
            $this->getDecryptionKeyPassphrase(),
            false,
            $this->getDecryptionKeyAlgorithm()
        );
    }

    /**
     * @return null|string
     */
    public function getDecryptionKeyPrivateKey(): ?string
    {
        return config('saml.sp.keys.encryption.private');
    }

    /**
     * @return null|string
     */
    public function getDecryptionKeyPassphrase(): ?string
    {
        return config('saml.sp.keys.encryption.passphrase');
    }

    /**
     * @return null|string
     */
    public function getDecryptionKeyAlgorithm(): ?string
    {
        return config('saml.sp.keys.encryption.algorithm');
    }

    /**
     * @return array
     */
    public function getContacts(): array
    {
        return config('saml.sp.contacts', []);
    }

    /**
     * @return array
     */
    public function getKeys(): array
    {
        return config('saml.sp.keys', []);
    }

    /**
     * @return string
     */
    public function getMetadataXML(): string
    {
        $entity = new EntityDescriptor($this->getEntityId());
        $sp = new SpSsoDescriptor();

        if($this->wantsSignedAssertions())
        {
            $sp->setWantAssertionsSigned(true);
        }

        $sp->addNameIDFormat($this->getNameIdFormat());

        //--------------------------------------------------------------------------------------------------------------
        // Add contacts
        //--------------------------------------------------------------------------------------------------------------
        collect($this->getContacts())->map(function($contact) use (&$sp)
        {
            $sp->addContactPerson(
                (new ContactPerson())
                    ->setGivenName(isset($contact['givenName']) ?? 'N/A')
                    ->setSurName(isset($contact['surName']) ?? 'N/A')
                    ->setContactType(isset($contact['contactType']) ?? 'N/A')
                    ->setCompany(isset($contact['company']) ?? 'N/A')
                    ->setEmailAddress(isset($contact['emailAddress']) ?? 'N/A')
                    ->setTelephoneNumber(isset($contact['telephoneNumber']) ?? 'N/A')
            );
        });

        //--------------------------------------------------------------------------------------------------------------
        // Add Assertion Consumer Service
        //--------------------------------------------------------------------------------------------------------------
        $sp->addAssertionConsumerService(
            (new AssertionConsumerService())
                ->setIsDefault(true)
                ->setBinding($this->getACSBinding())
                ->setLocation($this->getACSUrl())
        );

        //--------------------------------------------------------------------------------------------------------------
        // Add Single Logout Service
        //--------------------------------------------------------------------------------------------------------------
        $sp->addSingleLogoutService(
            (new SingleLogoutService())
                ->setBinding($this->getLogoutServiceBinding())
                ->setLocation($this->getLogoutServiceUrl())
        );

        //--------------------------------------------------------------------------------------------------------------
        // Set keys (signing and encryption)
        //--------------------------------------------------------------------------------------------------------------
        collect($this->getKeys())->map(function($item, $usage) use (&$sp)
        {
            $sp->addKeyDescriptor(
                (new KeyDescriptor())
                    ->setUse($usage)
                    ->setCertificate((new X509Certificate())->setData($item['x509']))
            );
        });

        $entity->addItem($sp);

        $context = new SerializationContext();

        $entity->serialize($context->getDocument(), $context);

        return $context->getDocument()->saveXML();
    }
}