﻿/*
 * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
 * See https://github.com/openiddict/openiddict-core for more information concerning
 * the license and the contributors participating to this project.
 */

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace OpenIddict.Client;

public class OpenIddictClientDispatcher : IOpenIddictClientDispatcher
{
    private readonly ILogger<OpenIddictClientDispatcher> _logger;
    private readonly IOptionsMonitor<OpenIddictClientOptions> _options;
    private readonly IServiceProvider _provider;

    /// <summary>
    /// Creates a new instance of the <see cref="OpenIddictClientDispatcher"/> class.
    /// </summary>
    public OpenIddictClientDispatcher(
        ILogger<OpenIddictClientDispatcher> logger,
        IOptionsMonitor<OpenIddictClientOptions> options,
        IServiceProvider provider)
    {
        _logger = logger;
        _options = options;
        _provider = provider;
    }

    public async ValueTask DispatchAsync<TContext>(TContext context) where TContext : BaseContext
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        await foreach (var handler in GetHandlersAsync())
        {
            try
            {
                await handler.HandleAsync(context);
            }

            catch (Exception exception) when (_logger.IsEnabled(LogLevel.Debug))
            {
                _logger.LogDebug(exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName);

                throw;
            }

            if (_logger.IsEnabled(LogLevel.Debug))
            {
                _logger.LogDebug(SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName);
            }

            switch (context)
            {
                case BaseRequestContext { IsRequestHandled: true }:
                    if (_logger.IsEnabled(LogLevel.Debug))
                    {
                        _logger.LogDebug(SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName);
                    }
                    return;

                case BaseRequestContext { IsRequestSkipped: true }:
                    if (_logger.IsEnabled(LogLevel.Debug))
                    {
                        _logger.LogDebug(SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName);
                    }
                    return;

                case BaseValidatingContext { IsRejected: true }:
                    if (_logger.IsEnabled(LogLevel.Debug))
                    {
                        _logger.LogDebug(SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName);
                    }
                    return;

                default: continue;
            }
        }

        async IAsyncEnumerable<IOpenIddictClientHandler<TContext>> GetHandlersAsync()
        {
            // Note: the descriptors collection is sorted during options initialization for performance reasons.
            var descriptors = _options.CurrentValue.Handlers;
            if (descriptors.Count == 0)
            {
                yield break;
            }

            for (var index = 0; index < descriptors.Count; index++)
            {
                var descriptor = descriptors[index];
                if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor))
                {
                    continue;
                }

                var handler = descriptor.ServiceDescriptor.ImplementationInstance is not null ?
                    descriptor.ServiceDescriptor.ImplementationInstance as IOpenIddictClientHandler<TContext> :
                    _provider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictClientHandler<TContext>;

                if (handler is null)
                {
                    throw new InvalidOperationException(SR.FormatID0138(descriptor.ServiceDescriptor.ServiceType));
                }

                yield return handler;
            }
        }

        async ValueTask<bool> IsActiveAsync(OpenIddictClientHandlerDescriptor descriptor)
        {
            for (var index = 0; index < descriptor.FilterTypes.Length; index++)
            {
                if (!(_provider.GetService(descriptor.FilterTypes[index]) is IOpenIddictClientHandlerFilter<TContext> filter))
                {
                    throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index]));
                }

                if (!await filter.IsActiveAsync(context))
                {
                    return false;
                }
            }

            return true;
        }
    }
}
