<?php

namespace AuthStack\SAMLServiceProvider\Validators;

use LightSaml\Credential\KeyHelper;
use LightSaml\Model\Protocol\Response;
use LightSaml\Credential\X509Certificate;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use LightSaml\Error\LightSamlSecurityException;
use LightSaml\Model\XmlDSig\SignatureXmlReader;
use LightSaml\Model\XmlDSig\SignatureStringReader;
use AuthStack\SAMLServiceProvider\Exceptions\SAMLException;

class SAMLResponse
{
    protected $idp;

    public function __construct()
    {
        $this->idp = sha1(config('saml.sp.IdentityProvider'));
    }

    /**
     * @param Response $response
     * @return SAMLResponse
     * @throws SAMLException
     */
    public function validate(Response $response)
    {
        return $this
            ->validateStatus($response)
            ->validateIssuer($response)
            ->validateDestination($response)
            ->validateSignature($response)
            ->validateIssuedAtTime($response)
        ;
    }

    /**
     * @param Response $response
     * @return $this
     * @throws SAMLException
     */
    public function validateStatus(Response $response): self
    {
        if($this->needsValidating('status'))
        {
            $status = $response->getStatus();

            if(!$status->isSuccess())
            {
                throw new SAMLException(sprintf("Invalid status. Message: %s. ", $status->getStatusMessage()));
            }
        }

        return $this;
    }

    /**
     * @param Response $response
     * @return $this
     * @throws SAMLException
     */
    public function validateIssuer(Response $response): self
    {
        if($this->needsValidating('issuer'))
        {
            $got = $response->getIssuer()->getValue();
            $expected = config(sprintf('saml.idp.%s.EntityId', $this->idp));

            $result = strcmp($got, $expected) === 0;

            if(false === $result)
            {
                throw new SAMLException(sprintf("Invalid SAMLResponse issuer. Got %s, expected %s", $got, $expected));
            }
        }

        return $this;
    }

    /**
     * @param Response $response
     * @return $this
     * @throws SAMLException
     */
    public function validateDestination(Response $response): self
    {
        if($this->needsValidating('destination'))
        {
            $destination = $response->getDestination();
            $expected = sprintf("%s%s", config('saml.sp.baseUrl'), config('saml.sp.AssertionConsumerService.location'));

            $result = strcmp($destination, $expected) === 0;

            if(false === $result)
            {
                throw new SAMLException(sprintf("Invalid destination in response. Expected %s, got: %s", $expected, $destination));
            }
        }

        return $this;
    }

    /**
     * @param Response $response
     *
     * @return $this
     * @throws LightSamlSecurityException
     */
    public function validateSignature(Response $response): self
    {
        if($this->needsValidating('signature'))
        {
            $signature = $this->getSignatureFromResponse($response);
            $key = $this->getIdPPublicKey();

            $signature->validate($key);
        }

        return $this;
    }

    /**
     * @param Response $response
     * @return $this
     * @throws SAMLException
     */
    protected function validateIssuedAtTime(Response $response): self
    {
        if($this->needsValidating('lifetime'))
        {
            $timezone = new \DateTimeZone("UTC");

            $issued_at  = $response->getIssueInstantDateTime()->setTimezone($timezone);
            $upper      = new \DateTime(config('saml.sp.validate.upper', '+1 minute'), $timezone);
            $lower      = new \DateTime(config('saml.sp.validate.lower', '-1 minute'), $timezone);

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

            if($issued_at < $lower)
            {
                throw new SAMLException(sprintf("Expired assertion. Issued at: %s, now: %s", $issued_at->format('c'), $lower->format('c')));
            }
        }

        return $this;
    }


    /**
     * @param Response $response
     *
     * @return SignatureXmlReader|SignatureStringReader
     */
    protected function getSignatureFromResponse(Response $response)
    {
        $signature = $response->getSignature();

        if(!($signature instanceof SignatureXmlReader) && !($signature instanceof SignatureStringReader))
        {
            throw new \LogicException("Unable to get signature from the SAML response - (Signature must be an instance of SignatureXmlReader or SignatureStringReader)");
        }

        return $signature;
    }


    /**
     * @return XMLSecurityKey
     */
    protected function getIdPPublicKey(): XMLSecurityKey
    {
        $key = KeyHelper::createPublicKey(
            (new X509Certificate)->setData(config(sprintf('saml.idp.%s.x509', $this->idp)))
        );

        return $key;
    }

    /**
     * @param $validationKey
     * @return bool
     */
    protected function needsValidating($validationKey): bool
    {
        $idpSpecificValidate = config(sprintf('saml.idp.%s.validate', $this->idp));

        if(!empty($idpSpecificValidate) && is_array($idpSpecificValidate) && array_key_exists($validationKey, $idpSpecificValidate))
        {
            return $idpSpecificValidate[$validationKey];
        }

        return config(sprintf('saml.sp.validate.%s', $validationKey), false);
    }
}
