// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Linq;

namespace Microsoft.Extensions.ApiDescriptions;

/// <summary>
/// Provides an implementation of <see cref="IDocumentProvider"/> to use for build-time generation of OpenAPI documents.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use.</param>
internal sealed class OpenApiDocumentProvider(IServiceProvider serviceProvider) : IDocumentProvider
{
    /// <summary>
    /// Serializes the OpenAPI document associated with a given document name to
    /// the provided writer.
    /// </summary>
    /// <param name="documentName">The name of the document to resolve.</param>
    /// <param name="writer">A text writer associated with the document to write to.</param>
    public async Task GenerateAsync(string documentName, TextWriter writer)
    {
        // See OpenApiServiceCollectionExtensions.cs to learn why we lowercase the document name
        var lowercasedDocumentName = documentName.ToLowerInvariant();

        var options = serviceProvider.GetRequiredService<IOptionsMonitor<OpenApiOptions>>();
        var namedOption = options.Get(lowercasedDocumentName);
        var resolvedOpenApiVersion = namedOption.OpenApiVersion;
        await GenerateAsync(lowercasedDocumentName, writer, resolvedOpenApiVersion);
    }

    /// <summary>
    /// Serializes the OpenAPI document associated with a given document name to
    /// the provided writer under the provided OpenAPI spec version.
    /// </summary>
    /// <param name="documentName">The name of the document to resolve.</param>
    /// <param name="writer">A text writer associated with the document to write to.</param>
    /// <param name="openApiSpecVersion">The OpenAPI specification version to use when serializing the document.</param>
    public async Task GenerateAsync(string documentName, TextWriter writer, OpenApiSpecVersion openApiSpecVersion)
    {
        // We need to retrieve the document name in a case-insensitive manner to support case-insensitive document name resolution.
        // The document service is registered with a key equal to the document name, but in lowercase.
        // The GetRequiredKeyedService() method is case-sensitive, which doesn't work well for OpenAPI document names here,
        // as the document name is also used as the route to retrieve the document, so we need to ensure this is lowercased to achieve consistency with ASP.NET Core routing.
        // See OpenApiServiceCollectionExtensions.cs for more info.
        var lowercasedDocumentName = documentName.ToLowerInvariant();

        // Microsoft.OpenAPI does not provide async APIs for writing the JSON
        // document to a file. See https://github.com/microsoft/OpenAPI.NET/issues/421 for
        // more info.
        var targetDocumentService = serviceProvider.GetRequiredKeyedService<OpenApiDocumentService>(lowercasedDocumentName);
        using var scopedService = serviceProvider.CreateScope();
        var document = await targetDocumentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider);
        var jsonWriter = new OpenApiJsonWriter(writer);
        await document.SerializeAsync(jsonWriter, openApiSpecVersion);
    }

    /// <summary>
    /// Provides all document names that are currently managed in the application.
    /// </summary>
    public IEnumerable<string> GetDocumentNames()
    {
        // Keyed services lack an API to resolve all registered keys.
        // We use the service provider to resolve an internal type.
        // This type tracks registered document names.
        // See https://github.com/dotnet/runtime/issues/100105 for more info.
        var documentServices = serviceProvider.GetServices<NamedService<OpenApiDocumentService>>();
        return documentServices.Select(docService => docService.Name);
    }
}
