<?php

declare(strict_types=1);

namespace AsyncAws\Symfony\Bundle\DependencyInjection;

use AsyncAws\Core\Credentials\CacheProvider;
use AsyncAws\Core\Credentials\ChainProvider;
use AsyncAws\Core\Credentials\CredentialProvider;
use AsyncAws\Core\Credentials\SymfonyCacheProvider;
use AsyncAws\Core\HttpClient\AwsHttpClientFactory;
use AsyncAws\Symfony\Bundle\Secrets\CachedEnvVarLoader;
use AsyncAws\Symfony\Bundle\Secrets\SsmVault;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
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\Reference;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class AsyncAwsExtension extends Extension
{
    /**
     * @param mixed[] $configs
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $usedServices = $this->registerConfiguredServices($container, $config);
        $usedServices = $this->registerInstalledServices($container, $config, $usedServices);
        $this->registerEnvLoader($container, $config);
        $this->autowireServices($container, $usedServices);
    }

    /**
     * @param array{
     *     register_service: bool,
     *     logger?: string|null,
     *     http_client?: string|null,
     *     credential_provider: string|null,
     *     credential_provider_cache: string|null,
     *     config: array<string, mixed>,
     *     secrets: array{
     *          enabled: bool,
     *          client: string|null,
     *          path: string|null,
     *          max_results: int|null,
     *          recursive: bool,
     *          cache: array{enabled: bool, pool: string, ttl: int},
     *     },
     *     clients: array<string, array{
     *          type: string,
     *          register_service: bool,
     *          logger?: string|null,
     *          http_client?: string|null,
     *          credential_provider: string|null,
     *          config: array<string, mixed>,
     *     }>,
     * } $config
     *
     * @return array<string, string|null>
     */
    private function registerConfiguredServices(ContainerBuilder $container, array $config): array
    {
        $availableServices = AwsPackagesProvider::getAllServices();
        $usedServices = [];
        $defaultConfig = $config;
        unset($defaultConfig['clients'], $defaultConfig['secrets']);

        foreach ($config['clients'] as $name => $data) {
            $client = $availableServices[$data['type']]['class'];
            if (!class_exists($client)) {
                throw new InvalidConfigurationException(\sprintf('You have configured "async_aws.%s" but the "%s" package is not installed. Try running "composer require %s"', $name, $data['type'], $availableServices[$data['type']]['package']));
            }

            $serviceConfig = array_merge($defaultConfig, $data);
            $serviceConfig['config'] = array_merge($defaultConfig['config'], $data['config']);
            if ($serviceConfig['register_service']) {
                $usedServices[$name] = $client;
                $this->addServiceDefinition($container, $name, $serviceConfig, $client);
            } else {
                $usedServices[$name] = null;
            }
        }

        return $usedServices;
    }

    /**
     * @param array{
     *     register_service: bool,
     *     clients: array<string, mixed>,
     *     logger?: string|null,
     *     http_client?: string|null,
     *     credential_provider: string|null,
     *     credential_provider_cache: string|null,
     *     config: array<string, mixed>,
     * } $config
     * @param array<string, string|null> $usedServices
     *
     * @return array<string, string|null>
     */
    private function registerInstalledServices(ContainerBuilder $container, array $config, array $usedServices): array
    {
        if (!$config['register_service']) {
            return $usedServices;
        }

        unset($config['clients']);
        $availableServices = AwsPackagesProvider::getAllServices();
        foreach ($availableServices as $name => $data) {
            if (\array_key_exists($name, $usedServices)) {
                continue;
            }

            $client = $data['class'];
            if (!class_exists($client)) {
                continue;
            }

            $usedServices[$name] = $client;
            $this->addServiceDefinition($container, $name, $config, $client);
        }

        return $usedServices;
    }

    /**
     * @param array{
     *     register_service: bool,
     *     logger?: string|null,
     *     http_client?: string|null,
     *     credential_provider: string|null,
     *     credential_provider_cache: string|null,
     *     config: array<string, mixed>,
     *     ...
     * } $config
     */
    private function addServiceDefinition(ContainerBuilder $container, string $name, array $config, string $clientClass): void
    {
        if (\array_key_exists('logger', $config)) {
            $logger = $config['logger'] ? new Reference($config['logger']) : null;
        } else {
            // Use default Symfony logger unless explicitly set to null.
            $logger = new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE);
        }
        if (\array_key_exists('http_client', $config)) {
            $httpClient = $config['http_client'] ? new Reference($config['http_client']) : null;
        } else {
            // Use default Symfony http_client unless explicitly set to null.
            if (class_exists(AwsHttpClientFactory::class)) {
                $httpClient = new Definition(HttpClientInterface::class);
                $httpClient->setFactory([AwsHttpClientFactory::class, 'createRetryableClient'])
                    ->setArguments([
                        new Reference('http_client', ContainerInterface::NULL_ON_INVALID_REFERENCE),
                        $logger,
                    ]);
            } else {
                $httpClient = new Reference('http_client', ContainerInterface::NULL_ON_INVALID_REFERENCE);
            }
        }

        $credentialServiceId = $config['credential_provider'];
        $credentialProviderCache = class_exists(SymfonyCacheProvider::class) && interface_exists(CacheInterface::class) ? $config['credential_provider_cache'] : null;

        if (null === $credentialServiceId) {
            $credentialServiceId = 'async_aws.credential';
            if (!$container->hasDefinition($credentialServiceId)) {
                $container->register($credentialServiceId, CredentialProvider::class)
                    ->setFactory([ChainProvider::class, 'createDefaultChain'])
                    ->setArguments([$httpClient, $logger])
                    ->addTag('monolog.logger', ['channel' => 'async_aws']);
            }
        }

        if (null !== $credentialProviderCache) {
            $container->register($credentialServiceId . '.cache', SymfonyCacheProvider::class)
                ->setDecoratedService($credentialServiceId)
                ->setArguments([
                    new Reference($credentialServiceId . '.cache.inner'),
                    new Reference($credentialProviderCache),
                    $logger,
                ])
                ->addTag('monolog.logger', ['channel' => 'async_aws']);

            $container->register($credentialServiceId . '.memory', CacheProvider::class)
                ->setDecoratedService($credentialServiceId)
                ->setArguments([
                    new Reference($credentialServiceId . '.memory.inner'),
                ]);
        }

        $definition = new Definition($clientClass);
        $definition->setArguments([
            $config['config'],
            $credentialServiceId ? new Reference($credentialServiceId) : null,
            $httpClient,
            $logger,
        ]);
        $definition->addTag('monolog.logger', ['channel' => 'async_aws']);
        $container->setDefinition(\sprintf('async_aws.client.%s', $name), $definition);
    }

    /**
     * @param array{
     *     register_service: bool,
     *     logger?: string|null,
     *     http_client?: string|null,
     *     credential_provider: string|null,
     *     credential_provider_cache: string|null,
     *     config: array<string, mixed>,
     *     secrets: array{
     *          enabled: bool,
     *          client: string|null,
     *          path: string|null,
     *          recursive: bool,
     *          max_results: int|null,
     *          cache: array{enabled: bool, pool: string, ttl: int},
     *     },
     *     clients: array<string, array{type: string, ...}>,
     * } $config
     */
    private function registerEnvLoader(ContainerBuilder $container, array $config): void
    {
        if (!$config['secrets']['enabled']) {
            return;
        }

        $availableServices = AwsPackagesProvider::getAllServices();
        if (!class_exists($className = $availableServices['ssm']['class'])) {
            throw new InvalidConfigurationException(\sprintf('You have enabled "async_aws.secrets" but the "%s" package is not installed. Try running "composer require %s"', 'ssm', $availableServices['ssm']['package']));
        }

        if (null !== $client = $config['secrets']['client']) {
            if (!isset($config['clients'][$client])) {
                throw new InvalidConfigurationException(\sprintf('The client "%s" configured in "async_aws.secrets" does not exists. Available clients are "%s"', $client, implode(', ', array_keys($config['clients']))));
            }
            if ('ssm' !== $config['clients'][$client]['type']) {
                throw new InvalidConfigurationException(\sprintf('The client "%s" configured in "async_aws.secrets" is not a SSM client.', $client));
            }
        } else {
            if (!isset($config['clients']['ssm'])) {
                $client = 'ssm';
            } else {
                $client = 'secrets';
                $i = 1;
                while (isset($config['clients'][$client])) {
                    $client = 'secrets_' . $i;
                }
            }
            $this->addServiceDefinition($container, $client, $config, $className);
        }

        $container->register(SsmVault::class)
            ->setAutoconfigured(true)
            ->setArguments([
                new Reference('async_aws.client.' . $client),
                $config['secrets']['path'],
                $config['secrets']['recursive'],
                $config['secrets']['max_results'],
            ]);

        if ($config['secrets']['cache']['enabled']) {
            if (!interface_exists(CacheInterface::class)) {
                throw new InvalidConfigurationException('You have enabled "async_aws.secrets.cache" but the "symfony/cache" package is not installed. Try running "composer require symfony/cache"');
            }

            $container->register(CachedEnvVarLoader::class)
                ->setDecoratedService(SsmVault::class)
                ->setArguments([
                    new Reference(CachedEnvVarLoader::class . '.inner'),
                    new Reference($config['secrets']['cache']['pool']),
                    $config['secrets']['cache']['ttl'],
                ]);
        }
    }

    /**
     * @param array<string, string|null> $usedServices
     */
    private function autowireServices(ContainerBuilder $container, array $usedServices): void
    {
        $awsServices = AwsPackagesProvider::getAllServices();
        foreach ($usedServices as $name => $client) {
            if (null === $client) {
                // This client is disabled.
                continue;
            }

            $serviceId = \sprintf('async_aws.client.%s', $name);
            if (isset($awsServices[$name])) {
                $container->setAlias($client, $serviceId);

                continue;
            }

            // Assert: Custom name
            $container->registerAliasForArgument($serviceId, $client, $name);
        }
    }
}
