<?php

declare(strict_types=1);

namespace Webauthn\Bundle\DependencyInjection;

use function array_key_exists;
use Cose\Algorithm\Algorithm;
use function count;
use function is_array;
use function sprintf;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Webauthn\AttestationStatement\AttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputChecker;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\Bundle\Controller\AssertionControllerFactory;
use Webauthn\Bundle\Controller\AssertionRequestController;
use Webauthn\Bundle\Controller\AssertionResponseController;
use Webauthn\Bundle\Controller\AttestationControllerFactory;
use Webauthn\Bundle\Controller\AttestationRequestController;
use Webauthn\Bundle\Controller\AttestationResponseController;
use Webauthn\Bundle\CredentialOptionsBuilder\ProfileBasedCreationOptionsBuilder;
use Webauthn\Bundle\CredentialOptionsBuilder\ProfileBasedRequestOptionsBuilder;
use Webauthn\Bundle\DependencyInjection\Compiler\AttestationStatementSupportCompilerPass;
use Webauthn\Bundle\DependencyInjection\Compiler\CoseAlgorithmCompilerPass;
use Webauthn\Bundle\DependencyInjection\Compiler\DynamicRouteCompilerPass;
use Webauthn\Bundle\DependencyInjection\Compiler\EventDispatcherSetterCompilerPass;
use Webauthn\Bundle\DependencyInjection\Compiler\ExtensionOutputCheckerCompilerPass;
use Webauthn\Bundle\DependencyInjection\Compiler\LoggerSetterCompilerPass;
use Webauthn\Bundle\Doctrine\Type as DbalType;
use Webauthn\Bundle\Policy\ClientOverridePolicy;
use Webauthn\Bundle\Repository\CredentialRecordRepositoryInterface;
use Webauthn\Bundle\Repository\PublicKeyCredentialSourceRepositoryInterface;
use Webauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepositoryInterface;
use Webauthn\Bundle\Security\Storage\OptionsStorage;
use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory;
use Webauthn\Bundle\Service\PublicKeyCredentialRequestOptionsFactory;
use Webauthn\CeremonyStep\CeremonyStepManager;
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
use Webauthn\CeremonyStep\TopOriginValidator;
use Webauthn\Counter\CounterChecker;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\FakeCredentialGenerator;
use Webauthn\MetadataService\CanLogData;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\MetadataService\StatusReportRepository;

final class WebauthnExtension extends Extension implements PrependExtensionInterface
{
    public function __construct(
        private readonly string $alias
    ) {
    }

    public function getAlias(): string
    {
        return $this->alias;
    }

