<?php

namespace AuthStack\SAMLServiceProvider\Model;

use LightSaml\Helper;
use LightSaml\SamlConstants;
use Illuminate\Http\Request;
use LightSaml\Model\Assertion\Issuer;
use LightSaml\Binding\AbstractBinding;
use LightSaml\Binding\BindingFactory;
use LightSaml\Binding\HttpPostBinding;
use LightSaml\Binding\HttpRedirectBinding;
use LightSaml\Model\Protocol\LogoutRequest;
use LightSaml\Model\Protocol\LogoutResponse;
use LightSaml\Context\Profile\MessageContext;
use LightSaml\Model\XmlDSig\SignatureXmlReader;
use LightSaml\Model\XmlDSig\SignatureStringReader;
use AuthStack\SAMLServiceProvider\Exceptions\SAMLException;
use AuthStack\SAMLServiceProvider\Model\LogoutResponse as LogoutResp;

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

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

    /**
     * @var LogoutRequest
     */
    protected $logout_request;

    /**
     * @var LogoutResponse
     */
    protected $logout_response;

    /**
     * Logout 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();
        }
    }

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

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

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

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

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

        return $this;
    }

    /**
     * @return LogoutRequest
     */
    public function getLogoutRequest(): LogoutRequest
    {
        return $this->logout_request;
    }

    /**
     * @return \AuthStack\SAMLServiceProvider\Model\LogoutResponse
     */
    public function getLogoutResponse()
    {
        return new LogoutResp($this->getLogoutRequest(), $this->sp, $this->idp);
    }

    /**
     * @return LogoutResponse
     */
    public function getLightSamlLogoutResponse(): LogoutResponse
    {
        return $this->logout_response;
    }

    /**
     * @return bool
     * @throws SAMLException
     */
    public function validateLogoutRequest(): bool
    {
        $request = $this->getLogoutRequest();

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

            $pubkey = $this->sp->getSigningPublicKey();

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

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

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

        //--------------------------------------------------------------------------------------------------------------
        // Check for the issued at time and compare it to allowed boundaries
        //--------------------------------------------------------------------------------------------------------------
        $iat = $request->getIssueInstantDateTime()->setTimezone(new \DateTimeZone('UTC'));
        $upper = $this->idp->getUpperDT();
        $lower = $this->idp->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;
    }

    /**
     * @throws SAMLException
     */
    public function send()
    {
        switch($this->idp->getLogoutBinding())
        {
            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->getLogoutBinding()), 500);
                break;
        }
    }

    /**
     * @return Logout
     */
    protected function create(): self
    {
        $request = new LogoutRequest();
        $request_id = Helper::generateID();

        $request
            ->setID($request_id)
            ->setDestination($this->idp->getLogoutURL())
            ->setIssueInstant(time())
            ->setIssuer(new Issuer($this->sp->getEntityId()))
        ;

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

        $this->logout_request = $request;

        return $this;
    }

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

        $context = new MessageContext();

        $context->setMessage($request);

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