<?php

namespace AuthStack\SAMLServiceProvider\Model;

use Illuminate\Http\Request;
use LightSaml\{
    Binding\BindingFactory,
    Helper,
    Model\XmlDSig\Signature,
    Model\XmlDSig\SignatureStringReader,
    Model\XmlDSig\SignatureXmlReader,
    SamlConstants,
    Model\Assertion\Issuer,
    Model\Protocol\AuthnRequest,
    Model\Protocol\NameIDPolicy,
    Model\Protocol\Response,
    Binding\AbstractBinding,
    Binding\HttpPostBinding,
    Binding\HttpRedirectBinding,
    Context\Profile\MessageContext
};
use AuthStack\SAMLServiceProvider\Exceptions\SAMLException;


class AuthN
{
    /**
     * @var ServiceProvider
     */
    protected $sp;

    /**
     * @var IdentityProvider
     */
    protected $idp;

    /**
     * @var AuthnRequest
     */
    protected $authn;

    /**
     * @var Response
     */
    protected $authn_response;

    /**
     * AuthN constructor.
     * @param bool $create
     */
    public function __construct(bool $create = false)
    {
        $this->sp = new ServiceProvider();
        $this->idp = new IdentityProvider($this->sp->getIdPEntityId());

        if($create)
        {
            $this->create();
        }
    }

    /**
     * @throws SAMLException
     */
    public function send()
    {
        switch($this->idp->getSSOBinding())
        {
            case SamlConstants::BINDING_SAML2_HTTP_POST:
                $this->sendWith(new HttpPostBinding());
                break;

            case SamlConstants::BINDING_SAML2_HTTP_REDIRECT:
                $this->sendWith(new HttpRedirectBinding());
                break;

            default:
                throw new SAMLException(sprintf("Unsupported binding: ", $this->idp->getSSOBinding()), 500);
                break;
        }
    }

    /**
     * @param Request|null $request
     * @return $this
     */
    public function receive(Request $request = null)
    {
        $request = $request ?? Request::createFromGlobals();

        $factory = new BindingFactory();
        $context = new MessageContext();

        $binding = $factory->getBindingByRequest($request);

        $binding->receive($request, $context);

        $this->authn_response = $context->getMessage();

        return $this;
    }

    /**
     * @return AuthnRequest
     */
    public function getAuthnRequest(): AuthnRequest
    {
        return $this->authn;
    }

    /**
     * @return AuthNResponse
     */
    public function getAuthnResponse(): AuthNResponse
    {
        $response = $this->getLightSamlAuthnResponse();

        return new AuthNResponse($response, $this->sp);
    }

    /**
     * @return Response
     */
    public function getLightSamlAuthnResponse(): Response
    {
        return $this->authn_response;
    }

    /**
     * @throws SAMLException
     * @return bool
     */
    public function validateAuthnResponse(): bool
    {
        $response = $this->getLightSamlAuthnResponse();

        //--------------------------------------------------------------------------------------------------------------
        // Validate IdP's signature. This is the most important of all security aspects.
        //--------------------------------------------------------------------------------------------------------------
        if($this->sp->validateSignature())
        {
            /** @var  $signature SignatureXmlReader|SignatureStringReader */
            $signature = $response->getSignature();
            $pubkey = $this->idp->getSigningPublicKey();

            try
            {
                $signature->validate($pubkey);
            }
            catch(\Exception $e)
            {
                throw new SAMLException($e->getMessage(), 400);
            }
        }

        //--------------------------------------------------------------------------------------------------------------
        // Validate status. AuthN status should be marked as successful
        //--------------------------------------------------------------------------------------------------------------
        if($this->sp->validateStatus())
        {
            if(!$response->getStatus()->isSuccess())
            {
                throw new SAMLException(
                    sprintf("Invalid status. Message: %s", $response->getStatus()->getStatusMessage()),
                    400
                );
            }
        }

        //--------------------------------------------------------------------------------------------------------------
        // Check the issuer. EntityId in the response MUST be equal to EntityId in IdentityProvider::class
        //--------------------------------------------------------------------------------------------------------------
        if($this->sp->validateIssuer())
        {
            if(!(0 === strcmp($response->getIssuer()->getValue(), $this->idp->getEntityID())))
            {
                throw new SAMLException(
                    sprintf("Invalid issuer. Expected %s, received: %s", $this->idp->getEntityID(), $response->getIssuer()->getValue()),
                    400
                );
            }
        }

        //--------------------------------------------------------------------------------------------------------------
        // Check whether the destination matches
        //--------------------------------------------------------------------------------------------------------------
        if($this->sp->validateDestination())
        {
            if(!(0 === strcmp($response->getDestination(), $this->sp->getACSUrl())))
            {
                throw new SAMLException(
                    sprintf("Invalid destination. Expected %s, received: %s", $this->sp->getACSUrl(), $response->getDestination()),
                    400
                );
            }
        }

        //--------------------------------------------------------------------------------------------------------------
        // Check for the issued at time and compare it to allowed boundaries
        //--------------------------------------------------------------------------------------------------------------
        if($this->sp->validateLifetime())
        {
            $iat = $response->getIssueInstantDateTime()->setTimezone(new \DateTimeZone('UTC'));
            $upper = $this->sp->getUpperDT();
            $lower = $this->sp->getLowerDT();

            if($iat > $upper)
            {
                throw new SAMLException(
                    sprintf("Assertion valid in the future. Check the time synchronization between IdP and SP. Issued at: %s, checked against: %s",
                        $iat->format('c'), $upper->format('c')),
                    400);
            }

            if($iat < $lower)
            {
                throw new SAMLException("Assertion expired. Issued at: %s, now: %s", $iat->format('c'), $lower->format('c'));
            }
        }

        return true;
    }

    /**
     * @return SignatureXmlReader|SignatureStringReader
     * @throws SAMLException
     */
    protected function getAuthnResponseSignature(): Signature
    {
        $signature = $this->getAuthnResponse()->getSignature();

        if(!($signature instanceof Signature))
        {
            throw new SAMLException("AuthN Response signature required by configuration but not present in the response", 400);
        }

        return $signature;
    }

    /**
     * @return AuthN
     */
    protected function create(): self
    {
        $authn = new AuthnRequest();

        $id = Helper::generateID();

        $authn
            ->setNameIDPolicy(new NameIDPolicy($this->sp->getNameIdFormat()))
            ->setAssertionConsumerServiceURL($this->sp->getACSUrl())
            ->setProtocolBinding($this->sp->getACSBinding())
            ->setID($id)
            ->setIssueInstant(new \DateTime("now", new \DateTimeZone("UTC")))
            ->setDestination($this->idp->getSSOUrl())
            ->setIssuer(new Issuer($this->sp->getEntityId()))
            ;

        if($this->idp->wantsSignedReguests())
        {
            $authn->setSignature($this->sp->getSignatureWriter());
        }

        $this->authn = $authn;

        return $this;
    }

    /**
     * @param AbstractBinding $binding
     */
    protected  function sendWith(AbstractBinding $binding)
    {
        $request = $this->getAuthnRequest();

        $context = new MessageContext();

        $context->setMessage($request);

        $binding->send($context)->send();
    }
}