    public function load(array $configs, ContainerBuilder $container): void
    {
        $processor = new Processor();
        /** @var array{options_storage: string, secured_rp_ids: array<string>, allowed_origins: array<string>, allow_subdomains: bool, event_dispatcher: string, clock: string, top_origin_validator: string|null, http_client: string, logger: string, fake_credential_generator: string, credential_repository: string, user_repository: string, counter_checker: string, metadata: array{enabled: bool, mds_repository: string, status_report_repository: string, certificate_chain_checker: string}, controllers: array{enabled: bool, creation?: array<string, array{options_builder: string|null, profile: string, user_entity_guesser: string, options_storage: string|null, options_handler: string, failure_handler: string, success_handler: string, hide_existing_credentials: bool, options_method: string, options_path: string, result_method: string, result_path: string|null, host: string|null, secured_rp_ids: array<string>}>, request?: array<string, array{options_builder: string|null, profile: string, options_storage: string|null, options_handler: string, failure_handler: string, success_handler: string, options_method: string, options_path: string, result_method: string, result_path: string|null, host: string|null, secured_rp_ids: array<string>}>}, creation_profiles: array<string, mixed>, request_profiles: array<string, mixed>, client_override_policy: array<string, mixed>, passkey_endpoints: array{enabled: bool, enroll: string|array<string, mixed>|null, manage: string|array<string, mixed>|null, prf_usage_details: string|array<string, mixed>|null}} $config */
        $config = $processor->processConfiguration($this->getConfiguration($configs, $container), $configs);

        $container->registerForAutoconfiguration(AttestationStatementSupport::class)->addTag(
            AttestationStatementSupportCompilerPass::TAG
        );
        $container->registerForAutoconfiguration(ExtensionOutputChecker::class)->addTag(
            ExtensionOutputCheckerCompilerPass::TAG
        );
        $container->registerForAutoconfiguration(CanDispatchEvents::class)->addTag(
            EventDispatcherSetterCompilerPass::TAG
        );
        $container->registerForAutoconfiguration(CanLogData::class)->addTag(LoggerSetterCompilerPass::TAG);
        $container->registerForAutoconfiguration(Algorithm::class)->addTag(CoseAlgorithmCompilerPass::TAG);
        $container->setAlias(OptionsStorage::class, $config['options_storage']);

        // @deprecated Will be removed in 6.0.0
        $container->setParameter('webauthn.secured_relying_party_ids', $config['secured_rp_ids']);
        $container->setParameter('webauthn.allowed_origins', $config['allowed_origins']);
        $container->setParameter('webauthn.allow_subdomains', $config['allow_subdomains']);
        $container->setAlias('webauthn.event_dispatcher', $config['event_dispatcher']);
        $container->setAlias('webauthn.clock', $config['clock']);
        if ($config['top_origin_validator'] !== null) {
            $container->setAlias(TopOriginValidator::class, $config['top_origin_validator']);
        }
        $container->setAlias('webauthn.http_client', $config['http_client']);
        $container->setAlias('webauthn.logger', $config['logger']);

        $container->setAlias(FakeCredentialGenerator::class, $config['fake_credential_generator']);
        $container->setAlias(CredentialRecordRepositoryInterface::class, $config['credential_repository']);
        // @deprecated Will be removed in 6.0.0. Kept for BC: code still type-hinting the legacy interface
        // resolves to the same service when the configured repository implements it.
        $container->setAlias(PublicKeyCredentialSourceRepositoryInterface::class, $config['credential_repository']);
        $container->setAlias(PublicKeyCredentialUserEntityRepositoryInterface::class, $config['user_repository']);

        $container->setAlias(CounterChecker::class, $config['counter_checker']);

        $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config/'));
        $this->loadMetadataServices($container, $loader, $config['metadata']);
        $this->loadControllersSupport($container, $config['controllers']);

        $container->setParameter('webauthn.creation_profiles', $config['creation_profiles']);
        $container->setParameter('webauthn.request_profiles', $config['request_profiles']);
        $container->setParameter('webauthn.client_override_policy', $config['client_override_policy']);

        $this->loadPasskeyEndpointsConfig($container, $config['passkey_endpoints']);

        $loader->load('services.php');
        $loader->load('cose.php');
        $loader->load('security.php');

        if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug') === true) {
            $loader->load('dev_services.php');
        }
    }

    /**
     * @param array<array-key, mixed> $config
     */
    public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
    {
        return new Configuration($this->alias);
    }

    public function prepend(ContainerBuilder $container): void
    {
        $config = $this->getDoctrineBundleConfiguration($container);
        if (! is_array($config)) {
            return;
        }
        /** @var array{dbal?: array{types?: array<string, class-string>}} $config */
        $types = $config['dbal']['types'] ?? [];
        $types += [
            'attested_credential_data' => DbalType\AttestedCredentialDataType::class,
            'aaguid' => DbalType\AAGUIDDataType::class,
            'base64' => DbalType\Base64BinaryDataType::class,
            'public_key_credential_descriptor' => DbalType\PublicKeyCredentialDescriptorType::class,
            'trust_path' => DbalType\TrustPathDataType::class,
        ];
        $config['dbal']['types'] = $types;
        $container->prependExtensionConfig('doctrine', $config);
    }

    /**
     * @return array<string, mixed>|null
     */
    private function getDoctrineBundleConfiguration(ContainerBuilder $container): ?array
    {
        if (! $container->hasParameter('kernel.bundles')) {
            return null;
        }
        $bundles = $container->getParameter('kernel.bundles');
        if (! is_array($bundles) || ! array_key_exists('DoctrineBundle', $bundles)) {
            return null;
        }
        $configs = $container->getExtensionConfig('doctrine');

        return count($configs) === 0 ? null : current($configs);
    }

    /**
     * @param array{enabled: bool, creation?: array<string, mixed>, request?: array<string, mixed>} $config
     */
    private function loadControllersSupport(ContainerBuilder $container, array $config): void
    {
        if ($config['enabled'] === false) {
            return;
        }

        /** @var array<string, array{options_builder: string|null, profile: string, user_entity_guesser: string, options_storage: string|null, options_handler: string, failure_handler: string, success_handler: string, hide_existing_credentials: bool, options_method: string, options_path: string, result_method: string, result_path: string|null, host: string|null, secured_rp_ids: array<string>}> $creation */
        $creation = $config['creation'] ?? [];
        /** @var array<string, array{options_builder: string|null, profile: string, options_storage: string|null, options_handler: string, failure_handler: string, success_handler: string, options_method: string, options_path: string, result_method: string, result_path: string|null, host: string|null, secured_rp_ids: array<string>}> $request */
        $request = $config['request'] ?? [];
        $this->loadCreationControllersSupport($container, $creation);
        $this->loadRequestControllersSupport($container, $request);
    }

    /**
     * @param array<string, array{options_builder: string|null, profile: string, user_entity_guesser: string, options_storage: string|null, options_handler: string, failure_handler: string, success_handler: string, hide_existing_credentials: bool, options_method: string, options_path: string, result_method: string, result_path: string|null, host: string|null, secured_rp_ids: array<string>}> $config
     */
    private function loadCreationControllersSupport(ContainerBuilder $container, array $config): void
    {
        foreach ($config as $name => $creationConfig) {
            if ($creationConfig['options_builder'] !== null) {
                $creationOptionsBuilderId = $creationConfig['options_builder'];
            } else {
                $creationOptionsBuilderId = sprintf('webauthn.controller.creation.options_builder.%s', $name);
                $creationOptionsBuilder = (new Definition(ProfileBasedCreationOptionsBuilder::class))
                    ->setArguments([
                        new Reference(SerializerInterface::class),
                        new Reference(ValidatorInterface::class),
                        new Reference(CredentialRecordRepositoryInterface::class),
                        new Reference(PublicKeyCredentialCreationOptionsFactory::class),
                        $creationConfig['profile'],
                        new Reference(ClientOverridePolicy::class),
                    ]);
                $container->setDefinition($creationOptionsBuilderId, $creationOptionsBuilder);
            }

            $attestationRequestControllerId = sprintf('webauthn.controller.creation.request.%s', $name);
            $attestationRequestController = (new Definition(AttestationRequestController::class))
                ->setFactory([new Reference(AttestationControllerFactory::class), 'createRequestController'])
                ->setArguments([
                    new Reference($creationOptionsBuilderId),
                    new Reference($creationConfig['user_entity_guesser']),
                    new Reference($creationConfig['options_storage'] ?? OptionsStorage::class),
                    new Reference($creationConfig['options_handler']),
                    new Reference($creationConfig['failure_handler']),
                    $creationConfig['hide_existing_credentials'] ?? false,
                ])
                ->addTag(DynamicRouteCompilerPass::TAG, [
                    'method' => $creationConfig['options_method'],
                    'path' => $creationConfig['options_path'],
                    'host' => $creationConfig['host'],
                ])
                ->addTag('controller.service_arguments');
            $container->setDefinition($attestationRequestControllerId, $attestationRequestController);

            $creationCeremonyStepManagerId = sprintf(
                'webauthn.controller.creation.response.ceremony_step_manager.%s',
                $name
            );

            // Get the profile configuration to check if conditional_create is enabled
            $profiles = $config['creation_profiles'] ?? [];
            $profileName = $creationConfig['profile'];
            $isConditionalCreate = isset($profiles[$profileName]['conditional_create']) && $profiles[$profileName]['conditional_create'] === true;

            $ceremonyFactoryMethod = $isConditionalCreate ? 'conditionalCreateCeremony' : 'creationCeremony';

            $container
                ->setDefinition($creationCeremonyStepManagerId, new Definition(CeremonyStepManager::class))
                ->setFactory([new Reference(CeremonyStepManagerFactory::class), $ceremonyFactoryMethod])
                // @deprecated Will be removed in 6.0.0
                ->setArguments([$creationConfig['secured_rp_ids']])
            ;

            $attestationResponseValidatorId = sprintf(
                'webauthn.controller.creation.response.attestation_validator.%s',
                $name
            );
            $attestationResponseValidator = new Definition(AuthenticatorAttestationResponseValidator::class);
            $attestationResponseValidator->setArguments([new Reference($creationCeremonyStepManagerId)]);
            $container->setDefinition($attestationResponseValidatorId, $attestationResponseValidator);

            $attestationResponseControllerId = sprintf('webauthn.controller.creation.response.%s', $name);
            $attestationResponseController = new Definition(AttestationResponseController::class);
            $attestationResponseController->setFactory(
                [new Reference(AttestationControllerFactory::class), 'createResponseController']
            );
            $attestationResponseController->setArguments([
                new Reference($creationConfig['options_storage'] ?? OptionsStorage::class),
                new Reference($creationConfig['success_handler']),
                new Reference($creationConfig['failure_handler']),
                null,
                new Reference($attestationResponseValidatorId),
            ]);
            if ($creationConfig['result_path'] !== null) {
                $attestationResponseController->addTag(DynamicRouteCompilerPass::TAG, [
                    'method' => $creationConfig['result_method'],
                    'path' => $creationConfig['result_path'],
                    'host' => $creationConfig['host'],
                ]);
            }
            $attestationResponseController->addTag('controller.service_arguments');
            $container->setDefinition($attestationResponseControllerId, $attestationResponseController);
        }
    }

    /**
     * @param array<string, array{options_builder: string|null, profile: string, options_storage: string|null, options_handler: string, failure_handler: string, success_handler: string, options_method: string, options_path: string, result_method: string, result_path: string|null, host: string|null, secured_rp_ids: array<string>}> $config
     */
    private function loadRequestControllersSupport(ContainerBuilder $container, array $config): void
    {
        foreach ($config as $name => $requestConfig) {
            if ($requestConfig['options_builder'] !== null) {
                $assertionOptionsBuilderId = $requestConfig['options_builder'];
            } else {
                $assertionOptionsBuilderId = sprintf('webauthn.controller.request.options_builder.%s', $name);
                $assertionOptionsBuilder = (new Definition(ProfileBasedRequestOptionsBuilder::class))
                    ->setArguments([
                        new Reference(SerializerInterface::class),
                        new Reference(ValidatorInterface::class),
                        new Reference(PublicKeyCredentialUserEntityRepositoryInterface::class),
                        new Reference(CredentialRecordRepositoryInterface::class),
                        new Reference(PublicKeyCredentialRequestOptionsFactory::class),
                        $requestConfig['profile'],
                        new Reference(FakeCredentialGenerator::class, ContainerInterface::NULL_ON_INVALID_REFERENCE),
                        new Reference(ClientOverridePolicy::class),
                    ]);
                $container->setDefinition($assertionOptionsBuilderId, $assertionOptionsBuilder);
            }

            $assertionRequestControllerId = sprintf('webauthn.controller.request.request.%s', $name);
            $assertionRequestController = (new Definition(AssertionRequestController::class))
                ->setFactory([new Reference(AssertionControllerFactory::class), 'createRequestController'])
                ->setArguments([
                    new Reference($assertionOptionsBuilderId),
                    new Reference($requestConfig['options_storage'] ?? OptionsStorage::class),
                    new Reference($requestConfig['options_handler']),
                    new Reference($requestConfig['failure_handler']),
                ])
                ->addTag(DynamicRouteCompilerPass::TAG, [
                    'method' => $requestConfig['options_method'],
                    'path' => $requestConfig['options_path'],
                    'host' => $requestConfig['host'],
                ])
                ->addTag('controller.service_arguments');
            $container->setDefinition($assertionRequestControllerId, $assertionRequestController);

            $requestCeremonyStepManagerId = sprintf(
                'webauthn.controller.request.response.ceremony_step_manager.%s',
                $name
            );
            $container
                ->setDefinition($requestCeremonyStepManagerId, new Definition(CeremonyStepManager::class))
                ->setFactory([new Reference(CeremonyStepManagerFactory::class), 'requestCeremony'])
                // @deprecated Will be removed in 6.0.0
                ->setArguments([$requestConfig['secured_rp_ids']])
            ;

            $assertionResponseValidatorId = sprintf(
                'webauthn.controller.request.response.assertion_validator.%s',
                $name
            );
            $assertionResponseValidator = new Definition(AuthenticatorAssertionResponseValidator::class);
            $assertionResponseValidator->setArguments([new Reference($requestCeremonyStepManagerId)]);
            $assertionResponseValidator->addTag(EventDispatcherSetterCompilerPass::TAG);
            $container->setDefinition($assertionResponseValidatorId, $assertionResponseValidator);

            $assertionResponseControllerId = sprintf('webauthn.controller.request.response.%s', $name);
            $assertionResponseController = new Definition(AssertionResponseController::class);
            $assertionResponseController->setFactory(
                [new Reference(AssertionControllerFactory::class), 'createResponseController']
            );
            $assertionResponseController->setArguments([
                new Reference($requestConfig['options_storage'] ?? OptionsStorage::class),
                new Reference($requestConfig['success_handler']),
                new Reference($requestConfig['failure_handler']),
                null,
                new Reference($assertionResponseValidatorId),
            ]);
            if ($requestConfig['result_path'] !== null) {
                $assertionResponseController->addTag(DynamicRouteCompilerPass::TAG, [
                    'method' => $requestConfig['result_method'],
                    'path' => $requestConfig['result_path'],
                    'host' => $requestConfig['host'],
                ]);
            }
            $assertionResponseController->addTag('controller.service_arguments');
            $container->setDefinition($assertionResponseControllerId, $assertionResponseController);
        }
    }

    /**
     * @param array{enabled: bool, mds_repository: string, status_report_repository: string, certificate_chain_checker: string} $config
     */
    private function loadMetadataServices(ContainerBuilder $container, FileLoader $loader, array $config): void
    {
        if ($config['enabled'] === false) {
            return;
        }
        $container->setAlias(MetadataStatementRepository::class, $config['mds_repository']);
        $container->setAlias(StatusReportRepository::class, $config['status_report_repository']);
        $container->setAlias(CertificateChainValidator::class, $config['certificate_chain_checker']);
        $loader->load('metadata_statement_supports.php');
    }

    /**
     * @param array{enabled: bool, enroll: string|array<string, mixed>|null, manage: string|array<string, mixed>|null, prf_usage_details: string|array<string, mixed>|null} $config
     */
    private function loadPasskeyEndpointsConfig(ContainerBuilder $container, array $config): void
    {
        $container->setParameter('webauthn.passkey_endpoints.enabled', $config['enabled'] ?? false);
        $container->setParameter('webauthn.passkey_endpoints.enroll', $config['enroll'] ?? null);
        $container->setParameter('webauthn.passkey_endpoints.manage', $config['manage'] ?? null);
        $container->setParameter('webauthn.passkey_endpoints.prf_usage_details', $config['prf_usage_details'] ?? null);
    }
}
