<?php

declare (strict_types=1);
namespace GuardDog\Vendor\Webauthn\CeremonyStep;

use GuardDog\Vendor\Psr\Log\LoggerInterface;
use GuardDog\Vendor\Psr\Log\NullLogger;
use GuardDog\Vendor\Webauthn\AttestationStatement\AttestationStatement;
use GuardDog\Vendor\Webauthn\AuthenticatorAssertionResponse;
use GuardDog\Vendor\Webauthn\AuthenticatorAttestationResponse;
use GuardDog\Vendor\Webauthn\Exception\AuthenticatorResponseVerificationException;
use GuardDog\Vendor\Webauthn\MetadataService\CanLogData;
use GuardDog\Vendor\Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use GuardDog\Vendor\Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use GuardDog\Vendor\Webauthn\MetadataService\MetadataStatementRepository;
use GuardDog\Vendor\Webauthn\MetadataService\Statement\MetadataStatement;
use GuardDog\Vendor\Webauthn\MetadataService\StatusReportRepository;
use GuardDog\Vendor\Webauthn\PublicKeyCredentialCreationOptions;
use GuardDog\Vendor\Webauthn\PublicKeyCredentialRequestOptions;
use GuardDog\Vendor\Webauthn\PublicKeyCredentialSource;
use GuardDog\Vendor\Webauthn\TrustPath\CertificateTrustPath;
use function count;
use function in_array;
use function sprintf;
final class CheckMetadataStatement implements CeremonyStep, CanLogData
{
    private LoggerInterface $logger;
    private null|MetadataStatementRepository $metadataStatementRepository = null;
    private null|StatusReportRepository $statusReportRepository = null;
    private null|CertificateChainValidator $certificateChainValidator = null;
    public function __construct()
    {
        $this->logger = new NullLogger();
    }
    public function enableMetadataStatementSupport(MetadataStatementRepository $metadataStatementRepository, StatusReportRepository $statusReportRepository, CertificateChainValidator $certificateChainValidator): void
    {
        $this->metadataStatementRepository = $metadataStatementRepository;
        $this->statusReportRepository = $statusReportRepository;
        $this->certificateChainValidator = $certificateChainValidator;
    }
    public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void
    {
        $this->certificateChainValidator = $certificateChainValidator;
    }
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
    public function process(PublicKeyCredentialSource $publicKeyCredentialSource, AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse, PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions, ?string $userHandle, string $host): void
    {
        if (!$publicKeyCredentialOptions instanceof PublicKeyCredentialCreationOptions || !$authenticatorResponse instanceof AuthenticatorAttestationResponse) {
            return;
        }
        $attestationStatement = $authenticatorResponse->attestationObject->attStmt;
        $attestedCredentialData = $authenticatorResponse->attestationObject->authData->attestedCredentialData;
        $attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create('No attested credential data found');
        $aaguid = $attestedCredentialData->aaguid->__toString();
        if ($publicKeyCredentialOptions->attestation === null || $publicKeyCredentialOptions->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) {
            $this->logger->debug('No attestation is asked.');
            if ($aaguid === '00000000-0000-0000-0000-000000000000' && in_array($attestationStatement->type, [AttestationStatement::TYPE_NONE, AttestationStatement::TYPE_SELF], \true)) {
                $this->logger->debug('The Attestation Statement is anonymous.');
                $this->checkCertificateChain($attestationStatement, null);
                return;
            }
            return;
        }
        // If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
        // => nothing to check
        if ($attestationStatement->type === AttestationStatement::TYPE_NONE) {
            $this->logger->debug('No attestation returned.');
            //No attestation is returned. We shall ensure that the AAGUID is a null one.
            //if ($aaguid !== '00000000-0000-0000-0000-000000000000') {
            //$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
            //    'aaguid' => $aaguid,
            //    'AttestationStatement' => $attestationStatement,
            //]);
            //$attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000');
            //    return;
            //}
            return;
        }
        if ($aaguid === '00000000-0000-0000-0000-000000000000') {
            //No need to continue if the AAGUID is null.
            // This could be the case e.g. with AnonCA type
            return;
        }
        //The MDS Repository is mandatory here
        $this->metadataStatementRepository !== null || throw AuthenticatorResponseVerificationException::create('The Metadata Statement Repository is mandatory when requesting attestation objects.');
        $metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid);
        // At this point, the Metadata Statement is mandatory
        $metadataStatement !== null || throw AuthenticatorResponseVerificationException::create(sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid));
        // We check the last status report
        $this->checkStatusReport($aaguid);
        // We check the certificate chain (if any)
        $this->checkCertificateChain($attestationStatement, $metadataStatement);
        // Check Attestation Type is allowed
        if (count($metadataStatement->attestationTypes) !== 0) {
            $type = $this->getAttestationType($attestationStatement);
            in_array($type, $metadataStatement->attestationTypes, \true) || throw AuthenticatorResponseVerificationException::create(sprintf('Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator.', $type));
        }
    }
    private function getAttestationType(AttestationStatement $attestationStatement): string
    {
        return match ($attestationStatement->type) {
            AttestationStatement::TYPE_BASIC => MetadataStatement::ATTESTATION_BASIC_FULL,
            AttestationStatement::TYPE_SELF => MetadataStatement::ATTESTATION_BASIC_SURROGATE,
            AttestationStatement::TYPE_ATTCA => MetadataStatement::ATTESTATION_ATTCA,
            AttestationStatement::TYPE_ECDAA => MetadataStatement::ATTESTATION_ECDAA,
            AttestationStatement::TYPE_ANONCA => MetadataStatement::ATTESTATION_ANONCA,
            default => throw AuthenticatorResponseVerificationException::create('Invalid attestation type'),
        };
    }
    private function checkStatusReport(string $aaguid): void
    {
        $statusReports = $this->statusReportRepository === null ? [] : $this->statusReportRepository->findStatusReportsByAAGUID($aaguid);
        if (count($statusReports) !== 0) {
            $lastStatusReport = end($statusReports);
            if ($lastStatusReport->isCompromised()) {
                throw AuthenticatorResponseVerificationException::create('The authenticator is compromised and cannot be used');
            }
        }
    }
    private function checkCertificateChain(AttestationStatement $attestationStatement, ?MetadataStatement $metadataStatement): void
    {
        $trustPath = $attestationStatement->trustPath;
        if (!$trustPath instanceof CertificateTrustPath) {
            return;
        }
        $authenticatorCertificates = $trustPath->certificates;
        if ($metadataStatement === null) {
            $this->certificateChainValidator?->check($authenticatorCertificates, []);
            return;
        }
        $trustedCertificates = CertificateToolbox::fixPEMStructures($metadataStatement->attestationRootCertificates);
        $this->certificateChainValidator?->check($authenticatorCertificates, $trustedCertificates);
    }
}
